Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: Full command reference for the pdcli command-line interface.

<!-- AUTO-GENERATED from the oclif manifest by scripts/gen-commands.mjs — do not edit by hand. -->

Reference for `pdcli` v0.22.0 (154 commands). Every command also accepts the global flags `--output table|json|yaml|csv`, `--profile`, `--no-color`, `--verbose`, `--no-retry`, `--timeout`, and `--limit`.
Reference for `pdcli` v0.22.0 (154 commands). Every command also accepts the global flags `--output table|json|yaml|csv`, `--jq`, `--fields`, `--resolve-fields`, `--profile`, `--no-color`, `--verbose`, `--no-retry`, `--timeout`, and `--limit`.

## Top-level

Expand Down
29 changes: 25 additions & 4 deletions scripts/gen-commands.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,28 @@ export function groupByTopic(manifest) {
const nonGlobalFlags = (c) =>
Object.entries(c.flags || {}).filter(([, f]) => f.helpGroup !== 'GLOBAL')

// The global flags every command inherits (BaseCommand.baseFlags, tagged
// helpGroup:'GLOBAL'). Derived from the manifest so the reference intro can't
// drift from the code — no hand-kept list to forget `--resolve-fields` again.
// `withOptions` renders `--output`'s value enum inline (GitHub-facing style).
const globalFlagTokens = (manifest, { withOptions = false } = {}) => {
const cmd = Object.values(manifest.commands).find((c) =>
Object.values(c.flags || {}).some((f) => f.helpGroup === 'GLOBAL'),
)
return Object.entries(cmd?.flags || {})
.filter(([, f]) => f.helpGroup === 'GLOBAL')
.map(([name, f]) => {
const opts = withOptions && f.options ? ` ${f.options.join('|')}` : ''
return `\`--${name}${opts}\``
})
Comment on lines +27 to +36

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 First-command heuristic may omit flags that don't appear on every command

globalFlagTokens picks the first command object that has any GLOBAL flag, then reads all GLOBAL flags from that one command. If any global flag (e.g. --limit) is conditionally absent from some commands' manifests, the first matching command might return an incomplete set. The approach is sound as long as every command with GLOBAL flags carries the full set (i.e. BaseCommand.baseFlags is inherited uniformly), but a command-specific override that drops a base flag would silently remove it from the generated intro without a warning.

Fix in Claude Code

}

// Join with an Oxford "and" before the last item: "a, b, and c".
const andList = (items) =>
items.length > 1
? `${items.slice(0, -1).join(', ')}, and ${items.at(-1)}`
: items.join('')

const argString = (c) =>
Object.entries(c.args || {})
.map(([name, a]) => (a.required ? ` <${name}>` : ` [${name}]`))
Expand Down Expand Up @@ -48,7 +70,7 @@ description: Full command reference for the pdcli command-line interface.

<!-- AUTO-GENERATED from the oclif manifest by scripts/gen-commands.mjs — do not edit by hand. -->

Reference for \`${bin}\` v${manifest.version} (${commands.length} commands). Every command also accepts the global flags \`--output table|json|yaml|csv\`, \`--profile\`, \`--no-color\`, \`--verbose\`, \`--no-retry\`, \`--timeout\`, and \`--limit\`.
Reference for \`${bin}\` v${manifest.version} (${commands.length} commands). Every command also accepts the global flags ${andList(globalFlagTokens(manifest, { withOptions: true }))}.

`
for (const topic of Object.keys(byTopic).sort()) {
Expand Down Expand Up @@ -98,9 +120,8 @@ description: Every ${bin} command, flag, and example — generated from the CLI
{/* AUTO-GENERATED from the oclif manifest by scripts/gen-commands.mjs — do not edit by hand. */}

All ${commands.length} commands in \`${bin}\` v${manifest.version}. Every command also
accepts the [global flags](/pdcli/reference/config/) \`--output\`, \`--jq\`,
\`--fields\`, \`--profile\`, \`--limit\`, \`--no-color\`, \`--verbose\`,
\`--no-retry\`, and \`--timeout\`. Run \`${bin} <command> --help\` for the live version.
accepts the [global flags](/pdcli/reference/config/) ${andList(globalFlagTokens(manifest))}.
Run \`${bin} <command> --help\` for the live version.

`
const topics = Object.keys(byTopic).sort()
Expand Down
19 changes: 15 additions & 4 deletions website/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// @ts-check
import { defineConfig } from 'astro/config'
import { unified } from '@astrojs/markdown-remark'
import starlight from '@astrojs/starlight'
import starlightLlmsTxt from 'starlight-llms-txt'

Expand All @@ -9,10 +10,20 @@ export default defineConfig({
site: 'https://wavyx.github.io',
base: '/pdcli',

// GFM tables in .md/.mdx (the MDX pipeline inherits these classic options,
// unlike a custom `processor`); smartypants off so code examples keep
// literal `--flags` and straight quotes.
markdown: { gfm: true, smartypants: false },
// GFM tables in .md/.mdx; smartypants OFF so prose keeps literal `--flags`,
// `--`, and straight quotes (Astro's smart punctuation would turn `--` into
// an en/em dash). Astro 6 deprecated the top-level `markdown.gfm` /
// `markdown.smartypants` booleans — configure them on the unified processor
// instead (removed in a future major otherwise).
//
// NOTE: the build still prints ONE `markdown.gfm`/`smartypants` deprecation
// line during `/llms-*.txt` generation. That is NOT from this config: the
// `starlight-llms-txt` plugin renders via the experimental Astro Container,
// which calls `validateConfig(ASTRO_CONFIG_DEFAULTS, …)` (astro/dist/container/
// index.js), and Astro's own defaults object still carries explicit
// `gfm`/`smartypants` keys — so it trips its own deprecation check. It is
// unfixable from here; it clears when Astro drops those keys from the defaults.
markdown: { processor: unified({ gfm: true, smartypants: false }) },
Comment on lines 3 to +26

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 markdown.processor requires Astro ≥ 6.4.0, but package.json declares ^6.3.1

markdown.processor and the unified export from @astrojs/markdown-remark were introduced in Astro 6.4.0. The declared peer range "astro": "^6.3.1" permits 6.3.x; if a lockfile (or a fresh npm install that resolves to 6.3.x) is in play, Astro silently ignores the unknown processor key and smartypants remains active — converting -- in prose to an en-dash and breaking all the literal flag examples this PR is trying to protect.

Additionally, @astrojs/markdown-remark is not listed as a direct dependency in package.json (only as a transitive dep of astro), so the import is fragile across package-manager and version changes.

Bump the lower bound in package.json to "astro": "^6.4.0" and add "@astrojs/markdown-remark": "..." as an explicit dependency to make both requirements auditable.

Fix in Claude Code


integrations: [
starlight({
Expand Down
23 changes: 9 additions & 14 deletions website/src/components/Home.astro
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,9 @@ const showcaseTerm = `<span class="dim">$</span> <span class="g">pdcli</span> <s
4774 Northwind expansion <span class="m">€ 8,500</span> Proposal
4710 Globex pilot <span class="m">€ 4,200</span> Qualified
4699 Initech seats <span class="m">€ 2,750</span> Contact
<span class="dim"> 4 open deals · €27,450 open value</span>

<span class="dim">$</span> <span class="g">pdcli</span> <span class="c">deal update</span> 4821 <span class="f">--status</span> won
<span class="ok">✓</span> Acme renewal → <span class="badge">Won</span> · activity logged
<span class="ok">✓</span> Updated deal 4821 (Acme renewal → <span class="badge">Won</span>)
<span class="dim">$</span> <span class="cur">▋</span>`;
---

Expand Down Expand Up @@ -437,24 +436,20 @@ const showcaseTerm = `<span class="dim">$</span> <span class="g">pdcli</span> <s
// ── Animated hero terminal: types three real pdcli commands in a loop. ──
const SCRIPT = [
{
cmd: 'pdcli pipeline health',
out: `<span class="dim"> ┌ SALES PIPELINE ───────────────── Q2 ┐</span>
Qualified <span class="n">18</span> deals <span class="m">€142,000</span>
Contact <span class="n">11</span> deals <span class="m">€ 98,500</span>
Proposal <span class="n">7</span> deals <span class="m">€ 76,200</span>
Negotiation <span class="n">4</span> deals <span class="m">€ 51,000</span>
<span class="dim"> ─────────────────────────────────────</span>
weighted forecast <span class="ok">€221,480</span>
<span class="dim"> win rate 32% · avg cycle 24d</span>`,
cmd: 'pdcli pipeline health --pipeline 1',
out: `<span class="dim"> STAGE OPEN VALUE STALE >14d NO NEXT STEP</span>
Qualified <span class="n">18</span> <span class="m">€142,000</span> <span class="warn">3</span> <span class="warn">5</span>
Proposal <span class="n">7</span> <span class="m">€ 76,200</span> <span class="warn">1</span> <span class="warn">2</span>
Negotiation <span class="n">4</span> <span class="m">€ 51,000</span> <span class="ok">0</span> <span class="warn">1</span>`,
},
{
cmd: "pdcli deal list --status open --jq '.[].id' | pdcli deal bulk-update --stage 5",
out: `<span class="ok"> ✓</span> 40 open deals moved → <span class="badge">Negotiation</span>`,
},
{
cmd: 'pdcli person import leads.csv --dry-run',
out: ` <span class="ok">+212</span> new <span class="n">~18</span> updates <span class="warn">!3</span> conflicts
<span class="dim"> dry run · nothing written · custom fields matched by name</span>`,
cmd: 'pdcli person import leads.csv --upsert --match-on email --dry-run',
out: ` <span class="ok">194</span> create · <span class="n">18</span> update · <span class="dim">nothing written</span>
<span class="dim"> dry run · custom fields matched by name</span>`,
},
{
cmd: 'pdcli mcp serve',
Expand Down
6 changes: 3 additions & 3 deletions website/src/content/docs/automation/exit-codes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ command shares the same ladder.
| `75` | Rate limited | `429` | Token budget exhausted. pdcli retries 429s with backoff; if they never clear it surfaces `75` (not `69`), and `--no-retry` surfaces the first one. Back off and retry after the reset window. |
| `77` | Auth / permission | `401`, `403` | Token is invalid, expired, or lacks scope — including a **failed OAuth refresh** (`invalid_grant`). Re-authenticate (`pdcli auth login`). Don't loop. A `403` after repeated `429`s is a rate-limit hard stop — wait for the reset, don't retry. |
| `78` | Config / account | `402`, missing domain/token, host-lock violation, no keychain | The CLI or account is misconfigured: no company domain, no keychain to write to, a `pdcli api` URL outside your host, a redirect off your host, or a `402` (plan lacks the feature). Fix config or the account — don't retry. |
| `8` | Findings present (`watch` only) | — | `pdcli watch` exits `8` when it surfaces new anomalies, so `pdcli watch \|\| notify` fires only on findings. Not part of the general ladder — specific to `watch`. |
| `8` | Findings present (`watch` only) | — | `pdcli watch` exits `8` when it surfaces new anomalies. Test for it explicitly (`pdcli watch; [ $? -eq 8 ] && notify`) — a bare `pdcli watch \|\| notify` also fires on any other failure, not just findings. Not part of the general ladder — specific to `watch`. |

The HTTP-status mapping lives in `src/lib/errors.js` (`exitCodeForStatus`): `400/422 → 65`,
`401/403 → 77`, `402 → 78`, `429 → 75`, `5xx → 69`. On top of that: an oclif parse error
Expand Down Expand Up @@ -56,7 +56,7 @@ human form; `--verbose` there adds the request path, status code, and `error_inf
## Branching on the code

```bash
pdcli deal get 42 --output json > deal.json
pdcli lookup deal --field "PO Number" --value PO-1234 --output json > deal.json
case $? in
0) echo "ok" ;;
3) echo "no match — create it" ;;
Expand All @@ -69,7 +69,7 @@ case $? in
esac
```

`pdcli` also prints `127` (via the command-not-found hook) when you invoke a command name
`pdcli` also exits `127` (via the command-not-found hook) when you invoke a command name
that doesn't exist.

See also: [Troubleshooting](/pdcli/reference/troubleshooting/) for what to do per failure,
Expand Down
7 changes: 4 additions & 3 deletions website/src/content/docs/automation/output.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,9 @@ produces.

## Pick columns with `--fields`

`--fields` limits a `table` or `csv` to the columns you name (by their data key, not
the header):
`--fields` limits the output to the columns you name (by their data key, not the
header). It projects `table` and `csv` columns, and also narrows each `json`/`yaml`
record to just those keys:

```bash
pdcli deal list --fields id,title,value
Expand Down Expand Up @@ -129,7 +130,7 @@ stage_id: 3
## Resolving custom fields in machine output

By default `json`, `yaml`, and `csv` keep custom-field values raw — hash keys and numeric
option IDs — so scripts have a stable shape to parse. On single-record `get` commands you can
option IDs — so scripts have a stable shape to parse. On `get` and core `list` commands you can
opt into readable names with `--resolve-fields`, which swaps hash keys for field names and
option IDs for labels. See [Custom fields](/pdcli/guides/custom-fields/) for the details.

Expand Down
8 changes: 3 additions & 5 deletions website/src/content/docs/concepts/api-model.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Pipedrive runs two API versions. `pdcli` routes each topic to the right one.

| Topic | API |
| ----- | --- |
| deals, persons, organizations, products, pipelines, stages, activities, projects, fields, search | **v2** |
| deals, persons, organizations, products, pipelines, stages, activities, projects, tasks, fields, search | **v2** |
| leads, notes, files, filters, webhooks, goals, users, mail | **v1** |

Core CRM is on **v2** because Pipedrive deprecated ~59 v1 core endpoints after 2025-12-31, so
Expand Down Expand Up @@ -55,12 +55,10 @@ cost depends on the verb:
| POST / PUT / PATCH | 10 |
| DELETE one | 6 |
| DELETE list | 10 |
| Search | 40 |
| Search | 20 |

There's a rolling **2-second burst window** plus a **daily budget** (scales with plan and
seats, resets at midnight server time). On a `429`, pdcli reads `x-ratelimit-reset`
(falling back to `Retry-After`, then a 2s default) to decide how long to wait before
retrying.
seats, resets at midnight server time).

Inspect the remaining budget with **`pdcli quota`** (alias `ratelimit`): it reports the
daily token budget remaining/limit and gates CI with `--min`/`--threshold` (exit `75` when
Expand Down
3 changes: 2 additions & 1 deletion website/src/content/docs/concepts/security.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ applies to file downloads and uploads.
`Authorization: Bearer` for OAuth) and are never echoed in logs, errors, or `--verbose`
output. `--verbose` shows the request path, status, and Pipedrive's `error_info`, but not
credentials.
- **User-Agent.** Every request identifies itself as `pdcli/<version>` (e.g. `pdcli/0.5.0`).
- **User-Agent.** Every request identifies itself as `pdcli/<version>`, where
`<version>` is the installed release.

For where each setting is stored and the full precedence order, see
[Config & environment](/pdcli/reference/config/).
18 changes: 13 additions & 5 deletions website/src/content/docs/guides/analytics.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ If a lever can't be computed (no decided deals, no won deals, or a zero cycle),

A one-call rollup of deal value, computed **server-side**: Pipedrive returns per-currency
totals, a probability-weighted total, and a deal count, so there's no list to page through.
It's the cheapest way to ask "what's my pipeline worth right now?"
It's one 40-token call regardless of deal count — the way to ask "what's my pipeline worth
right now?" without paging every deal.

```bash
pdcli deal summary
Expand Down Expand Up @@ -137,9 +138,14 @@ pipeline, inferred otherwise.

Days-in-current-stage for every open deal, bucketed, so you can see at a glance how much
value is going stale and where. For each stage it also mines the **completed** dwell
distribution (entry → next-entry across all deals) and reports per-stage **p50/p90**, then
flags how many open deals have now sat in the stage longer than its own p90 — the deals most
likely to be quietly dying.
distribution (entry → next-entry) and reports per-stage **p50/p90**, then flags how many open
deals have now sat in the stage longer than its own p90 — the deals most likely to be quietly
dying.

The dwell baseline is mined from the **open** deals it fetches, not the whole account, so it's
a "deals that are still around" sample: it skips the dwells of deals that already closed, which
can bias the p50/p90 lower than the true historical distribution. Read the p90 flag as a
relative signal, not a calibrated benchmark.

```bash
pdcli metrics aging --pipeline 1 --buckets 30,60,90
Expand Down Expand Up @@ -390,4 +396,6 @@ digest never fails for that reason).

`--format md|html` renders the packet as a shareable document — pipe it to Slack/email from
cron, or write it to a file with `--out`. These artifact formats are distinct from the global
`--output table|json|yaml|csv` (which stays for scripting the structured packet).
`--output table|json|yaml|csv` (which stays for scripting the structured packet), and
**mutually exclusive** with it: passing `--format` together with `--output`, `--jq`, or
`--fields` exits 64. Pick the document artifact or the machine format, not both.
10 changes: 8 additions & 2 deletions website/src/content/docs/guides/authentication.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ Non-interactively:
pdcli auth login --company acme --api-token <token>
```

Prefer the prompt or an environment variable over `--api-token` so the token stays
out of your shell history.
Prefer the interactive prompt over `--api-token` so the token stays out of your shell
history. If you'd rather not store anything in the keychain at all, skip `auth login`
entirely and pass credentials per run with the `PDCLI_*` [environment
variables](#ci-and-headless) — `auth login` does not read them.

## OAuth mode

Expand Down Expand Up @@ -92,6 +94,10 @@ Log out to clear stored credentials for the active profile (both modes):
pdcli auth logout
```

Any auth failure — missing or expired credentials, or a failed OAuth refresh — exits with
code `77`, so a script can branch on re-authentication deterministically. See
[Exit codes](/pdcli/automation/exit-codes/).

## CI and headless

For pipelines and agents, skip the keychain entirely and pass credentials via the
Expand Down
11 changes: 8 additions & 3 deletions website/src/content/docs/guides/backup.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,14 @@ pdcli sync warehouse --dir ./warehouse --full # rebuild from scratch

Each of the five incremental entities (deals, persons, organizations, activities, products)
appends to `<entity>.ndjson` (one JSON object per line) and advances its **own** high-water
mark in `manifest.json`. The watermark moves to the newest `update_time` seen **+ 1 second**
(the API's `updated_since` is inclusive, so this avoids re-emitting the boundary record), and
only after the append succeeds — an interrupted run replays rather than skips. `--since`
mark in `warehouse-manifest.json` (kept deliberately distinct from `backup`'s own
`manifest.json`, so a warehouse and a backup can share a directory). The watermark moves to
the newest `update_time` seen **exactly** — not +1s. Since `updated_since` is inclusive, the
boundary record is re-emitted on the next run: the feed is **at-least-once**, so dedupe
downstream by `(entity, id)`. Emitting a row twice is harmless; a +1s skip could silently
drop a record saved later in the same boundary second. The mark only advances after the
append succeeds — an interrupted run replays rather than skips — and it **never moves
backward**, so a one-off `--since` backfill can't rewind the maintained cursor. `--since`
overrides the start for every entity.

:::caution[Hard deletes are not captured]
Expand Down
12 changes: 10 additions & 2 deletions website/src/content/docs/guides/bulk.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -161,15 +161,18 @@ pdcli org upsert "D-42" --by "External ID" --field "Status=Active"
```

The match value is the positional argument; `--by` names the field to match on — a built-in
key or a **searchable** custom field (`address`, `varchar`, `text`, `double`, `monetary`,
`phone`):
key or a **searchable** custom field (`varchar`, `varchar_auto`, `text`, `double`, `phone`):

| Entity | Built-in `--by` keys |
| ------ | --------------------- |
| person | `email`, `name`, `phone` |
| org | `name` |
| deal | `title` |

`address` and `monetary` custom fields are deliberately **refused** with exit 64: v2 returns
those as objects (`{ value, currency }` / `{ value, … }`), so the scalar compare would never
match and every run would create a duplicate.

### Why it refuses instead of guessing

Pipedrive's `exact_match` search is **not** a unique-key lookup — it's case-insensitive, and
Expand Down Expand Up @@ -239,3 +242,8 @@ fi
It resolves a human field name to its custom-field key for you (deal, person, org, product,
lead). Matching is **case-sensitive**, and — like upsert — the search index is eventually
consistent, so a `lookup`-then-`create` loop can still double-create in fast pipelines.

Beyond the default exact match, `--match beginning|middle` does a prefix or substring search
(also case-sensitive; these modes need a value of **at least 2 characters**). Any mode returns
**at most 100 rows** — the per-entity `/search` endpoint's hard cap — so tighten the value if
you're brushing that ceiling.
13 changes: 10 additions & 3 deletions website/src/content/docs/guides/deal-products.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,16 @@ Pass at least one field flag, or it's exit 64 (`Nothing to update`). Any of `--p
combined; the server recomputes `sum` from the new values.

:::note
There is **no per-line duration or billing-cycle flag**. Pipedrive replaced the old
`duration`/`duration_unit` line-item fields with a product-level `billing_frequency` in 2024,
so recurring terms are configured on the product (`product`), not on each deal attachment.
pdcli has **no per-line billing-cycle flag yet** — but the v2 API does support it. `POST`/`PATCH
/api/v2/deals/{id}/products` accept per-line `billing_frequency`, `billing_frequency_cycles`,
and `billing_start_date` (a Growth-plan-and-above feature), so recurring terms can be set on the
individual attachment, not just the catalog product. Until pdcli exposes flags for them, reach
for the raw [`pdcli api`](/pdcli/guides/api/) escape hatch:

```bash
pdcli api PATCH /api/v2/deals/42/products/3 \
--body '{"billing_frequency":"monthly","billing_frequency_cycles":12}'
```
:::

## Remove a line item
Expand Down
Loading
Loading