kenso 0.0.0-stage → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # Kenso CLI
2
+
3
+ The Kenso CLI (`kenso`) is the command line for Kenso. It does bulk work in your Kenso workspace from files: add thousands of leads to a campaign, update or clean them up, import accounts and people, and build Studio templates. It is made for coding agents (Claude Code, Codex, Cursor) and the people who drive them: JSON on stdout, fixed exit codes, a dry run before every bulk write, and runs you can resume.
4
+
5
+ Every API operation a workspace member can call is also a command, such as `kenso campaigns list` or `kenso tags create`. So the CLI reaches what the Kenso MCP reaches, straight against `https://api.ken.so`.
6
+
7
+ ## Install
8
+
9
+ You need Node 22 or newer.
10
+
11
+ ```bash
12
+ npm i -g kenso
13
+ kenso --version
14
+ ```
15
+
16
+ To run it once without installing, use `npx kenso --version`.
17
+
18
+ ## 60-second quickstart
19
+
20
+ 1. Create an API key in app.ken.so at Settings > Integrations > API keys.
21
+ 2. Save the key, pick a workspace, then plan and apply a CSV import:
22
+
23
+ ```bash
24
+ echo $KEY | kenso auth login --with-key
25
+ kenso workspace list --q acme
26
+ kenso workspace use 1234
27
+ kenso campaigns list --limit 5
28
+ kenso contacts import --campaign 812 --file leads.csv --dry-run
29
+ kenso contacts import --campaign 812 --file leads.csv --yes
30
+ ```
31
+
32
+ `workspace list` and `workspace use` are only needed when your key reaches more than one workspace.
33
+
34
+ The dry run reads the file, maps the columns, checks every row, asks Kenso for a credit estimate, and writes nothing. It prints the plan, with the exact apply command in `next`:
35
+
36
+ ```json
37
+ {"run":"r_tdi5cdnx","dryRun":true,"command":"contacts import","rows":4,"valid":2,"invalid":2,"chunks":1,"chunkSize":1000,"campaignId":812,"mapping":{"Email":"email","First Name":"firstName","Last Name":"lastName","Company":"companyName","Pain Point":"customVariables.pain_point"},"customVariableKeys":["pain_point"],"estimate":{"kind":"leads","totalMillicredits":2000,"availableMillicredits":500000,"sufficient":true},"credits":{"needed":2,"available":500,"sufficient":true},"next":"kenso contacts import --campaign 812 --file leads.csv --yes","plan":"/home/you/.kenso/runs/r_tdi5cdnx/plan.json","invalidRows":"/home/you/.kenso/runs/r_tdi5cdnx/invalid.csv"}
38
+ ```
39
+
40
+ The apply sends the rows and prints one summary. Every input row has one line in `results.csv`:
41
+
42
+ ```json
43
+ {"run":"r_tdi5cdnx","rows":4,"ok":0,"created":1,"updated":0,"skipped":{"duplicate":1},"failed":0,"notFound":0,"invalid":2,"changed":1,"results":"/home/you/.kenso/runs/r_tdi5cdnx/results.csv","resultsNdjson":"/home/you/.kenso/runs/r_tdi5cdnx/results.ndjson","invalidRows":"/home/you/.kenso/runs/r_tdi5cdnx/invalid.csv","resumed":false,"durationMs":1840,"campaignId":812,"notSendable":0}
44
+ ```
45
+
46
+ This run exits 5 because two rows were invalid: open `invalid.csv` to see why, fix those rows, and import them again. Running the same command again is safe: an email that is already in the campaign comes back `updated` or `skipped`, never as a second lead.
47
+
48
+ ## CLI or MCP
49
+
50
+ - **Use the CLI for volume:** anything that starts from a file, anything over about 50 records, anything you may need to rerun or resume. Records never pass through the model's context: the agent passes a file path and reads a short summary.
51
+ - **Use the MCP for conversation and single records:** look up a campaign, draft a reply, read a report. The MCP is at `https://mcp.ken.so/mcp`, signs in with OAuth and needs no install ([connection guide](https://ken.so/mcp)).
52
+ - **Both use the same API and the same permissions.** `kenso mcp call` reaches MCP-only tools, such as skills, from the shell.
53
+
54
+ ## Docs
55
+
56
+ Every page is in the package and prints offline with `kenso docs <topic>`.
57
+
58
+ | Page | Topic | What it covers |
59
+ |---|---|---|
60
+ | [Authentication](auth.md) | `auth` | API keys, browser login, profiles, workspaces, `doctor` |
61
+ | [Leads](leads.md) | `leads` | Import, update, remove, pause, tag and export leads, with recipes |
62
+ | [Batch runs](batch.md) | `batch` | Dry run and apply, run folders, results files, resume, rate limits, `--each` |
63
+ | [Output and errors](output-and-errors.md) | `output-and-errors` | The output contract, error JSON, error codes, exit codes |
64
+ | [CRM (beta)](crm.md) | `crm` | Accounts, people, opportunities and CRM sync |
65
+ | [Studio (beta)](studio.md) | `studio` | Pull, push, validate, preview and fill templates |
66
+ | [Coding agents](agents.md) | `agents` | Claude Code, Codex, Cursor, the Agent Skill, environment variables, sandboxes |
67
+ | Command reference | `reference` | Every command with its flags, generated from the CLI at build time: `kenso docs reference`, or `docs/reference.md` in the installed package |
68
+
69
+ ```bash
70
+ kenso docs
71
+ kenso docs leads
72
+ ```
73
+
74
+ For machine-readable help, run `kenso describe`: it prints every command, its flags and its body schema as JSON.
package/docs/agents.md ADDED
@@ -0,0 +1,124 @@
1
+ # Coding agents
2
+
3
+ The CLI is built to be driven by a coding agent: Claude Code, Codex, Cursor, Gemini CLI or anything else that runs shell commands. An agent learns it from the Agent Skill, `kenso --help` and `kenso describe`, and never needs these web pages.
4
+
5
+ ## Set up an agent in three steps
6
+
7
+ 1. Install the CLI where the agent runs: `npm i -g kenso` (Node 22 or newer).
8
+ 2. Give it a key. Either set `KENSO_API_KEY` (and `KENSO_WORKSPACE_ID` when the key reaches several workspaces) in the agent's environment, or log in once on that machine:
9
+
10
+ ```bash
11
+ echo $KEY | kenso auth login --with-key
12
+ kenso workspace use 1234
13
+ ```
14
+
15
+ 3. Install the Agent Skill, so the agent knows the rules before its first command:
16
+
17
+ ```bash
18
+ kenso skill install
19
+ ```
20
+
21
+ ## The Agent Skill
22
+
23
+ `kenso skill install` writes the skill `kenso-cli` (one `SKILL.md`) where each agent looks for skills:
24
+
25
+ | Agent | `--client` | Folder |
26
+ |---|---|---|
27
+ | Claude Code | `claude-code` | `~/.claude/skills/kenso-cli/` |
28
+ | Codex | `codex` | `~/.agents/skills/kenso-cli/` |
29
+ | Cursor | `cursor` | `~/.cursor/skills/kenso-cli/` |
30
+
31
+ Without `--client`, it installs for the agent that runs it when that is Claude Code, Codex or Cursor (detected from the environment, see below), else for all three. It prints what it wrote:
32
+
33
+ ```json
34
+ {"installed":[{"client":"claude-code","path":"/home/you/.claude/skills/kenso-cli/SKILL.md","status":"created"}]}
35
+ ```
36
+
37
+ ```bash
38
+ kenso skill install --client claude-code
39
+ kenso skill install --client all
40
+ kenso skill install --dir .claude/skills/kenso-cli
41
+ kenso skill install --force
42
+ ```
43
+
44
+ - `--dir` writes the skill into that folder instead, for example a project's own skills folder. Name the folder `kenso-cli`, as agents expect the folder to match the skill name.
45
+ - A different `SKILL.md` already in a target folder is replaced only with `--force`; without it the command exits 7, writes nothing, and its `next` adds `--force`. The same file is left alone (`"status":"unchanged"`).
46
+ - `--dry-run` writes nothing and exits 0. It prints each target's status (`replaced` marks a different file there) and a `next` that applies it, with `--force` when a file would be replaced.
47
+ - Cursor also reads `~/.claude/skills/` and `~/.agents/skills/`, so after a default install for all three it may list `kenso-cli` more than once. To avoid that, install with `--client cursor` only, or remove the copies you do not need.
48
+
49
+ The skill also ships in the Kenso plugin for Claude Code (`Ken-Technology/ken-ai-plugin`), so `npx skills add Ken-Technology/ken-ai-plugin` installs it along with the MCP skills. To read it without installing, open `skill/kenso-cli/SKILL.md` in the package.
50
+
51
+ ## Rules the skill teaches
52
+
53
+ - **Data is on stdout, as JSON when piped.** Progress, warnings and errors are on stderr. Parse stdout; read stderr when the exit code is not 0.
54
+ - **Plan, then apply.** Run a write with `--dry-run`, read the plan, then run the exact command in its `next` (the same command with `--yes`).
55
+ - **Pass files, not rows.** Never paste a CSV into the conversation. Write the file, pass its path with `--file`, and read the summary.
56
+ - **Read failures only.** If the summary's `failed`, `notFound` and `invalid` are 0, the work is done. Otherwise read just the problem rows from `results.csv` or `invalid.csv`.
57
+ - **Follow `next`.** Every error with a known fix carries the exact command to run.
58
+ - **Branch on the exit code:** 0 ok, 2 fix the command, 3 fix the key, 5 some rows failed, 6 wait or raise a limit, 130 resume.
59
+ - **Look things up, do not guess.** `kenso describe <resource> <action>` prints any command's flags and body schema as JSON.
60
+
61
+ ## The token-saving pattern
62
+
63
+ Adding 5,000 leads through the MCP sends every lead through the model, hundreds of thousands of tokens. Through the CLI it is two commands and two short JSON objects:
64
+
65
+ ```bash
66
+ kenso contacts import --campaign 812 --file leads.csv --dry-run
67
+ kenso contacts import --campaign 812 --file leads.csv --yes
68
+ ```
69
+
70
+ Then, only if the summary shows failures:
71
+
72
+ ```bash
73
+ grep -E '^[0-9]+,(failed|not_found|invalid),' ~/.kenso/runs/r_tdi5cdnx/results.csv | head -50
74
+ ```
75
+
76
+ ## Environment variables
77
+
78
+ | Variable | What it does |
79
+ |---|---|
80
+ | `KENSO_API_KEY` | The API key. Wins over any saved profile |
81
+ | `KENSO_WORKSPACE_ID` | The workspace for every command. `--workspace` wins over it |
82
+ | `KENSO_PROFILE` | The saved profile to use. `--profile` wins over it |
83
+ | `KENSO_API_URL` | The API, default `https://api.ken.so` |
84
+ | `KENSO_CONFIG_DIR` | Where the config file and cache live, default `~/.kenso` |
85
+ | `KENSO_RUNS_DIR` | Where run folders live, default `<config dir>/runs` |
86
+ | `KENSO_MCP_URL` | The MCP for `kenso mcp`, default `https://mcp.ken.so/mcp` |
87
+
88
+ ## Cloud sandboxes
89
+
90
+ Cloud agents (Claude Code on the web, Codex cloud, Cursor background agents and similar) often run with a network allowlist. Allow:
91
+
92
+ - `api.ken.so`: every command
93
+ - `mcp.ken.so`: only `kenso mcp tools` and `kenso mcp call`
94
+ - `registry.npmjs.org`: to install the CLI with npm
95
+
96
+ Store the key as the platform's secret and expose it as `KENSO_API_KEY`. Run `kenso doctor` as the first command: it checks Node, the config, that the API is reachable and that the key works, and exits 0 when all four pass.
97
+
98
+ ```bash
99
+ kenso doctor
100
+ ```
101
+
102
+ Point the run folders at the workspace if the home folder does not persist between sessions: `KENSO_RUNS_DIR=./.kenso-runs`. Keep that folder out of git.
103
+
104
+ ## How Kenso knows an agent ran a command
105
+
106
+ The CLI checks these environment variables, in this order: `CLAUDECODE` and `CLAUDE_CODE_ENTRYPOINT` (Claude Code), `CODEX_SANDBOX` and `CODEX_THREAD_ID` (Codex), `CURSOR_AGENT` (Cursor), `GEMINI_CLI` (Gemini CLI), `OPENCODE` (OpenCode), then `AI_AGENT`, a free-form name any other agent can set. When one is set:
107
+
108
+ - every request carries `X-Ken-Client: Kenso CLI via <agent>`, and writes carry `X-Ken-Actor: agent`, so Kenso shows the change as made by you through that agent;
109
+ - help ends with a short "For agents" block;
110
+ - browser login never opens a browser itself: it prints the link for you to open.
111
+
112
+ `kenso doctor` shows the agent it detected.
113
+
114
+ ## MCP-only tools
115
+
116
+ A few Kenso MCP tools have no API route, such as the skills Kenso's own agents use. Reach them from the shell with the same key:
117
+
118
+ ```bash
119
+ kenso mcp tools
120
+ kenso mcp call skills --operation list
121
+ kenso mcp call contacts --operation list --args '{"limit":5}'
122
+ ```
123
+
124
+ `mcp call` prints the tool's structured result, else its text parsed as JSON, else the text. A tool error exits 1 with `mcp_tool_error`.
package/docs/auth.md ADDED
@@ -0,0 +1,108 @@
1
+ # Authentication
2
+
3
+ The CLI calls the Kenso API with a Kenso API key (`ken_` plus 43 characters). It sends the key as `Authorization: Bearer <key>` and never prints it, logs it or puts it in an error.
4
+
5
+ ## Log in with an API key
6
+
7
+ 1. In app.ken.so, go to Settings > Integrations > API keys and create a key.
8
+ 2. Pipe it to `kenso auth login --with-key`. The key is read from stdin, never from a flag, so it stays out of your shell history and process list.
9
+
10
+ ```bash
11
+ echo $KEY | kenso auth login --with-key
12
+ kenso auth login --with-key --profile client-a --workspace 1234 < key.txt
13
+ ```
14
+
15
+ The CLI checks the key with `GET /v1/me`, then saves it to `~/.kenso/config.json` (file mode 0600, folder mode 0700). It prints:
16
+
17
+ ```json
18
+ {"profile":"default","email":"you@acme.io","workspaceId":"1234","workspaceCount":1}
19
+ ```
20
+
21
+ When the key reaches several workspaces and you did not pass `--workspace`, no workspace is saved and the output carries `"next":"kenso workspace list --q <name>"`. A key in the wrong format exits 2 (`invalid_key_format`). A key Kenso rejects exits 3.
22
+
23
+ ## Browser login
24
+
25
+ ```bash
26
+ kenso auth login
27
+ kenso auth login --no-browser --device-name "build box"
28
+ ```
29
+
30
+ Browser login is rolling out. Until it is on for your account, `kenso auth login` answers `browser_login_unavailable` (exit 3) with `"next":"kenso auth login --with-key"`: use an API key as above.
31
+
32
+ When your workspace has it, the CLI prints a link and a code on stderr. You open the link, check the code and approve. The CLI then saves a new API key to the profile, the same kind you create in the app: it shows in your API key list, where you can revoke it. The CLI opens the browser itself only when no coding agent is detected and the machine has a display. An agent prints the link and asks you to open it. The login expires after 10 minutes (`login_expired`, exit 3), and a denied login exits 3 with `access_denied`.
33
+
34
+ ## CI, scripts and agents: use the environment
35
+
36
+ You do not need to log in on a machine that has the key in its environment:
37
+
38
+ ```bash
39
+ export KENSO_API_KEY=ken_...
40
+ export KENSO_WORKSPACE_ID=1234
41
+ kenso auth status
42
+ ```
43
+
44
+ `KENSO_API_KEY` wins over any saved profile. Store it as a secret in your CI or agent platform, never in a file in the repo.
45
+
46
+ ## Workspaces
47
+
48
+ A key reaches every workspace its user belongs to. Each command runs in one workspace, picked in this order:
49
+
50
+ 1. `--workspace <id>` on the command
51
+ 2. `KENSO_WORKSPACE_ID`
52
+ 3. the workspace saved in the profile (`kenso workspace use`)
53
+
54
+ ```bash
55
+ kenso workspace list --q acme
56
+ kenso workspace use 1234
57
+ kenso campaigns list --workspace 5678
58
+ ```
59
+
60
+ `workspace list` prints one page and says on stderr when there are more: add `--all` for every page. A command that needs a workspace when none is picked fails with `workspace_required` (exit 2) and `"next":"kenso workspace list --q <name>"`.
61
+
62
+ ## Profiles
63
+
64
+ A profile holds a key, a workspace, an optional API URL and the email. Use one per client or per key:
65
+
66
+ ```bash
67
+ kenso auth login --with-key --profile client-a < client-a-key.txt
68
+ kenso campaigns list --profile client-a
69
+ KENSO_PROFILE=client-a kenso auth status
70
+ ```
71
+
72
+ The profile is `--profile`, else `KENSO_PROFILE`, else the active one (the last one you logged in to).
73
+
74
+ ## Check the setup
75
+
76
+ ```bash
77
+ kenso auth status
78
+ kenso doctor
79
+ ```
80
+
81
+ `auth status` prints who the key belongs to and when it expires, and warns on stderr when that is within 14 days:
82
+
83
+ ```json
84
+ {"email":"you@acme.io","workspaceCount":3,"activeWorkspace":{"id":"1234","name":"Acme"},"keyExpiresAt":"2027-01-31T00:00:00Z","keySource":"profile","profile":"default"}
85
+ ```
86
+
87
+ `doctor` prints the version, Node, the config folder, the profile, the key source, the API URL, the workspace and the detected agent, then four checks: `node_version`, `config_permissions`, `api_reachable` and `key_valid`. It exits 0 when every check passes, 3 when the key is missing or rejected, else 1.
88
+
89
+ ## Log out
90
+
91
+ ```bash
92
+ kenso auth logout
93
+ kenso auth logout --profile client-a
94
+ ```
95
+
96
+ This removes the profile and its key from this machine. The key itself stays valid until you revoke it at Settings > Integrations > API keys.
97
+
98
+ ## Where things live
99
+
100
+ | What | Default | Override |
101
+ |---|---|---|
102
+ | Config file with profiles and keys | `~/.kenso/config.json` | `KENSO_CONFIG_DIR` |
103
+ | Run folders | `~/.kenso/runs/` | `KENSO_RUNS_DIR` |
104
+ | Cached workspace features (1 hour) | `~/.kenso/cache/` | follows `KENSO_CONFIG_DIR` |
105
+ | API | `https://api.ken.so` | `KENSO_API_URL` |
106
+ | MCP (for `kenso mcp`) | `https://mcp.ken.so/mcp` | `KENSO_MCP_URL` |
107
+
108
+ `--verbose` prints each request line to stderr with the key redacted.
package/docs/batch.md ADDED
@@ -0,0 +1,152 @@
1
+ # Batch runs
2
+
3
+ A batch command takes a file, sends its rows to Kenso in chunks, and prints one JSON summary. Every input row gets one line in a results file, every run can be resumed, and nothing is written until you pass `--yes`.
4
+
5
+ The batch commands:
6
+
7
+ - `contacts import`, `update`, `remove`, `restore`, `pause`, `resume`, `tag`, `untag` ([Leads](leads.md))
8
+ - `people import` and `accounts import` ([CRM](crm.md), beta)
9
+ - `studio fill` ([Studio](studio.md), beta)
10
+ - any generated command run with `--each <file>`, once per row (see below)
11
+
12
+ ## Plan, then apply
13
+
14
+ Without `--yes` a batch command plans and changes nothing. `--dry-run` asks for the plan explicitly. `--yes` applies. Passing both is a usage error. Two exceptions to the plan below: `studio fill` without `--yes` prints its credit estimate and `next`, with no run or `plan.json` ([Studio](studio.md)), and a non-GET `--each` without `--yes` or `--dry-run` is a usage error (exit 2) rather than a plan.
15
+
16
+ ```bash
17
+ kenso contacts import --campaign 812 --file leads.csv --dry-run
18
+ kenso contacts import --campaign 812 --file leads.csv --yes
19
+ ```
20
+
21
+ The dry run parses and checks every row, may call read-only endpoints (a campaign read, a credit estimate, a contacts search for `--where`), writes `plan.json`, and prints the plan:
22
+
23
+ ```json
24
+ {"run":"r_ubqu7wtx","dryRun":true,"command":"contacts remove","rows":3,"valid":3,"invalid":0,"chunks":1,"chunkSize":1000,"source":"ids","count":3,"firstIds":[11,12,13],"next":"kenso contacts remove --ids ids.txt --yes","plan":"/home/you/.kenso/runs/r_ubqu7wtx/plan.json"}
25
+ ```
26
+
27
+ Every plan has `run`, `dryRun`, `command`, `rows`, `valid`, `invalid`, `chunks`, `chunkSize`, `next` (the exact command that applies it) and `plan` (the file), plus fields of its own command. It adds `invalidRows` when rows were refused, `chunksDone` when it continues an unfinished run, and `alreadyDone: true` when the apply would change nothing.
28
+
29
+ ## The summary
30
+
31
+ An applied run prints one JSON object:
32
+
33
+ ```json
34
+ {"run":"r_ubqu7wtx","rows":3,"ok":2,"created":0,"updated":0,"skipped":{},"failed":0,"notFound":1,"invalid":0,"changed":2,"results":"/home/you/.kenso/runs/r_ubqu7wtx/results.csv","resultsNdjson":"/home/you/.kenso/runs/r_ubqu7wtx/results.ndjson","resumed":false,"durationMs":412,"undo":"kenso contacts restore --run r_ubqu7wtx"}
35
+ ```
36
+
37
+ | Field | Meaning |
38
+ |---|---|
39
+ | `run` | The run id: `r_` and 8 characters |
40
+ | `rows` | Input rows |
41
+ | `ok`, `created`, `updated` | Rows that succeeded, by kind |
42
+ | `skipped` | Rows Kenso left alone on purpose, counted by reason, for example `{"duplicate":12}` |
43
+ | `failed`, `notFound`, `invalid` | Rows that did not go through: refused or errored, id not found, refused locally |
44
+ | `changed` | Rows this invocation changed. 0 when a rerun had nothing left to do |
45
+ | `results`, `resultsNdjson` | Absolute paths of the results files |
46
+ | `invalidRows` | Path of `invalid.csv`, when rows were refused locally |
47
+ | `resumed` | True when this invocation continued an earlier run |
48
+ | `durationMs` | Wall time of this invocation |
49
+ | `pending` | Rows without a result yet, when the run did not finish |
50
+ | `stopped` | `{code, message, hint}` when a stop condition ended the run (for example `limit_reached`) |
51
+ | `interrupted` | True after Ctrl-C |
52
+ | `next` | `kenso runs resume <runId>` when the run can continue |
53
+
54
+ Commands add their own fields: `contacts import` adds `campaignId` and `notSendable`, `contacts remove` adds `undo`, `people import` adds `matched`, `addedToAccounts` and `emailFindsQueued`, `accounts import` adds `researchQueued` and `researchWaitingCredits`, `studio fill` adds `templateId`, `runs` and the estimate.
55
+
56
+ Exit codes: 0 when every row succeeded or was skipped, 5 when some rows are `failed`, `not_found` or `invalid`, 6 when a limit stopped the run, 130 after Ctrl-C. The full table is in [Output and errors](output-and-errors.md).
57
+
58
+ ## The run folder
59
+
60
+ Each run keeps its files in `~/.kenso/runs/<runId>/` (`KENSO_RUNS_DIR` moves the root), or in `--out <dir>` when you pass it. A run in `--out` is also linked under the runs folder, so `kenso runs` finds it. The folder is mode 0700 and its files 0600.
61
+
62
+ | File | What it holds |
63
+ |---|---|
64
+ | `run.json` | The command, its arguments, the input path and SHA-256, the workspace, the chunk plan and the status (`planned`, `running`, `done`, `failed`, `interrupted`) |
65
+ | `state.json` | The chunks done so far, rewritten after every chunk |
66
+ | `results.csv` | One line per input row: `row,status,id,reason,error`, then extra columns such as `email` or `sendable` |
67
+ | `results.ndjson` | The same rows as JSON lines |
68
+ | `invalid.csv` | Rows refused before sending, with the reason |
69
+ | `plan.json` | The last dry run's plan |
70
+ | `summary.json` | The last summary printed |
71
+
72
+ `row` is the 1-based number of the data row in the input (the CSV header is not counted). `status` is one of `created`, `updated`, `ok`, `skipped`, `failed`, `not_found` or `invalid`.
73
+
74
+ ```bash
75
+ kenso runs list
76
+ kenso runs list --limit 5 -o json
77
+ kenso runs show r_ubqu7wtx
78
+ ```
79
+
80
+ ## Rerun and resume
81
+
82
+ A run is identified by its command, its arguments, the SHA-256 of its input file, the workspace and the chunk size. `--yes`, `--dry-run`, `--wait`, `--concurrency` and the output flags are not part of it. Running the same command again on the same file:
83
+
84
+ - **continues an unfinished run** (interrupted, stopped or crashed): finished chunks are skipped and the summary says `"resumed":true`.
85
+ - **answers from a finished run where every row succeeded:** it prints that run's summary with `"changed":0` and `"alreadyDone":true`, sends no writes and exits 0 (a command may still read first: `contacts import` reads the campaign, and `contacts update --match email` looks up the campaign's contacts).
86
+ - **starts a new run** when the last one finished with failed or invalid rows, or when the file changed. Lead, people and account writes match existing records (an email in a campaign, a person by email or LinkedIn URL, an account by domain), so sending a row twice never creates a duplicate.
87
+
88
+ `kenso runs resume <runId>` does the same from the run id: it rebuilds the command from `run.json` and runs it with `--yes` from the folder it started in. It refuses when the input was stdin, or the file is gone or changed. Two processes cannot work on one run: the second gets `run_in_progress` (exit 7).
89
+
90
+ ```bash
91
+ kenso runs resume r_ubqu7wtx
92
+ ```
93
+
94
+ `studio fill` is the exception: a fill charges for every page it builds, so it never fills the same ids twice. See [Studio](studio.md).
95
+
96
+ ## Ctrl-C
97
+
98
+ The first Ctrl-C cancels the requests in flight and starts no new chunks. The CLI writes the state and prints the summary with `"interrupted":true` and `"next":"kenso runs resume <runId>"`, then exits 130. A chunk that did not finish is sent again when you resume, which is safe because the writes match existing records. A second Ctrl-C quits at once.
99
+
100
+ ## Chunks, concurrency and rate limits
101
+
102
+ | Command | Rows per call | At once |
103
+ |---|---|---|
104
+ | `contacts import` | 1,000 | 1 per campaign |
105
+ | `contacts update`, `remove`, `restore`, `pause`, `resume` | 1,000 | 4 (`--concurrency`) |
106
+ | `contacts tag`, `untag` | 1 per call, 25 per chunk | 4 chunks (`--concurrency`) |
107
+ | `people import` | 200 | 4 (`--concurrency`) |
108
+ | `accounts import` | 100 | 4 (`--concurrency`) |
109
+ | `studio fill` | 500 ids per fill run | 1 |
110
+ | `--each` | 1 | 4 (`--concurrency`) |
111
+
112
+ `--concurrency` takes 1 to 16. When the API's `RateLimit-Remaining` header drops below 10, the CLI sends one chunk at a time until it is back above 30.
113
+
114
+ Retries:
115
+
116
+ - **429:** the CLI waits for `Retry-After` (at least 1 second, at most 60 seconds per wait) and tries again, with up to 15 minutes of waiting per command. After that it exits 6 (`rate_limited`).
117
+ - **502, 503, 504 and network errors:** up to 3 retries with backoff, for GET, PUT, PATCH and DELETE, and for the POST routes the CLI knows are safe to repeat.
118
+ - **`contacts import`:** no quick retry. After a 5xx or a dropped connection it waits 130 seconds, longer than the gateway's request timeout, and resends the chunk once.
119
+ - **`contacts remove`:** after a 5xx or a dropped connection it waits 2 seconds and resends once. Ids the first call already removed are reported `ok` (reason `already_removed`), so the undo still restores them.
120
+ - **Other 4xx answers:** never retried. The row or chunk is marked `failed` with the API's code.
121
+
122
+ Kenso answers a bulk call with 200 even when some items fail, so the CLI reads each item's result and never trusts the HTTP status alone.
123
+
124
+ ## Progress
125
+
126
+ Progress goes to stderr, at most one line every 2 seconds (`chunk 7/20, 7000 rows, 6 failed`), and only when stderr is a terminal or you pass `--progress`. An agent gets only the summary.
127
+
128
+ ## Run any API operation once per row: --each
129
+
130
+ Every generated command takes `--each <file>`, a CSV or JSONL file or `-` for stdin. The command runs once per row through the same engine: one results line per row, resume, `--concurrency`. Row keys fill the route, query and body by API name (`accountId`) or flag name (`account-id`). A blank cell is left out and the literal `null` sends null. A GET runs right away; any other method needs `--yes` or `--dry-run`.
131
+
132
+ ```bash
133
+ kenso accounts update --each stages.csv --dry-run
134
+ kenso accounts update --each stages.csv --yes --concurrency 8
135
+ ```
136
+
137
+ With `stages.csv`:
138
+
139
+ ```csv
140
+ accountId,stage,overwritePinned
141
+ 501,engaged,true
142
+ 502,customer,true
143
+ ```
144
+
145
+ Each row is `ok` on a 2xx answer (with the created or returned id), `not_found` on 404, `failed` otherwise, or `invalid` when a required value is missing.
146
+
147
+ ## The token-saving pattern for agents
148
+
149
+ 1. Pass a file path, never the rows: `--file leads.csv`.
150
+ 2. Run `--dry-run`, read the plan, then run its `next`.
151
+ 3. Read the summary. If `failed`, `notFound` and `invalid` are 0, you are done.
152
+ 4. Otherwise read only the problem rows, for example `grep -E '^[0-9]+,(failed|not_found|invalid),' results.csv | head -50`, or `invalid.csv`.
package/docs/crm.md ADDED
@@ -0,0 +1,111 @@
1
+ # CRM (beta)
2
+
3
+ The CRM commands work on Kenso's Accounts lane: accounts (companies), the people on them, and opportunities. They are beta: they need the `accounts` feature flag on your workspace (opportunities need `opportunities`, CRM sync needs `ingestion`). Help and `kenso describe` hide them until your workspace has the flag; `--all-commands` shows them.
4
+
5
+ | Command | What it does |
6
+ |---|---|
7
+ | `accounts import` | Create accounts from a CSV or JSONL file. Research is charged per created account |
8
+ | `people import` | Create or match people from a file, optionally onto accounts |
9
+ | `accounts update --each`, `people update --each`, `opportunities update --each` | Edit many records from a file, one call per row |
10
+ | `crm-connections`, `crm-imports`, `crm-sync-issues` | Read and run CRM sync. Connecting a CRM stays in the app |
11
+
12
+ Import accounts first, then people: `people import` never creates an account.
13
+
14
+ ## Import accounts
15
+
16
+ ```bash
17
+ kenso accounts import --file accounts.csv
18
+ kenso accounts import --file accounts.csv --yes
19
+ ```
20
+
21
+ ```csv
22
+ name,domain,industry,employeeCount
23
+ Acme,acme.io,Software,120
24
+ Globex,globex.com,Manufacturing,"1,200"
25
+ ```
26
+
27
+ - **Columns** map by name, ignoring case, spaces and dashes: `name` (or `company`, `companyName`, `accountName`), `domains` (several, split on `,` or `;`) or one `domain` (or `website`, `companyDomain`), `industry`, `employeeCount`, `hqLocation`, `linkedinUrl`, `stage`, `angle`, `whyNow`, `fitScore`, `fundingRound`, `cadenceDays`, `ownerMemberId`, `kenCompanyId`, `sendsFromInboxId`, `sendsFromLinkedInAccountId` and `source`.
28
+ - **A domain is required.** A file with no domain column is a usage error, and a row without a domain is `invalid` (`domain_required`).
29
+ - **The dry run prices the research.** It asks for a credit estimate per 1,000 accounts and puts the sum in the plan: `kind`, `accountCount`, `totalMillicredits`, `availableMillicredits` and `sufficient`.
30
+ - **Rows go 100 per call** to `POST /v1/accounts/bulk-create`.
31
+
32
+ Each row comes back:
33
+
34
+ | Status | Meaning |
35
+ |---|---|
36
+ | `created` | A new account; `id` is the account id. Research is queued (`researchQueued` in the summary), or waits for credits (`researchWaitingCredits`) |
37
+ | `skipped`, reason `domain_taken` | A company that is already an account; `id` is that account |
38
+ | `failed` | Kenso refused the row (`reason` is the API's code, `error` names the field) or failed to save it. An item Kenso failed to save is sent once more before it counts as failed |
39
+
40
+ Rerunning the same file is safe: accounts that exist come back `domain_taken`.
41
+
42
+ ## Import people
43
+
44
+ ```bash
45
+ kenso people import --file people.csv
46
+ kenso people import --file people.csv --find-emails --yes
47
+ kenso people import --file people.jsonl --dry-run
48
+ ```
49
+
50
+ ```csv
51
+ fullName,email,title,accountDomain,role
52
+ Ann Lee,ann@acme.io,VP Sales,acme.io,champion
53
+ Bob Ray,,CTO,acme.io,
54
+ ```
55
+
56
+ - **Columns** map by name, ignoring case, spaces and dashes: `email`, `fullName` (or `name`), `firstName`, `lastName`, `title`, `linkedinUrl` (or `linkedin`, `profileUrl`), `accountDomain` (or `domain`, `website`, `companyDomain`), `accountId`, `role` and `priority`. Other columns are ignored with one warning.
57
+ - **A person needs a name:** `fullName`, or `firstName`, `lastName` or both (the CLI joins them). A row with none of them is `invalid` (`name_required`).
58
+ - **To put a person on an account,** give `accountDomain` or `accountId`. A row with both sends only `accountId`.
59
+ - **`--find-emails`** queues an email find (charged in credits) for each person without an email.
60
+ - **Rows go 200 per call** to `POST /v1/people/batch`.
61
+
62
+ People already in the workspace are matched by email, then LinkedIn URL, so running the same file again creates nobody twice.
63
+
64
+ | Result | Row status | reason |
65
+ |---|---|---|
66
+ | A new person | `created` | |
67
+ | An existing person, added to the account | `updated` | |
68
+ | An existing person already on the account | `skipped` | `already_on_account` |
69
+ | An existing person, no account asked | `skipped` | `matched` |
70
+ | The person was removed from the account before | `skipped` | `previously_removed` |
71
+ | The account does not exist | `failed` | `account_not_found`: the person was saved without an account. Import the account, then rerun to place them |
72
+ | Kenso refused the row | `failed` | the API's reason |
73
+
74
+ The summary adds `matched`, `addedToAccounts` and `emailFindsQueued`.
75
+
76
+ ## Edit accounts, people and opportunities
77
+
78
+ Apart from `accounts bulk-update`, which sets one `accountType` on up to 500 accounts, there is no bulk edit route, so run the generated `update` command once per row with `--each`. Columns are the command's flags by API name or flag name, and the id column fills the route:
79
+
80
+ ```bash
81
+ kenso accounts update --each accounts-edit.csv --dry-run
82
+ kenso accounts update --each accounts-edit.csv --yes
83
+ kenso people update --each people-edit.csv --yes
84
+ kenso opportunities update --each deals.csv --yes
85
+ ```
86
+
87
+ ```csv
88
+ accountId,stage,ownerMemberId,overwritePinned
89
+ 501,engaged,42,true
90
+ 502,customer,42,true
91
+ ```
92
+
93
+ A row that changes a field someone pinned in the app fails as a whole (`409 conflict`, `pinned_by_user`: nothing on that record changes, the other fields of the row included), unless the row sets `overwritePinned` to `true` (or you pass `--overwrite-pinned`). To see every flag of an update command:
94
+
95
+ ```bash
96
+ kenso describe accounts update
97
+ kenso accounts update --help
98
+ ```
99
+
100
+ ## CRM sync
101
+
102
+ CRM sync has generated commands only: read connections, preview, start an import, and work through sync issues. Connect the CRM itself in the app.
103
+
104
+ ```bash
105
+ kenso crm-connections list
106
+ kenso crm-connections get --connection-id 7
107
+ kenso crm-imports create --connection-id 7 --dry-run
108
+ kenso crm-sync-issues list --connection-id 7 --all
109
+ ```
110
+
111
+ Run `kenso crm-connections --help` (or add `--all-commands` when the actions are hidden) to see every action.