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
6 changes: 6 additions & 0 deletions website/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,13 @@ export default defineConfig({
{ label: 'Installation', slug: 'start/installation' },
{ label: 'Distribution (all channels)', slug: 'start/distribution' },
{ label: 'Quickstart', slug: 'start/quickstart' },
],
},
{
label: 'AI agents',
items: [
{ label: 'Quickstart for AI agents', slug: 'start/agents' },
{ label: 'MCP server', slug: 'guides/mcp' },
],
},
{
Expand Down
7 changes: 6 additions & 1 deletion website/src/components/Home.astro
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const features: { t: string; b: string; d: string }[] = [
},
{
t: 'Built for AI agents',
b: 'Self-documenting --help on every command and clean JSON output make pdcli easy for agents to discover and drive — a ready-made tool surface for your LLM workflows.',
b: 'Runs as an MCP server — pdcli mcp serve — read-only by default, giving Claude and other hosts a safe, typed tool surface. For shell-driven agents there is also self-documenting --help, clean JSON, and deterministic exit codes.',
d: '<rect x="4" y="6" width="16" height="12" rx="2"/><path d="M9 2v4M15 2v4M9 12h0M15 12h0M9.5 15.5a3 3 0 005 0"/>',
},
{
Expand Down Expand Up @@ -456,6 +456,11 @@ const showcaseTerm = `<span class="dim">$</span> <span class="g">pdcli</span> <s
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 mcp serve',
out: `<span class="ok"> ✓</span> pdcli MCP server ready — <span class="n">45</span> tools <span class="dim">(read-only)</span>
<span class="dim"> connect Claude · --allow-writes to expose create/update</span>`,
},
];

const cmdEl = document.getElementById('clr-aterm-cmd');
Expand Down
6 changes: 3 additions & 3 deletions website/src/content/docs/automation/github-actions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ jobs:
- uses: actions/setup-node@v5
with:
node-version: 20
- run: npm install -g @wavyx/pdcli@0.20.0
- run: npm install -g @wavyx/pdcli@0.22.0

# Preflight: config + keychain checks, zero network egress. Exits 78 on a
# config problem, failing the job before it spends any API budget.
Expand All @@ -73,7 +73,7 @@ vars.
jobs:
deals:
runs-on: ubuntu-latest
container: ghcr.io/wavyx/pdcli:0.20.0
container: ghcr.io/wavyx/pdcli:0.22.0
env:
PDCLI_COMPANY_DOMAIN: acme
PDCLI_API_TOKEN: ${{ secrets.PIPEDRIVE_API_TOKEN }}
Expand All @@ -82,7 +82,7 @@ jobs:
- run: pdcli deal list --status open --output json --jq '.[].id'
```

Pin the tag (`:0.20.0`) rather than `latest` for reproducible runs. See
Pin the tag (`:0.22.0`) rather than `latest` for reproducible runs. See
[Distribution](/pdcli/start/distribution/) for the image details.

## Gate on the exit code
Expand Down
9 changes: 8 additions & 1 deletion website/src/content/docs/concepts/api-model.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,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** |
| leads, notes, files, filters, webhooks, goals, users | **v1** |
| 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
pdcli never builds core CRUD on v1. The v1-only topics above have no v2 equivalent yet and are
Expand Down Expand Up @@ -62,6 +62,13 @@ seats, resets at midnight server time). On a `429`, pdcli reads `x-ratelimit-res
(falling back to `Retry-After`, then a 2s default) to decide how long to wait before
retrying.

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
too low), so a batch can check headroom first — `pdcli quota --min 5000 && pdcli sync
warehouse`. The budget is **company-wide** (shared across every integration on the
account), so treat the reading as a hint, not a reservation. (This is unrelated to the
sales-goal "quota" in the [analytics guide](/pdcli/guides/analytics/).)

### Backoff and the hard stop

On a `429`, pdcli waits for the window indicated by `x-ratelimit-reset` (falling back to
Expand Down
14 changes: 11 additions & 3 deletions website/src/content/docs/guides/api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,17 @@ When you hit v2 endpoints directly, remember the v2 conventions:
- **Bodies are JSON only.** v2 doesn't take form-encoded data.
- Lists use **cursor** pagination (`cursor`/`limit`, `limit` max 500); v1 lists use
**offset** pagination (`start`/`limit`). See
[How pdcli talks to Pipedrive](/pdcli/concepts/api-model/). `pdcli api` makes a single
request — it does **not** auto-paginate — so pass `cursor`/`start` yourself for more
pages.
[How pdcli talks to Pipedrive](/pdcli/concepts/api-model/). By default `pdcli api`
makes a single request; add **`--paginate`** (alias `--all`) to follow every page and
print one concatenated array. It works on `GET` only and infers the pager from the
path (`/api/v1/` offset vs `/api/v2/` cursor); `--limit` caps the total. Note the shape
shift for `--jq`: with `--paginate` you filter the bare item array (`.[]`), without it
the full envelope (`.data[]`).

```bash
# every open deal across all pages, as one array
pdcli api GET "/api/v2/deals?status=open" --paginate --jq '.[].id'
```

## When to reach for it

Expand Down
18 changes: 18 additions & 0 deletions website/src/content/docs/guides/bulk.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -221,3 +221,21 @@ is indexed. For repeatable sync, key on a stable external ID and let the index s
between runs; a periodic `backup diff` or a search will surface any duplicates that slip
through.
:::

### Just checking: `lookup`

When a script wants to **branch on existence itself** rather than have pdcli decide, use
`lookup` — a read-only exact-match resolver. It exits `0` with the matching row(s) when
found and **`3`** when nothing matches, so create-vs-update becomes a plain shell branch:

```bash
if pdcli lookup deal --field "PO Number" --value PO-1234 --first --jq .id >/tmp/id; then
pdcli deal update "$(cat /tmp/id)" --body '{"value":5000}' # found → update
else
pdcli deal create --body '{"title":"PO-1234","value":5000}' # exit 3 → create
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.
Comment on lines +231 to +241

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 Shell else silently swallows lookup errors as "not found"

The if/else branch treats every non-zero exit as "record missing, go create it." If pdcli lookup fails for any other reason — network outage (exit 69), rate-limit (exit 75), auth error (exit 77), etc. — the script falls straight into pdcli deal create, potentially creating a duplicate instead of surfacing the failure. The comment # exit 3 → create makes this look intentional but the guard is too broad. The same pattern appears in start/agents.mdx where pdcli lookup person … || pdcli person create … is described as a "clean create-if-missing pattern" — the || operator has the same flaw, running the create on any non-zero exit. Both examples should either check $? explicitly for exit 3 or call out that real callers should add that guard before using this pattern in production.

Fix in Claude Code

130 changes: 130 additions & 0 deletions website/src/content/docs/guides/mcp.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
---
title: MCP server
description: Expose pdcli to Claude and other AI hosts as a Model Context Protocol server — read-only by default, with writes gated behind an explicit flag.
---

`pdcli mcp serve` runs pdcli as a [Model Context Protocol](https://modelcontextprotocol.io)
(MCP) server over stdio, so an MCP host — Claude Desktop, Claude Code, or any other
client — can drive your Pipedrive account as a set of typed tools.

It is the same CLI you already trust, wrapped in a tool surface. Every tool call
re-invokes `pdcli` itself as a child process under your auth profile, so the tools
honor the same host-lock, the same keychain credentials, and the same rate-limit
handling as the commands you run by hand. Nothing new touches your token.

## Safe by default

The design goal is that connecting the server can't hurt you:

- **Read-only out of the box.** The default tool set is a curated **45 read-only
tools** — core entity list/get, `search`, deal intelligence
(`deal context`/`history`/`summary`), every metric, `funnel`, `digest`, `audit`,
`rep scorecard`, `user me`, and more. No tool that writes is even registered
unless you ask for it.
- **Writes are opt-in.** Every command is classified `read`, `write`, or
`destructive`. Writes and destructive operations are exposed **only** under
`--allow-writes`.
- **Dangerous surfaces are excluded entirely** — they never appear as tools, even
with `--all-tools`. That includes the raw `api` escape hatch, `auth:*`,
`config:*`, `alias:*`, `profile:*` (all of which manage your local machine, not
CRM data), `doctor`, `watch`, `changes` (advances a stateful watermark),
`sync warehouse`, `backup` (but `backup diff`, a zero-API local read, is kept),
`webhook listen`, and `mcp serve` itself.

## Quick start

Register the server with Claude Code in one line:

```bash
claude mcp add pipedrive -- pdcli mcp serve
```

Or add it to an `.mcp.json` (Claude Code project config, Claude Desktop, or any
MCP host) by hand:

```json
{
"mcpServers": {
"pipedrive": {
"command": "pdcli",
"args": ["mcp", "serve"]
}
}
}
```

That's the read-only server. To let the host create and update records, add the
flag to the `args` — `["mcp", "serve", "--allow-writes"]` — or to the
`claude mcp add` command after the `--`.

Authenticate pdcli once (`pdcli auth login`, or an env-var token) before starting a
host; the server itself needs no credentials — its child processes resolve them.

## The tool model

Tool selection happens in two stages: **scope**, then the **write gate**.

**Scope** decides which commands are candidates:

| Flag | Tools exposed |
| ----------------------- | ------------------------------------------------------------------- |
| _(none)_ | the curated core set — 45 read tools, small enough not to overwhelm a host |
| `--topics deal,person` | every command under those topics, instead of the curated set |
| `--all-tools` | every non-excluded command |

**The write gate** then filters that scope: reads are always exposed; `write` and
`destructive` tools appear only with `--allow-writes`. With the default scope,
`--allow-writes` adds ~14 core write tools — the create/update commands on core
entities plus the idempotent `upsert`s (`person`/`org`/`deal upsert`).

```bash
pdcli mcp serve # curated, read-only (default)
pdcli mcp serve --allow-writes # curated reads + core writes
pdcli mcp serve --topics deal,person,org # everything under those topics (reads)
pdcli mcp serve --all-tools --allow-writes # the whole CLI as tools
```

One command is deliberately kept out of the curated default even though it's a
read: `lookup`. Its exit-3 "no match" result is a normal branch for a script, but
an MCP host reads a non-zero exit as a **tool error** — noisy for an agent. It
stays reachable via `--all-tools`; for match-or-branch logic inside an agent,
prefer `search` or the `upsert` tools.

## Custom-field names, not hash keys

Every tool call forces `--resolve-fields`, so the host sees human-readable custom
field **names** (and option labels) instead of Pipedrive's 40-character hash keys —
on both input and output. An agent can ask for `"Renewal date"`, not
`"a1b2c3…"`. Each call also forces `--output=json` (so the parent's stdio channel
stays clean) and `--yes` (no interactive confirm can block a headless call).

## Timeouts and limits

Each tool call is a child process with guardrails:

- `--tool-timeout <seconds>` (default **120**) bounds how long a call may run
before its child is terminated (SIGTERM, escalating to SIGKILL).
- Output is capped at **16&nbsp;MB** across stdout and stderr combined; a runaway
call is killed rather than flooding the host.

Both a timeout and an overflow surface to the host as a tool error with a
self-describing message, never as a silent success.

## MCP vs. the CLI over bash

If your agent already has a shell — a terminal agent like Claude Code or Codex —
you may not need MCP at all. pdcli is built to be driven from bash directly:
`--output json`, `--jq`, self-describing `--help`, and
[deterministic exit codes](/pdcli/start/agents/) give a shell-capable agent
everything it needs, with the full command set and no tool-registration step.

Reach for `mcp serve` when the host **can't** run shell commands — Claude Desktop
and other GUI/chat hosts that speak MCP but have no terminal. There, the MCP server
turns pdcli into first-class typed tools with the same safety posture you'd get on
the command line.

:::tip
Start read-only. Add `--allow-writes` only once you trust the workflow, and prefer
scoping with `--topics` over `--all-tools` so the host sees a focused, relevant set
of tools rather than the entire CLI.
:::
27 changes: 26 additions & 1 deletion website/src/content/docs/start/agents.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,23 @@ pdcli is built to be driven by agents (Claude Code, Codex, CI bots). It is
self-describing, emits machine-readable JSON, and uses deterministic exit codes.
This page covers the conventions an agent needs.

## Expose pdcli over MCP

If your host speaks the [Model Context Protocol](https://modelcontextprotocol.io) —
Claude Desktop, Claude Code, and other GUI or chat clients — pdcli can register as
an MCP server and hand it a set of typed tools, read-only by default. Register it
with Claude Code in one line:

```bash
claude mcp add pipedrive -- pdcli mcp serve
```

Everything below still applies: the MCP tools re-invoke this same CLI under your
auth profile, with the same host-lock and exit-code semantics. If your agent has a
shell, driving pdcli from bash (as this page describes) is often simpler than MCP.
See the [MCP server guide](/pdcli/guides/mcp/) for the full tool model, the write
gate, and scoping flags.

## Authenticate with environment variables

Avoid the interactive `auth login` flow. Set credentials in the environment so no
Expand Down Expand Up @@ -44,6 +61,8 @@ script can react to the failure class without parsing text:
| ---- | ------------------------------------------------------------------------------------------------- |
| 0 | Success |
| 1 | Generic error |
| 3 | `lookup`: no record matched — not a failure; branch to create/upsert |
| 8 | `watch`: new findings since the last run — the trigger for `pdcli watch \|\| notify` |
| 64 | Usage / bad flags or arguments (unknown flag, missing arg, invalid value, missing CSV column) |
| 65 | Bad input data (API 400 / 422) |
| 69 | Service unavailable (API 5xx, or unreachable) |
Expand Down Expand Up @@ -90,7 +109,8 @@ performance, `digest` for the whole Monday packet in one fetch (`--format md|htm
a cron → Slack/email artifact), `changes` for an incremental cross-entity change feed with a
self-advancing watermark (no receiver to host, unlike webhooks), `deal context <id>` for a
one-call denormalized, prompt-ready bundle (deal + person + org + activities + notes +
products + flags), `backup diff` for a zero-API field-level diff of two snapshots, `watch` for
products + flags; add `--mail` to fold in a mail summary — opt-in, since it needs the
`mail:read` scope and email sync), `backup diff` for a zero-API field-level diff of two snapshots, `watch` for
an exit-code-gated anomaly poller that fires only on findings new since the last run
(`pdcli watch || notify`), `sync warehouse` for an incremental NDJSON export with per-entity
high-water marks, and `--updated-since` on the list commands for incremental polling.
Expand All @@ -102,6 +122,11 @@ writes the wrong one. `person import`/`org import --upsert --match-on <field>` a
match-or-create per CSV row, reporting created/updated/unchanged counts. Pair upsert with an
external key (a custom field carrying your system's ID) for clean, repeatable sync.

When you'd rather branch on existence yourself before deciding to create or update, `lookup`
is the lighter read-only probe: it finds a record by a field value and exits **0 when found**
(printing it) or **3 when nothing matched**, so `pdcli lookup person --field email --value … ||
pdcli person create …` is a clean create-if-missing pattern that never mutates on its own.

## The host-locked `api` escape hatch

When no dedicated command exists, call any endpoint directly. The request is
Expand Down
14 changes: 7 additions & 7 deletions website/src/content/docs/start/distribution.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ pdcli version
Pin a version in CI so a background release can't change behaviour mid-pipeline:

```bash
npm install -g @wavyx/pdcli@0.20.0
npm install -g @wavyx/pdcli@0.22.0
```

Auth: keychain by default (`pdcli auth login`), or env vars for scripts.
Expand All @@ -68,7 +68,7 @@ step where you'd rather not add an install line.

```bash
npx @wavyx/pdcli deal list
npx @wavyx/pdcli@0.20.0 deal list # pinned
npx @wavyx/pdcli@0.22.0 deal list # pinned
```

The global install is faster for daily use because npx resolves and caches the package
Expand All @@ -91,7 +91,7 @@ token never appears in the command or in shell history. Pin the tag in CI:

```bash
docker run --rm -e PDCLI_API_TOKEN -e PDCLI_COMPANY_DOMAIN \
ghcr.io/wavyx/pdcli:0.20.0 deal list --output json
ghcr.io/wavyx/pdcli:0.22.0 deal list --output json
```

The entrypoint is `pdcli`, so everything after the image name is passed straight
Expand Down Expand Up @@ -141,23 +141,23 @@ Five targets are published per release:
| `win32-x64` | Windows, 64-bit |

Each tarball filename embeds the build's commit SHA (for example
`pdcli-v0.20.0-1a2b3c4-linux-x64.tar.gz`), so there is no fixed, predictable
`pdcli-v0.22.0-1a2b3c4-linux-x64.tar.gz`), so there is no fixed, predictable
download URL. Grab the exact asset from the
[Releases page](https://github.com/wavyx/pdcli/releases), or let the
[`gh` CLI](https://cli.github.com/) resolve it for you:

```bash
# See the tarballs attached to a release:
gh release view v0.20.0 --json assets --jq '.assets[].name'
gh release view v0.22.0 --json assets --jq '.assets[].name'

# Download the one for your target by pattern (no need to know the SHA):
gh release download v0.20.0 --pattern '*linux-x64.tar.gz'
gh release download v0.22.0 --pattern '*linux-x64.tar.gz'
```

Then unpack it and put the `bin` directory on your `PATH`:

```bash
tar -xzf pdcli-v0.20.0-*-linux-x64.tar.gz
tar -xzf pdcli-v0.22.0-*-linux-x64.tar.gz
./pdcli/bin/pdcli version
```

Expand Down
Loading