kenso 0.1.16 → 0.1.17
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/dist/kenso.js +170 -71
- package/dist/operations.json +1 -1
- package/docs/crm.md +6 -32
- package/docs/leads.md +4 -4
- package/docs/reference.md +66 -27
- package/package.json +2 -2
- package/skill/kenso-cli/SKILL.md +16 -15
package/dist/kenso.js
CHANGED
|
@@ -2397,7 +2397,7 @@ var VERSION;
|
|
|
2397
2397
|
var init_version = __esm({
|
|
2398
2398
|
"src/core/version.ts"() {
|
|
2399
2399
|
"use strict";
|
|
2400
|
-
VERSION = true ? "0.1.
|
|
2400
|
+
VERSION = true ? "0.1.17" : "0.0.0-dev";
|
|
2401
2401
|
}
|
|
2402
2402
|
});
|
|
2403
2403
|
|
|
@@ -6215,8 +6215,10 @@ var init_each = __esm({
|
|
|
6215
6215
|
var execute_exports = {};
|
|
6216
6216
|
__export(execute_exports, {
|
|
6217
6217
|
DATA_KEY: () => DATA_KEY,
|
|
6218
|
+
HELP_FLAG_MAX: () => HELP_FLAG_MAX,
|
|
6218
6219
|
HELP_NOTES_MAX: () => HELP_NOTES_MAX,
|
|
6219
6220
|
buildRequest: () => buildRequest,
|
|
6221
|
+
helpFlagText: () => helpFlagText,
|
|
6220
6222
|
helpNotes: () => helpNotes,
|
|
6221
6223
|
operationExamples: () => operationExamples,
|
|
6222
6224
|
operationFlags: () => operationFlags,
|
|
@@ -6522,28 +6524,57 @@ async function runOperation(ctx, op, flags) {
|
|
|
6522
6524
|
ctx.output.object(unwrap(res.body));
|
|
6523
6525
|
return ExitCode.Ok;
|
|
6524
6526
|
}
|
|
6527
|
+
function normalizedNotes(op) {
|
|
6528
|
+
return (op.description ?? "").trim().replace(/\n[ \t]+/g, "\n");
|
|
6529
|
+
}
|
|
6525
6530
|
function helpNotes(op) {
|
|
6526
|
-
const text2 = (op
|
|
6531
|
+
const text2 = normalizedNotes(op);
|
|
6527
6532
|
if (text2.length <= HELP_NOTES_MAX) return text2;
|
|
6528
6533
|
const head = text2.slice(0, HELP_NOTES_MAX);
|
|
6529
6534
|
const end = Math.max(head.lastIndexOf(". "), head.lastIndexOf(".\n"));
|
|
6530
|
-
|
|
6531
|
-
|
|
6532
|
-
|
|
6535
|
+
return end > HELP_NOTES_MAX / 3 ? head.slice(0, end + 1) : `${head.trimEnd()}...`;
|
|
6536
|
+
}
|
|
6537
|
+
function helpFlagText(f) {
|
|
6538
|
+
const enumText = f.enum ? ` One of: ${f.enum.join(", ")}.` : "";
|
|
6539
|
+
let base = f.description;
|
|
6540
|
+
if (enumText && base.endsWith(enumText)) base = base.slice(0, -enumText.length);
|
|
6541
|
+
const placeholder = f.param ? /^\S+ \((?:path|query|body), ([\w-]+)\)$/.exec(base) : null;
|
|
6542
|
+
if (placeholder) return { text: `${placeholder[1]}${enumText}`, cut: false };
|
|
6543
|
+
let text2 = base;
|
|
6544
|
+
const sentence = /\.(?=\s+[A-Z])/.exec(text2);
|
|
6545
|
+
if (sentence) text2 = text2.slice(0, sentence.index + 1);
|
|
6546
|
+
if (text2.length > HELP_FLAG_MAX) {
|
|
6547
|
+
const head = text2.slice(0, HELP_FLAG_MAX);
|
|
6548
|
+
text2 = `${head.slice(0, Math.max(head.lastIndexOf(" "), HELP_FLAG_MAX / 2)).replace(/[,;:]$/, "")}...`;
|
|
6549
|
+
}
|
|
6550
|
+
return { text: `${text2}${enumText}`, cut: text2 !== base };
|
|
6533
6551
|
}
|
|
6534
6552
|
function operationToCommand(op) {
|
|
6535
6553
|
const flags = {};
|
|
6554
|
+
let cut = false;
|
|
6536
6555
|
for (const f of operationFlags(op)) {
|
|
6556
|
+
const shown = helpFlagText(f);
|
|
6557
|
+
cut ||= shown.cut;
|
|
6537
6558
|
const spec = {
|
|
6538
6559
|
type: f.type === "boolean" ? "boolean" : "string",
|
|
6539
6560
|
// The dispatcher would enforce `required` before --each rows can fill it, so the command checks it instead.
|
|
6540
|
-
description: f.required ? `${
|
|
6561
|
+
description: f.required ? `${shown.text} (required)` : shown.text
|
|
6541
6562
|
};
|
|
6542
6563
|
if (f.multiple) spec.multiple = true;
|
|
6564
|
+
if (FOOTER_FLAGS.has(f.flag) && !f.param) spec.helpHidden = true;
|
|
6543
6565
|
flags[f.flag] = spec;
|
|
6544
6566
|
}
|
|
6545
|
-
const write2 = op.method === "GET" ? "" : "\
|
|
6567
|
+
const write2 = op.method === "GET" ? "" : "\nA write: it sends at once. Run it with --dry-run first, then without.";
|
|
6546
6568
|
const api = `API: ${op.method} ${op.path} (${op.id}).${write2}`;
|
|
6569
|
+
const named = [...op.routeParams, ...op.queryParams].find((p) => p.flag !== p.name);
|
|
6570
|
+
const keys = named ? `keys fill flags, by flag or API name: ${named.flag} or ${named.name}` : "keys fill flags";
|
|
6571
|
+
const rows = `Many rows: --each <file>, once per CSV or JSONL row (${keys}${op.body ? "; other keys the body" : ""}; --concurrency 1-16)${op.method === "GET" ? "" : ", --dry-run then --yes"}.`;
|
|
6572
|
+
const more = [
|
|
6573
|
+
op.body ? "--data <json|@file|->: the body (flags win)." : "",
|
|
6574
|
+
// The same text helpNotes measures, so the pointer shows only when something was left out.
|
|
6575
|
+
cut || normalizedNotes(op).length > HELP_NOTES_MAX ? `Full notes: kenso describe ${op.resource} ${op.action}.` : ""
|
|
6576
|
+
].filter(Boolean);
|
|
6577
|
+
const footer = [rows, ...more.length ? [more.join(" ")] : []];
|
|
6547
6578
|
return {
|
|
6548
6579
|
resource: op.resource,
|
|
6549
6580
|
action: op.action,
|
|
@@ -6552,13 +6583,19 @@ function operationToCommand(op) {
|
|
|
6552
6583
|
|
|
6553
6584
|
${api}` : api,
|
|
6554
6585
|
flags,
|
|
6555
|
-
|
|
6586
|
+
// describe keeps the --data example; help shows the dry run and the apply, and for a write the file of rows
|
|
6587
|
+
// (agents copy examples, and one with only --<id> flags led them to one call per row).
|
|
6588
|
+
examples: [
|
|
6589
|
+
...operationExamples(op).filter((e) => !e.endsWith(" --data @body.json")),
|
|
6590
|
+
...op.method === "GET" ? [] : [`${commandOf(op)} --each rows.csv --dry-run # a file of rows: one command`]
|
|
6591
|
+
],
|
|
6592
|
+
helpFooter: footer,
|
|
6556
6593
|
...op.featureGates ? { beta: { featureGates: op.featureGates } } : {},
|
|
6557
6594
|
operationIds: [op.id],
|
|
6558
6595
|
run: (ctx, args) => runOperation(ctx, op, args.flags)
|
|
6559
6596
|
};
|
|
6560
6597
|
}
|
|
6561
|
-
var DATA_KEY, SCALAR_TYPES, LIST_PARAM_TEXT, HELP_NOTES_MAX;
|
|
6598
|
+
var DATA_KEY, SCALAR_TYPES, LIST_PARAM_TEXT, HELP_NOTES_MAX, HELP_FLAG_MAX, FOOTER_FLAGS;
|
|
6562
6599
|
var init_execute = __esm({
|
|
6563
6600
|
"src/generated/execute.ts"() {
|
|
6564
6601
|
"use strict";
|
|
@@ -6572,7 +6609,9 @@ var init_execute = __esm({
|
|
|
6572
6609
|
limit: "Page size.",
|
|
6573
6610
|
cursor: "Start at this cursor (the next: hint of a previous page)."
|
|
6574
6611
|
};
|
|
6575
|
-
HELP_NOTES_MAX =
|
|
6612
|
+
HELP_NOTES_MAX = 320;
|
|
6613
|
+
HELP_FLAG_MAX = 140;
|
|
6614
|
+
FOOTER_FLAGS = /* @__PURE__ */ new Set(["each", "concurrency", "data"]);
|
|
6576
6615
|
}
|
|
6577
6616
|
});
|
|
6578
6617
|
|
|
@@ -6909,13 +6948,14 @@ var init_import = __esm({
|
|
|
6909
6948
|
resource: "accounts",
|
|
6910
6949
|
action: "import",
|
|
6911
6950
|
summary: "Create accounts (companies) from a CSV or JSONL file, charged per created account; a file of people: people import",
|
|
6951
|
+
brief: "companies, charged per new account",
|
|
6912
6952
|
description: "Sends the rows to accounts.bulk-create, 100 per call. Columns map by name (case, spaces and dashes ignored) to the account fields: name (or company), domains (split on , or ;) or one domain (domain, website, companyDomain), industry, employeeCount, hqLocation, linkedinUrl, stage, angle, whyNow, fitScore, fundingRound, cadenceDays, ownerMemberId, kenCompanyId, sendsFromInboxId, sendsFromLinkedInAccountId and source. A company that is already an account is skipped (domain_taken) with that account's id. Without --yes it prints the plan with a credit estimate; research runs for each created account.",
|
|
6913
6953
|
flags: {
|
|
6914
6954
|
file: { type: "string", required: true, description: "The CSV or JSONL file, or - for stdin" },
|
|
6915
6955
|
concurrency: { type: "string", description: "Calls in flight, 1 to 16 (default 4)" },
|
|
6916
6956
|
out: { type: "string", description: "Keep the run's files in this directory instead of ~/.kenso/runs" }
|
|
6917
6957
|
},
|
|
6918
|
-
examples: ["kenso accounts import --file accounts.csv", "kenso accounts import --file accounts.csv --yes"],
|
|
6958
|
+
examples: ["kenso accounts import --file accounts.csv --dry-run", "kenso accounts import --file accounts.csv --yes"],
|
|
6919
6959
|
beta: { featureGates: { all: ["accounts"] } },
|
|
6920
6960
|
operationIds: ["accounts.bulk-create", "credit-estimates.create"],
|
|
6921
6961
|
async run(ctx, args) {
|
|
@@ -7008,7 +7048,8 @@ var init_api3 = __esm({
|
|
|
7008
7048
|
command2 = {
|
|
7009
7049
|
resource: "api",
|
|
7010
7050
|
action: "",
|
|
7011
|
-
summary: "Call any
|
|
7051
|
+
summary: "Call any API route, only when no command fits",
|
|
7052
|
+
brief: "only when no command fits",
|
|
7012
7053
|
description: "The raw escape hatch: sends Authorization, Ken-Version: 1.1 and X-Ken-Workspace-Id, prints the full JSON body. The path may carry a query string. --all (GET only) follows pagination.nextCursor and prints the data[] items as a list. Writes run without --yes.",
|
|
7013
7054
|
flags: {
|
|
7014
7055
|
data: { type: "string", description: "Request body: inline JSON, @file or - for stdin" },
|
|
@@ -7319,7 +7360,8 @@ var init_login = __esm({
|
|
|
7319
7360
|
command3 = {
|
|
7320
7361
|
resource: "auth",
|
|
7321
7362
|
action: "login",
|
|
7322
|
-
summary: "Save an API key
|
|
7363
|
+
summary: "Save an API key (--with-key reads it from stdin: pipe a variable in, no check first)",
|
|
7364
|
+
brief: "--with-key reads the key from stdin: pipe a variable in, no check first",
|
|
7323
7365
|
description: "With --with-key, reads one API key from stdin, checks it with GET /v1/me and saves it to the profile (--profile, KENSO_PROFILE or the active one) in ~/.kenso/config.json with mode 0600. The workspace is --workspace when the key can reach it, else the key's only workspace. The key is never a flag value. A key in any environment variable: pipe that variable in as the example shows, with no check first (an empty or malformed key is refused, exit 2, without printing it).",
|
|
7324
7366
|
flags: {
|
|
7325
7367
|
"with-key": { type: "boolean", description: "Read the API key from stdin" },
|
|
@@ -7434,7 +7476,7 @@ var init_logout = __esm({
|
|
|
7434
7476
|
command4 = {
|
|
7435
7477
|
resource: "auth",
|
|
7436
7478
|
action: "logout",
|
|
7437
|
-
summary: "Remove a saved profile and its key
|
|
7479
|
+
summary: "Remove a saved profile and its key",
|
|
7438
7480
|
description: "Removes the profile (--profile, KENSO_PROFILE or the active one) from ~/.kenso/config.json. The key itself stays valid: revoke it at Settings > Integrations > API keys.",
|
|
7439
7481
|
flags: {},
|
|
7440
7482
|
examples: ["kenso auth logout", "kenso auth logout --profile client-a"],
|
|
@@ -7470,7 +7512,7 @@ var init_status = __esm({
|
|
|
7470
7512
|
command5 = {
|
|
7471
7513
|
resource: "auth",
|
|
7472
7514
|
action: "status",
|
|
7473
|
-
summary: "Show
|
|
7515
|
+
summary: "Show the key's user, workspace and expiry",
|
|
7474
7516
|
description: `Reads GET /v1/me for the email and workspaces, and GET /v1/api-keys for the key's expiry. Warns on stderr when the key expires within ${EXPIRY_WARNING_DAYS} days.`,
|
|
7475
7517
|
flags: {},
|
|
7476
7518
|
examples: ["kenso auth status", "kenso auth status --profile client-a"],
|
|
@@ -8200,7 +8242,7 @@ var init_wait = __esm({
|
|
|
8200
8242
|
command6 = {
|
|
8201
8243
|
resource: "jobs",
|
|
8202
8244
|
action: "wait",
|
|
8203
|
-
summary: "Wait for a background job
|
|
8245
|
+
summary: "Wait for a background job and print it",
|
|
8204
8246
|
description: "Polls GET /v1/jobs/{jobId}?type=<type> every 2 s (every 10 s after the first minute) until the job succeeds, fails or is canceled, then prints the job. Exit 0 when it succeeded, 1 when it failed, was canceled or is still running when --timeout passes.",
|
|
8205
8247
|
flags: {
|
|
8206
8248
|
type: { type: "string", required: true, description: "The job type, for example contact_import or contact_export" },
|
|
@@ -8440,7 +8482,8 @@ var init_import2 = __esm({
|
|
|
8440
8482
|
command7 = {
|
|
8441
8483
|
resource: "contacts",
|
|
8442
8484
|
action: "import",
|
|
8443
|
-
summary: "Add leads from a CSV or JSONL file to a campaign
|
|
8485
|
+
summary: "Add leads from a CSV or JSONL file to a campaign",
|
|
8486
|
+
brief: "a file into a campaign",
|
|
8444
8487
|
description: "Maps columns by header (email, firstName, lastName, title, linkedInUrl, companyName, companyDomain; every other column becomes a custom variable), checks every row locally, then sends 1,000 rows per call to POST /v1/campaigns/{campaignId}/contacts. Without --yes it prints the plan and the credit estimate. Rerunning the same command on the same file resumes the run. Exit 5 when some rows failed or were invalid, 6 when the contact limit stopped the run.",
|
|
8445
8488
|
flags: {
|
|
8446
8489
|
campaign: { type: "string", description: "The campaign id (required unless --template)" },
|
|
@@ -8452,10 +8495,8 @@ var init_import2 = __esm({
|
|
|
8452
8495
|
out: OUT_FLAG
|
|
8453
8496
|
},
|
|
8454
8497
|
examples: [
|
|
8455
|
-
"kenso contacts import --
|
|
8456
|
-
"kenso contacts import --campaign 812 --file leads.csv",
|
|
8498
|
+
"kenso contacts import --campaign 812 --file leads.csv --dry-run",
|
|
8457
8499
|
"kenso contacts import --campaign 812 --file leads.csv --yes",
|
|
8458
|
-
`kenso contacts import --campaign 812 --file leads.csv --map '{"Pain": "customVariables.pain_point", "Notes": null}' --yes`,
|
|
8459
8500
|
"kenso contacts import --campaign 812 --file big.csv --server-import --yes"
|
|
8460
8501
|
],
|
|
8461
8502
|
operationIds: ["contacts.create", "campaigns.get", "credit-estimates.create", "contact-imports.create", "jobs.get"],
|
|
@@ -8647,7 +8688,8 @@ var init_update = __esm({
|
|
|
8647
8688
|
command8 = {
|
|
8648
8689
|
resource: "contacts",
|
|
8649
8690
|
action: "update",
|
|
8650
|
-
summary: "Edit leads from a CSV or JSONL file,
|
|
8691
|
+
summary: "Edit leads from a CSV or JSONL file, by id or email (also one lead)",
|
|
8692
|
+
brief: "a file, by id or email; also one lead",
|
|
8651
8693
|
description: "Matches each row to a contact by its id (--match id, an id or contactId column) or by email within a campaign (--match email --campaign <id>), then sends 1,000 items per call to POST /v1/contacts/bulk-update. A column that is absent or a blank cell leaves the field unchanged; the literal null clears it. Other columns are custom variables. Without --yes it prints the plan. Exit 5 when some rows failed or were not found.",
|
|
8652
8694
|
flags: {
|
|
8653
8695
|
file: { type: "string", description: "CSV (with a header row) or JSONL file, or - for stdin" },
|
|
@@ -8658,9 +8700,10 @@ var init_update = __esm({
|
|
|
8658
8700
|
concurrency: CONCURRENCY_FLAG
|
|
8659
8701
|
},
|
|
8660
8702
|
examples: [
|
|
8661
|
-
"kenso contacts update --file edits.csv --match id",
|
|
8703
|
+
"kenso contacts update --file edits.csv --match id --dry-run",
|
|
8662
8704
|
"kenso contacts update --file edits.csv --match id --yes",
|
|
8663
|
-
"kenso contacts update --file edits.csv --match email --campaign 812 --
|
|
8705
|
+
"kenso contacts update --file edits.csv --match email --campaign 812 --dry-run",
|
|
8706
|
+
"printf 'id,title\\n9001,VP Sales\\n' | kenso contacts update --file - --match id --dry-run # one lead"
|
|
8664
8707
|
],
|
|
8665
8708
|
operationIds: ["contacts.bulk-update", "contacts.list"],
|
|
8666
8709
|
async run(ctx, { flags }) {
|
|
@@ -8920,20 +8963,21 @@ function bulkCommand(kind) {
|
|
|
8920
8963
|
ids: { type: "string", description: "Contact ids: one per line, or a CSV/JSONL with an id column; - for stdin" }
|
|
8921
8964
|
};
|
|
8922
8965
|
if (kind === "restore") flags.run = { type: "string", description: "Restore what an earlier contacts remove run removed" };
|
|
8923
|
-
else flags.where = { type: "string", description: "contacts search filters as JSON, @file or - (the body of POST /v1/contacts/search).
|
|
8966
|
+
else flags.where = { type: "string", description: "contacts search filters as JSON, @file or - (the body of POST /v1/contacts/search: campaignIds, q matches names and emails, filters match fields such as ContactCompany.Name). Its dry-run plan shows the count and first ids: check the match there, never with a search or list first" };
|
|
8924
8967
|
if (kind === "remove") flags.confirm = { type: "string", description: "Required to remove more than 1,000 contacts: the exact count" };
|
|
8925
8968
|
flags.out = OUT_FLAG;
|
|
8926
8969
|
flags.concurrency = CONCURRENCY_FLAG;
|
|
8927
8970
|
return {
|
|
8928
8971
|
resource: "contacts",
|
|
8929
8972
|
action: kind,
|
|
8930
|
-
summary: kind === "restore" ? `${route.verb} contacts by
|
|
8973
|
+
summary: kind === "restore" ? `${route.verb} contacts by --ids or by a remove run (--run)` : `${route.verb} contacts by --ids file or --where filters`,
|
|
8974
|
+
brief: kind === "restore" ? "--ids or --run" : "--ids file or --where, no search first",
|
|
8931
8975
|
description: `Sends the ids to POST ${route.path} in chunks of 1,000 and reports each id: ok, skipped (nothing to do), not_found or failed. Ids come from --ids` + (kind === "restore" ? " or from an earlier remove run (--run <runId>)" : " or from a contacts search (--where); a rerun of an unfinished --where run keeps its ids") + ". Without --yes it prints the count and the first ids." + (kind === "remove" ? " More than 1,000 ids also needs --confirm <count>. The summary prints the undo command." : ""),
|
|
8932
8976
|
flags,
|
|
8933
8977
|
examples: [
|
|
8934
8978
|
`kenso contacts ${kind} --ids ids.txt`,
|
|
8935
8979
|
`kenso contacts ${kind} --ids ids.txt --yes`,
|
|
8936
|
-
...kind === "restore" ? ["kenso contacts restore --run r_ab3dxk2q --yes"] : [`kenso contacts ${kind} --where '{"campaignIds": [812], "
|
|
8980
|
+
...kind === "restore" ? ["kenso contacts restore --run r_ab3dxk2q --yes"] : [`kenso contacts ${kind} --where '{"campaignIds": [812], "filters": [{"conditions": {"field": "ContactCompany.Name", "operator": 2, "value": "acme"}}]}' --dry-run`],
|
|
8937
8981
|
...kind === "remove" ? ["kenso contacts remove --ids big.csv --confirm 2500 --yes"] : []
|
|
8938
8982
|
],
|
|
8939
8983
|
...kind === "pause" ? { beta: { featureGates: { all: ["pause_leads"] } } } : {},
|
|
@@ -9074,7 +9118,8 @@ function tagCommand(kind) {
|
|
|
9074
9118
|
return {
|
|
9075
9119
|
resource: "contacts",
|
|
9076
9120
|
action: kind,
|
|
9077
|
-
summary: `${kind === "tag" ? "Add a tag to" : "Remove a tag from"} contacts by
|
|
9121
|
+
summary: `${kind === "tag" ? "Add a tag to" : "Remove a tag from"} contacts by --ids only (a campaign: list its ids first)`,
|
|
9122
|
+
brief: "--ids only",
|
|
9078
9123
|
description: (kind === "tag" ? "Calls POST /v1/tags/{tagId}/assignments once per contact id (a contact that already has the tag is skipped). " : "Calls DELETE /v1/tags/{tagId}/assignments once per contact id. ") + "Ids come one per line, or from a CSV/JSONL with an id column. Without --yes it prints the count. Exit 5 when some ids failed or were not found.",
|
|
9079
9124
|
flags: {
|
|
9080
9125
|
tag: { type: "string", description: "The tag id (see kenso tags list)" },
|
|
@@ -9082,7 +9127,7 @@ function tagCommand(kind) {
|
|
|
9082
9127
|
out: OUT_FLAG,
|
|
9083
9128
|
concurrency: CONCURRENCY_FLAG
|
|
9084
9129
|
},
|
|
9085
|
-
examples: [`kenso contacts ${kind} --tag 31 --ids ids.txt`, `kenso contacts ${kind} --tag 31 --ids ids.txt --yes`],
|
|
9130
|
+
examples: [`kenso contacts ${kind} --tag 31 --ids ids.txt --dry-run`, `kenso contacts ${kind} --tag 31 --ids ids.txt --yes`],
|
|
9086
9131
|
operationIds: [kind === "tag" ? "tags.assign" : "tags.unassign"],
|
|
9087
9132
|
async run(ctx, { flags }) {
|
|
9088
9133
|
const tagId = idFlag(flags, "tag", name);
|
|
@@ -9197,7 +9242,8 @@ var init_export = __esm({
|
|
|
9197
9242
|
command9 = {
|
|
9198
9243
|
resource: "contacts",
|
|
9199
9244
|
action: "export",
|
|
9200
|
-
summary: "Export a campaign's contacts to
|
|
9245
|
+
summary: "Export a campaign's contacts to CSV (a segment: contact-exports create)",
|
|
9246
|
+
brief: "a campaign to CSV; a segment: contact-exports create",
|
|
9201
9247
|
description: "Creates a contact export of the campaign (POST /v1/contact-exports, kind contacts), waits for the contact_export job, then downloads GET /v1/contact-exports/{exportId}/content to --out. Without --out it prints the job (add --wait to wait for it).",
|
|
9202
9248
|
flags: {
|
|
9203
9249
|
campaign: { type: "string", description: "The campaign id" },
|
|
@@ -9389,7 +9435,8 @@ var init_describe = __esm({
|
|
|
9389
9435
|
command10 = {
|
|
9390
9436
|
resource: "describe",
|
|
9391
9437
|
action: "",
|
|
9392
|
-
summary: "
|
|
9438
|
+
summary: "The command map as JSON: commands, flags, body schemas",
|
|
9439
|
+
brief: "",
|
|
9393
9440
|
description: "Always JSON. No arguments: every command with its summary. A resource: its commands with their flags. A resource and an action: one command in full (method, path, flags, body schema, examples). Beta commands your workspace has not enabled are left out unless --all-commands; calling one still works.",
|
|
9394
9441
|
flags: {
|
|
9395
9442
|
// Used by scripts/build-reference.ts to write docs/reference.md in one call.
|
|
@@ -9469,7 +9516,7 @@ var init_batch = __esm({
|
|
|
9469
9516
|
var crm_default;
|
|
9470
9517
|
var init_crm = __esm({
|
|
9471
9518
|
"docs/crm.md"() {
|
|
9472
|
-
crm_default = "# CRM (beta)\n\nThe 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.\n\n| Command | What it does |\n|---|---|\n| `accounts import` | Create accounts from a CSV or JSONL file. Research is charged per created account |\n| `people import` | Create or match people from a file, optionally onto accounts |\n| `accounts update --each`, `people update --each`, `opportunities update --each` | Edit many records from a file, one call per row |\n| `crm-connections`, `crm-imports`, `crm-sync-issues` | Read and run CRM sync. Connecting a CRM stays in the app |\n\nImport accounts first, then people: `people import` never creates an account.\n\n## Import accounts\n\n```bash\nkenso accounts import --file accounts.csv\nkenso accounts import --file accounts.csv --yes\n```\n\n```csv\nname,domain,industry,employeeCount\nAcme,acme.io,Software,120\nGlobex,globex.com,Manufacturing,\"1,200\"\n```\n\n- **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`.\n- **A domain is required.** A file with no domain column is a usage error, and a row without a domain is `invalid` (`domain_required`).\n- **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`.\n- **Rows go 100 per call** to `POST /v1/accounts/bulk-create`.\n\nEach row comes back
|
|
9519
|
+
crm_default = "# CRM (beta)\n\nThe 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.\n\n| Command | What it does |\n|---|---|\n| `accounts import` | Create accounts from a CSV or JSONL file. Research is charged per created account |\n| `people import` | Create or match people from a file, optionally onto accounts |\n| `accounts update --each`, `people update --each`, `opportunities update --each` | Edit many records from a file, one call per row |\n| `crm-connections`, `crm-imports`, `crm-sync-issues` | Read and run CRM sync. Connecting a CRM stays in the app |\n\nImport accounts first, then people: `people import` never creates an account.\n\n## Import accounts\n\n```bash\nkenso accounts import --file accounts.csv\nkenso accounts import --file accounts.csv --yes\n```\n\n```csv\nname,domain,industry,employeeCount\nAcme,acme.io,Software,120\nGlobex,globex.com,Manufacturing,\"1,200\"\n```\n\n- **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`.\n- **A domain is required.** A file with no domain column is a usage error, and a row without a domain is `invalid` (`domain_required`).\n- **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`.\n- **Rows go 100 per call** to `POST /v1/accounts/bulk-create`.\n\nEach row comes back `created` (`id` is the new account; its research is queued, `researchQueued`, or waits for credits, `researchWaitingCredits`), `skipped` with reason `domain_taken` (already an account; `id` is that account) or `failed` (`reason` is the API's code, `error` names the field; an item Kenso failed to save is sent once more first). Rerunning the same file is safe: accounts that exist come back `domain_taken`.\n\n## Import people\n\n```bash\nkenso people import --file people.csv\nkenso people import --file people.csv --find-emails --yes\nkenso people import --file people.jsonl --dry-run\n```\n\n```csv\nfullName,email,title,accountDomain,role\nAnn Lee,ann@acme.io,VP Sales,acme.io,champion\nBob Ray,,CTO,acme.io,\n```\n\n- **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.\n- **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`).\n- **To put a person on an account,** give `accountDomain` or `accountId`. A row with both sends only `accountId`.\n- **`--find-emails`** queues an email find (charged in credits) for each person without an email.\n- **Rows go 200 per call** to `POST /v1/people/batch`.\n\nPeople already in the workspace are matched by email, then LinkedIn URL, so running the same file again creates nobody twice. A row comes back `created` (a new person), `updated` (an existing person added to the account), `skipped` with reason `already_on_account`, `matched` (no account asked) or `previously_removed`, or `failed`: `account_not_found` saves the person without an account (import the account, then rerun to place them), any other reason is the API's. The summary adds `matched`, `addedToAccounts` and `emailFindsQueued`.\n\n## Edit accounts, people and opportunities\n\nApart 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:\n\n```bash\nkenso accounts update --each accounts-edit.csv --dry-run # then the same with --yes\nkenso people update --each people-edit.csv --dry-run\nkenso opportunities update --each deals.csv --dry-run\n```\n\n```csv\naccountId,stage,ownerMemberId,overwritePinned\n501,engaged,42,true\n502,customer,42,true\n```\n\nA 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`, only with the person's OK). Every flag of an update command: `kenso accounts update --help`.\n\n## CRM sync\n\nCRM sync has generated commands only: read connections, preview, start an import, and work through sync issues. Connect the CRM itself in the app.\n\n```bash\nkenso crm-connections list\nkenso crm-connections get --connection-id 7\nkenso crm-imports create --connection-id 7 --dry-run\nkenso crm-sync-issues list --connection-id 7 --all\n```\n\nRun `kenso crm-connections --help` (or add `--all-commands` when the actions are hidden) to see every action.\n";
|
|
9473
9520
|
}
|
|
9474
9521
|
});
|
|
9475
9522
|
|
|
@@ -9477,7 +9524,7 @@ var init_crm = __esm({
|
|
|
9477
9524
|
var leads_default;
|
|
9478
9525
|
var init_leads = __esm({
|
|
9479
9526
|
"docs/leads.md"() {
|
|
9480
|
-
leads_default = '# Leads\n\nLeads 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`.\n\n| Command | What it does |\n|---|---|\n| `contacts import` | Add leads from a CSV or JSONL file to a campaign, with one result per row |\n| `contacts update` | Edit leads in bulk from a file, matched by id or by email |\n| `contacts remove` / `restore` | Remove (soft-delete) leads by id, and undo it |\n| `contacts pause` / `resume` | Pause and resume the campaign sequence of leads (pause is beta, `pause_leads` flag) |\n| `contacts tag` / `untag` | Add or remove a tag on leads by id |\n| `contacts export` | Download a campaign\'s leads as one CSV file |\n\nAll 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.\n\n## Import a CSV into a campaign\n\n```bash\nkenso contacts import --template\nkenso contacts import --campaign 812 --file leads.csv --dry-run\nkenso contacts import --campaign 812 --file leads.csv --yes\n```\n\n`--template` prints the accepted columns. A minimal file:\n\n```csv\nemail,firstName,lastName,companyName,Pain Point\nann@acme.io,Ann,Lee,Acme,Slow onboarding\ncid@globex.com,Cid,Moe,Globex,Churn\n```\n\n### Columns\n\nHeaders are matched without regard to case, spaces or punctuation. The known fields and the other headers each one reads:\n\n| Field | Also read from | Rule |\n|---|---|---|\n| `email` | email address, e-mail, work email | Required, unless the campaign is LinkedIn-only and the row has a linkedin.com/in/ URL |\n| `firstName` | first name, first, given name | Required |\n| `lastName` | last name, last, surname, family name | Required |\n| `title` | job title, position, role | Optional |\n| `linkedInUrl` | linkedin, linkedin url, linkedin profile, profile url | Optional |\n| `companyName` | company, organization, account, account name | `companyName` or `companyDomain` is required |\n| `companyDomain` | website, domain, company website, url | `companyName` or `companyDomain` is required |\n\nEvery 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`.\n\n### Cells\n\n- A blank cell is never sent, so importing a file never erases data on a lead that is already in the campaign.\n- The literal value `null` clears a custom variable.\n\n### Rename or drop columns with --map\n\n`--map` takes a JSON object, `@file` or `-` for stdin. A value names the target field, and `null` drops the column:\n\n```bash\nkenso contacts import --campaign 812 --file leads.csv --map \'{"Pain": "customVariables.pain_point", "Notes": null}\' --yes\n```\n\n### JSONL\n\nEach line is one lead. Keys work like CSV headers, and a `customVariables` object is spread into custom variables:\n\n```json\n{"email":"ann@acme.io","firstName":"Ann","lastName":"Lee","companyName":"Acme","customVariables":{"pain_point":"Slow onboarding"}}\n```\n\n```bash\nkenso contacts import --campaign 812 --file leads.jsonl --yes\ncat leads.jsonl | kenso contacts import --campaign 812 --file - --yes\n```\n\nA run read from stdin cannot be resumed with `kenso runs resume`, so prefer a file for large imports.\n\n### What the CLI checks before sending\n\nEvery row is checked locally first. A row that fails goes to `invalid.csv` with one of these reasons and is not sent:\n\n| Reason | Meaning |\n|---|---|\n| `missing_email` | The row has no email (and is not a LinkedIn-only lead) |\n| `invalid_email` | The email is not a valid address |\n| `missing_first_name` | The row has no first name |\n| `missing_last_name` | The row has no last name |\n| `missing_company` | The row has neither a company name nor a company domain |\n| `sales_navigator_url` | An email-less row whose LinkedIn URL is a Sales Navigator link: add the email or a linkedin.com/in/ URL |\n| `duplicate_in_file` | An earlier row of the file has the same email |\n\nKenso stays the authority and checks every row again.\n\n### What the dry run shows\n\nThe 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`.\n\n### How rows are sent\n\nThe 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`.\n\n### Reading the result\n\nEach row in `results.csv` gets one status:\n\n| Status | Meaning |\n|---|---|\n| `created` | A new lead in the campaign |\n| `updated` | The lead was already there and was updated |\n| `skipped` | Kenso kept the lead out on purpose. `reason` is `duplicate`, `blocklisted`, `already_contacted`, `excluded` or another code |\n| `failed` | Kenso refused the row (`reason` such as `invalid_email` or `validation`, and an `error` message), or the call failed |\n| `invalid` | The CLI refused the row before sending it (see the table above) |\n\nA saved lead that cannot be emailed yet has `sendable` false, and the summary counts those leads in `notSendable` with a warning on stderr.\n\nExit 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).\n\n## Import a very large file on the server\n\nFor 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.\n\n```bash\nkenso contacts import --campaign 812 --file big.csv --server-import --yes\nkenso contacts import --campaign 812 --file big.csv --server-import --timeout 1h --yes\n```\n\n## Update leads in bulk\n\n```bash\nkenso contacts update --file edits.csv --match id\nkenso contacts update --file edits.csv --match id --yes\nkenso contacts update --file edits.csv --match email --campaign 812 --yes\n```\n\n- `--match id` reads the contact id from an `id` or `contactId` column.\n- `--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.\n- A column that is absent, or a blank cell, leaves that field unchanged. The literal `null` clears it.\n- 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.\n- 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`.\n\nTo edit one lead, use a one-row file from stdin:\n\n```bash\nprintf \'id,title\\n9001,VP Sales\\n\' | kenso contacts update --file - --match id --yes\n```\n\n## Remove, restore, pause and resume\n\nThese 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 `-`, and does the search itself: its plan shows how many contacts match and the first ids, so run no separate search or list first.\n\n```bash\nkenso contacts remove --ids ids.txt\nkenso contacts remove --ids ids.txt --yes\nkenso contacts remove --where \'{"campaignIds": [812], "q": "acme"}\'\nkenso contacts remove --where \'{"campaignIds": [812], "q": "acme"}\' --yes\nkenso contacts remove --ids big.csv --confirm 2500 --yes\n```\n\n- Without `--yes`, the plan prints the count and the first ids.\n- Removing more than 1,000 contacts also needs `--confirm <count>`, and the count must match. The plan\'s `next` already carries the right `--confirm`.\n- 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`.\n- The remove summary carries the undo command, `"undo":"kenso contacts restore --run r_ubqu7wtx"`.\n\n```bash\nkenso contacts restore --run r_ubqu7wtx --yes\nkenso contacts restore --ids ids.txt --yes\nkenso contacts pause --where \'{"campaignIds": [812]}\' --yes\nkenso contacts resume --ids ids.txt --yes\n```\n\n`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.\n\n## Tag and untag\n\n```bash\nkenso tags list\nkenso contacts tag --tag 31 --ids ids.txt --yes\nkenso contacts untag --tag 31 --ids ids.txt --yes\n```\n\n`tag` calls `POST /v1/tags/{tagId}/assignments` once per contact. A contact that already has the tag is `skipped` with reason `already_tagged`.\n\n## Export a campaign\'s leads\n\n```bash\nkenso contacts export --campaign 812 --out contacts.csv\nkenso contacts export --campaign 812 --wait\n```\n\nWith `--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`.\n\n## Recipes\n\n**Tag every lead of a campaign.** List the ids, then tag them:\n\n```bash\nkenso contacts list --campaign-id 812 --all -o ids > ids.txt\nkenso contacts tag --tag 31 --ids ids.txt --yes\n```\n\n**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`.\n\n**Retry only what failed.** Filter `results.csv` to the `failed` rows, take those rows from the input by `row` number, and import that file.\n\n**Pause leads who replied elsewhere.** Plan with the filter (the plan shows the count), then apply:\n\n```bash\nkenso contacts pause --where @replied.json\nkenso contacts pause --where @replied.json --yes\n```\n\n**Edit one field on many leads by email.**\n\n```bash\nkenso contacts update --file titles.csv --match email --campaign 812 --dry-run\nkenso contacts update --file titles.csv --match email --campaign 812 --yes\n```\n';
|
|
9527
|
+
leads_default = '# Leads\n\nLeads 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`.\n\n| Command | What it does |\n|---|---|\n| `contacts import` | Add leads from a CSV or JSONL file to a campaign, with one result per row |\n| `contacts update` | Edit leads in bulk from a file, matched by id or by email |\n| `contacts remove` / `restore` | Remove (soft-delete) leads by id, and undo it |\n| `contacts pause` / `resume` | Pause and resume the campaign sequence of leads (pause is beta, `pause_leads` flag) |\n| `contacts tag` / `untag` | Add or remove a tag on leads by id |\n| `contacts export` | Download a campaign\'s leads as one CSV file |\n\nAll 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.\n\n## Import a CSV into a campaign\n\n```bash\nkenso contacts import --template\nkenso contacts import --campaign 812 --file leads.csv --dry-run\nkenso contacts import --campaign 812 --file leads.csv --yes\n```\n\n`--template` prints the accepted columns. A minimal file:\n\n```csv\nemail,firstName,lastName,companyName,Pain Point\nann@acme.io,Ann,Lee,Acme,Slow onboarding\ncid@globex.com,Cid,Moe,Globex,Churn\n```\n\n### Columns\n\nHeaders are matched without regard to case, spaces or punctuation. The known fields and the other headers each one reads:\n\n| Field | Also read from | Rule |\n|---|---|---|\n| `email` | email address, e-mail, work email | Required, unless the campaign is LinkedIn-only and the row has a linkedin.com/in/ URL |\n| `firstName` | first name, first, given name | Required |\n| `lastName` | last name, last, surname, family name | Required |\n| `title` | job title, position, role | Optional |\n| `linkedInUrl` | linkedin, linkedin url, linkedin profile, profile url | Optional |\n| `companyName` | company, organization, account, account name | `companyName` or `companyDomain` is required |\n| `companyDomain` | website, domain, company website, url | `companyName` or `companyDomain` is required |\n\nEvery 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`.\n\n### Cells\n\n- A blank cell is never sent, so importing a file never erases data on a lead that is already in the campaign.\n- The literal value `null` clears a custom variable.\n\n### Rename or drop columns with --map\n\n`--map` takes a JSON object, `@file` or `-` for stdin. A value names the target field, and `null` drops the column:\n\n```bash\nkenso contacts import --campaign 812 --file leads.csv --map \'{"Pain": "customVariables.pain_point", "Notes": null}\' --yes\n```\n\n### JSONL\n\nEach line is one lead. Keys work like CSV headers, and a `customVariables` object is spread into custom variables:\n\n```json\n{"email":"ann@acme.io","firstName":"Ann","lastName":"Lee","companyName":"Acme","customVariables":{"pain_point":"Slow onboarding"}}\n```\n\n```bash\nkenso contacts import --campaign 812 --file leads.jsonl --yes\ncat leads.jsonl | kenso contacts import --campaign 812 --file - --yes\n```\n\nA run read from stdin cannot be resumed with `kenso runs resume`, so prefer a file for large imports.\n\n### What the CLI checks before sending\n\nEvery row is checked locally first. A row that fails goes to `invalid.csv` with one of these reasons and is not sent:\n\n| Reason | Meaning |\n|---|---|\n| `missing_email` | The row has no email (and is not a LinkedIn-only lead) |\n| `invalid_email` | The email is not a valid address |\n| `missing_first_name` | The row has no first name |\n| `missing_last_name` | The row has no last name |\n| `missing_company` | The row has neither a company name nor a company domain |\n| `sales_navigator_url` | An email-less row whose LinkedIn URL is a Sales Navigator link: add the email or a linkedin.com/in/ URL |\n| `duplicate_in_file` | An earlier row of the file has the same email |\n\nKenso stays the authority and checks every row again.\n\n### What the dry run shows\n\nThe 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`.\n\n### How rows are sent\n\nThe 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`.\n\n### Reading the result\n\nEach row in `results.csv` gets one status:\n\n| Status | Meaning |\n|---|---|\n| `created` | A new lead in the campaign |\n| `updated` | The lead was already there and was updated |\n| `skipped` | Kenso kept the lead out on purpose. `reason` is `duplicate`, `blocklisted`, `already_contacted`, `excluded` or another code |\n| `failed` | Kenso refused the row (`reason` such as `invalid_email` or `validation`, and an `error` message), or the call failed |\n| `invalid` | The CLI refused the row before sending it (see the table above) |\n\nA saved lead that cannot be emailed yet has `sendable` false, and the summary counts those leads in `notSendable` with a warning on stderr.\n\nExit 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).\n\n## Import a very large file on the server\n\nFor 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.\n\n```bash\nkenso contacts import --campaign 812 --file big.csv --server-import --yes\nkenso contacts import --campaign 812 --file big.csv --server-import --timeout 1h --yes\n```\n\n## Update leads in bulk\n\n```bash\nkenso contacts update --file edits.csv --match id\nkenso contacts update --file edits.csv --match id --yes\nkenso contacts update --file edits.csv --match email --campaign 812 --yes\n```\n\n- `--match id` reads the contact id from an `id` or `contactId` column.\n- `--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.\n- A column that is absent, or a blank cell, leaves that field unchanged. The literal `null` clears it.\n- 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.\n- 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`.\n\nTo edit one lead, use a one-row file from stdin:\n\n```bash\nprintf \'id,title\\n9001,VP Sales\\n\' | kenso contacts update --file - --match id --yes\n```\n\n## Remove, restore, pause and resume\n\nThese 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 `-`, and does the search itself: its plan shows how many contacts match and the first ids, so run no separate search or list first. `q` matches names and email addresses only; a company is a `filters` condition on `ContactCompany.Name` (`operator` 2 is contains; `kenso describe contacts search` lists the fields and operators).\n\n```bash\nkenso contacts remove --ids ids.txt --dry-run\nkenso contacts remove --ids ids.txt --yes\nkenso contacts remove --where \'{"campaignIds": [812], "filters": [{"conditions": {"field": "ContactCompany.Name", "operator": 2, "value": "acme"}}]}\' --dry-run\nkenso contacts remove --where \'{"campaignIds": [812], "filters": [{"conditions": {"field": "ContactCompany.Name", "operator": 2, "value": "acme"}}]}\' --yes\nkenso contacts remove --ids big.csv --confirm 2500 --yes\n```\n\n- Without `--yes`, the plan prints the count and the first ids.\n- Removing more than 1,000 contacts also needs `--confirm <count>`, and the count must match. The plan\'s `next` already carries the right `--confirm`.\n- 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`.\n- The remove summary carries the undo command, `"undo":"kenso contacts restore --run r_ubqu7wtx"`.\n\n```bash\nkenso contacts restore --run r_ubqu7wtx --yes\nkenso contacts restore --ids ids.txt --yes\nkenso contacts pause --where \'{"campaignIds": [812]}\' --yes\nkenso contacts resume --ids ids.txt --yes\n```\n\n`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.\n\n## Tag and untag\n\n```bash\nkenso tags list\nkenso contacts tag --tag 31 --ids ids.txt --yes\nkenso contacts untag --tag 31 --ids ids.txt --yes\n```\n\n`tag` calls `POST /v1/tags/{tagId}/assignments` once per contact. A contact that already has the tag is `skipped` with reason `already_tagged`.\n\n## Export a campaign\'s leads\n\n```bash\nkenso contacts export --campaign 812 --out contacts.csv\nkenso contacts export --campaign 812 --wait\n```\n\nWith `--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`.\n\n## Recipes\n\n**Tag every lead of a campaign.** List the ids, then tag them:\n\n```bash\nkenso contacts list --campaign-id 812 --all -o ids > ids.txt\nkenso contacts tag --tag 31 --ids ids.txt --yes\n```\n\n**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`.\n\n**Retry only what failed.** Filter `results.csv` to the `failed` rows, take those rows from the input by `row` number, and import that file.\n\n**Pause leads who replied elsewhere.** Plan with the filter (the plan shows the count), then apply:\n\n```bash\nkenso contacts pause --where @replied.json\nkenso contacts pause --where @replied.json --yes\n```\n\n**Edit one field on many leads by email.**\n\n```bash\nkenso contacts update --file titles.csv --match email --campaign 812 --dry-run\nkenso contacts update --file titles.csv --match email --campaign 812 --yes\n```\n';
|
|
9481
9528
|
}
|
|
9482
9529
|
});
|
|
9483
9530
|
|
|
@@ -9509,7 +9556,7 @@ var init_studio = __esm({
|
|
|
9509
9556
|
var SKILL_default;
|
|
9510
9557
|
var init_SKILL = __esm({
|
|
9511
9558
|
"skill/kenso-cli/SKILL.md"() {
|
|
9512
|
-
SKILL_default = '---\nname: kenso-cli\ndescription: The Kenso CLI (kenso) for work from a file or over about 50 records - import, update, remove, pause, tag or export leads, import CRM accounts and people, edit Studio templates as local files and fill them, run an API operation per file row. For one record or a question, use the Kenso MCP.\n---\n\n1. Every write runs first with `--dry-run
|
|
9559
|
+
SKILL_default = '---\nname: kenso-cli\ndescription: The Kenso CLI (kenso) for work from a file (of any size, even a few ids) or over about 50 records - import, update, remove (delete), pause, tag or export leads, import CRM accounts and people, edit Studio templates as local files and fill them, run an API operation per file row, resume an interrupted run, sign in with a key from a variable (pipe it to kenso auth login --with-key, no check first). For records listed in the message (under about 50), one record or a question, use the Kenso MCP.\n---\n\n1. Every write runs first with `--dry-run`; apply with the plan\'s `next`. `--yes` at once only to continue an interrupted run or apply what the person confirmed.\n2. Many rows: one command on the file as it is (`--file`, `--ids`, `--each`). Run kenso only: no python, awk or shell tests, not even on a file or variable.\n3. Any output\'s `next` is the command to run; errors are JSON on stderr. `--force` only with the person\'s OK.\n4. Ids are flags: `--campaign 812` on contacts import, update, export; `--campaign-id 812` on API commands. `--workspace <id>`; by name `kenso workspace list --q acme`. Other commands: `kenso --help`; `kenso api <METHOD> <path>` only when none fits.\n\n```bash\nkenso contacts import --campaign 812 --file leads.csv --dry-run # skips leads already in; --map \'{"Pain": "customVariables.pain_point", "Notes": null}\'; over 200 MB --server-import\nkenso contacts update --file edits.csv --match id --dry-run # also one lead; or --match email --campaign 812\nkenso contacts remove --ids ids.txt --dry-run # also pause, resume, restore; over 1,000: --confirm <count>\nkenso contacts remove --where \'{"campaignIds": [812], "filters": [{"conditions": {"field": "ContactCompany.Name", "operator": 2, "value": "acme"}}]}\' --dry-run # also pause, resume; 2 = contains; no search first\nkenso contacts tag --tag 31 --ids ids.txt --dry-run # tag, restore: --ids only, so first kenso contacts list --campaign-id 812 --all -o ids > ids.txt\nkenso contact-imports create --source campaign --source-campaign-id 10601 --campaign-id 812 --dry-run # copy another campaign\'s leads in\nkenso contacts export --campaign 812 --out leads.csv # a segment: kenso contact-exports create --segment-id 55\nkenso runs resume <runId> # an interrupted run; terminal lost: kenso runs list finds it. A changed file is a new run\nkenso accounts import --file accounts.csv --dry-run # companies; a file of people: kenso people import --file people.csv\nkenso studio push 12 --dir ./tpl # to preview local edits push first (draft; live needs --publish), then studio preview 12\nkenso studio fill 12 --accounts ids.txt --dry-run # or --campaign 10546 --contacts leads.csv\nkenso opportunities update --each rows.csv --dry-run # any API command on a file of rows (CSV or JSONL; keys fill flags and body). One record: kenso campaigns update --campaign-id 812 --name Q4 --dry-run\nkenso mcp call list_skills # only to list Ken\'s skills\n```\n\nCharges: `accounts import` per new account, `studio fill` per page (again for ids from another file): show the estimate. `pinned_by_user`: with the person\'s OK, rerun with `--overwrite-pinned`. Workflows: `kenso workflows brief` first; secrets only as `{{secrets.NAME}}`. Key: `KENSO_API_KEY`, or pipe a variable, no check: `printf \'%s\' "$KEY" | kenso auth login --with-key`; never in a flag, output or file.\n';
|
|
9513
9560
|
}
|
|
9514
9561
|
});
|
|
9515
9562
|
|
|
@@ -9603,7 +9650,8 @@ var init_docs = __esm({
|
|
|
9603
9650
|
command11 = {
|
|
9604
9651
|
resource: "docs",
|
|
9605
9652
|
action: "",
|
|
9606
|
-
summary: "
|
|
9653
|
+
summary: "Offline docs: the topic list, or one topic",
|
|
9654
|
+
brief: "",
|
|
9607
9655
|
description: "Without a topic, lists the topics with one line each (NDJSON when piped). With a topic, prints that page's Markdown to stdout. The same pages are in the package's docs/ folder.",
|
|
9608
9656
|
flags: {},
|
|
9609
9657
|
positionals: [{ name: "topic", description: `One of ${NAMES}` }],
|
|
@@ -9690,7 +9738,8 @@ var init_doctor = __esm({
|
|
|
9690
9738
|
command12 = {
|
|
9691
9739
|
resource: "doctor",
|
|
9692
9740
|
action: "",
|
|
9693
|
-
summary: "Check
|
|
9741
|
+
summary: "Check Node, the config, the API and the key",
|
|
9742
|
+
brief: "",
|
|
9694
9743
|
description: "Prints the version, Node, platform, config location, profile, key source, API URL, workspace and detected agent, plus checks node_version, config_permissions, api_reachable and key_valid. Exits 0 when every check passes, 3 when the key check fails, else 1.",
|
|
9695
9744
|
flags: {},
|
|
9696
9745
|
examples: ["kenso doctor", "kenso doctor --profile client-a --output json"],
|
|
@@ -10006,13 +10055,14 @@ var init_call = __esm({
|
|
|
10006
10055
|
command13 = {
|
|
10007
10056
|
resource: "mcp",
|
|
10008
10057
|
action: "call",
|
|
10009
|
-
summary: "Call a hosted Kenso MCP tool, such as list_skills
|
|
10010
|
-
|
|
10058
|
+
summary: "Call a hosted Kenso MCP tool, such as list_skills (no mcp tools first)",
|
|
10059
|
+
brief: "list_skills runs directly",
|
|
10060
|
+
description: "Calls one Kenso MCP tool with the same API key and prints its result: structuredContent when the tool returns it, else its text parsed as JSON, else the text. Covers MCP-only tools, such as the skills: list_skills and load_skill take no kenso mcp tools first. A tool error exits 1 with the tool's message. A facade tool takes its operation with --operation.",
|
|
10011
10061
|
flags: {
|
|
10012
10062
|
operation: { type: "string", description: "The facade operation, for example list" },
|
|
10013
10063
|
args: { type: "string", description: "Tool arguments as a JSON object: inline, @file or - for stdin" }
|
|
10014
10064
|
},
|
|
10015
|
-
positionals: [{ name: "tool", required: true, description: "The tool name
|
|
10065
|
+
positionals: [{ name: "tool", required: true, description: "The tool name: list_skills, load_skill, or one from kenso mcp tools" }],
|
|
10016
10066
|
examples: [`kenso mcp call contacts --operation list --args '{"limit":5}'`, "kenso mcp call list_skills", `kenso mcp call load_skill --args '{"name":"create-campaign"}'`],
|
|
10017
10067
|
operationIds: [],
|
|
10018
10068
|
async run(ctx, { flags, positionals }) {
|
|
@@ -10069,6 +10119,7 @@ var init_tools = __esm({
|
|
|
10069
10119
|
resource: "mcp",
|
|
10070
10120
|
action: "tools",
|
|
10071
10121
|
summary: "List the hosted Kenso MCP's tools",
|
|
10122
|
+
brief: "only to find a tool name",
|
|
10072
10123
|
description: "Lists every tool of the Kenso MCP (KENSO_MCP_URL, default https://mcp.ken.so/mcp) as { name, description }, with the same API key. Call one with kenso mcp call <tool>.",
|
|
10073
10124
|
flags: {},
|
|
10074
10125
|
examples: ["kenso mcp tools", "kenso mcp tools --fields name"],
|
|
@@ -10202,6 +10253,7 @@ var init_import3 = __esm({
|
|
|
10202
10253
|
resource: "people",
|
|
10203
10254
|
action: "import",
|
|
10204
10255
|
summary: "Create or match people from a CSV or JSONL file, optionally onto accounts",
|
|
10256
|
+
brief: "a file of people",
|
|
10205
10257
|
description: "Sends the rows to people.batch, 200 per call. Columns map by name (case, spaces and dashes ignored): email, fullName (or name), firstName, lastName, linkedinUrl (or linkedin, profileUrl), title, accountDomain (or domain, website, companyDomain), accountId, role and priority. A person needs a name: fullName, or firstName and lastName. Other columns are ignored with a warning. People already in the workspace are matched by email, then LinkedIn URL, so running the same file again creates nothing. It never creates accounts: a row whose accountDomain matches no account fails with account_not_found. Without --yes it only prints the plan.",
|
|
10206
10258
|
flags: {
|
|
10207
10259
|
file: { type: "string", required: true, description: "The CSV or JSONL file, or - for stdin" },
|
|
@@ -10210,7 +10262,7 @@ var init_import3 = __esm({
|
|
|
10210
10262
|
out: { type: "string", description: "Keep the run's files in this directory instead of ~/.kenso/runs" }
|
|
10211
10263
|
},
|
|
10212
10264
|
examples: [
|
|
10213
|
-
"kenso people import --file people.csv",
|
|
10265
|
+
"kenso people import --file people.csv --dry-run",
|
|
10214
10266
|
"kenso people import --file people.csv --find-emails --yes",
|
|
10215
10267
|
"kenso people import --file people.jsonl --dry-run"
|
|
10216
10268
|
],
|
|
@@ -10291,7 +10343,8 @@ var init_list = __esm({
|
|
|
10291
10343
|
command16 = {
|
|
10292
10344
|
resource: "runs",
|
|
10293
10345
|
action: "list",
|
|
10294
|
-
summary: "List batch runs, newest first",
|
|
10346
|
+
summary: "List batch runs, newest first (finds an interrupted run)",
|
|
10347
|
+
brief: "finds an interrupted run",
|
|
10295
10348
|
description: "Lists the runs under the runs directory (KENSO_RUNS_DIR, else ~/.kenso/runs). Needs no API key.",
|
|
10296
10349
|
flags: {
|
|
10297
10350
|
limit: { type: "string", default: "20", description: "How many runs to list, 1-1000 (default 20)" }
|
|
@@ -10337,7 +10390,7 @@ var init_show = __esm({
|
|
|
10337
10390
|
command17 = {
|
|
10338
10391
|
resource: "runs",
|
|
10339
10392
|
action: "show",
|
|
10340
|
-
summary: "Show one batch run
|
|
10393
|
+
summary: "Show one batch run and its files",
|
|
10341
10394
|
flags: {},
|
|
10342
10395
|
positionals: [{ name: "runId", required: true, description: "The run id (see kenso runs list)" }],
|
|
10343
10396
|
examples: ["kenso runs show r_ab3dxk2q"],
|
|
@@ -10430,7 +10483,8 @@ var init_resume = __esm({
|
|
|
10430
10483
|
command18 = {
|
|
10431
10484
|
resource: "runs",
|
|
10432
10485
|
action: "resume",
|
|
10433
|
-
summary: "Continue
|
|
10486
|
+
summary: "Continue an interrupted batch run (its command with --yes, no dry run)",
|
|
10487
|
+
brief: "with --yes, no dry run",
|
|
10434
10488
|
description: "Rebuilds the run's command from run.json and runs it with --yes from the directory it started in. The engine finds the run by its identity and skips the chunks already done. Refused when the input file is gone or changed.",
|
|
10435
10489
|
flags: {},
|
|
10436
10490
|
positionals: [{ name: "runId", required: true, description: "The run id (see kenso runs list)" }],
|
|
@@ -10558,7 +10612,8 @@ var init_install = __esm({
|
|
|
10558
10612
|
command19 = {
|
|
10559
10613
|
resource: "skill",
|
|
10560
10614
|
action: "install",
|
|
10561
|
-
summary: "Install the kenso-cli
|
|
10615
|
+
summary: "Install the kenso-cli skill for Claude Code, Codex or Cursor",
|
|
10616
|
+
brief: "",
|
|
10562
10617
|
description: "Writes the Agent Skill bundled with this CLI to SKILL.md in each agent's skills folder: ~/.claude/skills/kenso-cli (Claude Code), ~/.agents/skills/kenso-cli (Codex) and ~/.cursor/skills/kenso-cli (Cursor). The default client is the coding agent running the command, else all three. --dir writes into that folder instead. A different SKILL.md already there is replaced only with --force (exit 7 without it). --dry-run prints the plan and writes nothing.",
|
|
10563
10618
|
flags: {
|
|
10564
10619
|
client: { type: "string", description: "claude-code, codex, cursor or all (default: the detected agent, else all)" },
|
|
@@ -11156,9 +11211,10 @@ var init_preview = __esm({
|
|
|
11156
11211
|
resource: "studio",
|
|
11157
11212
|
action: "preview",
|
|
11158
11213
|
summary: "Render a saved version for up to 3 accounts; for local edits, studio push them to the draft first",
|
|
11214
|
+
brief: "push local edits first",
|
|
11159
11215
|
description: "Calls asset-templates.preview: renders the draft (else the latest published version, or --version-id) for --account-ids (up to 3; default the 3 most recently touched accounts, else the bundle's samples) and prints each preview's link, screenshots, console errors and link issues. It takes up to a minute per account. The preview renders what is saved, not local files: with --dir it compares the folder to that version and warns when they differ (run kenso studio push first).",
|
|
11160
11216
|
flags: {
|
|
11161
|
-
dir: { type: "string", description: "A template folder to compare with the version being rendered" },
|
|
11217
|
+
dir: { type: "string", description: "A template folder to compare with the version being rendered; it renders no local edits (studio push them first)" },
|
|
11162
11218
|
"account-ids": { type: "string", description: "Up to 3 account ids, separated by commas" },
|
|
11163
11219
|
mode: { type: "string", description: "render (default) or interact" },
|
|
11164
11220
|
sample: { type: "string", description: "A bundle sample to render, such as samples/acme.json" },
|
|
@@ -11421,6 +11477,7 @@ var init_fill = __esm({
|
|
|
11421
11477
|
resource: "studio",
|
|
11422
11478
|
action: "fill",
|
|
11423
11479
|
summary: "Fill a template on many accounts or campaign leads (estimate first)",
|
|
11480
|
+
brief: "estimate first",
|
|
11424
11481
|
description: "Reads ids one per line, or a CSV/JSONL with an id column (--column, else id, else contactId), from --accounts, or from --contacts with --campaign for a campaign's leads. It always asks for the cost first (asset-runs.estimate) and prints it; --yes then creates one fill run per 500 ids, one after another, and --wait follows each run (every 5 s) until it completes, fails or is cancelled. The summary lists the run ids and their states. A fill charges for every page it builds, so the same ids are not filled twice: an unfinished fill continues, and a finished one is refused (exit 7) with how to retry only the failed ids.",
|
|
11425
11482
|
flags: {
|
|
11426
11483
|
accounts: { type: "string", description: "Account ids: a file, or - for stdin" },
|
|
@@ -11432,7 +11489,7 @@ var init_fill = __esm({
|
|
|
11432
11489
|
},
|
|
11433
11490
|
positionals: [TEMPLATE_ID],
|
|
11434
11491
|
examples: [
|
|
11435
|
-
"kenso studio fill 12 --accounts accounts.txt",
|
|
11492
|
+
"kenso studio fill 12 --accounts accounts.txt --dry-run",
|
|
11436
11493
|
"kenso studio fill 12 --accounts accounts.csv --column accountId --yes --wait",
|
|
11437
11494
|
"kenso studio fill 12 --campaign 345 --contacts leads.csv --yes"
|
|
11438
11495
|
],
|
|
@@ -11563,7 +11620,7 @@ var init_use = __esm({
|
|
|
11563
11620
|
command26 = {
|
|
11564
11621
|
resource: "workspace",
|
|
11565
11622
|
action: "use",
|
|
11566
|
-
summary: "
|
|
11623
|
+
summary: "Set the profile's default workspace",
|
|
11567
11624
|
description: "Checks the workspace with GET /v1/workspaces/{workspaceId}, then saves it to the profile (--profile, KENSO_PROFILE or the active one). --workspace and KENSO_WORKSPACE_ID still win for a single command.",
|
|
11568
11625
|
flags: {},
|
|
11569
11626
|
positionals: [{ name: "workspaceId", required: true, description: "The workspace id (see kenso workspace list)" }],
|
|
@@ -11721,6 +11778,24 @@ function wrap(text2, indent) {
|
|
|
11721
11778
|
if (line) out.push(indent + line);
|
|
11722
11779
|
return out.join("\n");
|
|
11723
11780
|
}
|
|
11781
|
+
function wrapCell(line, at) {
|
|
11782
|
+
const indent = " ".repeat(at);
|
|
11783
|
+
const out = [];
|
|
11784
|
+
let current = line.slice(0, at).trimEnd();
|
|
11785
|
+
let first = true;
|
|
11786
|
+
for (const word of line.slice(at).split(" ")) {
|
|
11787
|
+
const start = first ? `${line.slice(0, at)}` : `${indent}`;
|
|
11788
|
+
if (!first && current.length + 1 + word.length > 110) {
|
|
11789
|
+
out.push(current);
|
|
11790
|
+
current = `${indent}${word}`;
|
|
11791
|
+
} else {
|
|
11792
|
+
current = first ? `${start}${word}` : `${current} ${word}`;
|
|
11793
|
+
}
|
|
11794
|
+
first = false;
|
|
11795
|
+
}
|
|
11796
|
+
out.push(current);
|
|
11797
|
+
return out.join("\n");
|
|
11798
|
+
}
|
|
11724
11799
|
function columns(rows, indent = " ", maxWidth = 28) {
|
|
11725
11800
|
const width = Math.min(maxWidth, Math.max(0, ...rows.map(([left]) => left.length)));
|
|
11726
11801
|
return rows.map(([left, right]) => `${indent}${left.padEnd(width)} ${right}`.trimEnd()).join("\n");
|
|
@@ -11752,35 +11827,59 @@ async function loadArea(resource) {
|
|
|
11752
11827
|
}
|
|
11753
11828
|
async function renderTopHelp(opts) {
|
|
11754
11829
|
const lines = [
|
|
11755
|
-
`kenso ${VERSION}
|
|
11830
|
+
`kenso ${VERSION}, the Kenso CLI. Usage: kenso <resource> <action> [flags]; add --help to any of them.`,
|
|
11756
11831
|
"",
|
|
11757
|
-
"
|
|
11758
|
-
"
|
|
11759
|
-
"
|
|
11760
|
-
"",
|
|
11761
|
-
"Rules:",
|
|
11762
|
-
" Run every write with --dry-run first and read the plan. Then apply: a batch command (import, update, remove,",
|
|
11763
|
-
" tag, --each) with the plan's next (--yes); any other write without --dry-run.",
|
|
11764
|
-
" Many rows are one command with the file as it is (--file, --ids, --each), never one command per row: kenso",
|
|
11765
|
-
" checks and counts the rows. No scripts or shell tests.",
|
|
11766
|
-
" API resource commands take ids as flags (kenso campaigns get --campaign-id 812), never as bare arguments.",
|
|
11767
|
-
" An error is one JSON line on stderr: run its next.",
|
|
11832
|
+
"Rules: every write runs first with --dry-run. Then a batch command (import, update, remove, tag, --each) applies",
|
|
11833
|
+
"with the plan's next (--yes), any other write by running again without --dry-run. A file is one command as it is",
|
|
11834
|
+
"(--file, --ids, --each), never one per row; no python, awk or shell tests. Ids are flags (--campaign-id 812).",
|
|
11835
|
+
"Any output's next is the command to run; errors are JSON on stderr.",
|
|
11768
11836
|
"",
|
|
11769
11837
|
"Commands:"
|
|
11770
11838
|
];
|
|
11771
11839
|
const getFeatures = lazyFeatures(opts);
|
|
11772
11840
|
const hiddenSpecs = { count: 0, gates: /* @__PURE__ */ new Set(), names: [] };
|
|
11773
11841
|
const rows = [];
|
|
11842
|
+
const also = [];
|
|
11774
11843
|
for (const resource of Object.keys(handWritten).sort()) {
|
|
11775
|
-
|
|
11844
|
+
const specs = await visibleSpecs(await loadArea(resource), opts, getFeatures, hiddenSpecs);
|
|
11845
|
+
const withPositionals = (spec, name) => {
|
|
11776
11846
|
const positionals = (spec.positionals ?? []).map((p) => p.required ? `<${p.name}>` : `[${p.name}]`).join(" ");
|
|
11777
|
-
|
|
11778
|
-
|
|
11847
|
+
return `${name}${positionals ? ` ${positionals}` : ""}`;
|
|
11848
|
+
};
|
|
11849
|
+
if (opts.allCommands) {
|
|
11850
|
+
for (const spec of specs) {
|
|
11851
|
+
rows.push([withPositionals(spec, commandName(spec).slice("kenso ".length)), `${spec.summary}${spec.beta ? " (beta)" : ""}`]);
|
|
11852
|
+
}
|
|
11853
|
+
continue;
|
|
11779
11854
|
}
|
|
11855
|
+
const groups = /* @__PURE__ */ new Map();
|
|
11856
|
+
for (const spec of specs) {
|
|
11857
|
+
if (!spec.action) {
|
|
11858
|
+
if (spec.brief !== void 0) also.push(`${withPositionals(spec, spec.resource)}${spec.brief ? ` (${spec.brief})` : ""}`);
|
|
11859
|
+
else rows.push([withPositionals(spec, spec.resource), spec.summary]);
|
|
11860
|
+
continue;
|
|
11861
|
+
}
|
|
11862
|
+
const hint = `${spec.brief ? ` (${spec.brief})` : ""}${spec.beta ? " (beta)" : ""}`;
|
|
11863
|
+
const names = groups.get(hint) ?? [];
|
|
11864
|
+
names.push(withPositionals(spec, spec.action));
|
|
11865
|
+
groups.set(hint, names);
|
|
11866
|
+
}
|
|
11867
|
+
const actions15 = [...groups.entries()].map(([hint, names]) => `${names.join(", ")}${hint}`);
|
|
11868
|
+
if (actions15.length > 0) rows.push([resource, actions15.join(", ")]);
|
|
11780
11869
|
}
|
|
11781
|
-
|
|
11870
|
+
const at = 4 + Math.min(22, Math.max(0, ...rows.map(([left]) => left.length)));
|
|
11871
|
+
lines.push(
|
|
11872
|
+
columns(rows, " ", 22).split("\n").map((line) => line.length > 110 && line.slice(at - 2, at) === " " && line[at] !== " " ? wrapCell(line, at) : line).join("\n")
|
|
11873
|
+
);
|
|
11874
|
+
if (also.length > 0) lines.push(wrap(`Also: ${also.join(", ")}.`, " "));
|
|
11782
11875
|
if (hiddenSpecs.names.length > 0) {
|
|
11783
|
-
|
|
11876
|
+
const byArea = /* @__PURE__ */ new Map();
|
|
11877
|
+
for (const name of hiddenSpecs.names.sort()) {
|
|
11878
|
+
const [area = "", action = ""] = name.split(" ");
|
|
11879
|
+
byArea.set(area, [...byArea.get(area) ?? [], action]);
|
|
11880
|
+
}
|
|
11881
|
+
const named = [...byArea.entries()].map(([area, actions15]) => `${area} ${actions15.join("|")}`);
|
|
11882
|
+
lines.push(wrap(`Beta, hidden here but they run (the server decides): ${named.join(", ")}.`, " "));
|
|
11784
11883
|
}
|
|
11785
11884
|
const generated = await listGenerated({ features: opts.allCommands ? [] : getFeatures, env: opts.env });
|
|
11786
11885
|
const visible = generated.filter((g) => opts.allCommands || !g.beta);
|
|
@@ -11798,18 +11897,17 @@ async function renderTopHelp(opts) {
|
|
|
11798
11897
|
} else {
|
|
11799
11898
|
const names = new Set(sorted.map(([resource]) => resource));
|
|
11800
11899
|
const common = COMMON_RESOURCES.filter((r) => names.has(r)).join(", ");
|
|
11900
|
+
const hidden = generated.length - visible.length + hiddenSpecs.count;
|
|
11801
11901
|
lines.push(
|
|
11802
11902
|
"",
|
|
11803
|
-
`API resources (${names.size}),
|
|
11903
|
+
`API resources (${names.size}), named like their /v1 route (kenso contact-exports); kenso <resource> --help lists actions.`,
|
|
11804
11904
|
wrap(`Common: ${common}.`, " "),
|
|
11805
|
-
|
|
11905
|
+
` Every resource: kenso --help --all-commands${hidden > 0 ? ` (also shows ${hidden} hidden beta commands)` : ""}.`
|
|
11806
11906
|
);
|
|
11807
11907
|
}
|
|
11808
11908
|
}
|
|
11809
|
-
const hidden = generated.length - visible.length + hiddenSpecs.count;
|
|
11810
|
-
if (hidden > 0) lines.push("", `${hidden} beta command${hidden === 1 ? " is" : "s are"} hidden; --all-commands shows them.`);
|
|
11811
11909
|
if (opts.allCommands) lines.push("", "Global flags:", columns(GLOBAL_FLAG_HELP), "", EXIT_CODES);
|
|
11812
|
-
else lines.push("", GLOBAL_FLAGS_LINE
|
|
11910
|
+
else lines.push("", GLOBAL_FLAGS_LINE);
|
|
11813
11911
|
return withAgentBlock(lines.join("\n"), opts.agent);
|
|
11814
11912
|
}
|
|
11815
11913
|
async function renderResourceHelp(resource, opts) {
|
|
@@ -11851,6 +11949,7 @@ async function renderResourceHelp(resource, opts) {
|
|
|
11851
11949
|
lines.push("", `Beta, hidden for this workspace (they still run; the server decides): ${hiddenActions.join(", ")}.`);
|
|
11852
11950
|
}
|
|
11853
11951
|
lines.push("", `Run kenso ${resource} <action> --help for an action's flags.`);
|
|
11952
|
+
if (all.length === 0) lines.push("A file of rows is one command, never one per row: kenso " + resource + " <action> --each <file> --dry-run.");
|
|
11854
11953
|
return withAgentBlock(lines.join("\n"), opts.agent);
|
|
11855
11954
|
}
|
|
11856
11955
|
function renderCommandHelp(spec, opts) {
|
|
@@ -11860,13 +11959,13 @@ function renderCommandHelp(spec, opts) {
|
|
|
11860
11959
|
lines.push("", "Arguments:", columns(spec.positionals.map((p) => [`<${p.name}>`, `${p.description}${p.required ? "" : " (optional)"}`])));
|
|
11861
11960
|
}
|
|
11862
11961
|
const paired = (name) => spec.flags[name]?.type === "boolean" && spec.flags[`no-${name}`]?.type === "boolean";
|
|
11863
|
-
const flagRows = Object.entries(spec.flags).filter(([name]) => !(name.startsWith("no-") && paired(name.slice(3)))).sort(([a], [b]) => a.localeCompare(b)).map(([name, flag]) => {
|
|
11962
|
+
const flagRows = Object.entries(spec.flags).filter(([name, flag]) => !flag.helpHidden && !(name.startsWith("no-") && paired(name.slice(3)))).sort(([a], [b]) => a.localeCompare(b)).map(([name, flag]) => {
|
|
11864
11963
|
const notes = [flag.required ? "required" : "", flag.default !== void 0 ? `default ${String(flag.default)}` : ""].filter(Boolean);
|
|
11865
11964
|
const label = paired(name) ? flagLabel(name, flag).replace(`--${name}`, `--[no-]${name}`) : flagLabel(name, flag);
|
|
11866
11965
|
return [label, `${flag.description}${notes.length ? ` (${notes.join(", ")})` : ""}`];
|
|
11867
11966
|
});
|
|
11868
11967
|
if (flagRows.length > 0) lines.push("", "Flags:", columns(flagRows));
|
|
11869
|
-
lines.push("", GLOBAL_FLAGS_LINE);
|
|
11968
|
+
lines.push("", ...spec.helpFooter ?? [], GLOBAL_FLAGS_LINE);
|
|
11870
11969
|
if (spec.examples.length > 0) lines.push("", "Examples:", ...spec.examples.map((e) => ` ${e}`));
|
|
11871
11970
|
return withAgentBlock(lines.join("\n"), opts.agent);
|
|
11872
11971
|
}
|
|
@@ -11894,7 +11993,7 @@ var init_help = __esm({
|
|
|
11894
11993
|
["-h, --help", "Show help"],
|
|
11895
11994
|
["--version", "Print the CLI version (only on its own: kenso --version)"]
|
|
11896
11995
|
];
|
|
11897
|
-
GLOBAL_FLAGS_LINE = "Global flags: --dry-run, -y/--yes, --workspace <id>,
|
|
11996
|
+
GLOBAL_FLAGS_LINE = "Global flags: --dry-run, -y/--yes, --workspace <id>, -o json|ndjson|table|ids, --fields, --query, -v.";
|
|
11898
11997
|
COMMON_RESOURCES = [
|
|
11899
11998
|
"accounts",
|
|
11900
11999
|
"analytics",
|