# Dench CLI

Agent-facing CLI for the Dench agent workspace.

## Usage

Run without installing. The npm package is `@dench.com/cli`, but the binary
it installs is named `dench`, so always invoke it as
`npx -y -p @dench.com/cli dench <command>`. A bare
`npx -y @dench.com/cli <command>` will fail because `npx` looks for a bin
matching the package's last path segment (`cli`) and there isn't one.

```bash
npx -y -p @dench.com/cli dench signin --name "AI Agent - Billing Repo"
```

Or install globally:

```bash
npm install -g @dench.com/cli
dench signin --name "AI Agent - Billing Repo"
```

`--kind` accepts any string. Suggested values: `claude_code`, `codex`, `cursor`,
`hermes`, `openclaw`, or any custom kind such as `aider`, `goose`, or
`some_custom_agent`. Defaults to `other` when omitted. Values are normalized
to lowercase snake_case before send (e.g. `--kind "Claude Code"` becomes
`claude_code`).

The default backend is `https://dench.com` (production). Use `dench backend`
to switch:

```bash
dench backend show
dench backend set production
dench backend set staging
dench backend set local                  # http://localhost:3000
dench backend set https://example.com    # custom URL
dench backend clear                      # revert to production
```

For one-shot overrides (no config change):

```bash
dench --prod <cmd>
dench --staging <cmd>
dench --backend local <cmd>
dench --host https://example.com <cmd>
DENCH_HOST=https://example.com dench <cmd>
```

The smooth setup flow is:

1. Run `dench signin --name "AI Agent - Billing Repo"` once.
2. Let the CLI open the approval link. If it cannot, ask the human to open the
   printed link and confirm the email/workspace. Use `--no-open` to print the
   link without opening it.
3. Run `dench status --mine --json` and `dench crm entries list task --json`
   to verify setup. Tasks live as CRM `task` entries — there is no separate
   `dench tasks` command.

The CLI selects the approved session for later commands automatically.
Setup is complete after approval, both verification commands work, and you know
which Dench agent/session is selected.

Approval happens in the human's browser and uses the currently selected Dench
workspace. The human should switch to the intended workspace before approving.
The CLI opens the approval link automatically when possible. If opening fails,
use the printed link manually.

If multiple sessions already exist, do not log in again to fix ambiguity and do
not silently guess from the newest timestamp, agent name, or kind. List sessions
and ask the human which exact session to use:

```bash
dench sessions
dench use <session-key-or-workspace-slug>
```

Use a human-readable `DENCH_SESSION_KEY` at sign-in time only for long-lived
agents or when the human asks for a stable identity:

```bash
DENCH_SESSION_KEY=billing-repo-agent dench signin --name "AI Agent - Billing Repo"
DENCH_SESSION_KEY=billing-repo-agent dench status --mine --json
```

Do not set `DENCH_SESSION_KEY` to values like `auto:...` or `explicit:...`.
Those are internal scopes shown by `dench sessions`; select them with
`dench use <session-key-or-workspace-slug>` instead.

`dench logout` removes only the current selected session for this local agent
context. Other saved sessions can still be reused by future chats. To remove the
wrong local session, run `dench logout --session <session-key>`. Use
`dench logout --all` only when you want to clear every local Dench session.

Do not create or edit local agent memory files during setup. Use Dench
memory only when the human asks or when saving a stable workspace fact.

Tasks live as CRM `task` entries — there is no separate `dench tasks`,
`dench task`, `dench claim`, or `dench log` command. Create one via
`dench crm entries create task --data '{"Name":"…","Status":"To Do"}'`
only when the human assigns durable work or coordination benefits from
a tracked entry (confirm field names with `dench crm fields list task`
first — unknown fields are rejected). For ad-hoc requests, work directly.

Use `dench context` when you need to know who you are, which workspace you
are using, pending approvals you requested, connected apps, and the best
next commands. Add `--json` for structured output.

## Past chat threads

Browse and search the user's past chat threads in this workspace (own +
shared). Useful for pulling in earlier context when the user refers to
something they discussed before. The shared chat harness uses these same CLI
commands through its execution tool; there is no separate agent-only chat-history command family.

```bash
dench chat list [--query <substr>] [--limit N] [--include-archived] [--json]
dench chat search "<query>" [--limit N] [--json]
dench chat read <thread-id> [--limit N] [--json]
```

Visibility scope: only threads created by the current user, plus threads
in the same organization marked as `shared`.

`dench chat search` is a keyword full-text search across past message
text — pick concrete tokens that the prior chat would have used (file
paths, product names, error strings) rather than paraphrases.

Inside a sandbox, `dench chat` uses `DENCH_API_KEY` + `DENCH_RUN_ID`
automatically; locally, run `dench signin` first.

## Billing

Fresh workspaces verify a card for abuse prevention before starter credits are
issued. The card verification does not charge the human. New workspaces receive
$5 in starter AI credits, with automatic overage disabled by default.

Check AI credits:

```bash
dench billing status --json
```

Create a top-up link for the human to pay:

```bash
dench billing topup --amount 5 --json
```

Do not claim the top-up succeeded until Stripe returns success or
`dench billing status --json` shows the new credits.

## External Apps

List connected apps:

```bash
dench integrations --json
```

`dench integrations` is an alias for `dench tool status` with no toolkit.

Other tool commands:

```bash
dench tool status [toolkit] --json
dench tool connect <toolkit> --json
dench tool search "list recent gmail emails" --toolkit gmail
dench tool run <tool_slug> --args '{"key":"value"}' --json
```

`dench tool connect gmail`, `outlook`, `linkedin`, and `calendar` print Dench
sign-in links using the same application, scopes, and callback as the web
app. Sign in with `dench signin` or use a member-owned Dench API key, then open
the returned link to authorize your account. New mailbox and LinkedIn
connections are private by default. An organization-only key cannot own a
personal connection.

Check the result with `dench tool status <toolkit> --json`. `calendar` shows
both Google and Outlook calendars; `google-calendar` (`googlecalendar`) and
`outlook-calendar` (`outlookcalendar`, `microsoft-calendar`) select one provider.
These status checks use Dench's own connection state and need no gateway key.
Other toolkits connect as connected apps. `--callback-url` is only supported
for connected apps; Dench-connected accounts use Dench's managed callback.
In the JSON, each connection has a `source` of `app` or `dench`, and
`nativeAvailable: false` means Dench-connected accounts could not be checked.

Start with compact `dench tool search` output. It lists ranked tools with slug,
toolkit, connection status, and read-only/approval hints. Add `--json` only when
you need full raw schemas or exact argument details.

Run clear read-only information retrieval tools through Dench without requesting
manual approval first. This includes slugs containing `FETCH`, `GET`, `LIST`,
`SEARCH`, `READ`, or `FIND`, such as `GMAIL_FETCH_EMAILS`:

```bash
dench tool run GMAIL_FETCH_EMAILS --args '{"max_results":10}' --json
```

Dench enforces the final policy and returns `requiresApproval` when approval is
needed.

For Gmail and other email tools, summarize safely. Prefer metadata and snippets.
Do not repeat OTP codes, security codes, passwords, tokens, API keys, or secrets
from email bodies. Non-JSON `tool run` display redacts likely sensitive codes;
`--json` preserves the raw provider response for cases that need it.

Run `dench tool --help` or `dench integrations --help` for command help.

For staging:

```bash
dench signin --staging
dench signin --host https://staging.dench.com
```

## Saved prospect lists

Find supports separate past seniority and profile/work-description filters.
Past seniority can match a different job from the past employer: a former
Microsoft engineer who founded a separate startup can match both. Founder
includes Co-Founder; Founding Team and C-level are separate choices. Company
business focus is distinct from legal company type.

```bash
dench leads find --titles "CTO,VP of Engineering,Director of Engineering" \
  --headcount "51-200,201-500,501-1000" --company-keywords "SaaS,B2B,Cloud" \
  --past-company-names "Apple,Microsoft,Google,Alphabet,Amazon,Nvidia,Meta,Facebook,Tesla" \
  --profile-keywords "AI infrastructure,Cloud migration,DevOps" \
  --locations "California,Washington,Oregon,British Columbia" \
  --past-seniorities "Founder" --limit 25 --json
```

These fields are also available in Find's editable filters, `search_people`,
and the API. `dench leads filters --filter past-seniorities` lists choices.
Descriptions use word matching; missing evidence does not match. Candidate
qualification is bounded, so keep paging when a continuation token is returned,
including after an empty page. Company focus needs another candidate filter.

People found with `dench leads find` stay in draft lists, not the CRM.
Generated results, `--save-as` searches, and `dench lists save` remain unpublished
until you click **Save list** in chat or Find. This keeps the same artifact id,
rows and reviews, then adds the list to Find Leads and the saved-list directory.
Enriching contacts or explicitly importing people does not publish the draft.
Existing lists without draft metadata remain saved.

For new prospect discovery, the lead-finding skill reads
`/workspace/ORGANISATION.md` and does not create `ICP.md`. The current
request overrides that brief. The result is one saved list.
Ordinary web, company, or hiring research does not require lead-finding or ICP
questions, reads, or scoring. There is no mandatory per-profile ICP decision.
In Find, click **Find** in a Work email, Phone, or Personal email cell to
request that field. The control discloses credit use; the lookup starts directly.
Different people run concurrently; wait for a person's current lookup before
starting another field. The cell shimmers with **Enriching**, then shows the
returned value or **Not found**. Header play targets selected profiles, or the
whole saved list when none are selected, with credit confirmation inline.
Bulk progress floats temporarily and disappears when finished, including misses.
Contact results persist in the list; other columns remain under View settings.

```bash
dench leads find --titles "VP Sales" --save-as "Q3 targets" --json
dench lists save --title "10 RevOps leaders" --people-file /tmp/shortlist.json --json
# Click Save list in chat or Find before looking it up in the saved directory.
dench lists --search "Q3 targets" --limit 25 --json
dench lists rows <artifactId> --enrichment enriched --limit 100 --json
dench lists rows <artifactId> --enrichment enriched --limit 100 --cursor <continueCursor> --json
```

`lists save` takes a non-empty JSON array of person rows (`name` or
`fullName` required), consolidating selected discoveries into a new list
without searching, enriching, or importing CRM rows. It leaves source lists
unchanged. Optional `--filters-json` stores criteria/source evidence metadata.
In chat, explicitly present the final list; research batches stay in activity
details instead of becoming competing result sets.

Scoring is optional and only when the user asks. Saved assessments remain available
in list reads and CSV exports; the **ICP fit** and **Why this lead** columns are
temporarily hidden in Find. Scores are not conversion probabilities or CRM
statuses. To save assessments for canonical
rows returned by `lists rows`, create a JSON array of
`{row, suggestion: {fit, score, reason}}` (1–100 items per file):

```bash
dench lists assess <artifactId> --assessments-file /tmp/icp-assessments.json --json
```

`fit` is `likely_fit`, `unlikely_fit`, or `unknown`; `score` is a whole number
from 0 to 100. The CLI validates the file before writes and reports partial
failures; retrying updates existing assessments. Re-read rows to verify every
saved score and reason. Older profiles have no saved score until assessed.

Directory and row reads return `page`, `continueCursor`, and `isDone`.
Directory/search reads show only saved lists; row reads accept a draft's id too.
Follow cursors even through empty filtered pages. Lists survive new chats;
read fresh rows and contacts instead of an old transcript or CSV. Each row
has its original zero-based `index`. Enriched means a contact was returned,
not merely that a lookup completed. Never resubmit pending/uncertain jobs.

Only when the user explicitly asks to import the selected profiles:

```bash
dench artifacts import <artifactId> --to lead --rows 0,4,9 --status "Contacted" --view "Q3 outbound" --pin
```

This imports exactly those original indices and known contacts, reusing existing
CRM records. Inspect the actual Lead status options first. Omit `--status`
to keep the default New; use `--enrichment enriched` for contact-bearing rows.
Enrichment is optional for explicit imports. Import sets status only, even
Qualified: it never purchases contacts, sends outreach, or converts records.
See the `lead-finding` skill and the
[sales pipeline playbook](https://dench.com/references/SALES.md).

## Outreach campaigns

`dench campaign` runs email + LinkedIn outreach campaigns (the Outreach →
Campaigns page) from the terminal, from a brief to a launch: search for people
with filters and exclusions, add them from a search, CSV, saved list, CRM view
or one at a time, write the steps and AI-written lines, preview the exact
messages, check readiness, launch, and follow the replies. `<campaign>` is an
id or an exact, case-insensitive name. Confirm `dench campaign help` lists
`draft`, `search`, `add`, `steps`, `person` and `preview`. It needs a workspace
member's credentials (`dench signin`), a paid workspace for searches, and
connected sender accounts (`dench campaign accounts` lists them).

```bash
dench campaign accounts --json        # email + LinkedIn sender ids, warm-up, availability and capacity
dench campaign draft --name "Founder outreach" --request-id founders-q4 --timezone America/Los_Angeles --window "Mon-Fri 9-18" --daily-cap 30 --email-mailboxes <mailboxId1>,<mailboxId2> --linkedin-accounts <linkedinId>
dench campaign search --filters '{"titles":["Founder"],"locations":["United States"]}' --not-industries "Staffing and Recruiting" --limit 25 --out audience.json
dench campaign add <campaign> --people-file audience.json
dench campaign steps set <campaign> --file steps.json
dench campaign personalize set <campaign> --file lines.json
dench campaign preview <campaign> --person "Ada Lovelace"
dench campaign launch <campaign> --check
dench campaign launch <campaign> --ack-linkedin-risk
dench campaign get <campaign>          # status, schedule, funnel, Now/Next, holds, people
dench campaign replies <campaign> --since 7d
dench campaign people <campaign> --status paused
dench campaign events <campaign>
dench campaign pause|resume|archive <campaign>
dench campaign stop <campaign> --person <enrollmentId>
```

`dench campaign create --name "Founder outreach"` makes the same kind of draft
as `draft` (which also takes `--request-id`, so a retry returns the same
draft). A search costs 1 cent per person returned and prints the most it can
cost before it runs; `add --people-file` adds those people without searching
again. Other ways to fill a campaign:
`add <campaign> --csv people.csv`, `--list <artifactId>` (a saved Find list),
`--search '<filters json>'` (search and add in one billed call),
`--object lead --view "Q3 targets"`, `--object people --entries <id1>,<id2>`,
or `--email ada@example.com` for one person. `--not-industries`,
`--not-titles`, `--not-domains` and the other `--not-<filter>` flags leave
values out of a search.

A campaign holds at most 300 people; for more, `dench campaign duplicate
<campaign>` makes a new draft with the same steps, schedule and senders, and no
people, and the next page of the search goes into the copy. Fix a mistake in a
draft with `dench campaign remove <campaign> --person <enrollmentId>` or
`dench campaign person set`; `dench campaign sync` pulls new contact details
from the CRM records. `steps set` replaces every step and works on drafts only;
`steps edit --file steps.json` changes a step's text and waits in place, also
after launch. `launch --check` never launches: it lists each blocker with the
command that fixes it and exits 1 when blocked. The schedule, daily cap and
stop-on-reply stay editable with `dench campaign set`. Every command takes
`--json`. The full guide, from reading a brief to monitoring the launch,
including the `steps.json` shape and the `--window` spec, is
`references/CAMPAIGNS.md` (`dench campaign help` for the flag list).
`dench email multichannel` drives the same campaigns with fewer controls.

Use `--email-mailboxes` / `--linkedin-accounts` for campaign sender pools
(up to 50 accessible accounts per channel). Accepts comma-separated ids or a
JSON array; omit a flag to preserve it, or pass `none` / `'[]'` to clear it.
API fields: `emailMailboxAccountIds` / `linkedinAccountIds`. Pool campaigns
omit per-step sender ids; legacy steps remain until conversion. Launch still
checks availability, and each conversation keeps its original sender.

`dench campaign accounts --json` discovers both mailboxes and LinkedIn
accounts, including unused senders. Only the acting member's private and
workspace-shared connections are listed; provider credentials are never
returned. LinkedIn summaries expose `linkedinAccountId`, display/owner labels,
availability, `remainingToday` (invites/messages/profiles) and
`remainingInvitesThisWeek`. Capacity is an account-local usage snapshot before
queued work and campaign limits; today's invites also respect the weekly cap.
The API equivalent is `GET $DENCH_API_BASE/campaigns/accounts/linkedin`.

Outreach needs a workspace member's credentials (a `dench signin` session, a
member-bound `DENCH_API_KEY`, or an agent session); an organization-only key
is refused.

## Workspace files and reusable setup

Use `dench files` for saved workspace documents and `dench stage` for outputs.
Harness A keeps a separate scratch directory per job. The old filesystem and
home-sync daemons are legacy compatibility infrastructure; they are not needed
to execute a command or restore a saved environment.

```sh
dench files cat /reports/example.md
dench env list --json
dench env save openai-report --path ./openai-report --json
dench env show openai-report --json
dench env run openai-report -- node report.mjs
dench secrets set OPENAI_API_KEY --stdin < /private/path/key.txt
```

A saved directory requires `dench-env.json`, `SKILL.md`, an explicit file allowlist
and verification commands. It builds dependencies once, publishes a ready
revision, and reuses the prepared snapshot or compatible bundle. Only the chosen
environment is loaded. Secrets are encrypted separately and injected into the
executing process; they never become dependency archives or memory facts.

New automation runs select the latest ready revision; active jobs and their
children retain the resolved revision. State is isolated by job or automation
unless an explicit shared state key is chosen. State writes and refreshed
credential files use compare-and-set versions; conflicts do not replay business
operations. Rotate credentials without reinstalling packages.

On a different local OS/runtime, explicitly prepare a separate platform cache
once using `dench env run openai-report --prepare-local -- node report.mjs`.
Normal ready runs never invoke a package installer. The local executor provides
separate directories, not operating-system isolation.

See [the environment reference](https://dench.com/references/ENVIRONMENTS.md)
for the manifest, cold-start costs, mutable state and recovery receipts. Use
`dench memory search/save` for facts and preferences, `dench context set` for
standing context, and `dench secrets` for credentials. An old memory claiming
that a service is configured cannot replace a successful storage receipt.

## Sandbox / API key auth (no `dench signin` required)

Inside a Dench sandbox the Dench API key is set in the environment when the
sandbox starts. When `DENCH_API_KEY` is present, the CLI skips the
`dench signin` flow entirely and uses the key as a bearer for every org-scoped
command — `dench crm`, `dench files`, and other workspace operations. The
selected connection determines the backend; hosted credentials stay scoped to
the authorized workspace and job.

```bash
export DENCH_API_KEY=dench_pk_live_…
# Optional but enables nicer URLs in CLI output:
export DENCH_API_URL=https://app.dench.com
export DENCH_ORG_ID=…
export DENCH_ORG_SLUG=…

dench crm entries list people
```

Auth precedence:

1. A locally-stored `dench signin` session for the current backend (set via
   `dench backend`, `--host`, `--prod`, or `--staging`; `DENCH_HOST=<url>`
   does the same for one command).
2. `DENCH_API_KEY` (sandbox / automation path).

Plain terminal usage without a saved session or `DENCH_API_KEY` returns
`login_required`.

Commands that need to impersonate a specific agent (`dench memory`,
`dench approval request`, etc.) still require an agent session
(`dench signin`) — the unified API key is org-scoped and cannot speak
as a single agent. Those commands return a clear "agent session
required" error in API-key mode.

### Focused workspace context

`dench context --instructions` reads explicit IDENTITY, RULES and TOOLS notes,
plus document availability/revisions. It does not load business profiles,
personal profiles or aggregate memory. Fetch only what the task needs:

```sh
dench context --sections organisation,user,memory --json
```

Section names are `identity`, `organisation`, `user`, `memory`, `rules`, and
`tools`. Multiple names use one query. Reading never fills profiles or writes
memory. Edit the selected context with
`dench context set <identity|organisation|user|tools|rules> --stdin`; older profile
aliases remain compatibility paths. Personal USER context is unavailable to shared
channel sessions. `dench context --json` keeps its existing workspace overview.
