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/leads.md ADDED
@@ -0,0 +1,214 @@
1
+ # Leads
2
+
3
+ Leads are contacts in a campaign. The lead commands live under `kenso contacts`, and `kenso leads` is the same thing: `kenso leads import` runs `kenso contacts import`.
4
+
5
+ | Command | What it does |
6
+ |---|---|
7
+ | `contacts import` | Add leads from a CSV or JSONL file to a campaign, with one result per row |
8
+ | `contacts update` | Edit leads in bulk from a file, matched by id or by email |
9
+ | `contacts remove` / `restore` | Remove (soft-delete) leads by id, and undo it |
10
+ | `contacts pause` / `resume` | Pause and resume the campaign sequence of leads (pause is beta, `pause_leads` flag) |
11
+ | `contacts tag` / `untag` | Add or remove a tag on leads by id |
12
+ | `contacts export` | Download a campaign's leads as one CSV file |
13
+
14
+ All of them except `export` are batch commands: without `--yes` they print a plan and change nothing. [Batch runs](batch.md) explains plans, run folders, results files and resume.
15
+
16
+ ## Import a CSV into a campaign
17
+
18
+ ```bash
19
+ kenso contacts import --template
20
+ kenso contacts import --campaign 812 --file leads.csv --dry-run
21
+ kenso contacts import --campaign 812 --file leads.csv --yes
22
+ ```
23
+
24
+ `--template` prints the accepted columns. A minimal file:
25
+
26
+ ```csv
27
+ email,firstName,lastName,companyName,Pain Point
28
+ ann@acme.io,Ann,Lee,Acme,Slow onboarding
29
+ cid@globex.com,Cid,Moe,Globex,Churn
30
+ ```
31
+
32
+ ### Columns
33
+
34
+ Headers are matched without regard to case, spaces or punctuation. The known fields and the other headers each one reads:
35
+
36
+ | Field | Also read from | Rule |
37
+ |---|---|---|
38
+ | `email` | email address, e-mail, work email | Required, unless the campaign is LinkedIn-only and the row has a linkedin.com/in/ URL |
39
+ | `firstName` | first name, first, given name | Required |
40
+ | `lastName` | last name, last, surname, family name | Required |
41
+ | `title` | job title, position, role | Optional |
42
+ | `linkedInUrl` | linkedin, linkedin url, linkedin profile, profile url | Optional |
43
+ | `companyName` | company, organization, account, account name | `companyName` or `companyDomain` is required |
44
+ | `companyDomain` | website, domain, company website, url | `companyName` or `companyDomain` is required |
45
+
46
+ Every other column becomes a custom variable, named like its header in snake_case: a `Pain Point` column fills `{{pain_point}}` in your sequence. Names Kenso keeps for its own fields (`city`, `country`, `phone_number` and others) are not sent: the CLI warns and the plan lists them under `droppedColumns`. Rename them with `--map`.
47
+
48
+ ### Cells
49
+
50
+ - A blank cell is never sent, so importing a file never erases data on a lead that is already in the campaign.
51
+ - The literal value `null` clears a custom variable.
52
+
53
+ ### Rename or drop columns with --map
54
+
55
+ `--map` takes a JSON object, `@file` or `-` for stdin. A value names the target field, and `null` drops the column:
56
+
57
+ ```bash
58
+ kenso contacts import --campaign 812 --file leads.csv --map '{"Pain": "customVariables.pain_point", "Notes": null}' --yes
59
+ ```
60
+
61
+ ### JSONL
62
+
63
+ Each line is one lead. Keys work like CSV headers, and a `customVariables` object is spread into custom variables:
64
+
65
+ ```json
66
+ {"email":"ann@acme.io","firstName":"Ann","lastName":"Lee","companyName":"Acme","customVariables":{"pain_point":"Slow onboarding"}}
67
+ ```
68
+
69
+ ```bash
70
+ kenso contacts import --campaign 812 --file leads.jsonl --yes
71
+ cat leads.jsonl | kenso contacts import --campaign 812 --file - --yes
72
+ ```
73
+
74
+ A run read from stdin cannot be resumed with `kenso runs resume`, so prefer a file for large imports.
75
+
76
+ ### What the CLI checks before sending
77
+
78
+ Every row is checked locally first. A row that fails goes to `invalid.csv` with one of these reasons and is not sent:
79
+
80
+ | Reason | Meaning |
81
+ |---|---|
82
+ | `missing_email` | The row has no email (and is not a LinkedIn-only lead) |
83
+ | `invalid_email` | The email is not a valid address |
84
+ | `missing_first_name` | The row has no first name |
85
+ | `missing_last_name` | The row has no last name |
86
+ | `missing_company` | The row has neither a company name nor a company domain |
87
+ | `sales_navigator_url` | An email-less row whose LinkedIn URL is a Sales Navigator link: add the email or a linkedin.com/in/ URL |
88
+ | `duplicate_in_file` | An earlier row of the file has the same email |
89
+
90
+ Kenso stays the authority and checks every row again.
91
+
92
+ ### What the dry run shows
93
+
94
+ The dry run maps the columns, checks the rows, reads the campaign, and asks for a credit estimate (`credit-estimates create`, kind `leads`). Its plan holds `rows`, `valid`, `invalid`, `mapping`, `customVariableKeys`, `estimate`, `credits` (`needed`, `available`, `sufficient`) and `next`, the exact apply command. If the estimate call fails, the plan still prints with `"estimate":null` and an `estimateError`.
95
+
96
+ ### How rows are sent
97
+
98
+ The CLI sends 1,000 rows per call to `POST /v1/campaigns/{campaignId}/contacts`, one call at a time per campaign, because two overlapping upserts of the same email could create duplicates. After a 5xx answer or a dropped connection it does not retry right away: it waits 130 seconds, so the first request has finished on the server, then resends the chunk once. Rows the first request saved come back `updated`.
99
+
100
+ ### Reading the result
101
+
102
+ Each row in `results.csv` gets one status:
103
+
104
+ | Status | Meaning |
105
+ |---|---|
106
+ | `created` | A new lead in the campaign |
107
+ | `updated` | The lead was already there and was updated |
108
+ | `skipped` | Kenso kept the lead out on purpose. `reason` is `duplicate`, `blocklisted`, `already_contacted`, `excluded` or another code |
109
+ | `failed` | Kenso refused the row (`reason` such as `invalid_email` or `validation`, and an `error` message), or the call failed |
110
+ | `invalid` | The CLI refused the row before sending it (see the table above) |
111
+
112
+ A saved lead that cannot be emailed yet has `sendable` false, and the summary counts those leads in `notSendable` with a warning on stderr.
113
+
114
+ Exit codes: 0 when no row failed (skipped rows are not failures), 5 when some rows failed or were invalid, 6 when the plan's or campaign's lead limit stopped the run (`limit_reached`; raise the limit, then rerun the same command to send the rest).
115
+
116
+ ## Import a very large file on the server
117
+
118
+ For up to 100,000 rows, `--server-import` uploads the CSV as one server import job and waits for it (default 30 minutes, `--timeout`). It reports the job's counts but no per-row results.
119
+
120
+ ```bash
121
+ kenso contacts import --campaign 812 --file big.csv --server-import --yes
122
+ kenso contacts import --campaign 812 --file big.csv --server-import --timeout 1h --yes
123
+ ```
124
+
125
+ ## Update leads in bulk
126
+
127
+ ```bash
128
+ kenso contacts update --file edits.csv --match id
129
+ kenso contacts update --file edits.csv --match id --yes
130
+ kenso contacts update --file edits.csv --match email --campaign 812 --yes
131
+ ```
132
+
133
+ - `--match id` reads the contact id from an `id` or `contactId` column.
134
+ - `--match email` finds each email among the leads of `--campaign`. An email it cannot find is `not_found`. The email column only finds the lead; it is never sent as an edit.
135
+ - A column that is absent, or a blank cell, leaves that field unchanged. The literal `null` clears it.
136
+ - The lead fields you can edit are `firstName`, `lastName`, `title`, `linkedInUrl`, and `email` with `--match id`. `companyName` and `companyDomain` cannot change here and are not sent. Every other column is a custom variable, mapped the same way as in an import, and `--map` works here too.
137
+ - A row with nothing to change is `skipped` with reason `no_changes` and is not sent. Rows go 1,000 per call to `POST /v1/contacts/bulk-update`.
138
+
139
+ To edit one lead, use a one-row file from stdin:
140
+
141
+ ```bash
142
+ printf 'id,title\n9001,VP Sales\n' | kenso contacts update --file - --match id --yes
143
+ ```
144
+
145
+ ## Remove, restore, pause and resume
146
+
147
+ These four take contact ids from `--ids` (one id per line, or a CSV or JSONL file with an `id` or `contactId` column, or `-` for stdin), or from a search with `--where` (remove, pause and resume). `--where` takes the body of `POST /v1/contacts/search` as JSON, `@file` or `-`.
148
+
149
+ ```bash
150
+ kenso contacts remove --ids ids.txt
151
+ kenso contacts remove --ids ids.txt --yes
152
+ kenso contacts remove --where '{"campaignIds": [812], "q": "acme"}' --yes
153
+ kenso contacts remove --ids big.csv --confirm 2500 --yes
154
+ ```
155
+
156
+ - Without `--yes`, the plan prints the count and the first ids.
157
+ - Removing more than 1,000 contacts also needs `--confirm <count>`, and the count must match. The plan's `next` already carries the right `--confirm`.
158
+ - Each id comes back `ok`, `skipped` when there was nothing to do (for example `not_deleted` when you restore a contact that is not removed), `not_found` or `failed`.
159
+ - The remove summary carries the undo command, `"undo":"kenso contacts restore --run r_ubqu7wtx"`.
160
+
161
+ ```bash
162
+ kenso contacts restore --run r_ubqu7wtx --yes
163
+ kenso contacts restore --ids ids.txt --yes
164
+ kenso contacts pause --where '{"campaignIds": [812]}' --yes
165
+ kenso contacts resume --ids ids.txt --yes
166
+ ```
167
+
168
+ `restore` takes `--ids` or `--run <runId>` of an earlier remove, not `--where`: a search cannot find removed contacts. `pause` is a beta command for workspaces with the `pause_leads` flag.
169
+
170
+ ## Tag and untag
171
+
172
+ ```bash
173
+ kenso tags list
174
+ kenso contacts tag --tag 31 --ids ids.txt --yes
175
+ kenso contacts untag --tag 31 --ids ids.txt --yes
176
+ ```
177
+
178
+ `tag` calls `POST /v1/tags/{tagId}/assignments` once per contact. A contact that already has the tag is `skipped` with reason `already_tagged`.
179
+
180
+ ## Export a campaign's leads
181
+
182
+ ```bash
183
+ kenso contacts export --campaign 812 --out contacts.csv
184
+ kenso contacts export --campaign 812 --wait
185
+ ```
186
+
187
+ With `--out`, the CLI creates the export, waits for the job (default 30 minutes, `--timeout`) and writes one CSV file (mode 0600). Without `--out` it prints the export job; `--wait` waits for it, or run the `next` it prints, `kenso jobs wait <jobId> --type contact_export`.
188
+
189
+ ## Recipes
190
+
191
+ **Tag every lead of a campaign.** List the ids, then tag them:
192
+
193
+ ```bash
194
+ kenso contacts list --campaign-id 812 --all -o ids > ids.txt
195
+ kenso contacts tag --tag 31 --ids ids.txt --yes
196
+ ```
197
+
198
+ **Fix the invalid rows and send them.** `invalid.csv` keeps each refused row's number and reason. Fix those rows in a new file and import it. Rerunning the original file also works: rows Kenso already saved come back `updated` or `skipped`.
199
+
200
+ **Retry only what failed.** Filter `results.csv` to the `failed` rows, take those rows from the input by `row` number, and import that file.
201
+
202
+ **Pause leads who replied elsewhere.** Search by filter, check the count, then apply:
203
+
204
+ ```bash
205
+ kenso contacts pause --where @replied.json
206
+ kenso contacts pause --where @replied.json --yes
207
+ ```
208
+
209
+ **Edit one field on many leads by email.**
210
+
211
+ ```bash
212
+ kenso contacts update --file titles.csv --match email --campaign 812 --dry-run
213
+ kenso contacts update --file titles.csv --match email --campaign 812 --yes
214
+ ```
@@ -0,0 +1,154 @@
1
+ # Output and errors
2
+
3
+ Every command follows one contract, so a script or an agent can parse any of them the same way.
4
+
5
+ ## stdout and stderr
6
+
7
+ - **stdout carries data only.** Progress, warnings, notices and errors go to stderr.
8
+ - No colors, spinners or pagers. On a terminal you get readable tables; in a pipe you get JSON.
9
+ - The CLI never prompts. A batch command (an import, update, remove, tag or `--each` write) takes `--yes`; without it, the command plans or refuses. A single generated write, such as `kenso tags delete --tag-id 7`, and a write through `kenso api` send at once: add `--dry-run` to see the request first, then run the command without it.
10
+
11
+ ## Formats
12
+
13
+ | What the command returns | stdout is a terminal | stdout is a pipe or file |
14
+ |---|---|---|
15
+ | One object | key-value lines | compact JSON on one line |
16
+ | A list | a table (first 8 scalar fields, cells cut at 60 characters) | NDJSON: one JSON object per line |
17
+ | A batch run | key-value lines | one JSON summary object |
18
+ | No content (HTTP 204) | nothing | nothing |
19
+
20
+ `-o` or `--output` picks the format: `json`, `ndjson`, `table` or `ids`.
21
+
22
+ - `-o json` on a list prints one JSON array. On a terminal, `-o json` pretty-prints an object.
23
+ - `-o ids` prints one id per line, ready for `--ids -`. Items without an `id` are skipped with one warning.
24
+
25
+ ```bash
26
+ kenso campaigns list --limit 3
27
+ kenso campaigns list --limit 3 -o json
28
+ kenso contacts list --campaign-id 812 --all -o ids
29
+ ```
30
+
31
+ ## Shape the output
32
+
33
+ - `--fields a,b.c` keeps only those fields (dots reach into nested objects).
34
+ - `--query <JMESPath>` filters or reshapes in-process, so you need no `jq`. On a list it sees the whole list.
35
+ - Neither applies to a batch summary, which always prints in full.
36
+
37
+ ```bash
38
+ kenso campaigns list --fields id,name,status
39
+ kenso campaigns list --all --query "[?status=='active'].id"
40
+ kenso me get --query "length(features)"
41
+ ```
42
+
43
+ ## Lists and pages
44
+
45
+ A list command prints one page. When there are more, it prints the command for the next page on stderr, for example `next: kenso tags list --limit 1 --cursor MQ`. `--all` follows every page and streams the items. `--limit` sets the page size and `--cursor` starts at a cursor.
46
+
47
+ ```bash
48
+ kenso tags list --limit 50
49
+ kenso tags list --all
50
+ ```
51
+
52
+ ## Errors
53
+
54
+ When stderr is not a terminal, an error is one JSON line on stderr:
55
+
56
+ ```json
57
+ {"error":{"code":"unknown_flag","message":"Unknown flag --fiel for kenso contacts import.","field":"fiel","hint":"did you mean --file?","next":"kenso contacts import --help"}}
58
+ ```
59
+
60
+ | Key | Always there | Meaning |
61
+ |---|---|---|
62
+ | `code` | yes | A stable snake_case code to branch on |
63
+ | `message` | yes | One sentence for a person |
64
+ | `field` | no | The flag or input field at fault |
65
+ | `hint` | no | What to change |
66
+ | `next` | no | The exact command to run next, such as `kenso auth login`, `kenso workspace list --q <name>` or the same command with `--yes` |
67
+ | `httpStatus` | no | The API's HTTP status, when the API answered |
68
+ | `requestId` | no | The API's request id: quote it when you contact Kenso support |
69
+
70
+ On a terminal the same error prints as text: `Error: <message>`, then `field:`, `hint:`, `next:` and `requestId:` lines. The key never appears in an error.
71
+
72
+ ### How API errors map
73
+
74
+ The CLI looks at the HTTP status first, then at the API's own `error.code`, which wins when present.
75
+
76
+ | HTTP status | Default code | Exit |
77
+ |---|---|---|
78
+ | 400, 422 | `validation_failed` | 2 |
79
+ | 401 | `unauthorized` (`next`: `kenso auth login`) | 3 |
80
+ | 403 | `forbidden`: the key's user or role cannot do this in this workspace | 3 |
81
+ | 404 | `not_found` | 4 |
82
+ | 409 | `conflict` | 7 |
83
+ | 429 | `rate_limited`, after the CLI waited and retried | 6 |
84
+ | 5xx | `server_error`, after retries where they are safe | 8 |
85
+ | anything else | `http_error` | 1 |
86
+
87
+ Two API codes change the exit code: `workspace_required` exits 2 with `"next":"kenso workspace list --q <name>"`, and `limit_reached` (the plan's or the campaign's lead limit) exits 6.
88
+
89
+ ### Errors from the CLI itself
90
+
91
+ | Code | Exit | When |
92
+ |---|---|---|
93
+ | `usage_error` | 2 | A required flag or argument is missing, or two flags conflict (`--yes` with `--dry-run`) |
94
+ | `unknown_command` | 2 | No such command; `hint` suggests the closest one |
95
+ | `unknown_flag`, `invalid_flag`, `invalid_flag_value`, `unexpected_argument` | 2 | A flag or argument the command does not take |
96
+ | `invalid_query` | 2 | `--query` is not valid JMESPath |
97
+ | `file_unreadable` | 2 | An input file is missing or cannot be parsed |
98
+ | `config_invalid` | 2 | `~/.kenso/config.json` is not valid JSON: fix or delete it |
99
+ | `feature_not_enabled` | 2 | Every action of the resource is a beta command your workspace has not enabled |
100
+ | `invalid_key_format`, `key_required` | 2 | `auth login --with-key` read something that is not a Kenso key, or stdin is a terminal instead of a pipe |
101
+ | `invalid_workspace_id` | 2 | `kenso workspace use` got an id that is not a positive whole number |
102
+ | `unknown_topic` | 2 | `kenso docs` got a topic it does not have; `hint` lists the topics |
103
+ | `file_unwritable` | 2 or 1 | An output file cannot be written: `contacts export --out` (2), `skill install` (1) |
104
+ | `no_api_key` | 3 | No key in `KENSO_API_KEY` or the profile (`next`: `kenso auth login`) |
105
+ | `browser_login_unavailable`, `access_denied`, `login_expired` | 3 | Browser login is off, was denied, or timed out |
106
+ | `run_not_found` | 4 | `kenso runs show` or `resume` got an unknown run id |
107
+ | `docs_not_found` | 4 | `kenso docs reference` before the reference was generated (a source checkout without `pnpm build`) |
108
+ | `run_in_progress`, `already_filled` | 7 | Another process is working on the run, or `studio fill` already ran with this file and these flags |
109
+ | `skill_exists` | 7 | `kenso skill install` found a different `SKILL.md`; `next` adds `--force` to replace it |
110
+ | `job_timeout`, `job_failed`, `job_canceled` | 1 | `kenso jobs wait` (or a command that waits on a job) ran out of time, or the job failed or was canceled |
111
+ | `mcp_tool_error` | 1 | `kenso mcp call`: the MCP tool answered with an error |
112
+ | `unsafe_path` | 1 | `studio pull` refused a file path that would land outside `--dir` |
113
+ | `network_error` | 8 | The API could not be reached after retries |
114
+ | `upload_failed`, `download_failed` | 8 or 1 | A signed upload or download of a Studio file failed: 8 when storage was unreachable or answered 5xx, 1 on a 4xx (often an expired link: run the command again) |
115
+ | `pull_incomplete` | 8 | `studio pull`: a file came back with neither text nor a download link |
116
+ | `unexpected_response` | 1 or 8 | The API answered something the CLI could not read |
117
+ | `unexpected_error` | 1 | A bug: run again with `--verbose` and report it |
118
+ | `interrupted` | 130 | Ctrl-C |
119
+
120
+ ## Exit codes
121
+
122
+ | Code | Meaning |
123
+ |---|---|
124
+ | 0 | OK, including a batch where every row succeeded or was skipped |
125
+ | 1 | Unexpected error |
126
+ | 2 | Usage or input error: a bad flag, a bad file, a validation error |
127
+ | 3 | Auth: no key, an invalid, revoked or expired key, or HTTP 401 or 403 |
128
+ | 4 | Not found |
129
+ | 5 | Partial: the batch finished and some rows failed, were not found or were invalid. See `results.csv` |
130
+ | 6 | Rate limited after retries, or a plan or campaign lead limit stopped the run (`limit_reached`) |
131
+ | 7 | Conflict or invalid state (HTTP 409), or a run another process holds |
132
+ | 8 | Server error after retries (5xx), or the API is unreachable |
133
+ | 130 | Interrupted. A batch run can be resumed: see its `next` |
134
+
135
+ Skipped rows (duplicates, blocklisted, already contacted) are not failures: a batch with only created, updated and skipped rows exits 0.
136
+
137
+ ## Flags
138
+
139
+ - Command flags go after the command: `kenso contacts import --campaign 812`. Global flags can go anywhere.
140
+ - A boolean flag takes `--flag`, `--no-flag`, `--flag=true` and `--flag=false`.
141
+ - `--help` on any command prints its flags, arguments and examples. `kenso describe <resource> <action>` prints the same as JSON, with the body schema.
142
+
143
+ ## Beta commands
144
+
145
+ Commands that need a workspace feature flag (CRM, Studio and others) are beta. Help and `kenso describe` hide the ones your workspace has not enabled and say how many they hid; `--all-commands` shows them. Calling a hidden command still works: Kenso decides. `kenso <resource>` on a resource whose every action is hidden answers `feature_not_enabled` (exit 2) and names the flags it needs.
146
+
147
+ ```bash
148
+ kenso --help --all-commands
149
+ kenso describe --all-commands
150
+ ```
151
+
152
+ ## What the CLI sends
153
+
154
+ Every API call carries `Authorization: Bearer <key>`, `Ken-Version: 1.1`, `X-Ken-Workspace-Id` (when a workspace is picked), `User-Agent: kenso-cli/<version> (node <version>; <platform>)` and `X-Ken-Client: Kenso CLI`. When a coding agent runs the CLI, `X-Ken-Client` names it (`Kenso CLI via Claude Code`) and writes carry `X-Ken-Actor: agent`, so Kenso's activity log shows who made a change.