Releases: wavyx/pdcli
Releases · wavyx/pdcli
v0.18.0
Second contract-hardening pass from a full quality audit, closing the
exit-code, machine-output, and data-safety gaps that a 1.0 freeze would lock
in. Most items are bug fixes; the BREAKING notes are exit-code/output changes a
script could observe.
Fixed — data safety (the headline)
- CSV
--upsertno longer deletes a matched record's other emails/phones.
The match (identity) field is now excluded from the update diff, so matching
a person by one email never PATCHes their email list down to that single
value. Same for phones. - Upsert matching on a monetary/address custom field is refused (exit 64)
instead of silently never matching and creating a duplicate on every run
(v2 returns those fields as objects the scalar compare can't match). - Excel "CSV UTF-8" imports work: a leading UTF-8 BOM is stripped instead
of corrupting the first column name into a "missing name column" error. - A non-integer
org_id/owner_idCSV cell is rejected (exit 65) instead of
serializing asnulland silently unlinking the relation; a non-numeric
value for a numeric custom field is likewise rejected. changes --limitno longer silently skips rows sharing the cut row's
update-time second; they replay next run (it warns, never silently loses,
when a single second exceeds the limit).--output csvno longer emits a silently-blank header+rows when a command
has no preset columns — it derives columns from the data (sochanges --output csvcan't advance its watermark over never-written rows).
Security
- The HTTP transport no longer follows redirects (
redirect: 'manual') and
refuses any 3xx (exit 78), so the API token is never re-sent to a redirect
target off your host-locked company domain.
Changed
- BREAKING — exit codes corrected to the ladder. Network unreachable / DNS
failure /--timeoutnow exit 69 (was 70); exhausted 429 retries exit
75 (was 69); a failed/rejecteddeal/lead convertexits 65 (was
70); a failed OAuth refresh (invalid_grant) exits 77 (was 65);
malformedpdcli api --bodyJSON exits 65 (was 70). Exit 70 is now
truly internal-bug-only. - BREAKING — aliased commands keep their real exit code. An alias for a
command that fails now propagates the command's exit code (e.g.watch's
exit 8, auth 77, usage 64) instead of collapsing every failure to 1, and
prints the error message it previously swallowed. - Mutations honor
--output. Every delete/remove, both converts,
deal bulk-update,person/org import, andbackupnow emit a structured
JSON/yaml/csv object (honoring--jq/--fields) instead of prose on stdout;
the converts expose the new record id (deal_id/lead_id) in JSON. Human
one-liners remain in interactive table mode. --fieldsnow projects keys forjson/yamloutput, not only table/csv.
Fixed — other
pdcli apireads a body piped on stdin (the documented behavior was
unreachable); a usage/parse error in a TTY now honors an explicit
--output json.search --item-types <one> --limitis clamped to 100 (the scoped endpoint
rejects more) instead of surfacing a confusing API 400.deal history --limitcaps how much it fetches instead of walking the whole
changelog; bulk-target parsing rejects blank/zero/undefinedids and
malformed piped JSON;digestrejects--formatcombined with--output.
Chore
npm run docs:commandsbuilds the manifest first (worked from a dirty tree
only); removed the unusedundicidependency; added the 10 missing
--helptopic descriptions; config tests isolate the store (PDCLI_CONFIG_DIR)
so they never touch your real profiles.
v0.17.0
Contract-hardening release — the last changes to the machine-facing surface
(exit codes + error output) before the 1.0 stability freeze.
Changed
- BREAKING — usage errors now exit 64, not 70. Malformed invocations
(unknown flag, missing argument, invalid flag value) are oclif parse errors;
they previously fell through to exit 70 (EX_SOFTWARE, "internal bug") and
now correctly exit 64 (EX_USAGE) per the sysexits ladder. Exit 70 is now
reserved for genuinely unexpected internal failures. Scripts branching on the
exit code should treat 64 as "fix your command line". - BREAKING — piped errors emit JSON. Error output now follows the same
format resolution as success output: human (table) in a TTY, but a JSON
envelope ({ error, message, exitCode, … }) on stderr whenever output is a
machine format —--output json|yaml|csv, a non-tableprofile
default_output, or when stdout is piped. Previously a piped run emitted
JSON on success but human text on failure; a non-interactive consumer now
always gets a parseable error. Pass--output tableto force human errors.
Fixed
- OAuth: a token that expired during a 429 backoff is now refreshed. The
refresh was gated on the first attempt, so a rate-limit retry consumed the
window and the later 401 failed unrecoverably; the refresh is now a single
free round independent of the retry budget (works under--no-retrytoo). - CSV/
--field: a field definition missingfield_nameno longer throws when
matching by hash code (null-guarded, matching the other resolvers). - Error reporting no longer crashes if reading the profile's
default_output
throws (e.g. a corrupt config) while formatting another error.
v0.16.0
Added
- Idempotent writes — match-or-create that never guesses:
person upsert,org upsert,deal upsert— match a record by--by
(a built-in key — person email/name/phone, org name, deal title — or a
searchable custom field), then create it if absent or PATCH only the
changed fields if exactly one matches. More than one match refuses with
exit 65 rather than writing the wrong record: searchexact_matchis not
a unique key, so every candidate is re-verified client-side before the
count decides.--dry-runpreviews the action; table prints a one-line
summary,--output jsonemits the full action result.person import --upsert --match-on <field>and the same onorg import—
the CSV equivalent: each row is matched on its--match-onvalue, then
created or PATCHed. Per-row failures (ambiguous matches, empty match
values) are collected without aborting the batch and reported as
created / updated / unchangedcounts; a batch whose failures are all
data-validation errors exits 65, otherwise 1.--dry-runlooks up and
reports the counts without writing.
Changed
diffBody(upsert) compares emails/phones by their value set
(case-insensitive, ignoring theprimary/labelflags the API echoes) and
treatslabel_idsand multi-option custom fields order-insensitively, so
re-running an unchanged upsert issues no PATCH.- CSV import now rejects duplicate column headers (exit 65) instead of silently
keeping only the last cell.
v0.15.0
Added
- Continuous automation primitives:
watch— anomaly poller built on the hygiene checks: emits only the
findings that are NEW since the last run (a per-profile state),
and exits 8 when new findings arm the gate (default: must-severity)
so cron can branch —pdcli watch || notify.--peekreads without
advancing state;--checksnarrows the checks;--severity must|should|all
arms the gate. A finding that clears and later re-trips fires again.sync warehouse— incremental NDJSON export for a data warehouse.
Appends only records changed since the last run, per entity, with
independent high-water marks inmanifest.json; the first run seeds a
full export and--fullrebuilds from scratch.--sinceoverrides the
start. Pull-based CDC captures creates/updates only — hard deletes are
not surfaced (reconcile against a periodic fullbackup).
Changed
- Hoisted the shared
--sinceparser (resolveSince) into
src/lib/period.jssochangesandsync warehouseshare one
implementation. No behavior change.
v0.14.0
Added
- Agent & CI primitives — built for cron jobs and AI agents:
changes— incremental change feed across deals, persons,
organizations, activities and products with a self-advancing
per-profile watermark: each run resumes where the last left off and
advances to the newest change, tagging every rowcreatedvs
updated.--since <ts|Nd>sets/overrides the start;--peekreads
without advancing. Turnkey for*/5 * * * * pdcli changes | ….deal context <id>— one-call denormalized bundle (deal + person +
org + activities + notes + products + participants) with custom
fields resolved to names and derived risk flags (missing contact,
stale, past close, no next activity). It does the joins v2's API
won't (there is norelated_objects).--no-activities/-notes/ -products/-participantstrim the cost.backup diff <A> <B>— field-level diff between two backup snapshots,
computed entirely locally with zero API calls. Reports added /
removed / modified records and per-field changes, resolving custom
field names from each snapshot's own captured schema (--rawkeeps
hash keys).backupis now a topic.
v0.13.0
Added
- The "Monday packet" — forecasting, rep performance, and a one-command
digest that fans a single fetch into the existing analytics:metrics forecast— open pipeline bucketed by close-month into
commit / best-case / weighted views. Commit counts deals whose
effective win-probability (--commit-threshold, default 70) clears
the bar, at full value; weighted uses
deal probability ?? stage default ?? 100. Values are segregated
per currency (never cross-summed), with a per-currency totals line.rep scorecard— per-owner win rate, cycle, velocity and deal
hygiene (stale / past-close / no-date / no-contact), with owner names
resolved from the users roster. Account-wide by default; narrow with
--pipeline,--owner,--period.digest— the whole packet from ONE pipeline-scoped fetch: velocity,
pipeline health, coverage, funnel, forecast and must-fix hygiene.
Cheap by default;--deepmines per-deal changelogs to add aging,
slippage and stage-skips.--format md|html(optionally--out <file>) renders a shareable artifact for cron → Slack/email; a
missing revenue goal degrades to no coverage section rather than
failing.
Changed
- Extracted the duplicated
>1-pipelineguard and the Goals-API
resolution recipe into sharedsrc/lib/pipelines.jsand
src/lib/goals.js; the seven pipeline-scoped commands now share one
tested implementation. No behavior change.
Fixed
metrics coverageno longer blind-sums open value across currencies
(a meaningless cross-currency ratio against a single-currency quota).
A multi-currency open pipeline now errors with exit 64; scope it with
the new--currency <code>flag.digestapplies the same guard by
omitting its coverage section (with a stderr note) on mixed currencies.
v0.12.0
Added
- Time-intelligence analytics, each mining real stage/close-date
history from per-deal changelogs (20 tokens/deal; a >100-deal
warning prints once):metrics aging— days-in-current-stage bucketed
(--buckets, default 30,60,90) with per-stage p50/p90 dwell
baselines; flags deals past their stage's p90.metrics slippage—expected_close_datepushes: count, net days
slipped, original→current close date, serial offenders
(--min-pushes).metrics conversion-matrix— full stage-to-stage transition counts
including backward edges and won/lost terminals (the flow graph the
funnel collapses).audit stage-skips— forward gate-skips and backward regressions
with actor attribution (informational; always exits 0).
v0.11.0
Added
- Custom-field lifecycle:
field create/update/deleteplus
field option add/removefor enum/set fields (v2 Fields write API).
Deleting a field or option confirms with a default of No — record
data stored in it is lost.field list/getnow also coverlead
andnotefields. - Relations:
deal participant list/add/remove(the buying
committee),deal/person/org follower list/add/remove, and
org relationship list/add/remove(parent/related hierarchies). task list/get/create/update/delete— v2-native project tasks
(requires the Projects add-on; accounts without it get a clean
configuration error).- Conversions:
lead convert <id>anddeal convert <id>(async jobs
with--waitpolling and--timeout-secs). Converting a deal
deletes the source deal on success and confirms with a default of
No. deal summary— server-side per-currency totals, weighted values,
and counts in a single call (--status/--pipeline/--stage/--filter).- Scoped search: a single
--item-typesvalue routessearchto the
per-entity v2 endpoints; the deal scope adds
--status/--person/--orgfilters. deal list --archivedandproject list --archived.activity type list(thekey_stringcolumn feeds
activity --type),file update/delete,filter delete, and
note comment list/add/update/delete.
Changed
- Every destructive command added in this release confirms with a
default of No — pressing Enter aborts.
v0.10.0
Added
deal product list/add/update/remove— line items on deals at last.
Cursor-paginated listing with sort options;addtakes--product,
--price, and--quantity(default 1) plus discount/tax/comments;
amounts are validated client-side (exit 64 on garbage). No
--durationflag: product durations were retired by Pipedrive in
2024 in favor of billing frequencies.user listanduser find <term>(--by-emailfor exact lookups) —
every--owner <id>flag in the CLI is finally usable without
copying ids out of the web app.- List power-params on
deal,person,org,activity, and
productlists:--filter <saved-filter-id>,--ids(max 100),
--sort-by/--sort-direction,--updated-since/--updated-until
where the endpoint supports them.--idsand--filterare mutually
exclusive — the API silently ignores ids when a filter is present. deal history <id>— field-change audit trail, newest-first, with
--fieldfiltering and--resolve-fieldsrendering custom-field
names and option labels.webhook create --event-objectaccepts the six v2 object types added
for projects and line items (project, task, board, phase,
deal_product, deal_installment).- Daily-budget awareness: a 429 with
x-daily-ratelimit-token-remaining: 0
fails fast with a clear message instead of backing off blindly;
--verboselogs the remaining daily token budget on every request.
Changed
- List page size raised from 100 to 500 rows per request (the v2
maximum) — large pulls use 5x fewer requests and rate-limit tokens.
Fixed
activity list --typenever worked against the live API (the v2
endpoint rejects atypequery parameter); the flag now filters
client-side.
v0.9.0
Changed
- BREAKING:
--jqnow receives single records as the bare object
instead of a one-element array.pdcli deal get 1 --jq '.id'works
directly; scripts using the old--jq '.[0].id'form must drop the
.[0]. List output is unchanged (still an array). --resolve-fieldsnow also applies to the core list commands (deal,
person,org,activity,product) in json/yaml/csv output — one
field-definitions fetch covers the whole list. (Supersedes the 0.8.0
note that scoped the flag to single-record gets.)
Fixed
- File uploads, downloads, and form posts now go through the same
transport pipeline as every other request: 429 backoff honoring
x-ratelimit-reset/Retry-After,--no-retry, the 429-to-403
escalation hard stop, 5xx retry, and automatic OAuth token refresh.
Note: this also means transient 5xx during an upload is retried —
pass--no-retryif duplicate-creation on retry is a concern. - Alias mutations take an advisory lock, so concurrent pdcli processes
no longer overwrite each other's alias changes (last-write-wins data
loss). Contention exits 75; an unwritable config directory reports a
clear configuration error (exit 78). - A failing command can no longer pass its own happy-path test suite —
the test harness surfaces command errors instead of swallowing them.