# ZergDesk CLI

Standalone first-party workspace and CRM client, patterned after ZergScholar's
library CLI. Requires Node.js 22+, npm with registry access for the CSV parser,
and a reachable ZergDesk service. It runs independently of Nuxt, a database
connection, ZTC, and local vault access.

Public agent setup: https://zergdesk.com/agents.md. The portable skill is at
https://zergdesk.com/skills/zergdesk-cli/SKILL.md and the token API reference at
https://zergdesk.com/docs/api.md.

```sh
npm install -g https://zergdesk.com/downloads/cli/zergai-zergdesk-0.6.0.tgz
zergdesk --help
zdesk --version
```

The two installed names run the same client. Discover releases and checksums at
https://zergdesk.com/downloads/cli/latest.json; pin a version during sync work.
For this release, update by reinstalling its site-hosted tarball. The shared
`update` command uses the npm registry, where a package may not yet be published.
In a source checkout, `node cli/bin/zergdesk.mjs` runs the client directly.

## Authentication and profiles

CLI 0.6.0 adds project prospects, two-way source reconciliation, acquisition reports and evidence-backed introductions. Campaign cohorts, custom funnels, observed evidence and analyst reports remain available.
Read the [campaign guide](https://zergdesk.com/docs/campaign-analytics.md) for
measurement and reconciliation. Registered agents and human-approved queues
remain supported. Read the
[queue operating contract](https://zergdesk.com/docs/action-queues.md) before
using a channel agent. Save a distinct profile/token for each agent, then set
workspace and project defaults once with `config set`; commands reuse them.

```sh
zdesk agents register --data '{"name":"BDR","capabilities":["research"]}'
zdesk agents heartbeat AGENT_ID
zdesk queues list
zdesk actions create --data @request.json
zdesk actions submit ACTION_ID --expected-version 1
zdesk actions request-changes ACTION_ID --expected-version 2 --data '{"reason":"Use a specific company event."}'
zdesk actions update ACTION_ID --expected-version 3 --data @revision.json
zdesk actions review ACTION_ID --expected-version CURRENT_VERSION
zdesk actions events --cursor SAVED_CURSOR --limit 100
zdesk actions history ACTION_ID
```

`agents` supports list/register/update/heartbeat; `queues` supports
list/create/update. `actions` supports list/create/get/update/history/events and
submit/review/request-changes/approve/reject/claim/complete/fail/cancel/release.
Updates/transitions require `--expected-version` (or `expectedVersion` in JSON).
Lists support `--limit` (1–200), `--query offset=N`, and actions support
`--query queueId=ID` / `--query state=STATE`. Events return
`{events,cursor,hasMore}`; checkpoint only after consuming each page.

Tokens cannot approve/reject/release, including the human owner's token. Native
CLI human decisions require an explicitly supplied `--session-env`; agents must
not obtain sessions to bypass approval. `actions claim` requires `--output
NEW_PATH` to save its lease privately. Complete with a lease token and receipt
containing `detail` and optional `externalId`/`url`; `fail` with a reason enters
delivery uncertainty. Expired/uncertain work never auto-retries. Only the human
owner can resolve it: release with a reason returns to draft; release with a
receipt marks verified delivery done. Desk coordinates agents and records
results; a claim does not send a message through any provider.

For concurrent BDR work, prefer `notes append ENTITY ID --data
'{"body":"Observation","clientMutationId":"stable-key"}'` and `tags update
firms|opportunities ID --data '{"add":["lane"],"remove":["old"]}'`. These append
immutable activities/apply atomic tag deltas. Reusing a note key with different
content is a conflict. `--expected-version` is optional on tag deltas. Use
`firms list --search Radiant --field name --exact` for case-insensitive equality;
default search continues to match substrings. Use IDs after discovery.

Prefer a scoped token from your secret manager:

```sh
export ZDESK_BASE_URL=https://zergdesk.com
export ZDESK_WORKSPACE=your-workspace-slug
export ZDESK_PROJECT_ID=your-project-id
# Set ZDESK_API_TOKEN using your secret manager, not a command argument.
zdesk whoami
zdesk workspaces list
zdesk projects list
```

`--token-env NAME` selects a different environment variable. `--token-file PATH`
reads an explicit private credential file instead; it takes precedence over the
default environment token and saved profile. Do not combine it with `--token-env`.
The file must be a regular file with mode **0600**, must not be a symlink, and
must be at most 64 KiB. Contents can be a plain token with an optional trailing
newline, or the complete issued-token JSON `{token,apiToken:{id,...}}`. The CLI
never deletes or modifies this input file; retire it yourself after importing.

The CLI accepts no
literal `--token`, `--password`, or session-cookie argument. Environment tokens
take precedence over saved profile tokens. `--workspace`, `--project` and
`--base-url` override their environment variables, which override profile values.
`--workspace` is a **slug**; `--project` is the project's **ID**. Use `projects
list` to discover IDs. Pipeline reads and writes require a selected project.

ZergDesk uses Zerg SSO for browser sign-in. Create a scoped token in
**Account → API tokens**, set it in `ZDESK_API_TOKEN` through your secret manager,
then run `auth configure`. Local-password login and password reset are disabled;
`auth login --email`, `--password-env`, and credential JSON fail locally with SSO
setup instructions and send no credentials.

For an explicit low-level session exchange, set a private environment variable
to the **SSO browser session cookie value**, then run:

```sh
zdesk auth login --session-env MY_DESK_SESSION --workspace your-slug --project PROJECT_ID
```

The session is used transiently and never saved, printed, or logged out. Login
issues a 90-day token with `crm:read,workspace:read` by default. Request write
access explicitly with `--scopes crm:read,crm:write,workspace:read`; workspace
administration uses `workspace:write`. `--name NAME` labels the token and
`--expires-in-days 1..365` sets its lifetime. Permissions remain bounded by
current workspace membership and the optional token project binding.

`auth configure --token-file issued-token.json` saves the known token ID from
the issuance envelope, making future revocation easier to track. A plain or
environment token has no discoverable ID unless it matches the saved token;
replacing credentials clears stale token-ID metadata. The secret itself is never
printed by configuration commands.

`auth login` saves only the scoped token and context to a private config file,
defaulting to `~/.config/zergdesk/config.json` (honoring `XDG_CONFIG_HOME`). Use
`--config PATH` / `ZDESK_CONFIG` and `--profile NAME` / `ZDESK_PROFILE` for isolated
agent credentials. Config files must be regular files with mode 0600; symlinks
and readable-by-other-users credentials are rejected. New directories use 0700.

```sh
zdesk auth configure --profile crm-agent   # save the selected environment token
zdesk auth configure --profile crm-agent --token-file issued-token.json
zdesk config set --profile crm-agent --workspace your-slug --project PROJECT_ID
zdesk config show --profile crm-agent    # context and auth presence, no secret
zdesk auth logout --profile crm-agent    # forget local token; no remote revoke
```

Saved credentials are pinned to the origin where they were configured. Changing
`--base-url` cannot forward a saved token to another origin. An explicit
environment token or token file uses the origin you select. Only HTTPS and
explicit HTTP loopback development servers are accepted; redirects are refused.

Token management remains session-only. Use `--session-env NAME` or
`ZDESK_SESSION`; token creation requires a new private output file:

```sh
zdesk auth tokens list --session-env MY_DESK_SESSION
zdesk auth tokens create --session-env MY_DESK_SESSION --data @token-request.json --output agent-token.json
zdesk auth tokens revoke TOKEN_ID --session-env MY_DESK_SESSION
```

The issuance JSON schema is `{name,workspaceId,projectId?,scopes,expiresInDays?}`.
The output file contains the one-time secret. Local logout does not revoke a
remote token or clear environment variables; revoke explicitly when retiring it.

## CRM inspection and data preparation

```sh
zdesk pipeline get
zdesk firms list --search acme
zdesk firms list --search acme --field name
zdesk firms get FIRM_ID
zdesk people list --search ada@example.com
zdesk people get PERSON_ID
zdesk opportunities list
zdesk decks get DECK_ID
zdesk crm search --search 'knowledge base'
zdesk crm export --output crm-before-cleanup.json
```

Lists and search project the complete authorized pipeline response locally;
matching is case-insensitive across record JSON by default. Add `--field NAME`
to search only that supported field while returning complete matching records.
This avoids matching a firm name mentioned only in provenance notes.

The field whitelist is:

| Entity | Supported fields |
| --- | --- |
| firms | id, name, website, kind, checkSize, stage, type, status, kanbanOrder, tier, owner, introPath, notes, contacts, lastContact, obsidianPath, inCurrentRaise, industry, companySize, arr, mrr, renewalDate, tags, productIds, segmentIds |
| people | id, name, email, phone, company, firmId, role, linkedin, location, notes, privateNotes, obsidianPath, lastContact |
| opportunities | id, firmId, name, stageId, amount, closeDate, ownerUserId, nextStep, notes, kanbanOrder, createdAt, updatedAt, ownerLabel, contactRoles, productIds, segmentIds, competitorIds, dealType, source, nextStepDate, lostReason, tags, stageHistory, stageEnteredAt, lastActivityAt |
| decks | id, name, currentVersionId, obsidianPath |

`--field` requires `--search` and an entity `list` or `crm search` command.
CRM search accepts the union of these fields, checks only the entities that
support the selected field, and skips records without a value. Unsupported
fields fail before HTTP. Field selection does not add server access or return
records outside the selected workspace/project.

These commands do not invent server pagination or silently request other
workspaces/projects. `crm export` preserves the entire returned
payload, including metadata, relationships, notes, decks, blurbs and memos, in a
versioned bundle `{schemaVersion:1,exportedAt,source:{baseUrl,workspace,projectId},data}`.
This is a read-only starting point for future knowledge-base reconciliation;
there is no automatic vault sync or enrichment. Use explicit `crm import` and
`crm merge` commands for requested changes; `crm export --include-sales` adds
the paginated sales workflow collections to the same bundle.

## Explicit writes

```sh
zdesk firms create --data @new-firm.json
zdesk firms update FIRM_ID --data '{"notes":"Reviewed source and contact path"}'
zdesk people update PERSON_ID --data @person-patch.json
zdesk opportunities create --data @opportunity.json
zdesk opportunities update OPPORTUNITY_ID --data '{"nextStep":"Schedule follow-up"}'
zdesk opportunities delete OPPORTUNITY_ID
zdesk decks create --data '{"name":"Seed pitch"}'
zdesk memos create --session-env MY_DESK_SESSION --data @memo.json
zdesk ztc ask --data '{"message":"Summarize the selected CRM project"}'
```

Firm/person/opportunity updates send only the supplied fields and positional ID;
the API validates and merges partial updates, preserving omitted fields. Creates
use the API's ID-bearing payload schema. **CRM IDs must be globally unique
across workspaces**: generate a random UUID once per source record, optionally
with an entity prefix, and persist the mapping for later sync runs. Avoid names,
email addresses, predictable counters, or slugs as record IDs. For example:

```sh
node --input-type=module -e 'import { randomUUID } from "node:crypto"; console.log(JSON.stringify({id: `firm_${randomUUID()}`, name: "Acme Ventures"}, null, 2))'
```

Save that generated ID with its source reference and reuse it for updates and
relationship fields; retries must not create a fresh ID. The API accepts a
nonempty ID of at most 200 characters for firms, people and opportunities. A
conflicting JSON `id` is rejected
before HTTP. All writes require the relevant server scope and membership role.
No mutation is retried automatically: after an ambiguous network failure, inspect
remote state before repeating a create, update, delete, or agent operation.

## Versions, incremental changes and knowledge-base sync

Accounts, people, deals and all six sales collections return `version` (integer),
`updatedAt` (ISO instant) and optional `provenance`. Send the last version you read
with `--expected-version N`; use 0 for a create-only write. A stale precondition
returns HTTP 409 with `data.currentVersion`. Missing preconditions retain legacy
upsert behavior; use them for every reconciliation write. Server-managed version
and timestamp fields in old full-object clients are accepted but not trusted.

```sh
zdesk firms get FIRM_ID
zdesk firms update FIRM_ID --expected-version 4 --data @patch.json
zdesk crm changes --limit 200 --output changes-page-1.json
zdesk crm changes --cursor OPAQUE_NEXT_CURSOR --limit 200 --output changes-page-2.json
zdesk crm history firms FIRM_ID
```

Changes return `{changes,nextCursor,hasMore}`. Each change carries `entity`,
`recordId`, `operation` (create/update/delete), `version`, `actor` (user/token
IDs), `provenance`, `updatedAt` and an authorized `record` snapshot; deletes are
tombstones with `record:null`. Cursors are opaque and bound to the workspace,
project and optional entity filter. Follow `hasMore` and persist `nextCursor`
only after processing the page. Start without a cursor for the historical feed.
Private person notes follow the same access checks as ordinary CRM reads.

The CLI can prepare and apply a reviewable patch plan. Write a source JSON file:

```json
{
  "records": [
    {
      "entity": "firms",
      "id": "PERSISTED_RANDOM_ID",
      "expectedVersion": 4,
      "data": {"notes": "The intended source edit"},
      "provenance": {"system": "obsidian", "path": "Companies/Example.md"}
    }
  ]
}
```

```sh
zdesk crm sync plan --file source-patches.json --output sync-plan.json
# Review ready/unchanged/conflict records and their currentVersion.
zdesk crm sync apply --file sync-plan.json --output sync-results.json
```

Both commands require an explicit workspace and project. Plans pin their origin,
workspace and project and support at most 5,000 records. Entities are firms,
people, opportunities, activities, tasks, resources, catalogs, views, sequences.
Keep `id`, `expectedVersion` and `provenance` outside `data`; do not include
server-owned timestamps or version fields there. Objects/arrays in supplied
fields replace those values. Each record must supply a nonnegative expected
version, including 0 for a new record. Planning performs reads only; it marks
stale baselines as conflicts and identical patches as unchanged. Apply writes
only ready records with their preconditions, reports every result, and returns
exit 4 if any fail or conflict. It never retries a mutation or auto-resolves a
conflict. Create referenced accounts/people before dependent records.

Persist returned record versions alongside the source mapping after successful
writes; keep failed/conflicting source changes pending for review. After a
transport failure the result is uncertain: inspect the current record before
replanning. Retain authored vault content and Desk edits. No background vault
watcher is installed by this CLI.

Workspace admins can recover a historical record with a new attributed version:

```sh
zdesk crm history firms FIRM_ID
zdesk crm restore --expected-version 5 --data @restore.json
```

`restore.json` is `{entity,id,historyId}` using an entry ID from history. Restore
requires the current version, follows current reference/plan/privacy checks,
and never rewrites the audit trail.

## Record workspace, My day and connected applications

```sh
zdesk records workspace firms FIRM_ID
zdesk records workspace opportunities DEAL_ID --limit 30
zdesk my-day --query timeZone=America/Los_Angeles
zdesk my-day --query assignment=all
zdesk connections list
zdesk connections items CONNECTION_ID --limit 50
zdesk connections jobs CONNECTION_ID --limit 20
zdesk connections sync CONNECTION_ID
```

Record workspaces include linked activities, tasks, documents, meetings, notes
and deck engagement attributed by explicit share-link targets. They respect the
selected project and current member's private capture visibility. `my-day`
defaults to the current user's work, the next seven days of meetings, and UTC
for task dates. `timeZone` accepts an IANA zone. Tasks can be completed/snoozed
with versioned `tasks update`; logging calls/meetings and attaching material use
the existing activities/resources/upload commands. Unanswered email is a
chronological candidate queue, not a guarantee that a reply is owed.

Capture is opt-in. Connect the source in the app's **Connected apps** section or
use CLI metadata plus a separately supplied read credential:

| Provider | Source | Credential and selector |
| --- | --- | --- |
| `zmail` | `https://zerg-mail.com` | Mailbox-scoped capability token; inbox/sent/archive metadata |
| `zcal` | `https://zergcalendar.com` | `org:read` API token; `resourceId` is the calendar ID |
| `zergmeeting` | `https://zergmeet.com` | `org:read` API token; organization meeting metadata |
| `gmail` | Existing Google connection in Desk Settings | `accountId`; no token supplied in this command |

```sh
# calendar-connection.json contains provider, label and resourceId; no token.
zdesk connections create --credential-env MY_ZCAL_TOKEN --data @calendar-connection.json
zdesk connections update CONNECTION_ID --data '{"enabled":false}'
zdesk connections update CONNECTION_ID --credential-file /private/provider-token --data '{"enabled":true}'
zdesk connections disconnect CONNECTION_ID
```

Create metadata: `{provider,label,resourceId?,accountId?,origin?,visibility?,
intervalMinutes?}`. The selected project is injected. Visibility defaults to
`private`, interval to 15 minutes (allowed 5–1,440). Only configured trusted
origins are accepted. Credentials are encrypted by the server and never returned.
The CLI accepts upstream secrets only via `--credential-env` or a private mode
0600 `--credential-file`; it leaves credential files intact. Desk and provider
tokens are separate credentials.

Private capture is visible only to the connection owner. Explicitly choose
`visibility:"workspace"` to publish unambiguous matched metadata into CRM
activities. Matching uses exact normalized contact emails; an account needs one
unambiguous firm association and a deal needs a single candidate in the selected
project. Unmatched/ambiguous metadata stays in owner review. Human edits to a
published activity cause a publication conflict rather than an overwrite.

Sync queues a durable job and returns immediately. Inspect `connections jobs`
for completion, counts and errors; `connections list` shows last success, next
run, health and reauthentication needs. Jobs resume from persisted cursors and
retry transient provider failures with backoff. Pausing stops processing;
disconnect erases the stored connector credential and withdraws generated
shared metadata, while retaining human-authored notes and audit evidence.
Disconnecting a capture source does not delete its upstream account. Previously published metadata remains in CRM history and change feeds; visibility changes do not retroactively purge shared evidence.

Capture reads metadata only; it does not send email, calendar invitations,
recordings or transcripts. ZCal reconciles a rolling 30-day past/90-day future
window; ZMail cycles bounded inbox, sent and archive pages. Gmail bootstraps and then
follows mailbox history. ZergMeeting uses a cursor feed; older app deployments
must expose that feed before the connection becomes healthy.

Run `zdesk help projects`, `zdesk help decks`, or `zdesk --help` for named workspace,
member, project, calendar, dataroom, signature, sharing and chat operations.
Named workspace routes can take an explicit slug; if omitted, the configured
workspace slug is used.

Bearer tokens currently support identity, workspace/project discovery, project
creation/update/deletion, pipeline reads/exports/search, firm/person/opportunity
writes, deck creation/versioning/file uploads, Dataroom uploads/list/downloads,
sales workflow collections, import/duplicate/merge operations, and the read-only
ZTC assistant. Other named operations
use an explicitly supplied browser session with `--session-env NAME`. That
session is transient, sent only to the selected origin, and never stored. Token
scope restrictions remain in force when no session is selected.

## Complete API catalog

```sh
zdesk routes
zdesk routes pipeline
zdesk api GET /api/pipeline/deck-share-intelligence --query deckId=DECK_ID
zdesk api PUT '/api/pipeline/memos/[id]' --param id=MEMO_ID --data @memo.json
```

The offline catalog contains every current method/path, including session-only,
public share, billing and integration routes. A route listing does not confer
token access. Unknown same-origin `/api/...` paths remain available for forward
compatibility, with server validation unchanged. Generic `api` commands send
JSON; use `decks upload` and `dataroom upload` for bounded multipart files.
Binary downloads are available from the authenticated file API endpoints.

`--query key=value` and `--param key=value` may be repeated. Tenant query
selectors `ws` and `projectId` are added to tenant routes; conflicting explicit
query selectors are rejected. Payloads accept `--data '{...}'`, `--data @file.json`
or `--data -` (stdin), and must be JSON objects.

## Output, verification and development

JSON results go to stdout and bounded `{error:{message,details?}}` JSON to stderr.
HTTP error messages include meaningful server diagnostics and a bounded summary
of validation fields; structured details remain available, with server stacks
removed. Active credentials are redacted before diagnostic truncation. HTML
error pages are omitted.
`--json` is accepted for compatibility; JSON is already the default. `--output`
writes a new mode-0600 file atomically after a complete response; existing files
and symlinks are never overwritten. Known output collisions fail before HTTP.
Input and response JSON are bounded to 8 MiB. The default timeout is 60 seconds;
`--timeout` accepts 1–3600 seconds. No streaming or automatic retries.

Public `health`, `status`, and `server-version` commands, plus explicit
`api GET /api/health` and `api GET /api/version`, do not select credentials or
open token files. They optionally read a valid profile's saved origin; missing,
corrupt, unreadable or non-regular config (including `/dev/null`) is ignored.
Origin precedence remains `--base-url`, `ZDESK_BASE_URL`, saved profile, then the
production default. Invalid explicit origins still fail locally.

Exit codes: 0 success; 1 local I/O/config; 2 usage; 3 local credentials;
4 HTTP/payload failure (including server 401/403); 5 network/timeout.

```sh
npm ci --ignore-scripts
npm test
npm run test:mutation
npm run routes:sync
npm run routes:check
npm pack
```

Runtime dependencies remain empty. Development matches the host app and Scholar's
Vitest 2.1.9 / Stryker 9.6.1 family; property tests use fast-check. CLI tests run
real subprocesses against loopback HTTP plus direct public entrypoint tests with
HTTP boundary fakes and real private filesystem state. Only method/path metadata
is bundled from the server; the app source and secrets are excluded.

The shared npm wrapper supplies release-channel-preserving `zdesk update` and
`zdesk update --check` behavior after installation from a published package.


## Current feature limits and workarounds

- Browser SSO is required to create a workspace and issue the first API token.
  Use Account → API tokens, or explicitly exchange an existing SSO browser
  session. There is no password bootstrap or CLI OAuth device flow.
- API tokens cover the documented CRM and workspace/project operations. Broader
  browser-only routes require an explicit transient session; a listed API route
  is not a promise of bearer-token access.
- Entity search downloads the authorized pipeline and filters locally. These
  search commands have no server pagination, exact-match flag, regex search or
  fuzzy matching. Workflow collection lists paginate separately. Use
  `--field` to reduce unrelated matches and inspect IDs before updating.
- Deck and general attachment uploads support PDF/PPTX or arbitrary files up
  to 32 MiB through the named upload commands. Download files through the
  authenticated API; JSON exports include metadata, not the binary contents.
- Imports report per-record success and failure rather than committing a whole
  batch atomically. There is no automatic vault sync. Keep source-to-ID mappings,
  export before and after a batch, and reconcile ambiguous network failures by
  reading the record. Duplicate merges preview first and apply transactionally.
- IDs, not names or email addresses, identify records. Firm/contact links and
  opportunities require valid IDs in the same workspace. Project bindings can
  further restrict a token's access.
- Read the packaged [changelog](CHANGELOG.md) before upgrading. Release wiring
  and local package versions do not imply an npm publication.

### Sales automation (0.2.0)

Use a workspace token with `crm:write` and select a project for uploads and deals.
No browser session is required to create decks, add versions, upload PDF/PPTX
files, or upload general attachments. Uploads are limited to 32 MiB. PPTX
conversion uses the workspace server's configured Google integration; PDF
uploads need no external conversion service.

```sh
zdesk decks create --data '{"id":"deck_RANDOM_UUID","versionId":"version_RANDOM_UUID","name":"Sales deck"}'
zdesk decks version-update --data '{"deckId":"deck_RANDOM_UUID","action":"add","version":{"id":"version_NEW_UUID","version":"0.2","name":"New version","status":"draft"}}'
zdesk decks upload deck_RANDOM_UUID version_NEW_UUID --file ./sales.pdf
zdesk dataroom upload --file ./case-study.md --data '{"id":"file_RANDOM_UUID","description":"Case study draft","category":"case-study"}'
zdesk dataroom list
```

Deck creation also accepts `googleSlidesUrl` and `obsidianPath`. Explicit IDs
return 409 when taken, so retries never silently duplicate or replace a deck.
Version changes validate deck, version and parent within the selected tenant
and project. Promote another version before deleting or demoting the current
version. Attachment download is `GET /api/dataroom/FILE_ID/download` with the same
`ws` and `projectId`; it requires `crm:read`. To attach an uploaded file to an
account or deal, save a `resources` record with `firmId`/`opportunityId`, a title,
a kind (`attachment`, `contract`, `invoice`, `case-study`, `collateral`) and its
absolute authenticated download URL. Resources can also hold HTTP(S) links or
markdown directly.

```sh
zdesk crm import firms --file accounts.csv --dry-run
zdesk crm import firms --file accounts.csv
zdesk crm import opportunities --file deals.json
zdesk crm duplicates firms
zdesk crm duplicates people
zdesk crm merge firms SOURCE_ID TARGET_ID
zdesk crm merge firms SOURCE_ID TARGET_ID --apply
```

Imports accept a JSON array (or `{ "records": [...] }`) of partial records, or
CSV with field-name headers. Quoted commas and multiline cells are supported.
Numbers must be finite, booleans are `true`/`false`, arrays use JSON in a CSV cell.
Blank numeric/date/nullable relationship cells are omitted to preserve existing
values; use JSON `null` to clear them. Files are limited to 8 MiB. Requests contain
at most 500 records, continue after record errors, and report each result. The CLI
returns exit 4 if any record fails. After a network failure, inspect stored IDs
before retrying; mutations are not retried automatically. Dry runs validate
against current server records and do not create dependencies for later batches.

Duplicate discovery uses exact normalized firm names/domains or person emails;
these are review candidates, never automatic merges. Merges preview by default
and require explicit `--apply`, workspace admin rights and a workspace-wide token.
People merges additionally require private-note admin access. Target scalar
values win, blanks are filled from the source, notes and lists are combined,
and record references are transferred in one transaction before the source is
removed. Preview output lists conflicting scalar values. Export before merging
and review the preview; the CLI never chooses a merge automatically.

Sales collections have named `list`, `create`, and `update ID` commands:
`activities`, `tasks`, `resources`, `catalogs`, `views`, and `sequences`.
They use GET/PUT `/api/pipeline/COLLECTION` with `crm:read`/`crm:write`.
`views delete ID` removes a saved view. Lists accept `--query firmId=ID`,
`opportunityId`, `personId`, `kind`, `status`, `limit` and `offset`; default and
maximum page size are 1000, and responses include `pagination.hasMore`.
Catalogs are workspace-wide products, segments and competitors, support archive
status, and keep their kind after creation. Sequence tracking is manual: saving
an enrollment never sends outreach. Log received/sent email as an `activities`
record with a stable `messageId` for rerun safety; Gmail connector ownership
remains with the signed-in browser user.

For a complete sales-workspace backup, run `crm export --include-sales --output
NEW_FILE.json`. The opt-in flag follows every page of activities, tasks, resources,
catalogs, saved views and sequences and adds them to the bundle's `data` object.
The file is created only after all reads succeed, uses mode 0600, and refuses
overwrite. Without the flag, exports retain the existing pipeline snapshot shape.
See [sales CRM contracts](../docs/sales-crm.md) for the UI and data model details.


## Campaigns, evidence and sales intelligence (0.5.0)

Campaigns are fixed project cohorts associated with segment labels. Funnels
choose contacts, accounts or deals and contain typed branching/looping graphs.
The Sales overview separates current statuses from evidence-backed historical
conversion. Read the [campaign operating contract](https://zergdesk.com/docs/campaign-analytics.md)
for JSON schemas, eligibility, coverage and the five-lane reconciliation.

```sh
zdesk campaigns list
zdesk campaigns seed --output seed-preview.json
zdesk campaigns create --data @campaign.json
zdesk campaigns members CAMPAIGN_ID --expected-version CURRENT_VERSION --data @members.json
zdesk campaigns members CAMPAIGN_ID --expected-version CURRENT_VERSION --data @members.json --apply
zdesk campaigns launch CAMPAIGN_ID --expected-version CURRENT_VERSION
zdesk funnels create --data @funnel.json
zdesk funnels validate --data @funnel.json
zdesk funnels publish FUNNEL_ID --expected-version CURRENT_VERSION
zdesk observations record --data @observation.json
zdesk observations correct OBSERVATION_ID --data @correction.json
zdesk analytics overview
zdesk analytics funnel --query campaignId=CAMPAIGN_ID
zdesk analytics drilldown --query campaignId=CAMPAIGN_ID --query nodeId=NODE_ID
zdesk reports generate --data @report-request.json
zdesk reports list
zdesk reports get REPORT_ID --output report.json
zdesk reports settings get
zdesk reports settings update --expected-version CURRENT_VERSION --data '{"enabled":true}'
zdesk crm export --include-sales --include-campaigns --output desk-complete.json
```

`campaigns` supports list/get/create/update/members/launch/clone/archive/seed.
`funnels` supports list/get/create/update/validate/publish. `observations`
supports list/record/correct. `analytics` supports overview/funnel/drilldown/export.
`reports` supports list/get/generate/settings get/settings update. Collection
GETs accept `--limit 1..1000` and `--query offset=N`; follow `pagination.hasMore`.
Use `--data @file` for structured input and `--query key=value` for filters.
Campaign/funnel writes other than create require `--expected-version`.
Member/seed calls preview by default; `--apply` selects append/apply. Explicit
JSON modes are supported and conflicting flags fail before a request.

`--include-campaigns` follows all campaign, funnel, observation and report pages,
preserves every immutable published funnel revision and merged-record identity
ledger, then includes analyst settings in the usual private export envelope. It can be
combined with `--include-sales`. Neither flag silently truncates a collection.
For incremental pulls use `crm changes`; observations remain immutable and
corrections are new source-identified records. Mutations never retry automatically.
Missing historical dates stay unknown. Queue progress does not prove an external
recipient accepted or replied, and generating recommendations never sends outreach.


## Structured prospects and sales intelligence (0.6.0)

The sales overview counts an explicit project prospect list, independently of campaign enrollment. A contact identity may be shared across a workspace while its prospect status, primary lane, owner/group and next step differ between projects. Market lanes, multi-valued use-case segments and fixed campaign cohorts are separate classifications.

```sh
zdesk outreach settings get
zdesk outreach settings update --expected-version CURRENT --data @settings-patch.json
zdesk prospects list --query primaryLaneId=LANE
zdesk prospects get PERSON_ID
zdesk prospects create --data '{"personId":"PERSON_ID","statusId":"sourced"}'
zdesk prospects update PERSON_ID --expected-version CURRENT --data @prospect-patch.json
zdesk analytics acquisition
zdesk analytics matrix --query groupBy=lane
zdesk analytics accounts --query noProspectContact=true
zdesk analytics acquisition --query asOf=2026-10-22T17:41:00Z --query compareWith=2026-10-22T16:58:00Z
```

Inspect the server's project status vocabulary and metric membership before mapping source statuses. Parked/DNC remain in the prospect denominator; DNC and custom statuses with `blocksOutbound:true` block outbound authorization and claims across the workspace. A current `messaged` status is not proof of an accepted LinkedIn connection. Status snapshots and dated historical journeys are separate measurement modes; periods before the recorded baseline remain unavailable.

### Two-way source reconciliation

Register the specific source dataset/connector once, keeping its writer location/version. External IDs such as a CSV row ID belong to that source instance; names are not identities. Several sources may map to the same Desk record.

```sh
zdesk sources register --data @source-registration.json
zdesk sources list
zdesk sources bindings --query sourceId=SOURCE
zdesk crm sync plan --file source-v2.json --output new-plan.json
zdesk crm sync apply --file new-plan.json --output new-result.json
zdesk crm sync pull --query sourceId=SOURCE
zdesk crm sync ack --expected-version BINDING_VERSION --data @verified-ack.json
zdesk crm sync conflicts --query sourceId=SOURCE --query status=open
zdesk crm sync resolve --expected-version CONFLICT_VERSION --data @resolution.json
zdesk reconciliations list --query sourceId=SOURCE
```

`source-v2.json` contains:

```json
{
  "schemaVersion": 2,
  "sourceId": "SOURCE",
  "runId": "stable-run-id",
  "rows": [{
    "externalId": "P0001",
    "entity": "prospects",
    "personId": "PERSON_ID",
    "sourceRevision": "source-revision-or-content-hash",
    "fields": {"statusId": "drafted", "nextStep": "Review message"}
  }]
}
```

Supported v2 entities are `prospects`, `people`, `firms` and `opportunities`; use `recordId` for the latter three. Optional collection `deltas` apply stable-identity additions/removals. Omitted fields are untouched. Set `dryRun:true` in the source input for a read-only preview; dry-run plans cannot be applied. `prospects import plan/apply --file PATH` are v2-only aliases. Existing schema-v1 `crm sync plan/apply` stays record-version based.

The CLI saves v2 plans with the selected origin/workspace/project and refuses apply under another scope. Apply passes the server plan ID/version; it does not blindly replay edited local rows. Source changes and Desk changes are compared with their last acknowledged baseline. Independent edits merge; overlapping edits become reviewable conflicts. Safe fields can apply even when another field conflicts. A409 requires a fresh plan; mutations never retry.

Pull returns binding `pendingPull` patches. Apply a patch to the source, verify its resulting values, then ACK those exact fields with `sourceId`, `bindingId`, `sourceRevision`, `writerVersion` and `expectedVersion`. Generating a patch is not acknowledgement. Conflict resolution takes `conflictId`, `choice:desk|source|edited`, optional explicit `value`, and `expectedRecordVersion`.

`prospects reconcile --file mapping.json` previews the existing hitlist's exact source IDs, sequence membership, status/owner/lane aliases and explicitly selected activity observations. Add `--apply` only with the reviewed `previewHash`; unresolved source/sequence differences stay visible. Reconciliation does not expand account contacts, infer acceptance from messages, invent event dates or create placeholder deals.

### Conversations, signals, proof and introductions

`conversations`, `signals`, `proofs`, `relationships`, `chosen-paths` and `intro-requests` each support `list`, `get ID`, `create --data JSON` and `update ID --expected-version N --data JSON`. Create defaults to expectedVersion0. Use `--query` for project/person/account/campaign filters. The API reference documents each strict record schema.

```sh
zdesk conversations list --query state=meeting-being-set
zdesk signals list --query firmId=ACCOUNT_ID
zdesk proofs list --query campaignId=CAMPAIGN_ID
zdesk relationships import --file reviewed-edges.json
zdesk relationships import --file reviewed-edges.json --apply
zdesk relationships paths --query fromMemberId=MEMBER_ID --query targetPersonId=TARGET_ID
zdesk relationships reach --query connectorPersonId=CONNECTOR_ID
zdesk chosen-paths create --data @chosen-path.json
zdesk intro-requests create --data @intro-request.json
zdesk intelligence changes --cursor SAVED_CURSOR --limit 100
```

Relationship imports use `{rows:[...]}` and default to preview. Save the returned `previewToken` into the same import file before applying; unchanged rows must retain their IDs and version preconditions. Apply requires each selected row to carry `reviewed:true`. Import only consented evidence; never create contacts or verified relationships from unreviewed mentions. Private evidence remains private, including paths, counts, history, exports and model context. Workspace publication requires the custodian's human browser session and consent reference. An explicitly supplied human `--session-env` can be used by the human operator; agents must not obtain browser sessions to bypass permissions.

Chosen paths are explicit decisions. Ranking prioritizes chosen paths and evidence of completed intros, committed money or accepted dated next steps, not message volume. An intro request does not imply double opt-in or a completed introduction. Queue an exact `intro-request` action addressed to the connector, supplying its `introRequestId`, current `introRequestVersion`, returned `introPathFingerprint`, and explicit target references. The existing human review/approval/claim/receipt workflow still applies; changed request/path evidence invalidates approval.

Internal proof approval and customer permission are separate. Tokens can propose proof text but cannot grant sharing permission; changing approved wording or material clears approval.

### Archives, merges and complete exports

```sh
zdesk opportunities archive DEAL_ID --expected-version N
zdesk opportunities archive DEAL_ID --expected-version N --apply
zdesk opportunities restore DEAL_ID --expected-version N --apply
zdesk opportunities bulk-archive --file reviewed-deal-versions.json --apply
zdesk crm merge firms SOURCE TARGET
zdesk crm merge firms SOURCE TARGET --apply --source-version N --target-version M
zdesk crm export --include-sales --include-campaigns --include-sales-intelligence --output new-private-export.json
```

Archival defaults to preview. Bulk input is `{ids:[...],expectedVersions:{"DEAL_ID":N}}`; every applied item requires its reviewed version. Archived deals are excluded from active reports; include them for recovery/export through `--query includeArchived=true`. Merge preview returns source/target versions; apply requires both and never silently adopts newer versions.

`--include-sales-intelligence` explicitly adds every prospect page, outreach settings, source registrations/bindings/reconciliation plans/conflicts, conversations/signals/proofs, ACL-visible relationship/chosen-path/intro-request records and their paginated history. Combine it with existing export flags. Output is private0600, atomic and cannot overwrite an existing file.

Queue creation, routing changes and enabling require the sending account owner's browser authority. A delegated approver does not become the sender account owner. Tokens may pause their own queues but cannot grant or resume authority. Human approval additionally requires the viewed `expectedPayloadHash` and `expectedQueueVersion` in the transition JSON. Batch browser review in Desk approves individually reviewed exact revisions and never sends messages.
