v0.21.0 — dev loop: quota, lookup, localhost mock#26
Conversation
- pdcli quota (alias ratelimit): reads the daily token-budget headers (x-daily-ratelimit-token-remaining/-limit) via one cheap GET; --min / --threshold gate CI with exit 75; absent headers print n/a, exit 0. The budget is company-wide, so treat the reading as a hint. - pdcli lookup <entity> --field <name> --value <v>: exact-match id lookup via GET /api/v2/itemSearch/field (name to hash resolved from the fields cache). --first yields one row for shell composition (--jq .id); no match exits 3 so scripts branch create-vs-update. Case-sensitive and eventually consistent. - Localhost mock: PDCLI_BASE_URL is honored only when it resolves to localhost/127.0.0.1/::1 (anything else exits 78, reaffirming the host-lock); a loud stderr banner prints; the real keychain token is never sent to the override host (PDCLI_API_TOKEN required). Prism recipe documented. - MCP: lookup and quota classified read; lookup added to the curated set. Claude-Session: https://claude.ai/code/session_018dRMUUj9vpKp3tqKQFsvE9
Security:
- The localhost mock now forces the credential to PDCLI_API_TOKEN and
token auth (and disables OAuth refresh) at the client layer, so a
skipAuth command (auth status/login) can no longer send the real
keychain/OAuth token to the override host. Withholding was previously
presence-only.
quota:
- --threshold fails closed: if the daily-limit header is absent it errors
(exit 69) instead of silently exiting 0.
- The probe catches a RateLimitError and still prints the reading from
the captured headers, so an exhausted budget shows numbers (and gates)
instead of erroring out with exit 75 and no output.
lookup:
- Tolerates both the wrapped ({item}) and flat ({id}) result shapes, so a
flat response no longer yields undefined rows and a false "found".
- Exit 3 (no match) is now documented in the exit-code contract.
- Held out of the MCP curated default: its exit 3 reads as a tool error
to agents (still reachable via --all-tools).
docs: the mock recipe points at a downloadable spec, not a private path.
Claude-Session: https://claude.ai/code/session_018dRMUUj9vpKp3tqKQFsvE9
|
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
| const rl = this.apiClient.lastRateLimit | ||
| const remaining = rl.dailyRemaining |
There was a problem hiding this comment.
Unguarded dereference of
lastRateLimit
this.apiClient.lastRateLimit is typed as { ... } | undefined until the first HTTP response is received. The probe request is made just before this code and all non-RateLimitError exceptions are re-thrown, so in every currently reachable path rl is an object. However, if a future code path (e.g. a quota invocation without the probe, or a refactor of the try/catch) ever allows control to reach here without a prior response, rl.dailyRemaining will throw a TypeError with no useful message rather than an actionable CLI error. A defensive early return keeps the invariant explicit.
| if (pct == null) { | ||
| throw new CliError( | ||
| 'cannot evaluate --threshold: no daily-limit header in the API ' + | ||
| 'response (x-daily-ratelimit-token-limit absent)', | ||
| { exitCode: 69 }, | ||
| ) | ||
| } |
There was a problem hiding this comment.
Exit 69 from
--threshold when limit header is absent is undocumented
When --threshold N is set but the x-daily-ratelimit-token-limit header is absent, this branch exits 69 ("fail closed"). Exit 69 is documented in exit-codes.mdx as "service unavailable / Pipedrive is down — safe to retry later," and --min/--threshold are only documented as "exit 75 when budget is too low." A CI script that gates on pdcli quota --threshold 10 would not know that a missing header produces exit 69 rather than 75, making it hard to write a correct handler. Worth adding a line to the exit-codes table or the quota flag description.
Third pre-v1 release (dev-loop primitives).
Added
pdcli quota/ratelimit— daily token-budget readout with--min/--thresholdCI gates (exit 75). Prints the reading even when the budget is exhausted.pdcli lookup <entity> --field --value— exact-match id lookup via the field-search endpoint (name→hash resolved),--firstfor--jq .id, no-match exits 3 for create-vs-update branching.PDCLI_BASE_URLhonored only for localhost (else exit 78); the keychain/OAuth token is never sent to the override host; loud stderr banner; Prism recipe documented.Quality gate
skipAuthcommand can't leak the real keychain token to a localhost mock. Also: lookup tolerates both response shapes,--thresholdfails closed, the probe shows a low budget instead of erroring, exit 3 documented.evil.com,localhost.evil.com,127.0.0.1.evil.com(exit 78); valid localhost banners to stderr and targets the override.itemSearch/fieldelement shape.https://claude.ai/code/session_018dRMUUj9vpKp3tqKQFsvE9
Greptile Summary
This PR adds three dev-loop primitives:
pdcli quota(daily token-budget readout with CI gates),pdcli lookup(exact-match id resolution via the field-search endpoint), and a guardedPDCLI_BASE_URLlocalhost mock override. The mock implementation is notably careful: host-lock is enforced at the URL level, the real keychain/OAuth token is replaced withPDCLI_API_TOKENat client-construction time, and theonRefreshcallback is nulled so a 401 from the mock can never trigger a real token mint.quotaprobes/api/v1/users/me, snapshotslastRateLimitoff the response (including 429s), and uses--min/--thresholdfor CI gating;--thresholdfails closed (exit 69) when the daily-limit header is absent, which is correct but not yet documented in the exit-codes table.lookupresolves human field names to API key hashes, caps results at 100 (the endpoint's limit), tolerates both wrapped and flat response shapes, and exits 3 on no-match for create-vs-update branching.localhost/127.0.0.1/[::1]viaURL.hostnamecomparison; token substitution is enforced at the client layer soskipAuthcommands cannot inadvertently leak stored credentials.Confidence Score: 4/5
The three new commands are safe to merge. Security properties of the mock override are well-implemented and tested; the quota and lookup commands are straightforward additions with good test coverage.
The implementation is sound across all three features. The two findings are both in quota.js: an unguarded
rl.dailyRemainingdereference (not reachable today but fragile under future refactors), and an undocumented exit 69 fromquota --thresholdwhen the daily-limit header is missing. Neither affects correct behavior on the current code paths. The mock token-substitution logic, host-lock, and lookup field-resolution are all implemented correctly and thoroughly tested.src/commands/quota.js warrants a second look for the missing null guard and the undocumented exit-69 case; website/src/content/docs/automation/exit-codes.mdx should be updated to document that exit code.
Important Files Changed
Sequence Diagram
%%{init: {'theme': 'neutral'}}%% sequenceDiagram participant CLI participant createClient participant transport participant API as Pipedrive / Mock Note over CLI,API: pdcli quota CLI->>createClient: usingMock? resolveMockOrigin / normal origin CLI->>transport: GET /api/v1/users/me transport->>API: request (with auth headers) API-->>transport: 200 or 429 + rate-limit headers transport->>transport: "api.lastRateLimit = {dailyRemaining, dailyLimit, reset}" alt 429 RateLimitError transport-->>CLI: throw RateLimitError (caught, swallowed) end CLI->>CLI: read lastRateLimit, compute pct CLI->>CLI: --min / --threshold gate (exit 75 or 69) Note over CLI,API: pdcli lookup CLI->>transport: "GET /api/v2/{entity}Fields" transport->>API: request API-->>transport: field list CLI->>CLI: resolver.nameToKey(flags.field) CLI->>transport: "GET /api/v2/itemSearch/field?term=...&field=..." transport->>API: request API-->>transport: "data [{item: {id,...}}]" alt "rows.length === 0" CLI-->>CLI: exit 3 (no match) else CLI-->>CLI: outputResults (--first rows[0]) end Note over CLI,API: PDCLI_BASE_URL mock flow CLI->>createClient: "PDCLI_BASE_URL=http://127.0.0.1:4010" createClient->>createClient: resolveMockOrigin, validate localhost createClient->>createClient: "token=PDCLI_API_TOKEN, authMode=token, onRefresh=undefined" CLI->>transport: any request transport->>API: http://127.0.0.1:4010 (dummy token, no keychain)%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% sequenceDiagram participant CLI participant createClient participant transport participant API as Pipedrive / Mock Note over CLI,API: pdcli quota CLI->>createClient: usingMock? resolveMockOrigin / normal origin CLI->>transport: GET /api/v1/users/me transport->>API: request (with auth headers) API-->>transport: 200 or 429 + rate-limit headers transport->>transport: "api.lastRateLimit = {dailyRemaining, dailyLimit, reset}" alt 429 RateLimitError transport-->>CLI: throw RateLimitError (caught, swallowed) end CLI->>CLI: read lastRateLimit, compute pct CLI->>CLI: --min / --threshold gate (exit 75 or 69) Note over CLI,API: pdcli lookup CLI->>transport: "GET /api/v2/{entity}Fields" transport->>API: request API-->>transport: field list CLI->>CLI: resolver.nameToKey(flags.field) CLI->>transport: "GET /api/v2/itemSearch/field?term=...&field=..." transport->>API: request API-->>transport: "data [{item: {id,...}}]" alt "rows.length === 0" CLI-->>CLI: exit 3 (no match) else CLI-->>CLI: outputResults (--first rows[0]) end Note over CLI,API: PDCLI_BASE_URL mock flow CLI->>createClient: "PDCLI_BASE_URL=http://127.0.0.1:4010" createClient->>createClient: resolveMockOrigin, validate localhost createClient->>createClient: "token=PDCLI_API_TOKEN, authMode=token, onRefresh=undefined" CLI->>transport: any request transport->>API: http://127.0.0.1:4010 (dummy token, no keychain)Reviews (1): Last reviewed commit: "chore: release v0.21.0" | Re-trigger Greptile