@series-a/cli 1.2.0 → 1.3.1

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.
@@ -0,0 +1,2196 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { call } from "../api.mjs";
3
+ import { emit, fail, ctxFrom, withCommonOpts, table, truncate, confirmDestructive } from "../ui.mjs";
4
+
5
+ function parseCsv(text) {
6
+ // Tiny CSV parser: header row + comma-separated values, quoted fields supported.
7
+ const lines = text.split(/\r?\n/).filter((l) => l.trim().length);
8
+ if (!lines.length) return [];
9
+ const split = (line) => {
10
+ const out = []; let cur = ""; let q = false;
11
+ for (let i = 0; i < line.length; i++) {
12
+ const c = line[i];
13
+ if (q) {
14
+ if (c === '"' && line[i+1] === '"') { cur += '"'; i++; }
15
+ else if (c === '"') { q = false; }
16
+ else cur += c;
17
+ } else {
18
+ if (c === ",") { out.push(cur); cur = ""; }
19
+ else if (c === '"') { q = true; }
20
+ else cur += c;
21
+ }
22
+ }
23
+ out.push(cur);
24
+ return out.map((s) => s.trim());
25
+ };
26
+ const headers = split(lines[0]).map((h) => h.toLowerCase());
27
+ return lines.slice(1).map((line) => {
28
+ const cells = split(line);
29
+ const row = {};
30
+ headers.forEach((h, i) => { row[h] = cells[i] ?? ""; });
31
+ return row;
32
+ });
33
+ }
34
+
35
+ export function registerLeads(program) {
36
+ const leads = program.command("leads").description("Manage leads");
37
+
38
+ withCommonOpts(leads.command("list"))
39
+ .description("List leads")
40
+ .option("--status <s>", "filter by status")
41
+ .option("--persona <p>", "filter by persona_tag")
42
+ .option("--search <q>", "search name or company")
43
+ .option("--limit <n>", "max rows", "50")
44
+ .action(async function () {
45
+ try {
46
+ const o = this.optsWithGlobals();
47
+ const r = await call("leads", "list", {
48
+ status: o.status, persona_tag: o.persona, search: o.search, limit: Number(o.limit),
49
+ }, ctxFrom(this));
50
+ emit(this, r, (d) => {
51
+ process.stdout.write(table(
52
+ ["id", "name", "title", "company", "status"],
53
+ (d.leads || []).map((l) => [
54
+ truncate(l.id, 8), truncate(l.name, 28), truncate(l.title, 24),
55
+ truncate(l.company, 22), l.status ?? "",
56
+ ]),
57
+ ) + `\n${d.leads?.length ?? 0} rows\n`);
58
+ });
59
+ } catch (e) { fail(e); }
60
+ });
61
+
62
+ withCommonOpts(leads.command("get <id>"))
63
+ .description("Show a single lead")
64
+ .action(async function (id) {
65
+ try {
66
+ const r = await call("leads", "get", { id }, ctxFrom(this));
67
+ emit(this, r, (d) => process.stdout.write(JSON.stringify(d.lead, null, 2) + "\n"));
68
+ } catch (e) { fail(e); }
69
+ });
70
+
71
+ withCommonOpts(leads.command("set-status <id> <status>"))
72
+ .description("Update a lead's status")
73
+ .action(async function (id, status) {
74
+ try {
75
+ const r = await call("leads", "update_status", { id, status }, ctxFrom(this));
76
+ emit(this, r, (d) => process.stdout.write(`✓ ${d.lead?.name ?? id} → ${d.lead?.status}\n`));
77
+ } catch (e) { fail(e); }
78
+ });
79
+
80
+ withCommonOpts(leads.command("import"))
81
+ .description("Bulk upsert leads from a CSV or JSON/JSONL file")
82
+ .requiredOption("--file <path>", "leads.csv | leads.json | leads.jsonl")
83
+ .option("--persona <tag>", "default persona_tag for all rows")
84
+ .option("--source <s>", "default source", "cli")
85
+ .action(async function (opts) {
86
+ try {
87
+ const text = await readFile(opts.file, "utf8");
88
+ let rows;
89
+ if (opts.file.endsWith(".jsonl")) {
90
+ rows = text.split(/\r?\n/).filter(Boolean).map((l) => JSON.parse(l));
91
+ } else if (opts.file.endsWith(".json")) {
92
+ const parsed = JSON.parse(text);
93
+ rows = Array.isArray(parsed) ? parsed : (parsed.leads ?? []);
94
+ } else {
95
+ rows = parseCsv(text);
96
+ }
97
+ if (opts.persona) rows.forEach((r) => { r.persona_tag = r.persona_tag || opts.persona; });
98
+ rows.forEach((r) => { r.source = r.source || opts.source; });
99
+
100
+ // Send in chunks of 200 to respect API limit (500).
101
+ const chunks = [];
102
+ for (let i = 0; i < rows.length; i += 200) chunks.push(rows.slice(i, i + 200));
103
+ let total = 0;
104
+ for (const chunk of chunks) {
105
+ const r = await call("leads", "bulk_upsert", { leads: chunk }, ctxFrom(this));
106
+ total += r.count ?? 0;
107
+ }
108
+ emit(this, { upserted: total, total_rows: rows.length }, (d) =>
109
+ process.stdout.write(`✓ Upserted ${d.upserted}/${d.total_rows} leads\n`));
110
+ } catch (e) { fail(e); }
111
+ });
112
+
113
+ withCommonOpts(leads.command("import-from <source>"))
114
+ .description("Import leads from any source: linkedin_search | company_search | post_reactors | paste_profiles | csv | heyreach | sales_nav_url | sales_nav_keywords")
115
+ .option("--query <q>", "search keywords (linkedin_search / company_search / sales_nav_keywords)")
116
+ .option("--url <url>", "full Sales Navigator search URL (sales_nav_url)")
117
+ .option("--post <url>", "LinkedIn post URL (post_reactors)")
118
+ .option("--file <path>", "CSV or pasted-profile text file (csv / paste_profiles / heyreach)")
119
+ .option("--audience <name>", "audience (segment) name or id to attach to")
120
+ .option("--campaign <id>", "campaign id (required for heyreach)")
121
+ .option("--priority <p>", "queue priority for Sales Navigator imports: normal | high | rush")
122
+ .option("--limit <n>", "max prospects for LinkedIn sources", "50")
123
+ .option("--no-enrich", "skip the LinkedIn verify + enrich chain")
124
+ .action(async function (source) {
125
+ try {
126
+ const o = this.optsWithGlobals();
127
+ const params = {
128
+ source,
129
+ query: o.query,
130
+ search_url: o.url,
131
+ post_url: o.post,
132
+ audience: o.audience,
133
+ campaign_id: o.campaign,
134
+ priority: o.priority,
135
+ limit: Number(o.limit),
136
+ enrich: o.enrich !== false,
137
+ };
138
+ if (o.file) {
139
+ const text = await readFile(o.file, "utf8");
140
+ if (source === "paste_profiles") params.text = text;
141
+ else params.csv = text;
142
+ }
143
+ const r = await call("leads", "import", params, ctxFrom(this));
144
+ emit(this, r, (d) => {
145
+ if (d.schedule) {
146
+ process.stdout.write(
147
+ `${d.reused ? "↻ Reusing existing" : "✓ Queued"} background import — ${d.schedule.target_count} leads\n` +
148
+ ` schedule: ${d.schedule.id} (${d.schedule.status})\n` +
149
+ ` It drip-feeds inside your LinkedIn read limits. Track with \`advisoros leads import-schedules\`.\n`,
150
+ );
151
+ return;
152
+ }
153
+ process.stdout.write(
154
+ `✓ Imported ${d.imported ?? 0} leads (${d.duplicates ?? 0} duplicates, ${d.skipped ?? 0} skipped, ${d.failed ?? 0} failed)\n` +
155
+ (d.job_id ? ` job: ${d.job_id}\n` : ""));
156
+ });
157
+ } catch (e) { fail(e); }
158
+ });
159
+
160
+ withCommonOpts(leads.command("import-schedules"))
161
+ .description("List queued/running Sales Navigator background imports")
162
+ .option("--status <s>", "filter by status (queued|running|awaiting_capacity|paused|completed|failed|cancelled)")
163
+ .option("--all", "include finished imports")
164
+ .option("--limit <n>", "max rows", "25")
165
+ .action(async function () {
166
+ try {
167
+ const o = this.optsWithGlobals();
168
+ const r = await call("leads", "import_schedules", {
169
+ status: o.status,
170
+ active: o.all ? false : true,
171
+ limit: Number(o.limit),
172
+ }, ctxFrom(this));
173
+ emit(this, r, (d) => table(d.schedules ?? [], [
174
+ ["id", (s) => s.id.slice(0, 8)],
175
+ ["status", (s) => s.status],
176
+ ["progress", (s) => `${s.imported_count}/${s.target_count}`],
177
+ ["priority", (s) => s.priority],
178
+ ["source", (s) => s.source],
179
+ ["target", (s) => truncate(s.search_url || s.search_query || "", 48)],
180
+ ["reason", (s) => truncate(s.last_reason || "", 40)],
181
+ ]));
182
+ } catch (e) { fail(e); }
183
+ });
184
+
185
+ withCommonOpts(leads.command("import-schedule <action> <id>"))
186
+ .description("Pause, resume or cancel a scheduled import (action: pause|resume|cancel)")
187
+ .action(async function (action, id) {
188
+ try {
189
+ const r = await call("leads", "import_schedule_update", { id, action }, ctxFrom(this));
190
+ emit(this, r, (d) =>
191
+ process.stdout.write(`✓ Import ${d.schedule.id} is now ${d.schedule.status}\n`));
192
+ } catch (e) { fail(e); }
193
+ });
194
+
195
+
196
+ withCommonOpts(leads.command("import-status <jobId>"))
197
+ .description("Poll a running lead import")
198
+ .action(async function (jobId) {
199
+ try {
200
+ const r = await call("leads", "import_status", { job_id: jobId }, ctxFrom(this));
201
+ emit(this, r, (d) => process.stdout.write(JSON.stringify(d.job, null, 2) + "\n"));
202
+ } catch (e) { fail(e); }
203
+ });
204
+
205
+ withCommonOpts(leads.command("update <id>"))
206
+ .description("Update a single lead")
207
+ .option("--name <v>").option("--title <v>").option("--company <v>")
208
+ .option("--email <v>").option("--linkedin <v>").option("--persona <v>")
209
+ .option("--score <n>").option("--status <v>").option("--notes <v>")
210
+ .action(async function (id) {
211
+ try {
212
+ const o = this.optsWithGlobals();
213
+ const params = { id };
214
+ for (const [k, v] of Object.entries({
215
+ name: o.name, title: o.title, company: o.company, email: o.email,
216
+ linkedin_url: o.linkedin, persona_tag: o.persona,
217
+ score: o.score !== undefined ? Number(o.score) : undefined,
218
+ status: o.status, notes: o.notes,
219
+ })) if (v !== undefined) params[k] = v;
220
+ const r = await call("leads", "update", params, ctxFrom(this));
221
+ emit(this, r, (d) => process.stdout.write(`✓ Updated ${d.lead?.name ?? id}\n`));
222
+ } catch (e) { fail(e); }
223
+ });
224
+
225
+ withCommonOpts(leads.command("delete <id>"))
226
+ .description("Delete a lead")
227
+ .option("-y, --yes", "skip the confirmation prompt")
228
+ .action(async function (id) {
229
+ try {
230
+ await confirmDestructive(this, `Delete lead ${id}. This cannot be undone.`);
231
+ const r = await call("leads", "delete", { id }, ctxFrom(this));
232
+ emit(this, r, () => process.stdout.write(`✓ Deleted ${id}\n`));
233
+ } catch (e) { fail(e); }
234
+ });
235
+
236
+ withCommonOpts(leads.command("verify <id>"))
237
+ .description("Run LinkedIn verification on a lead")
238
+ .action(async function (id) {
239
+ try {
240
+ const r = await call("leads", "verify", { id }, ctxFrom(this));
241
+ emit(this, r, (d) => process.stdout.write(JSON.stringify(d.verify, null, 2) + "\n"));
242
+ } catch (e) { fail(e); }
243
+ });
244
+ }
245
+
246
+ export function registerCampaigns(program) {
247
+ const c = program.command("campaigns").description("Manage LinkedIn campaigns");
248
+
249
+ withCommonOpts(c.command("list"))
250
+ .description("List campaigns")
251
+ .option("--status <s>", "filter by status")
252
+ .option("--limit <n>", "max rows", "50")
253
+ .action(async function () {
254
+ try {
255
+ const o = this.optsWithGlobals();
256
+ const r = await call("campaigns", "list", { status: o.status, limit: Number(o.limit) }, ctxFrom(this));
257
+ emit(this, r, (d) => {
258
+ process.stdout.write(table(
259
+ ["id", "name", "status", "sequence", "daily", "senders"],
260
+ (d.campaigns || []).map((cp) => [
261
+ truncate(cp.id, 8), truncate(cp.name, 30), cp.status,
262
+ truncate(cp.sequence_id, 8), cp.daily_send_limit ?? "",
263
+ (cp.sender_accounts || []).length,
264
+ ]),
265
+ ) + `\n${d.campaigns?.length ?? 0} rows\n`);
266
+ });
267
+ } catch (e) { fail(e); }
268
+ });
269
+
270
+ withCommonOpts(c.command("show <id>"))
271
+ .description("Show campaign details + stats")
272
+ .action(async function (id) {
273
+ try {
274
+ const r = await call("campaigns", "get", { id }, ctxFrom(this));
275
+ emit(this, r, (d) => process.stdout.write(JSON.stringify(d.campaign, null, 2) + "\n"));
276
+ } catch (e) { fail(e); }
277
+ });
278
+
279
+ withCommonOpts(c.command("create"))
280
+ .description("Create a new draft campaign")
281
+ .requiredOption("--name <name>", "campaign name")
282
+ .option("--description <text>", "description")
283
+ .option("--goal <text>", "campaign goal")
284
+ .option("--sequence <id>", "attach a sequence")
285
+ .option("--senders <csv>", "comma-separated sender_account_ids")
286
+ .option("--daily-limit <n>", "daily send cap", "25")
287
+ .action(async function (opts) {
288
+ try {
289
+ const r = await call("campaigns", "create", {
290
+ name: opts.name,
291
+ description: opts.description,
292
+ goal: opts.goal,
293
+ sequence_id: opts.sequence,
294
+ sender_accounts: opts.senders ? opts.senders.split(",").map((s) => s.trim()).filter(Boolean) : [],
295
+ daily_send_limit: Number(opts.dailyLimit),
296
+ }, ctxFrom(this));
297
+ emit(this, r, (d) => process.stdout.write(`✓ Created campaign ${d.campaign?.id}\n`));
298
+ } catch (e) { fail(e); }
299
+ });
300
+
301
+ withCommonOpts(c.command("update <campaignId>"))
302
+ .description("Edit an existing campaign")
303
+ .option("--name <name>", "rename campaign")
304
+ .option("--description <text>", "description")
305
+ .option("--goal <text>", "campaign goal")
306
+ .option("--sequence <id>", "attach a sequence")
307
+ .option("--senders <csv>", "comma-separated sender_account_ids")
308
+ .option("--daily-limit <n>", "daily send cap")
309
+ .option("--status <status>", "draft|active|paused|completed|archived")
310
+ .action(async function (campaignId, opts) {
311
+ try {
312
+ const payload = { id: campaignId };
313
+ if (opts.name !== undefined) payload.name = opts.name;
314
+ if (opts.description !== undefined) payload.description = opts.description;
315
+ if (opts.goal !== undefined) payload.goal = opts.goal;
316
+ if (opts.sequence !== undefined) payload.sequence_id = opts.sequence;
317
+ if (opts.senders !== undefined) {
318
+ payload.sender_accounts = opts.senders.split(",").map((s) => s.trim()).filter(Boolean);
319
+ }
320
+ if (opts.dailyLimit !== undefined) payload.daily_send_limit = Number(opts.dailyLimit);
321
+ if (opts.status !== undefined) payload.status = opts.status;
322
+ const r = await call("campaigns", "update", payload, ctxFrom(this));
323
+ emit(this, r, (d) => process.stdout.write(`✓ Updated campaign ${d.campaign?.id}\n`));
324
+ } catch (e) { fail(e); }
325
+ });
326
+
327
+ withCommonOpts(c.command("rename <campaignId> <name>"))
328
+ .description("Rename a campaign (shortcut for update --name)")
329
+ .action(async function (campaignId, name) {
330
+ try {
331
+ const r = await call("campaigns", "update", { id: campaignId, name }, ctxFrom(this));
332
+ emit(this, r, (d) => process.stdout.write(`✓ Renamed campaign ${d.campaign?.id ?? campaignId} → ${d.campaign?.name ?? name}\n`));
333
+ } catch (e) { fail(e); }
334
+ });
335
+
336
+
337
+
338
+ withCommonOpts(c.command("add-leads <campaignId>"))
339
+ .description("Attach leads to a campaign")
340
+ .option("--lead-ids <csv>", "comma-separated lead UUIDs")
341
+ .option("--file <path>", "file with one lead id per line")
342
+ .action(async function (campaignId, opts) {
343
+ try {
344
+ let ids = [];
345
+ if (opts.leadIds) ids = opts.leadIds.split(",").map((s) => s.trim()).filter(Boolean);
346
+ if (opts.file) {
347
+ const text = await readFile(opts.file, "utf8");
348
+ ids.push(...text.split(/\r?\n/).map((s) => s.trim()).filter(Boolean));
349
+ }
350
+ if (!ids.length) return fail(new Error("--lead-ids or --file required"));
351
+ const r = await call("campaigns", "add_leads", { campaign_id: campaignId, lead_ids: ids }, ctxFrom(this));
352
+ emit(this, r, (d) => process.stdout.write(`✓ Added ${d.added}/${d.requested} leads (${d.valid} valid in workspace)\n`));
353
+ } catch (e) { fail(e); }
354
+ });
355
+
356
+ withCommonOpts(c.command("remove-leads <campaignId>"))
357
+ .description("Un-enrol leads from a campaign")
358
+ .option("--lead-ids <ids>", "comma-separated lead ids")
359
+ .option("--all", "remove every enrolled lead")
360
+ .option("-y, --yes", "skip the confirmation prompt")
361
+ .option("--dry-run", "show what would be removed")
362
+ .action(async function (campaignId, opts) {
363
+ try {
364
+ const ids = opts.leadIds ? opts.leadIds.split(",").map((s) => s.trim()).filter(Boolean) : [];
365
+ if (!ids.length && !opts.all) return fail(new Error("--lead-ids or --all required"));
366
+ const params = { campaign_id: campaignId, lead_ids: ids, all: !!opts.all };
367
+ if (opts.dryRun) {
368
+ const d = await call("campaigns", "remove_leads", { ...params, dry_run: true }, ctxFrom(this));
369
+ return emit(this, d, (x) => process.stdout.write(`Would remove ${x.would_remove} lead(s) from ${campaignId}\n`));
370
+ }
371
+ await confirmDestructive(this, `Remove ${opts.all ? "ALL enrolled leads" : `${ids.length} lead(s)`} from campaign ${campaignId}.`);
372
+ const r = await call("campaigns", "remove_leads", params, ctxFrom(this));
373
+ emit(this, r, (d) => process.stdout.write(`✓ Removed ${d.removed} lead(s)\n`));
374
+ } catch (e) { fail(e); }
375
+ });
376
+
377
+ withCommonOpts(c.command("preflight <id>"))
378
+ .description("Check whether a campaign is safe to launch (read-only)")
379
+ .action(async function (id) {
380
+ try {
381
+ const r = await call("campaigns", "preflight", { campaign_id: id }, ctxFrom(this));
382
+ emit(this, r, renderPreflight);
383
+ } catch (e) { fail(e); }
384
+ });
385
+
386
+ withCommonOpts(c.command("launch <id>"))
387
+ .description("Activate a campaign — runs preflight first, then requires --confirm")
388
+ .option("--confirm", "actually launch (without it, this is a dry run)")
389
+ .option("--force", "launch even if preflight found blockers")
390
+ .action(async function (id, opts) {
391
+ try {
392
+ if (!opts.confirm) {
393
+ const d = await call("campaigns", "launch", { campaign_id: id, dry_run: true }, ctxFrom(this));
394
+ return emit(this, d, (x) => {
395
+ renderPreflight(x.preflight ?? x);
396
+ process.stdout.write(
397
+ x.preflight?.ok
398
+ ? `\nDry run only. Re-run with --confirm to launch.\n`
399
+ : `\nDry run only. Fix the blockers above, then re-run with --confirm.\n`,
400
+ );
401
+ });
402
+ }
403
+ const r = await call(
404
+ "campaigns", "launch", { campaign_id: id, force: !!opts.force }, ctxFrom(this),
405
+ );
406
+ emit(this, r, (d) => process.stdout.write(`✓ ${d.campaign?.name ?? id} → ${d.campaign?.status}\n`));
407
+ } catch (e) { fail(e); }
408
+ });
409
+
410
+ withCommonOpts(c.command("pause <id>"))
411
+ .description("Pause a campaign")
412
+ .action(async function (id) {
413
+ try {
414
+ const r = await call("campaigns", "pause", { campaign_id: id }, ctxFrom(this));
415
+ emit(this, r, (d) => process.stdout.write(`✓ ${d.campaign?.name ?? id} → ${d.campaign?.status}\n`));
416
+ } catch (e) { fail(e); }
417
+ });
418
+
419
+ withCommonOpts(c.command("resume <id>"))
420
+ .description("Resume a paused campaign")
421
+ .option("--confirm", "required — resuming restarts sending")
422
+ .option("--force", "resume even if preflight found blockers")
423
+ .action(async function (id, opts) {
424
+ try {
425
+ if (!opts.confirm) {
426
+ const d = await call("campaigns", "resume", { campaign_id: id, dry_run: true }, ctxFrom(this));
427
+ return emit(this, d, (x) => {
428
+ renderPreflight(x.preflight ?? x);
429
+ process.stdout.write(`\nDry run only. Re-run with --confirm to resume.\n`);
430
+ });
431
+ }
432
+ const r = await call("campaigns", "resume", { campaign_id: id, force: !!opts.force }, ctxFrom(this));
433
+ emit(this, r, (d) => process.stdout.write(`✓ ${d.campaign?.name ?? id} → ${d.campaign?.status}\n`));
434
+ } catch (e) { fail(e); }
435
+ });
436
+
437
+ withCommonOpts(c.command("archive <id>"))
438
+ .description("Archive a campaign (reversible, keeps history)")
439
+ .option("-y, --yes", "skip the confirmation prompt")
440
+ .action(async function (id) {
441
+ try {
442
+ await confirmDestructive(this, `Archive campaign ${id}. It will stop sending.`);
443
+ const r = await call("campaigns", "archive", { campaign_id: id }, ctxFrom(this));
444
+ emit(this, r, (d) => process.stdout.write(`✓ ${d.campaign?.name ?? id} → archived\n`));
445
+ } catch (e) { fail(e); }
446
+ });
447
+
448
+ withCommonOpts(c.command("clone <id>"))
449
+ .description("Duplicate a campaign as a fresh draft (no leads, no history)")
450
+ .option("--name <name>", "name for the copy")
451
+ .action(async function (id, opts) {
452
+ try {
453
+ const r = await call("campaigns", "clone", { campaign_id: id, name: opts.name }, ctxFrom(this));
454
+ emit(this, r, (d) => process.stdout.write(`✓ Cloned to ${d.campaign?.id} — ${d.campaign?.name}\n`));
455
+ } catch (e) { fail(e); }
456
+ });
457
+
458
+ withCommonOpts(c.command("delete <id>"))
459
+ .description("Permanently delete a campaign and its enrolments")
460
+ .option("-y, --yes", "skip the confirmation prompt")
461
+ .option("--dry-run", "show what would be deleted")
462
+ .option("--force", "delete even when the campaign has dispatch history")
463
+ .action(async function (id, opts) {
464
+ try {
465
+ if (opts.dryRun) {
466
+ const d = await call("campaigns", "delete", { campaign_id: id, dry_run: true }, ctxFrom(this));
467
+ return emit(this, d, (x) =>
468
+ process.stdout.write(
469
+ `Would delete campaign ${x.campaign?.name ?? id} (${x.dispatches} dispatched actions)\n`,
470
+ ));
471
+ }
472
+ await confirmDestructive(
473
+ this,
474
+ `Permanently delete campaign ${id} and every lead enrolment on it. This cannot be undone.`,
475
+ { expect: id.slice(0, 8) },
476
+ );
477
+ const r = await call(
478
+ "campaigns", "delete", { campaign_id: id, force: !!opts.force }, ctxFrom(this),
479
+ );
480
+ emit(this, r, (d) => process.stdout.write(`✓ Deleted ${d.name ?? id}\n`));
481
+ } catch (e) { fail(e); }
482
+ });
483
+ }
484
+
485
+ function renderPreflight(p) {
486
+ if (!p) return;
487
+ process.stdout.write(
488
+ `${p.ok ? "✓ Ready to launch" : "✖ Not ready"} — ${p.name ?? p.campaign_id} (${p.status})\n`,
489
+ );
490
+ const c = p.checks ?? {};
491
+ process.stdout.write(
492
+ ` leads: ${c.leads_enrolled} steps: ${c.sequence_steps} senders: ${c.sender_accounts} daily limit: ${c.daily_send_limit ?? "—"}\n`,
493
+ );
494
+ for (const b of p.blockers ?? []) process.stdout.write(` ✖ ${b}\n`);
495
+ for (const w of p.warnings ?? []) process.stdout.write(` ! ${w}\n`);
496
+ }
497
+
498
+
499
+
500
+ function delayLabel(s) {
501
+ const unit = s?.config?.delay_unit || "days";
502
+ const n = s?.delay_days ?? 0;
503
+ return unit === "hours" ? `${n}h` : unit === "minutes" ? `${n}m` : `${n}d`;
504
+ }
505
+
506
+ export function registerSequences(program) {
507
+ const s = program.command("sequences").description("Manage LinkedIn sequences");
508
+
509
+ withCommonOpts(s.command("list"))
510
+ .description("List sequences")
511
+ .option("--limit <n>", "max rows", "50")
512
+ .action(async function () {
513
+ try {
514
+ const o = this.optsWithGlobals();
515
+ const r = await call("sequences", "list", { limit: Number(o.limit) }, ctxFrom(this));
516
+ emit(this, r, (d) => {
517
+ process.stdout.write(table(
518
+ ["id", "name", "status"],
519
+ (d.sequences || []).map((s) => [s.id, truncate(s.name, 40), s.status]),
520
+ ) + `\n${d.sequences?.length ?? 0} rows\n`);
521
+ });
522
+ } catch (e) { fail(e); }
523
+ });
524
+
525
+ withCommonOpts(s.command("show <id>"))
526
+ .description("Show sequence + steps")
527
+ .action(async function (id) {
528
+ try {
529
+ const r = await call("sequences", "get", { id }, ctxFrom(this));
530
+ emit(this, r, (d) => {
531
+ process.stdout.write(`Sequence: ${d.sequence?.name} (${d.sequence?.status})\n\n`);
532
+ process.stdout.write(table(
533
+ ["#", "action", "channel", "delay", "variant", "preview"],
534
+ (d.steps || []).map((s) => [
535
+ s.step_order, s.action, s.channel, delayLabel(s), s.variant,
536
+ truncate(s.message_content, 60),
537
+ ]),
538
+ ) + "\n");
539
+ });
540
+ } catch (e) { fail(e); }
541
+ });
542
+
543
+ withCommonOpts(s.command("create"))
544
+ .description("Create a new (empty, draft) sequence")
545
+ .requiredOption("--name <name>", "sequence name")
546
+ .option("--description <text>", "description")
547
+ .option("--trigger <type>", "trigger_type", "manual")
548
+ .option("--stage <stage>", "launch_stage (liberate|architect|uncover|navigate|catalyse|harness|review|graduation)")
549
+ .action(async function (opts) {
550
+ try {
551
+ const r = await call("sequences", "create", {
552
+ name: opts.name,
553
+ description: opts.description,
554
+ trigger_type: opts.trigger,
555
+ launch_stage: opts.stage,
556
+ }, ctxFrom(this));
557
+ emit(this, r, (d) => {
558
+ process.stdout.write(`✓ Created sequence ${d.sequence?.id}\n name: ${d.sequence?.name}\n status: ${d.sequence?.status}\n`);
559
+ });
560
+ } catch (e) { fail(e); }
561
+ });
562
+
563
+ withCommonOpts(s.command("update <id>"))
564
+ .description("Update sequence metadata (name/description/status/stage)")
565
+ .option("--name <name>", "rename")
566
+ .option("--description <text>", "set description (pass empty string to clear)")
567
+ .option("--status <s>", "draft|active|paused|archived")
568
+ .option("--stage <stage>", "launch_stage (pass '' to clear)")
569
+ .action(async function (id, opts) {
570
+ try {
571
+ const params = { id };
572
+ if (opts.name != null) params.name = opts.name;
573
+ if (opts.description != null) params.description = opts.description || null;
574
+ if (opts.status != null) params.status = opts.status;
575
+ if (opts.stage != null) params.launch_stage = opts.stage || null;
576
+ const r = await call("sequences", "update", params, ctxFrom(this));
577
+ emit(this, r, (d) => {
578
+ process.stdout.write(`✓ Updated ${d.sequence?.id}\n name: ${d.sequence?.name}\n status: ${d.sequence?.status}\n stage: ${d.sequence?.launch_stage || "—"}\n`);
579
+ });
580
+ } catch (e) { fail(e); }
581
+ });
582
+
583
+ withCommonOpts(s.command("add-step <id>"))
584
+ .description("Append a step to a sequence")
585
+ .requiredOption(
586
+ "--action <action>",
587
+ "viewProfile|addReaction|sendInvitation|sendMessage|loopBack|createOpportunity|enrichLead|ifAccepted|ifReplied|ifConnection|ifNetworkDistance|stopOnReply|wait|wait_3h|wait_1d|wait_2d|wait_3d|wait_5d",
588
+ )
589
+ .option("--text <message>", "message_content (required for sendInvitation/sendMessage)")
590
+ .option("--delay <days>", "delay_days before this step", "0")
591
+ .option("--delay-hours <hours>", "wait this many hours instead of days")
592
+ .option("--delay-minutes <minutes>", "wait this many minutes instead of days")
593
+ .option("--variant <v>", "variant label", "A")
594
+ .option("--step-type <t>", "action|condition|delay (defaults from --action)")
595
+ .option("--channel <c>", "channel (defaults from --action)")
596
+ .option("--branch <label>", "branch_label: yes|no|first|second_or_third|follower|out_of_network (children of a condition)")
597
+ .option("--parent-step <id>", "parent_step_id (defaults to the tail of the sequence)")
598
+ .option("--root", "start a new root step instead of appending to the tail (rarely correct)")
599
+
600
+ .option("--order <n>", "explicit step_order (defaults to max+1)")
601
+ .action(async function (id, opts) {
602
+ try {
603
+ const r = await call("sequences", "add_step", {
604
+ sequence_id: id,
605
+ action: opts.action,
606
+ message_content: opts.text,
607
+ delay_days: Number(opts.delay) || 0,
608
+ delay_hours: opts.delayHours !== undefined ? Number(opts.delayHours) : undefined,
609
+ delay_minutes: opts.delayMinutes !== undefined ? Number(opts.delayMinutes) : undefined,
610
+ variant: opts.variant,
611
+ step_type: opts.stepType,
612
+ channel: opts.channel,
613
+ branch_label: opts.branch,
614
+ parent_step_id: opts.parentStep,
615
+ root: opts.root === true,
616
+ step_order: opts.order ? Number(opts.order) : undefined,
617
+ }, ctxFrom(this));
618
+ emit(this, r, (d) => {
619
+ const st = d.step || {};
620
+ process.stdout.write(`✓ Added step ${st.id}\n #${st.step_order} ${st.action} (${st.channel}) delay=${delayLabel(st)} variant=${st.variant}\n parent=${st.parent_step_id || "root"}${st.branch_label ? ` branch=${st.branch_label}` : ""}\n`);
621
+ for (const w of d.graph?.warnings || []) process.stdout.write(` ⚠ ${w.message}\n`);
622
+ });
623
+ } catch (e) { fail(e); }
624
+
625
+ });
626
+
627
+ withCommonOpts(s.command("replace-steps <id>"))
628
+ .description("Replace every step of a sequence with a validated tree from a JSON file (atomic)")
629
+ .requiredOption("--file <path>", "JSON file: [{ temp_id, parent, branch_label, action, message_content, delay_days | delay_hours | delay_minutes }] (a flat ordered list is treated as a chain)")
630
+ .option("--dry-run", "validate the graph without writing")
631
+ .action(async function (id, opts) {
632
+ try {
633
+ const raw = await readFile(opts.file, "utf8");
634
+ const parsed = JSON.parse(raw);
635
+ const steps = Array.isArray(parsed) ? parsed : parsed.steps;
636
+ if (!Array.isArray(steps)) throw new Error("file must contain an array of steps or { steps: [...] }");
637
+ const r = await call("sequences", "replace_steps", {
638
+ sequence_id: id,
639
+ steps,
640
+ dry_run: opts.dryRun === true,
641
+ }, ctxFrom(this));
642
+ emit(this, r, (d) => {
643
+ if (d.dry_run) {
644
+ process.stdout.write(`✓ Graph valid — ${d.planned_steps} step(s), nothing written\n`);
645
+ return;
646
+ }
647
+ process.stdout.write(`✓ Replaced steps on ${d.sequence_id} — ${d.replaced_steps} step(s)\n`);
648
+ for (const st of d.steps || []) {
649
+ process.stdout.write(` #${st.step_order} ${st.action}${st.branch_label ? ` [${st.branch_label}]` : ""} parent=${st.parent_step_id || "root"}\n`);
650
+ }
651
+ for (const w of d.graph?.warnings || []) process.stdout.write(` ⚠ ${w.message}\n`);
652
+ });
653
+ } catch (e) { fail(e); }
654
+ });
655
+
656
+
657
+
658
+ withCommonOpts(s.command("clone <id>"))
659
+ .description("Duplicate a sequence (and all its steps) as a new draft")
660
+ .option("--name <name>", "name for the clone (default: '<src> (copy)')")
661
+ .action(async function (id, opts) {
662
+ try {
663
+ const r = await call("sequences", "clone", { id, name: opts.name }, ctxFrom(this));
664
+ emit(this, r, (d) => {
665
+ process.stdout.write(`✓ Cloned to ${d.sequence?.id}\n name: ${d.sequence?.name}\n copied steps: ${d.cloned_steps}\n`);
666
+ });
667
+ } catch (e) { fail(e); }
668
+ });
669
+
670
+ withCommonOpts(s.command("validate <id>"))
671
+ .description("Run safety + char-limit validation on a sequence (same checks as 'campaigns launch')")
672
+ .action(async function (id) {
673
+ try {
674
+ const r = await call("sequences", "validate", { id }, ctxFrom(this));
675
+ emit(this, r, (d) => {
676
+ const status = d.ok ? "✓ PASS" : "✖ FAIL";
677
+ process.stdout.write(`${status} — ${d.step_count} step(s), ${d.errors.length} error(s), ${d.warnings.length} warning(s)\n`);
678
+ for (const e of d.errors) {
679
+ process.stdout.write(` ERROR #${e.step_order ?? "?"} ${e.action ?? ""} ${e.kind}` +
680
+ (e.field ? ` [${e.field}]` : "") +
681
+ (e.length != null ? ` ${e.length}/${e.limit} chars (+${e.excess})` : "") + "\n");
682
+ }
683
+ for (const w of d.warnings) {
684
+ process.stdout.write(` WARN #${w.step_order} ${w.action} ${w.kind}` +
685
+ (w.field ? ` [${w.field}]` : "") +
686
+ ` ${w.length}/${w.limit} chars (+${w.excess})\n`);
687
+ }
688
+ if (!d.ok) process.exitCode = 1;
689
+ });
690
+ } catch (e) { fail(e); }
691
+ });
692
+
693
+ withCommonOpts(s.command("update-step <stepId>"))
694
+ .description("Patch one step in place (message, delay, variant, channel, action)")
695
+ .option("--action <action>", "step action")
696
+ .option("--message <text>", "message_content")
697
+ .option("--delay <days>", "delay_days")
698
+ .option("--delay-hours <hours>", "wait this many hours instead of days")
699
+ .option("--delay-minutes <minutes>", "wait this many minutes instead of days")
700
+ .option("--variant <v>", "variant")
701
+ .option("--channel <c>", "channel")
702
+ .action(async function (stepId, opts) {
703
+ try {
704
+ const params = { step_id: stepId };
705
+ if (opts.action !== undefined) params.action = opts.action;
706
+ if (opts.message !== undefined) params.message_content = opts.message;
707
+ if (opts.delay !== undefined) params.delay_days = Number(opts.delay);
708
+ if (opts.delayHours !== undefined) params.delay_hours = Number(opts.delayHours);
709
+ if (opts.delayMinutes !== undefined) params.delay_minutes = Number(opts.delayMinutes);
710
+ if (opts.variant !== undefined) params.variant = opts.variant;
711
+ if (opts.channel !== undefined) params.channel = opts.channel;
712
+ const r = await call("sequences", "update_step", params, ctxFrom(this));
713
+ emit(this, r, (d) => process.stdout.write(`✓ Updated step ${d.step?.id ?? stepId} (${d.step?.action})\n`));
714
+ } catch (e) { fail(e); }
715
+ });
716
+
717
+ withCommonOpts(s.command("delete-step <stepId>"))
718
+ .description("Delete one step; its children are re-parented")
719
+ .option("-y, --yes", "skip the confirmation prompt")
720
+ .option("--dry-run", "show what would change")
721
+ .action(async function (stepId, opts) {
722
+ try {
723
+ if (opts.dryRun) {
724
+ const d = await call("sequences", "delete_step", { step_id: stepId, dry_run: true }, ctxFrom(this));
725
+ return emit(this, d, (x) =>
726
+ process.stdout.write(`Would delete step ${stepId}; ${x.reparented_children} child step(s) re-parented\n`));
727
+ }
728
+ await confirmDestructive(this, `Delete sequence step ${stepId}. Child steps will be re-parented.`);
729
+ const r = await call("sequences", "delete_step", { step_id: stepId }, ctxFrom(this));
730
+ emit(this, r, (d) =>
731
+ process.stdout.write(`✓ Deleted step ${stepId} (${d.reparented_children} child step(s) re-parented)\n`));
732
+ } catch (e) { fail(e); }
733
+ });
734
+
735
+ withCommonOpts(s.command("delete <id>"))
736
+ .description("Delete a sequence and all its steps")
737
+ .option("-y, --yes", "skip the confirmation prompt")
738
+ .option("--dry-run", "show what would be deleted")
739
+ .option("--force", "delete even if campaigns still use it (they are detached)")
740
+ .action(async function (id, opts) {
741
+ try {
742
+ if (opts.dryRun) {
743
+ const d = await call("sequences", "delete", { id, dry_run: true }, ctxFrom(this));
744
+ return emit(this, d, (x) =>
745
+ process.stdout.write(
746
+ `Would delete sequence ${x.sequence?.name ?? id}; used by ${x.used_by_campaigns?.length ?? 0} campaign(s)\n`,
747
+ ));
748
+ }
749
+ await confirmDestructive(this, `Delete sequence ${id} and every step on it. This cannot be undone.`);
750
+ const r = await call("sequences", "delete", { id, force: !!opts.force }, ctxFrom(this));
751
+ emit(this, r, (d) =>
752
+ process.stdout.write(`✓ Deleted sequence ${d.sequence_id} (${d.detached_campaigns} campaign(s) detached)\n`));
753
+ } catch (e) { fail(e); }
754
+ });
755
+ }
756
+
757
+
758
+ function parseSnoozeDuration(s) {
759
+ // Accepts e.g. "30m", "2h", "1d", "45" (minutes). Returns minutes (int).
760
+ if (!s) return null;
761
+ const m = String(s).trim().match(/^(\d+)\s*([mhd]?)$/i);
762
+ if (!m) return null;
763
+ const n = parseInt(m[1], 10);
764
+ if (!Number.isFinite(n) || n <= 0) return null;
765
+ const unit = (m[2] || "m").toLowerCase();
766
+ const mult = unit === "h" ? 60 : unit === "d" ? 60 * 24 : 1;
767
+ return n * mult;
768
+ }
769
+
770
+ export function registerInbox(program) {
771
+ const inbox = program.command("inbox").description("Inspect and act on the unified inbox");
772
+
773
+ withCommonOpts(inbox.command("threads"))
774
+ .description("List recent inbox messages")
775
+ .option("--campaign <id>", "filter by campaign_id")
776
+ .option("--lead <id>", "filter by lead_id")
777
+ .option("--needs-attention", "only messages flagged as needing attention")
778
+ .option("--limit <n>", "max rows", "50")
779
+ .action(async function () {
780
+ try {
781
+ const o = this.optsWithGlobals();
782
+ const r = await call("inbox", "list_threads", {
783
+ campaign_id: o.campaign, lead_id: o.lead,
784
+ needs_attention: o.needsAttention || undefined,
785
+ limit: Number(o.limit),
786
+ }, ctxFrom(this));
787
+ emit(this, r, (d) => {
788
+ process.stdout.write(table(
789
+ ["created", "dir", "campaign", "body"],
790
+ (d.messages || []).map((m) => [
791
+ (m.created_at || "").slice(0, 16).replace("T", " "),
792
+ m.direction === "outbound" ? "→" : "←",
793
+ truncate(m.campaign_name || m.campaign_id, 20),
794
+ truncate((m.body || "").replace(/\s+/g, " "), 80),
795
+ ]),
796
+ ) + `\n${d.messages?.length ?? 0} rows\n`);
797
+ });
798
+ } catch (e) { fail(e); }
799
+ });
800
+
801
+ const sendAction = async function (leadId, opts) {
802
+ try {
803
+ const text = String(opts.text || "").trim();
804
+ if (!text) { fail(new Error("--text cannot be empty")); return; }
805
+ if (text.length > 7900) {
806
+ fail(new Error(`--text is ${text.length} chars; hard cap is 7900`));
807
+ return;
808
+ }
809
+ if (!opts.confirm && !opts.yes) {
810
+ process.stderr.write(
811
+ `Refusing to send — re-run with --confirm to actually dispatch.\n` +
812
+ ` lead: ${leadId}\n` +
813
+ ` sender: ${opts.sender || "(auto)"}\n` +
814
+ ` chars: ${text.length}\n` +
815
+ ` text: ${truncate(text.replace(/\s+/g, " "), 120)}\n`,
816
+ );
817
+ process.exit(2);
818
+ }
819
+ const r = await call("inbox", "send_dm", {
820
+ lead_id: leadId,
821
+ text,
822
+ sender_account_id: opts.sender,
823
+ }, ctxFrom(this));
824
+ emit(this, r, (d) => {
825
+ const s = d.sent || {};
826
+ const b = s.budget || {};
827
+ process.stdout.write(
828
+ `✓ ${s.channel_kind === "linkedin_invite" ? "Invite note" : "DM"} sent to ${s.lead_name || leadId}\n` +
829
+ ` sender: ${s.sender_account_id}\n` +
830
+ ` chars: ${s.chars}\n` +
831
+ ` budget: invites ${b.invites_sent}/${b.invites_cap}, messages ${b.messages_sent}/${b.messages_cap}\n` +
832
+ (s.external_id ? ` msg id: ${s.external_id}\n` : ""),
833
+ );
834
+ });
835
+ } catch (e) { fail(e); }
836
+ };
837
+
838
+ withCommonOpts(inbox.command("send <leadId>"))
839
+ .description("Send a one-off LinkedIn DM (or first-touch invite note) to a lead")
840
+ .requiredOption("--text <message>", "message body (auto-truncated to channel limit)")
841
+ .option("--sender <accountId>", "sender_account_id (defaults to most-recently-updated linkedin_tool sender)")
842
+ .option("--confirm", "actually dispatch (otherwise prints a dry-run preview and exits)")
843
+ .action(sendAction);
844
+
845
+ // `reply` is a semantic alias for `send` — the server auto-resolves the most
846
+ // recent LinkedIn thread for (lead, sender) and routes via sendMessage.
847
+ withCommonOpts(inbox.command("reply <leadId>"))
848
+ .description("Reply on the most recent LinkedIn thread for a lead (alias of `send`; falls back to invite note if no thread)")
849
+ .requiredOption("--text <message>", "reply body (auto-truncated to channel limit)")
850
+ .option("--sender <accountId>", "sender_account_id (defaults to most-recently-updated linkedin_tool sender)")
851
+ .option("--confirm", "actually dispatch (otherwise prints a dry-run preview and exits)")
852
+ .action(sendAction);
853
+
854
+ withCommonOpts(inbox.command("mark-read <messageIds...>"))
855
+ .description("Mark one or more inbox messages as read")
856
+ .action(async function (messageIds) {
857
+ try {
858
+ if (!messageIds?.length) { fail(new Error("At least one messageId required")); return; }
859
+ const r = await call("inbox", "mark_read", { message_ids: messageIds }, ctxFrom(this));
860
+ emit(this, r, (d) => {
861
+ process.stdout.write(`✓ Marked ${d.marked_read}/${d.requested} message(s) read\n`);
862
+ });
863
+ } catch (e) { fail(e); }
864
+ });
865
+
866
+ withCommonOpts(inbox.command("snooze <messageId>"))
867
+ .description("Snooze a message until a future time")
868
+ .option("--for <duration>", "duration like 30m, 2h, 1d (default unit: minutes)")
869
+ .option("--until <iso>", "explicit ISO datetime (e.g. 2026-05-27T09:00:00Z)")
870
+ .option("--clear", "clear an existing snooze on this message")
871
+ .action(async function (messageId, opts) {
872
+ try {
873
+ const params = { message_id: messageId };
874
+ if (opts.clear) {
875
+ params.clear = true;
876
+ } else if (opts.until) {
877
+ params.until = opts.until;
878
+ } else if (opts.for) {
879
+ const mins = parseSnoozeDuration(opts.for);
880
+ if (mins == null) { fail(new Error("Invalid --for value; use e.g. 30m, 2h, 1d")); return; }
881
+ params.minutes = mins;
882
+ } else {
883
+ fail(new Error("Provide --for <duration>, --until <iso>, or --clear"));
884
+ return;
885
+ }
886
+ const r = await call("inbox", "snooze", params, ctxFrom(this));
887
+ emit(this, r, (d) => {
888
+ if (opts.clear) process.stdout.write(`✓ Snooze cleared on ${d.snoozed?.id}\n`);
889
+ else process.stdout.write(`✓ Snoozed ${d.snoozed?.id} until ${d.snoozed?.snoozed_until}\n`);
890
+ });
891
+ } catch (e) { fail(e); }
892
+ });
893
+ }
894
+
895
+ export function registerKnowledge(program) {
896
+ const k = program.command("knowledge").description("Manage the workspace knowledge base");
897
+
898
+ withCommonOpts(k.command("list"))
899
+ .description("List active knowledge items")
900
+ .option("--category <c>", "filter by category (positioning, frameworks, case-studies, …)")
901
+ .option("--search <q>", "search name or summary")
902
+ .option("--limit <n>", "max rows", "50")
903
+ .action(async function () {
904
+ try {
905
+ const o = this.optsWithGlobals();
906
+ const r = await call("knowledge", "list", {
907
+ category: o.category, search: o.search, limit: Number(o.limit),
908
+ }, ctxFrom(this));
909
+ emit(this, r, (d) => {
910
+ process.stdout.write(table(
911
+ ["id", "type", "category", "name", "tags"],
912
+ (d.items || []).map((i) => [
913
+ truncate(i.id, 8), i.type, i.category,
914
+ truncate(i.name, 40),
915
+ truncate((i.tags || []).join(","), 30),
916
+ ]),
917
+ ) + `\n${d.items?.length ?? 0} rows\n`);
918
+ });
919
+ } catch (e) { fail(e); }
920
+ });
921
+
922
+ withCommonOpts(k.command("get <id>"))
923
+ .description("Show a knowledge item (full JSON, including extracted_text)")
924
+ .action(async function (id) {
925
+ try {
926
+ const r = await call("knowledge", "get", { id }, ctxFrom(this));
927
+ emit(this, r, (d) => process.stdout.write(JSON.stringify(d.item, null, 2) + "\n"));
928
+ } catch (e) { fail(e); }
929
+ });
930
+
931
+ // `add` mirrors the web UI flow: insert → (URL: firecrawl-scrape) → AI auto-tag.
932
+ // Choose exactly one source: --url, --text, or --file.
933
+ withCommonOpts(k.command("add"))
934
+ .description("Add a knowledge item (URL is auto-scraped + AI-classified, same as the web UI)")
935
+ .option("--url <url>", "scrape this URL via Firecrawl")
936
+ .option("--text <body>", "inline text body")
937
+ .option("--file <path>", "read body from a local text/markdown file")
938
+ .option("--name <title>", "display title (defaults to URL hostname or filename)")
939
+ .option("--category <c>", "seed category (AI may re-classify)")
940
+ .option("--tags <csv>", "seed tags, comma-separated (AI merges with classifier tags)")
941
+ .option("--visibility <v>", "workspace | private (default: workspace)")
942
+ .option("--no-summarize", "skip AI auto-tag/classify (raw insert only)")
943
+ .option("--no-scrape", "skip Firecrawl scrape for --url (URL stored as bookmark)")
944
+ .option("--style-reference", "flag as a Tone-of-Voice style reference (for known author documents)")
945
+ .option("--auto-style", "score as a style reference on arrival and auto-flag if it meets the threshold (default 70)")
946
+ .action(async function (opts) {
947
+ try {
948
+ const sources = [opts.url, opts.text, opts.file].filter(Boolean);
949
+ if (sources.length !== 1) {
950
+ return fail(new Error("Provide exactly one of --url, --text, or --file"));
951
+ }
952
+ let extracted_text;
953
+ let derivedName = opts.name;
954
+ if (opts.file) {
955
+ extracted_text = await readFile(opts.file, "utf8");
956
+ if (!derivedName) derivedName = opts.file.split(/[\\/]/).pop();
957
+ } else if (opts.text) {
958
+ extracted_text = opts.text;
959
+ }
960
+ const params = {
961
+ content_url: opts.url || undefined,
962
+ extracted_text,
963
+ name: derivedName,
964
+ category: opts.category,
965
+ tags: opts.tags ? opts.tags.split(",").map((s) => s.trim()).filter(Boolean) : undefined,
966
+ visibility: opts.visibility,
967
+ skip_summarize: opts.summarize === false ? true : undefined,
968
+ skip_scrape: opts.scrape === false ? true : undefined,
969
+ is_style_reference: opts.styleReference === true ? true : undefined,
970
+ auto_style_reference: opts.autoStyle === true ? true : undefined,
971
+ };
972
+ const r = await call("knowledge", "add", params, ctxFrom(this));
973
+ emit(this, r, (d) => {
974
+ const it = d.item || {};
975
+ process.stdout.write(
976
+ `✓ Added knowledge item ${it.id}\n` +
977
+ ` name: ${it.name}\n` +
978
+ ` type: ${it.type}\n` +
979
+ ` category: ${it.category}\n` +
980
+ ` tags: ${(it.tags || []).join(", ") || "(none)"}\n` +
981
+ (it.summary ? ` summary: ${truncate(it.summary, 240)}\n` : "") +
982
+ (it.is_style_reference ? ` ★ style reference\n` : "") +
983
+ (d.ai?.success === false ? ` ⚠ AI classify: ${d.ai.error}\n` : "") +
984
+ (d.style_reference_triage?.data?.results?.[0] && !it.is_style_reference
985
+ ? ` style triage: ${d.style_reference_triage.data.results[0].score}/100 — ${d.style_reference_triage.data.results[0].reason}\n`
986
+ : ""),
987
+ );
988
+ });
989
+ } catch (e) { fail(e); }
990
+ });
991
+
992
+ withCommonOpts(k.command("update <id>"))
993
+ .description("Patch a knowledge item")
994
+ .option("--name <s>", "rename")
995
+ .option("--summary <s>", "set summary")
996
+ .option("--category <c>", "set category")
997
+ .option("--tags <csv>", "replace tags (comma-separated)")
998
+ .option("--visibility <v>", "workspace | private")
999
+ .option("--style-reference <on|off>", "flag/unflag as a Tone-of-Voice style reference")
1000
+ .action(async function (id, opts) {
1001
+ try {
1002
+ const patch = {
1003
+ id,
1004
+ name: opts.name,
1005
+ summary: opts.summary,
1006
+ category: opts.category,
1007
+ tags: opts.tags ? opts.tags.split(",").map((s) => s.trim()).filter(Boolean) : undefined,
1008
+ visibility: opts.visibility,
1009
+ is_style_reference: opts.styleReference === "on" ? true : opts.styleReference === "off" ? false : undefined,
1010
+ };
1011
+ const r = await call("knowledge", "update", patch, ctxFrom(this));
1012
+ emit(this, r, (d) => process.stdout.write(`✓ Updated ${d.item?.id}\n`));
1013
+ } catch (e) { fail(e); }
1014
+ });
1015
+
1016
+ withCommonOpts(k.command("approve <id...>"))
1017
+ .description("Confirm knowledge item(s) as the client's own material (unlocks voice training + evidence use)")
1018
+ .option("--author-name <name>", "Attribute to a named expert")
1019
+ .option("--authorship <kind>", "human|expert|client|team (default human)")
1020
+ .action(async function (ids, opts) {
1021
+ try {
1022
+ const payload = ids.length > 1
1023
+ ? { ids, author_name: opts.authorName, authorship: opts.authorship }
1024
+ : { id: ids[0], author_name: opts.authorName, authorship: opts.authorship };
1025
+ const r = await call("knowledge", "approve", payload, ctxFrom(this));
1026
+ emit(this, r, (d) => process.stdout.write(`✓ Confirmed ${(d.items || [d.item]).filter(Boolean).length} item(s)\n`));
1027
+ } catch (e) { fail(e); }
1028
+ });
1029
+
1030
+ withCommonOpts(k.command("reject <id...>"))
1031
+ .description("Mark knowledge item(s) as NOT the client's own material")
1032
+ .action(async function (ids) {
1033
+ try {
1034
+ const payload = ids.length > 1 ? { ids } : { id: ids[0] };
1035
+ const r = await call("knowledge", "reject", payload, ctxFrom(this));
1036
+ emit(this, r, (d) => process.stdout.write(`✓ Rejected ${(d.items || [d.item]).filter(Boolean).length} item(s)\n`));
1037
+ } catch (e) { fail(e); }
1038
+ });
1039
+
1040
+ withCommonOpts(k.command("set-authorship <id>"))
1041
+ .description("Set who wrote a knowledge item (human|expert|client|team|ai|unknown)")
1042
+ .requiredOption("--authorship <kind>", "human|expert|client|team|ai|unknown")
1043
+ .option("--author-name <name>", "Named author")
1044
+ .option("--approval-status <status>", "approved|pending|rejected")
1045
+ .action(async function (id, opts) {
1046
+ try {
1047
+ const r = await call("knowledge", "set_authorship", {
1048
+ id,
1049
+ authorship: opts.authorship,
1050
+ author_name: opts.authorName,
1051
+ approval_status: opts.approvalStatus,
1052
+ }, ctxFrom(this));
1053
+ emit(this, r, (d) => {
1054
+ const it = d.item || {};
1055
+ process.stdout.write(`✓ ${it.id}: ${it.authorship}${it.author_name ? ` (${it.author_name})` : ""} · ${it.approval_status}\n`);
1056
+ });
1057
+ } catch (e) { fail(e); }
1058
+ });
1059
+
1060
+ withCommonOpts(k.command("reclassify <id>"))
1061
+ .description("Re-run AI auto-tag/classify (same as the UI 'sparkles' button)")
1062
+ .action(async function (id) {
1063
+ try {
1064
+ const r = await call("knowledge", "reclassify", { id }, ctxFrom(this));
1065
+ emit(this, r, (d) => {
1066
+ const it = d.item || {};
1067
+ process.stdout.write(
1068
+ `✓ Reclassified ${it.id}\n` +
1069
+ ` category: ${it.category}\n` +
1070
+ ` tags: ${(it.tags || []).join(", ") || "(none)"}\n` +
1071
+ (it.summary ? ` summary: ${truncate(it.summary, 240)}\n` : ""),
1072
+ );
1073
+ });
1074
+ } catch (e) { fail(e); }
1075
+ });
1076
+
1077
+ withCommonOpts(k.command("deactivate <id>"))
1078
+ .description("Soft-delete (sets is_active = false)")
1079
+ .action(async function (id) {
1080
+ try {
1081
+ const r = await call("knowledge", "deactivate", { id }, ctxFrom(this));
1082
+ emit(this, r, (d) => process.stdout.write(`✓ Deactivated ${d.item?.id}\n`));
1083
+ } catch (e) { fail(e); }
1084
+ });
1085
+ }
1086
+
1087
+ export function registerContent(program) {
1088
+ const c = program.command("content").description("Browse, create, schedule and publish content");
1089
+
1090
+ withCommonOpts(c.command("list"))
1091
+ .description("List content items")
1092
+ .option("--status <s>", "filter by status (draft|scheduled|published|…)")
1093
+ .option("--type <t>", "filter by type (article|linkedin_post|…)")
1094
+ .option("--approval <s>", "filter by approval_status")
1095
+ .option("--search <q>", "search title or body")
1096
+ .option("--limit <n>", "max rows", "50")
1097
+ .action(async function () {
1098
+ try {
1099
+ const o = this.optsWithGlobals();
1100
+ const r = await call("content", "list", {
1101
+ status: o.status, type: o.type, approval_status: o.approval,
1102
+ search: o.search, limit: Number(o.limit),
1103
+ }, ctxFrom(this));
1104
+ emit(this, r, (d) => {
1105
+ process.stdout.write(table(
1106
+ ["id", "type", "status", "title"],
1107
+ (d.content || []).map((c) => [truncate(c.id, 8), c.type, c.status, truncate(c.title, 60)]),
1108
+ ) + `\n${d.content?.length ?? 0} rows\n`);
1109
+ });
1110
+ } catch (e) { fail(e); }
1111
+ });
1112
+
1113
+ withCommonOpts(c.command("show <id>"))
1114
+ .description("Show a content item")
1115
+ .action(async function (id) {
1116
+ try {
1117
+ const r = await call("content", "get", { id }, ctxFrom(this));
1118
+ emit(this, r, (d) => process.stdout.write(JSON.stringify(d.content, null, 2) + "\n"));
1119
+ } catch (e) { fail(e); }
1120
+ });
1121
+
1122
+ withCommonOpts(c.command("create"))
1123
+ .description("Create a new content item")
1124
+ .requiredOption("--title <t>", "title")
1125
+ .option("--type <t>", "article | linkedin_post | newsletter | …", "linkedin_post")
1126
+ .option("--body <text>", "inline body")
1127
+ .option("--body-file <path>", "read body from a local file")
1128
+ .option("--platforms <csv>", "target platforms (linkedin,blog,…)")
1129
+ .option("--tags <csv>", "tags")
1130
+ .option("--scheduled <ts>", "ISO timestamp (sets status=scheduled)")
1131
+ .option("--campaign <id>", "campaign_id")
1132
+ .action(async function (opts) {
1133
+ try {
1134
+ let body = opts.body;
1135
+ if (opts.bodyFile) body = await readFile(opts.bodyFile, "utf8");
1136
+ const r = await call("content", "create", {
1137
+ title: opts.title, type: opts.type, body: body ?? "",
1138
+ target_platforms: opts.platforms ? opts.platforms.split(",").map((s) => s.trim()) : undefined,
1139
+ tags: opts.tags ? opts.tags.split(",").map((s) => s.trim()) : undefined,
1140
+ scheduled_date: opts.scheduled, status: opts.scheduled ? "scheduled" : undefined,
1141
+ campaign_id: opts.campaign,
1142
+ }, ctxFrom(this));
1143
+ emit(this, r, (d) => process.stdout.write(`✓ Created content ${d.content?.id}\n`));
1144
+ } catch (e) { fail(e); }
1145
+ });
1146
+
1147
+ withCommonOpts(c.command("update <id>"))
1148
+ .description("Patch a content item")
1149
+ .option("--title <t>")
1150
+ .option("--body <text>")
1151
+ .option("--body-file <path>")
1152
+ .option("--status <s>")
1153
+ .option("--platforms <csv>")
1154
+ .option("--tags <csv>")
1155
+ .option("--scheduled <ts>")
1156
+ .action(async function (id, opts) {
1157
+ try {
1158
+ let body = opts.body;
1159
+ if (opts.bodyFile) body = await readFile(opts.bodyFile, "utf8");
1160
+ const r = await call("content", "update", {
1161
+ id, title: opts.title, body, status: opts.status,
1162
+ target_platforms: opts.platforms ? opts.platforms.split(",").map((s) => s.trim()) : undefined,
1163
+ tags: opts.tags ? opts.tags.split(",").map((s) => s.trim()) : undefined,
1164
+ scheduled_date: opts.scheduled,
1165
+ }, ctxFrom(this));
1166
+ emit(this, r, (d) => process.stdout.write(`✓ Updated ${d.content?.id}\n`));
1167
+ } catch (e) { fail(e); }
1168
+ });
1169
+
1170
+ withCommonOpts(c.command("schedule <id> <isoTimestamp>"))
1171
+ .description("Schedule a content item (Taplio when connected, otherwise built-in cron)")
1172
+ .option("--channel <c>", "linkedin | blog | newsletter")
1173
+ .option("--publish-as <p>", "personal | company")
1174
+ .option("--company-page <id>", "company page id when publishing as company")
1175
+ .option("--timezone <tz>", "IANA timezone label to store")
1176
+ .action(async function (id, ts, opts) {
1177
+ try {
1178
+ const r = await call("content", "schedule", {
1179
+ id, scheduled_for: ts, channel: opts.channel,
1180
+ publish_as: opts.publishAs, company_page_id: opts.companyPage, timezone: opts.timezone,
1181
+ }, ctxFrom(this));
1182
+ emit(this, r, (d) => process.stdout.write(`✓ Scheduled via ${d.engine} for ${d.scheduled_for}\n`));
1183
+ } catch (e) { fail(e); }
1184
+ });
1185
+
1186
+ withCommonOpts(c.command("unschedule <id>"))
1187
+ .description("Cancel a scheduled content item on whichever engine holds it")
1188
+ .action(async function (id) {
1189
+ try {
1190
+ const r = await call("content", "unschedule", { id }, ctxFrom(this));
1191
+ emit(this, r, (d) => process.stdout.write(`✓ Unscheduled ${d.unscheduled ?? id}${d.engine ? ` (${d.engine})` : ""}\n`));
1192
+ } catch (e) { fail(e); }
1193
+ });
1194
+
1195
+ withCommonOpts(c.command("approve <id>"))
1196
+ .description("Approve a content item (approval_status=approved)")
1197
+ .action(async function (id) {
1198
+ try {
1199
+ const r = await call("content", "approve", { id }, ctxFrom(this));
1200
+ emit(this, r, (d) => process.stdout.write(`✓ Approved ${d.content?.id}\n`));
1201
+ } catch (e) { fail(e); }
1202
+ });
1203
+
1204
+ withCommonOpts(c.command("bulk-approve [ids...]"))
1205
+ .description("Approve many content items (all pending when no ids given)")
1206
+ .option("--campaign <id>", "only pending items in this campaign")
1207
+ .option("-y, --yes", "skip the confirmation prompt")
1208
+ .action(async function (ids, opts) {
1209
+ try {
1210
+ if (!ids?.length) await confirmDestructive(this, "Approve every pending content item in this workspace.");
1211
+ const r = await call("content", "bulk_approve", {
1212
+ ids: ids?.length ? ids : undefined, campaign_id: opts.campaign,
1213
+ }, ctxFrom(this));
1214
+ emit(this, r, (d) => process.stdout.write(`✓ Approved ${d.approved} items\n`));
1215
+ } catch (e) { fail(e); }
1216
+ });
1217
+
1218
+ withCommonOpts(c.command("bulk-schedule <ids...>"))
1219
+ .description("Schedule several content items on a cadence via the live engine")
1220
+ .option("--start <iso>", "first slot (default: in 1 hour)")
1221
+ .option("--interval-hours <n>", "hours between posts", "24")
1222
+ .option("--channel <c>", "linkedin | blog | newsletter")
1223
+ .option("--publish-as <p>", "personal | company")
1224
+ .action(async function (ids, opts) {
1225
+ try {
1226
+ const r = await call("content", "bulk_schedule", {
1227
+ ids, start: opts.start, interval_hours: Number(opts.intervalHours),
1228
+ channel: opts.channel, publish_as: opts.publishAs,
1229
+ }, ctxFrom(this));
1230
+ emit(this, r, (d) => {
1231
+ process.stdout.write(table(
1232
+ ["id", "ok", "engine", "when / error"],
1233
+ (d.results || []).map((x) => [truncate(x.id, 8), x.ok ? "yes" : "no", x.engine ?? "—", x.scheduled_for ?? truncate(x.error ?? "", 50)]),
1234
+ ) + `\n${d.scheduled} scheduled, ${d.failed} failed\n`);
1235
+ });
1236
+ } catch (e) { fail(e); }
1237
+ });
1238
+
1239
+ withCommonOpts(c.command("publish <id>"))
1240
+ .description("Publish a content item immediately")
1241
+ .option("--channel <c>", "linkedin | blog | newsletter")
1242
+ .option("--publish-as <p>", "personal | company")
1243
+ .option("--company-page <id>", "company page id when publishing as company")
1244
+ .option("-y, --yes", "skip the confirmation prompt")
1245
+ .action(async function (id, opts) {
1246
+ try {
1247
+ await confirmDestructive(this, `Publish content item ${id} right now.`);
1248
+ const r = await call("content", "publish_now", {
1249
+ id, channel: opts.channel, publish_as: opts.publishAs, company_page_id: opts.companyPage,
1250
+ }, ctxFrom(this));
1251
+ emit(this, r, (d) => process.stdout.write(`✓ Published via ${d.worker}\n`));
1252
+ } catch (e) { fail(e); }
1253
+ });
1254
+
1255
+
1256
+ withCommonOpts(c.command("delete <id>"))
1257
+ .description("Delete a content item")
1258
+ .option("-y, --yes", "skip the confirmation prompt")
1259
+ .action(async function (id) {
1260
+ try {
1261
+ await confirmDestructive(this, `Delete content item ${id}. This cannot be undone.`);
1262
+ const r = await call("content", "delete", { id }, ctxFrom(this));
1263
+ emit(this, r, (d) => process.stdout.write(`✓ Deleted ${d.deleted}\n`));
1264
+ } catch (e) { fail(e); }
1265
+ });
1266
+
1267
+ withCommonOpts(c.command("generate"))
1268
+ .description("Generate a draft via agent-content-creator (KB + reference library grounded, URL-sanitised, citations recorded)")
1269
+ .requiredOption("--angle <text>", "topic / angle for the draft")
1270
+ .option("--format <f>", "blog | linkedin_post | video | explainer_video", "blog")
1271
+ .option("--signal <id...>", "intelligence_signals ids to ground on (repeatable)")
1272
+ .option("--reference <id...>", "knowledge_items ids to ground on (repeatable)")
1273
+ .option("--grounding-text <text>", "extra freeform grounding to append")
1274
+ .option("--wait", "poll until the job finishes (up to 10 min)")
1275
+ .option("--timeout <seconds>", "how long --wait polls before giving up", "600")
1276
+ .action(async function (opts) {
1277
+ try {
1278
+ // Always dispatch asynchronously and poll the returned job id. Holding
1279
+ // one long HTTP request open was what produced spurious timeouts, and
1280
+ // polling by the id the server handed back is what keeps --wait from
1281
+ // ever reporting job_not_found for a job that exists.
1282
+ const ctx = ctxFrom(this);
1283
+ const r = await call("content", "generate", {
1284
+ angle: opts.angle,
1285
+ format: opts.format,
1286
+ signal_ids: opts.signal,
1287
+ reference_item_ids: opts.reference,
1288
+ grounding_text: opts.groundingText,
1289
+ async: true,
1290
+ }, ctx);
1291
+ const jobId = r?.job_id || r?.job?.id || null;
1292
+ if (!opts.wait || !jobId) {
1293
+ emit(this, r, (d) => {
1294
+ if (d.content) process.stdout.write(`✓ Draft ${d.content.id} (${d.content.type}, ${d.citations?.length ?? 0} citations)\n`);
1295
+ else process.stdout.write(`⏳ Queued job ${d.job_id} — poll with: advisoros agents job-status ${d.job_id}\n`);
1296
+ });
1297
+ return;
1298
+ }
1299
+ const deadline = Date.now() + Number(opts.timeout || 600) * 1000;
1300
+ let last = null;
1301
+ while (Date.now() < deadline) {
1302
+ await new Promise((res) => setTimeout(res, 5000));
1303
+ let st;
1304
+ try {
1305
+ st = await call("agents", "job_status", { job_id: jobId }, ctx);
1306
+ } catch (e) {
1307
+ // A transient lookup failure must never end the wait — the job id
1308
+ // came from the server, so keep polling until the deadline.
1309
+ continue;
1310
+ }
1311
+ last = st?.job || st;
1312
+ const status = last?.status;
1313
+ if (status === "completed" || status === "succeeded" || status === "failed") break;
1314
+ }
1315
+ emit(this, { job_id: jobId, job: last }, (d) => {
1316
+ const st = d.job?.status || "unknown";
1317
+ const ids = d.job?.result?.content_ids || [];
1318
+ process.stdout.write(st === "completed" || st === "succeeded"
1319
+ ? `✓ Job ${d.job_id} ${st}${ids.length ? ` — draft ${ids[0]}` : ""}\n`
1320
+ : `⏳ Job ${d.job_id} is ${st} — check with: advisoros agents job-status ${d.job_id}\n`);
1321
+ });
1322
+ } catch (e) { fail(e); }
1323
+ });
1324
+
1325
+ withCommonOpts(c.command("citations <id>"))
1326
+ .description("List every source (signal, reference, transcript, published) recorded for a content item")
1327
+ .action(async function (id) {
1328
+ try {
1329
+ const r = await call("content", "citations", { id }, ctxFrom(this));
1330
+ emit(this, r, (d) => {
1331
+ process.stdout.write(table(
1332
+ ["kind", "id", "title", "url"],
1333
+ (d.citations || []).map((c) => [c.source_kind, truncate(c.source_id ?? "", 8), truncate(c.title ?? "", 40), truncate(c.source_url ?? "", 60)]),
1334
+ ) + `\n${d.citations?.length ?? 0} citations\n`);
1335
+ });
1336
+ } catch (e) { fail(e); }
1337
+ });
1338
+ }
1339
+
1340
+
1341
+ export function registerCalendar(program) {
1342
+ const cal = program.command("calendar").description("Manage the content calendar");
1343
+
1344
+ withCommonOpts(cal.command("list"))
1345
+ .description("List scheduled calendar posts")
1346
+ .option("--from <date>", "YYYY-MM-DD (inclusive)")
1347
+ .option("--to <date>", "YYYY-MM-DD (inclusive)")
1348
+ .option("--platform <p>", "filter by platform")
1349
+ .option("--status <s>", "filter by status")
1350
+ .option("--campaign <id>", "filter by campaign_id")
1351
+ .option("--limit <n>", "max rows", "100")
1352
+ .action(async function () {
1353
+ try {
1354
+ const o = this.optsWithGlobals();
1355
+ const r = await call("calendar", "list", {
1356
+ from: o.from, to: o.to, platform: o.platform, status: o.status,
1357
+ campaign_id: o.campaign, limit: Number(o.limit),
1358
+ }, ctxFrom(this));
1359
+ emit(this, r, (d) => {
1360
+ process.stdout.write(table(
1361
+ ["id", "date", "time", "platform", "status", "title"],
1362
+ (d.posts || []).map((p) => [
1363
+ truncate(p.id, 8), p.scheduled_date, p.scheduled_time ?? "—",
1364
+ p.platform, p.status, truncate(p.title, 50),
1365
+ ]),
1366
+ ) + `\n${d.posts?.length ?? 0} rows\n`);
1367
+ });
1368
+ } catch (e) { fail(e); }
1369
+ });
1370
+
1371
+ withCommonOpts(cal.command("show <id>"))
1372
+ .description("Show a calendar post")
1373
+ .action(async function (id) {
1374
+ try {
1375
+ const r = await call("calendar", "get", { id }, ctxFrom(this));
1376
+ emit(this, r, (d) => process.stdout.write(JSON.stringify(d.post, null, 2) + "\n"));
1377
+ } catch (e) { fail(e); }
1378
+ });
1379
+
1380
+ withCommonOpts(cal.command("create"))
1381
+ .description("Create a calendar post")
1382
+ .requiredOption("--title <t>", "title")
1383
+ .requiredOption("--date <YYYY-MM-DD>", "scheduled date")
1384
+ .option("--time <HH:MM>", "scheduled time")
1385
+ .option("--platform <p>", "linkedin | blog | twitter | …", "linkedin")
1386
+ .option("--content <text>", "post body")
1387
+ .option("--content-file <path>", "read body from a local file")
1388
+ .option("--hashtags <csv>", "hashtags (no #)")
1389
+ .option("--cta <text>", "call to action")
1390
+ .option("--campaign <id>", "campaign_id")
1391
+ .option("--status <s>", "draft | scheduled | published", "draft")
1392
+ .option("--at <iso>", "ISO timestamp — schedules the post immediately via the live engine")
1393
+ .action(async function (opts) {
1394
+ try {
1395
+ let content = opts.content;
1396
+ if (opts.contentFile) content = await readFile(opts.contentFile, "utf8");
1397
+ const r = await call("calendar", "create", {
1398
+ scheduled_for: opts.at,
1399
+ title: opts.title, scheduled_date: opts.date, scheduled_time: opts.time,
1400
+ platform: opts.platform, content: content ?? "",
1401
+ hashtags: opts.hashtags ? opts.hashtags.split(",").map((s) => s.trim()) : undefined,
1402
+ cta: opts.cta, campaign_id: opts.campaign, status: opts.status,
1403
+ }, ctxFrom(this));
1404
+ emit(this, r, (d) => process.stdout.write(`✓ Created calendar post ${d.post?.id}\n`));
1405
+ } catch (e) { fail(e); }
1406
+ });
1407
+
1408
+ withCommonOpts(cal.command("update <id>"))
1409
+ .description("Patch a calendar post")
1410
+ .option("--title <t>")
1411
+ .option("--date <YYYY-MM-DD>")
1412
+ .option("--time <HH:MM>")
1413
+ .option("--content <text>")
1414
+ .option("--content-file <path>")
1415
+ .option("--status <s>")
1416
+ .option("--platform <p>")
1417
+ .option("--hashtags <csv>")
1418
+ .option("--cta <text>")
1419
+ .action(async function (id, opts) {
1420
+ try {
1421
+ let content = opts.content;
1422
+ if (opts.contentFile) content = await readFile(opts.contentFile, "utf8");
1423
+ const r = await call("calendar", "update", {
1424
+ id, title: opts.title, scheduled_date: opts.date, scheduled_time: opts.time,
1425
+ content, status: opts.status, platform: opts.platform,
1426
+ hashtags: opts.hashtags ? opts.hashtags.split(",").map((s) => s.trim()) : undefined,
1427
+ cta: opts.cta,
1428
+ }, ctxFrom(this));
1429
+ emit(this, r, (d) => process.stdout.write(`✓ Updated ${d.post?.id}\n`));
1430
+ } catch (e) { fail(e); }
1431
+ });
1432
+
1433
+ withCommonOpts(cal.command("delete <id>"))
1434
+ .description("Delete a calendar post (cancels it on Taplio first when applicable)")
1435
+ .option("-y, --yes", "skip the confirmation prompt")
1436
+ .action(async function (id) {
1437
+ try {
1438
+ await confirmDestructive(this, `Delete calendar post ${id}. This cannot be undone.`);
1439
+ const r = await call("calendar", "delete", { id }, ctxFrom(this));
1440
+ emit(this, r, (d) => process.stdout.write(`✓ Deleted ${d.deleted ?? d.unscheduled ?? id}\n`));
1441
+ } catch (e) { fail(e); }
1442
+ });
1443
+
1444
+ withCommonOpts(cal.command("engine"))
1445
+ .description("Show which scheduler is live for this workspace (Taplio or built-in cron)")
1446
+ .option("--channel <c>", "linkedin | blog | newsletter", "linkedin")
1447
+ .action(async function (opts) {
1448
+ try {
1449
+ const r = await call("calendar", "engine", { channel: opts.channel }, ctxFrom(this));
1450
+ emit(this, r, (d) => {
1451
+ process.stdout.write(
1452
+ `Engine: ${d.engine}\n` +
1453
+ `Channel: ${d.channel}\n` +
1454
+ `Taplio connected: ${d.taplio_connected ? "yes" : "no"}\n` +
1455
+ `Dispatch worker: ${d.dispatch_worker}\n` +
1456
+ `Reason: ${d.reason}\n` +
1457
+ `Pending scheduled: ${d.scheduled_pending}\n` +
1458
+ (d.recent_failures?.length
1459
+ ? `\nRecent failures:\n` + table(
1460
+ ["id", "when", "attempts", "error"],
1461
+ d.recent_failures.map((f) => [truncate(f.id, 8), f.scheduled_for ?? "—", f.dispatch_attempts ?? 0, truncate(f.dispatch_error ?? "", 60)]),
1462
+ ) + "\n"
1463
+ : ""),
1464
+ );
1465
+ });
1466
+ } catch (e) { fail(e); }
1467
+ });
1468
+
1469
+ withCommonOpts(cal.command("schedule <id>"))
1470
+ .description("Schedule (or reschedule) a calendar post via the live engine")
1471
+ .requiredOption("--at <iso>", "ISO timestamp, e.g. 2026-09-10T09:00:00Z")
1472
+ .option("--channel <c>", "linkedin | blog | newsletter")
1473
+ .option("--publish-as <p>", "personal | company")
1474
+ .option("--company-page <id>", "company page id when publishing as company")
1475
+ .option("--timezone <tz>", "IANA timezone label to store")
1476
+ .action(async function (id, opts) {
1477
+ try {
1478
+ const r = await call("calendar", "schedule", {
1479
+ id, scheduled_for: opts.at, channel: opts.channel,
1480
+ publish_as: opts.publishAs, company_page_id: opts.companyPage, timezone: opts.timezone,
1481
+ }, ctxFrom(this));
1482
+ emit(this, r, (d) => process.stdout.write(`✓ Scheduled via ${d.engine} for ${d.scheduled_for}\n`));
1483
+ } catch (e) { fail(e); }
1484
+ });
1485
+
1486
+ withCommonOpts(cal.command("unschedule <id>"))
1487
+ .description("Cancel a scheduled calendar post (cancels on Taplio when it owns it)")
1488
+ .action(async function (id) {
1489
+ try {
1490
+ const r = await call("calendar", "unschedule", { id }, ctxFrom(this));
1491
+ emit(this, r, (d) => process.stdout.write(`✓ Unscheduled ${d.unscheduled} (${d.engine})\n`));
1492
+ } catch (e) { fail(e); }
1493
+ });
1494
+
1495
+ withCommonOpts(cal.command("publish-now <id>"))
1496
+ .description("Publish a calendar post immediately")
1497
+ .option("-y, --yes", "skip the confirmation prompt")
1498
+ .action(async function (id) {
1499
+ try {
1500
+ await confirmDestructive(this, `Publish calendar post ${id} right now.`);
1501
+ const r = await call("calendar", "publish_now", { id }, ctxFrom(this));
1502
+ emit(this, r, (d) => process.stdout.write(`✓ Published via ${d.worker}\n`));
1503
+ } catch (e) { fail(e); }
1504
+ });
1505
+
1506
+ withCommonOpts(cal.command("bulk-schedule <ids...>"))
1507
+ .description("Schedule several calendar posts on a cadence")
1508
+ .option("--start <iso>", "first slot (default: in 1 hour)")
1509
+ .option("--interval-hours <n>", "hours between posts", "24")
1510
+ .option("--channel <c>", "linkedin | blog | newsletter")
1511
+ .action(async function (ids, opts) {
1512
+ try {
1513
+ const r = await call("calendar", "bulk_schedule", {
1514
+ ids, start: opts.start, interval_hours: Number(opts.intervalHours), channel: opts.channel,
1515
+ }, ctxFrom(this));
1516
+ emit(this, r, (d) => {
1517
+ process.stdout.write(table(
1518
+ ["id", "ok", "engine", "when / error"],
1519
+ (d.results || []).map((x) => [truncate(x.id, 8), x.ok ? "yes" : "no", x.engine ?? "—", x.scheduled_for ?? truncate(x.error ?? "", 50)]),
1520
+ ) + `\n${d.scheduled} scheduled, ${d.failed} failed\n`);
1521
+ });
1522
+ } catch (e) { fail(e); }
1523
+ });
1524
+ }
1525
+
1526
+
1527
+ export function registerSignals(program) {
1528
+ const s = program.command("signals").description("Read and manage market intelligence signals");
1529
+
1530
+ withCommonOpts(s.command("list"))
1531
+ .description("List intelligence signals")
1532
+ .option("--type <t>", "signal_type (news|funding|hiring|…)")
1533
+ .option("--category <c>", "filter by category")
1534
+ .option("--agent <name>", "filter by agent_name")
1535
+ .option("--unread", "only unread")
1536
+ .option("--min-score <n>", "min relevance_score")
1537
+ .option("--search <q>", "search title/summary")
1538
+ .option("--limit <n>", "max rows", "50")
1539
+ .action(async function () {
1540
+ try {
1541
+ const o = this.optsWithGlobals();
1542
+ const r = await call("signals", "list", {
1543
+ signal_type: o.type, category: o.category, agent_name: o.agent,
1544
+ is_read: o.unread ? false : undefined, min_score: o.minScore,
1545
+ search: o.search, limit: Number(o.limit),
1546
+ }, ctxFrom(this));
1547
+ emit(this, r, (d) => {
1548
+ process.stdout.write(table(
1549
+ ["id", "score", "type", "agent", "title"],
1550
+ (d.signals || []).map((g) => [
1551
+ truncate(g.id, 8), g.relevance_score ?? "", g.signal_type,
1552
+ truncate(g.agent_name, 22), truncate(g.title, 60),
1553
+ ]),
1554
+ ) + `\n${d.signals?.length ?? 0} rows\n`);
1555
+ });
1556
+ } catch (e) { fail(e); }
1557
+ });
1558
+
1559
+ withCommonOpts(s.command("show <id>"))
1560
+ .description("Show a signal")
1561
+ .action(async function (id) {
1562
+ try {
1563
+ const r = await call("signals", "get", { id }, ctxFrom(this));
1564
+ emit(this, r, (d) => process.stdout.write(JSON.stringify(d.signal, null, 2) + "\n"));
1565
+ } catch (e) { fail(e); }
1566
+ });
1567
+
1568
+ withCommonOpts(s.command("create"))
1569
+ .description("Create a manual signal")
1570
+ .requiredOption("--title <t>", "title")
1571
+ .requiredOption("--agent <name>", "agent_name (source)")
1572
+ .option("--type <t>", "signal_type", "news")
1573
+ .option("--category <c>", "category", "general")
1574
+ .option("--url <url>", "source url")
1575
+ .option("--summary <text>", "short summary")
1576
+ .option("--why <text>", "why_it_matters")
1577
+ .option("--action <text>", "recommended_action")
1578
+ .option("--score <n>", "relevance_score 0-100", "50")
1579
+ .option("--flags <csv>", "trigger_flags (funding,hiring,…)")
1580
+ .action(async function (opts) {
1581
+ try {
1582
+ const r = await call("signals", "create", {
1583
+ title: opts.title, agent_name: opts.agent, signal_type: opts.type,
1584
+ category: opts.category, url: opts.url, summary: opts.summary,
1585
+ why_it_matters: opts.why, recommended_action: opts.action,
1586
+ relevance_score: Number(opts.score),
1587
+ trigger_flags: opts.flags ? opts.flags.split(",").map((s) => s.trim()) : undefined,
1588
+ }, ctxFrom(this));
1589
+ emit(this, r, (d) => process.stdout.write(`✓ Created signal ${d.signal?.id}\n`));
1590
+ } catch (e) { fail(e); }
1591
+ });
1592
+
1593
+ withCommonOpts(s.command("update <id>"))
1594
+ .description("Patch a signal")
1595
+ .option("--title <t>")
1596
+ .option("--summary <text>")
1597
+ .option("--why <text>")
1598
+ .option("--action <text>")
1599
+ .option("--score <n>")
1600
+ .option("--category <c>")
1601
+ .option("--sentiment <s>")
1602
+ .option("--flags <csv>")
1603
+ .action(async function (id, opts) {
1604
+ try {
1605
+ const r = await call("signals", "update", {
1606
+ id, title: opts.title, summary: opts.summary,
1607
+ why_it_matters: opts.why, recommended_action: opts.action,
1608
+ relevance_score: opts.score ? Number(opts.score) : undefined,
1609
+ category: opts.category, sentiment: opts.sentiment,
1610
+ trigger_flags: opts.flags ? opts.flags.split(",").map((s) => s.trim()) : undefined,
1611
+ }, ctxFrom(this));
1612
+ emit(this, r, (d) => process.stdout.write(`✓ Updated ${d.signal?.id}\n`));
1613
+ } catch (e) { fail(e); }
1614
+ });
1615
+
1616
+ withCommonOpts(s.command("read <id>"))
1617
+ .description("Mark a signal as read (use --unread to flip back)")
1618
+ .option("--unread", "mark as unread instead")
1619
+ .action(async function (id, opts) {
1620
+ try {
1621
+ const r = await call("signals", "mark_read", { id, is_read: !opts.unread }, ctxFrom(this));
1622
+ emit(this, r, (d) => process.stdout.write(`✓ ${d.signal?.id} → is_read=${d.signal?.is_read}\n`));
1623
+ } catch (e) { fail(e); }
1624
+ });
1625
+
1626
+ withCommonOpts(s.command("delete <id>"))
1627
+ .description("Delete a signal")
1628
+ .option("-y, --yes", "skip the confirmation prompt")
1629
+ .action(async function (id) {
1630
+ try {
1631
+ await confirmDestructive(this, `Delete signal ${id}. This cannot be undone.`);
1632
+ const r = await call("signals", "delete", { id }, ctxFrom(this));
1633
+ emit(this, r, (d) => process.stdout.write(`✓ Deleted ${d.deleted}\n`));
1634
+ } catch (e) { fail(e); }
1635
+ });
1636
+
1637
+ withCommonOpts(s.command("cleanup"))
1638
+ .description("Clean up the signal stream (duplicates, irrelevant, quarantined, stale, low score). Dry-run unless --confirm")
1639
+ .option("--modes <csv>", "duplicates,irrelevant,quarantined,stale,low_score", "duplicates")
1640
+ .option("--older-than <days>", "for the stale mode")
1641
+ .option("--max-score <n>", "for the low_score mode")
1642
+ .option("--include-read", "also clean signals already read and marked relevant")
1643
+ .option("--scan-limit <n>", "how many recent signals to scan", "5000")
1644
+ .option("--confirm", "actually delete (default is a dry run)")
1645
+ .option("-y, --yes", "skip the confirmation prompt")
1646
+ .action(async function (opts) {
1647
+ try {
1648
+ if (opts.confirm) {
1649
+ await confirmDestructive(this, "Permanently delete the matching signals. This cannot be undone.");
1650
+ }
1651
+ const r = await call("signals", "cleanup", {
1652
+ modes: String(opts.modes).split(",").map((m) => m.trim()).filter(Boolean),
1653
+ older_than_days: opts.olderThan ? Number(opts.olderThan) : undefined,
1654
+ max_score: opts.maxScore ? Number(opts.maxScore) : undefined,
1655
+ keep_read: opts.includeRead ? false : undefined,
1656
+ scan_limit: Number(opts.scanLimit),
1657
+ confirm: !!opts.confirm,
1658
+ }, ctxFrom(this));
1659
+ emit(this, r, (d) => {
1660
+ const c = d.counts || {};
1661
+ process.stdout.write(table(
1662
+ ["bucket", "count"],
1663
+ Object.entries(c).map(([k, v]) => [k, String(v)]),
1664
+ ) + "\n");
1665
+ process.stdout.write(d.dry_run
1666
+ ? `Dry run — scanned ${d.scanned}, would delete ${d.would_delete}. Re-run with --confirm to apply.\n`
1667
+ : `✓ Scanned ${d.scanned}, deleted ${d.deleted}\n`);
1668
+ });
1669
+ } catch (e) { fail(e); }
1670
+ });
1671
+ }
1672
+
1673
+ export function registerPipelines(program) {
1674
+ const p = program.command("pipelines").description("Manage the opportunities / deal pipeline");
1675
+
1676
+ withCommonOpts(p.command("list"))
1677
+ .description("List opportunities (pipeline)")
1678
+ .option("--status <s>", "filter by status")
1679
+ .option("--stage <s>", "filter by pipeline_stage")
1680
+ .option("--outcome <s>", "qualification_outcome (pending|qualified_in|qualified_out)")
1681
+ .option("--urgency <s>", "low | medium | high")
1682
+ .option("--search <q>", "search title/account")
1683
+ .option("--limit <n>", "max rows", "50")
1684
+ .action(async function () {
1685
+ try {
1686
+ const o = this.optsWithGlobals();
1687
+ const r = await call("pipelines", "list", {
1688
+ status: o.status, pipeline_stage: o.stage,
1689
+ qualification_outcome: o.outcome, urgency: o.urgency,
1690
+ search: o.search, limit: Number(o.limit),
1691
+ }, ctxFrom(this));
1692
+ emit(this, r, (d) => {
1693
+ process.stdout.write(table(
1694
+ ["id", "stage", "score", "value", "account", "title"],
1695
+ (d.opportunities || []).map((op) => [
1696
+ truncate(op.id, 8), op.pipeline_stage, op.score ?? "",
1697
+ `${op.deal_currency ?? ""}${op.deal_value ?? 0}`,
1698
+ truncate(op.account, 22), truncate(op.title, 40),
1699
+ ]),
1700
+ ) + `\n${d.opportunities?.length ?? 0} rows\n`);
1701
+ });
1702
+ } catch (e) { fail(e); }
1703
+ });
1704
+
1705
+ withCommonOpts(p.command("show <id>"))
1706
+ .description("Show an opportunity")
1707
+ .action(async function (id) {
1708
+ try {
1709
+ const r = await call("pipelines", "get", { id }, ctxFrom(this));
1710
+ emit(this, r, (d) => process.stdout.write(JSON.stringify(d.opportunity, null, 2) + "\n"));
1711
+ } catch (e) { fail(e); }
1712
+ });
1713
+
1714
+ withCommonOpts(p.command("create"))
1715
+ .description("Create an opportunity")
1716
+ .requiredOption("--title <t>", "title")
1717
+ .option("--account <a>", "account / company")
1718
+ .option("--type <t>", "revenue | risk | …", "revenue")
1719
+ .option("--urgency <u>", "low | medium | high", "medium")
1720
+ .option("--stage <s>", "pipeline_stage", "engaged")
1721
+ .option("--value <n>", "deal_value", "0")
1722
+ .option("--currency <c>", "deal_currency", "USD")
1723
+ .option("--why <text>", "why_it_matters")
1724
+ .option("--action <text>", "recommended_action")
1725
+ .option("--contact <name>", "contact_name")
1726
+ .option("--email <e>", "contact_email")
1727
+ .option("--close <YYYY-MM-DD>", "close_date")
1728
+ .option("--notes <text>", "notes")
1729
+ .action(async function (opts) {
1730
+ try {
1731
+ const r = await call("pipelines", "create", {
1732
+ title: opts.title, account: opts.account, type: opts.type,
1733
+ urgency: opts.urgency, pipeline_stage: opts.stage,
1734
+ deal_value: Number(opts.value), deal_currency: opts.currency,
1735
+ why_it_matters: opts.why, recommended_action: opts.action,
1736
+ contact_name: opts.contact, contact_email: opts.email,
1737
+ close_date: opts.close, notes: opts.notes,
1738
+ }, ctxFrom(this));
1739
+ emit(this, r, (d) => process.stdout.write(`✓ Created opportunity ${d.opportunity?.id}\n`));
1740
+ } catch (e) { fail(e); }
1741
+ });
1742
+
1743
+ withCommonOpts(p.command("update <id>"))
1744
+ .description("Patch an opportunity")
1745
+ .option("--title <t>")
1746
+ .option("--account <a>")
1747
+ .option("--stage <s>", "pipeline_stage")
1748
+ .option("--status <s>")
1749
+ .option("--value <n>", "deal_value")
1750
+ .option("--currency <c>")
1751
+ .option("--urgency <u>")
1752
+ .option("--score <n>")
1753
+ .option("--why <text>")
1754
+ .option("--action <text>")
1755
+ .option("--contact <name>")
1756
+ .option("--email <e>")
1757
+ .option("--close <YYYY-MM-DD>")
1758
+ .option("--notes <text>")
1759
+ .option("--lost-reason <text>")
1760
+ .action(async function (id, opts) {
1761
+ try {
1762
+ const r = await call("pipelines", "update", {
1763
+ id, title: opts.title, account: opts.account,
1764
+ pipeline_stage: opts.stage, status: opts.status,
1765
+ deal_value: opts.value ? Number(opts.value) : undefined,
1766
+ deal_currency: opts.currency, urgency: opts.urgency,
1767
+ score: opts.score ? Number(opts.score) : undefined,
1768
+ why_it_matters: opts.why, recommended_action: opts.action,
1769
+ contact_name: opts.contact, contact_email: opts.email,
1770
+ close_date: opts.close, notes: opts.notes, lost_reason: opts.lostReason,
1771
+ }, ctxFrom(this));
1772
+ emit(this, r, (d) => process.stdout.write(`✓ Updated ${d.opportunity?.id}\n`));
1773
+ } catch (e) { fail(e); }
1774
+ });
1775
+
1776
+ withCommonOpts(p.command("stage <id> <pipelineStage>"))
1777
+ .description("Move an opportunity to a new pipeline_stage")
1778
+ .action(async function (id, stage) {
1779
+ try {
1780
+ const r = await call("pipelines", "update_stage", { id, pipeline_stage: stage }, ctxFrom(this));
1781
+ emit(this, r, (d) => process.stdout.write(`✓ ${d.opportunity?.id} → ${d.opportunity?.pipeline_stage}\n`));
1782
+ } catch (e) { fail(e); }
1783
+ });
1784
+
1785
+ withCommonOpts(p.command("qualify <id> <outcome>"))
1786
+ .description("Set qualification_outcome (pending|qualified_in|qualified_out)")
1787
+ .action(async function (id, outcome) {
1788
+ try {
1789
+ const r = await call("pipelines", "qualify", { id, qualification_outcome: outcome }, ctxFrom(this));
1790
+ emit(this, r, (d) => process.stdout.write(`✓ ${d.opportunity?.id} → ${d.opportunity?.qualification_outcome}\n`));
1791
+ } catch (e) { fail(e); }
1792
+ });
1793
+
1794
+ withCommonOpts(p.command("delete <id>"))
1795
+ .description("Delete an opportunity")
1796
+ .option("-y, --yes", "skip the confirmation prompt")
1797
+ .action(async function (id) {
1798
+ try {
1799
+ await confirmDestructive(this, `Delete pipeline record ${id}. This cannot be undone.`);
1800
+ const r = await call("pipelines", "delete", { id }, ctxFrom(this));
1801
+ emit(this, r, (d) => process.stdout.write(`✓ Deleted ${d.deleted}\n`));
1802
+ } catch (e) { fail(e); }
1803
+ });
1804
+ }
1805
+
1806
+ export function registerAgents(program) {
1807
+ const a = program.command("agents").description("List, create, configure and invoke agents");
1808
+
1809
+ withCommonOpts(a.command("trigger <name>"))
1810
+ .description("Trigger an agent by name with optional JSON input")
1811
+ .option("--input <json>", "JSON input payload", "{}")
1812
+ .action(async function (name) {
1813
+ try {
1814
+ const o = this.optsWithGlobals();
1815
+ let input = {};
1816
+ try { input = JSON.parse(o.input); } catch { throw new Error("--input must be valid JSON"); }
1817
+ const r = await call("agents", "trigger", { agent_name: name, input }, ctxFrom(this));
1818
+ emit(this, r, (d) => process.stdout.write(JSON.stringify(d.run, null, 2) + "\n"));
1819
+ } catch (e) { fail(e); }
1820
+ });
1821
+
1822
+ withCommonOpts(a.command("history"))
1823
+ .description("Recent agent runs and jobs")
1824
+ .option("--agent <name>", "filter by agent name")
1825
+ .option("--days <n>", "look-back window", "30")
1826
+ .option("--limit <n>", "max rows", "50")
1827
+ .action(async function () {
1828
+ try {
1829
+ const o = this.optsWithGlobals();
1830
+ const r = await call("agents", "history", {
1831
+ agent_name: o.agent, days: Number(o.days), limit: Number(o.limit),
1832
+ }, ctxFrom(this));
1833
+ emit(this, r, (d) => {
1834
+ process.stdout.write(table(
1835
+ ["when", "agent", "status", "results"],
1836
+ (d.runs || []).map((x) => [
1837
+ String(x.created_at ?? "").slice(0, 16), truncate(x.agent_name, 30),
1838
+ x.status ?? "", String(x.results_count ?? ""),
1839
+ ]),
1840
+ ) + `\n${d.runs?.length ?? 0} runs, ${d.jobs?.length ?? 0} jobs\n`);
1841
+ });
1842
+ } catch (e) { fail(e); }
1843
+ });
1844
+
1845
+ withCommonOpts(a.command("list"))
1846
+ .description("List available agents")
1847
+ .option("--scope <s>", "workspace | global | both (default: both)")
1848
+ .option("--status <s>", "filter by status")
1849
+ .option("--layer <l>", "filter by layer")
1850
+ .action(async function () {
1851
+ try {
1852
+ const o = this.optsWithGlobals();
1853
+ const r = await call("agents", "list", {
1854
+ scope: o.scope, status: o.status, layer: o.layer,
1855
+ }, ctxFrom(this));
1856
+ emit(this, r, (d) => {
1857
+ process.stdout.write(table(
1858
+ ["name", "status", "layer", "schedule"],
1859
+ (d.agents || []).map((a) => [
1860
+ truncate(a.name, 32), a.status ?? "", a.layer ?? "", truncate(a.schedule, 20),
1861
+ ]),
1862
+ ) + `\n${d.agents?.length ?? 0} rows\n`);
1863
+ });
1864
+ } catch (e) { fail(e); }
1865
+ });
1866
+
1867
+ withCommonOpts(a.command("show <nameOrId>"))
1868
+ .description("Show an agent (by id or name)")
1869
+ .action(async function (key) {
1870
+ try {
1871
+ const isUuid = /^[0-9a-f-]{36}$/i.test(key);
1872
+ const r = await call("agents", "get", isUuid ? { id: key } : { name: key }, ctxFrom(this));
1873
+ emit(this, r, (d) => process.stdout.write(JSON.stringify(d.agent, null, 2) + "\n"));
1874
+ } catch (e) { fail(e); }
1875
+ });
1876
+
1877
+ withCommonOpts(a.command("create"))
1878
+ .description("Create a custom agent")
1879
+ .requiredOption("--name <name>", "agent name (unique per workspace)")
1880
+ .requiredOption("--layer <l>", "layer (intelligence | content | sales | …)")
1881
+ .option("--description <text>")
1882
+ .option("--status <s>", "planned | active | paused", "active")
1883
+ .option("--schedule <cron>", "cron schedule")
1884
+ .option("--type <t>", "agent_type")
1885
+ .option("--icon <icon>", "icon name", "bot")
1886
+ .option("--tools <text>", "tool descriptor")
1887
+ .option("--outputs <csv>", "expected outputs")
1888
+ .option("--scope <s>", "workspace | global (default: workspace)")
1889
+ .option("--pipeline <json>", "pipeline JSON")
1890
+ .action(async function (opts) {
1891
+ try {
1892
+ let pipeline; if (opts.pipeline) try { pipeline = JSON.parse(opts.pipeline); }
1893
+ catch { return fail(new Error("--pipeline must be valid JSON")); }
1894
+ const r = await call("agents", "create", {
1895
+ name: opts.name, layer: opts.layer, description: opts.description,
1896
+ status: opts.status, schedule: opts.schedule, agent_type: opts.type,
1897
+ icon: opts.icon, tools: opts.tools, scope: opts.scope,
1898
+ outputs: opts.outputs ? opts.outputs.split(",").map((s) => s.trim()) : undefined,
1899
+ pipeline,
1900
+ }, ctxFrom(this));
1901
+ emit(this, r, (d) => process.stdout.write(`✓ Created agent ${d.agent?.name} (${d.agent?.id})\n`));
1902
+ } catch (e) { fail(e); }
1903
+ });
1904
+
1905
+ withCommonOpts(a.command("update <id>"))
1906
+ .description("Patch an agent")
1907
+ .option("--name <name>")
1908
+ .option("--description <text>")
1909
+ .option("--status <s>")
1910
+ .option("--schedule <cron>")
1911
+ .option("--layer <l>")
1912
+ .option("--type <t>")
1913
+ .option("--icon <icon>")
1914
+ .option("--tools <text>")
1915
+ .option("--outputs <csv>")
1916
+ .option("--pipeline <json>")
1917
+ .action(async function (id, opts) {
1918
+ try {
1919
+ let pipeline; if (opts.pipeline) try { pipeline = JSON.parse(opts.pipeline); }
1920
+ catch { return fail(new Error("--pipeline must be valid JSON")); }
1921
+ const r = await call("agents", "update", {
1922
+ id, name: opts.name, description: opts.description, status: opts.status,
1923
+ schedule: opts.schedule, layer: opts.layer, agent_type: opts.type,
1924
+ icon: opts.icon, tools: opts.tools,
1925
+ outputs: opts.outputs ? opts.outputs.split(",").map((s) => s.trim()) : undefined,
1926
+ pipeline,
1927
+ }, ctxFrom(this));
1928
+ emit(this, r, (d) => process.stdout.write(`✓ Updated agent ${d.agent?.id}\n`));
1929
+ } catch (e) { fail(e); }
1930
+ });
1931
+
1932
+ withCommonOpts(a.command("set-status <id> <status>"))
1933
+ .description("Change an agent's status (active|paused|planned|…)")
1934
+ .action(async function (id, status) {
1935
+ try {
1936
+ const r = await call("agents", "set_status", { id, status }, ctxFrom(this));
1937
+ emit(this, r, (d) => process.stdout.write(`✓ ${d.agent?.name ?? id} → ${d.agent?.status}\n`));
1938
+ } catch (e) { fail(e); }
1939
+ });
1940
+
1941
+ withCommonOpts(a.command("run <name>"))
1942
+ .description("Invoke an agent")
1943
+ .option("--input <json>", "JSON input payload", "{}")
1944
+ .action(async function (name, opts) {
1945
+ try {
1946
+ let input = {};
1947
+ try { input = JSON.parse(opts.input); }
1948
+ catch { return fail(new Error("--input must be valid JSON")); }
1949
+ const r = await call("agents", "run", { agent_name: name, input }, ctxFrom(this));
1950
+ emit(this, r, (d) => process.stdout.write(JSON.stringify(d.run, null, 2) + "\n"));
1951
+ } catch (e) { fail(e); }
1952
+ });
1953
+
1954
+ withCommonOpts(a.command("job <id>"))
1955
+ .alias("job-status")
1956
+ .description("Check agent_jobs status (accepts a full or short job id)")
1957
+ .action(async function (id) {
1958
+ try {
1959
+ const r = await call("agents", "job_status", { job_id: id }, ctxFrom(this));
1960
+ emit(this, r, (d) => process.stdout.write(JSON.stringify(d.job, null, 2) + "\n"));
1961
+ } catch (e) { fail(e); }
1962
+ });
1963
+
1964
+ withCommonOpts(a.command("pipeline [name]"))
1965
+ .alias("inspect")
1966
+ .description("Inspect an agent's configuration and content pipeline (default: Content Creator Agent)")
1967
+ .action(async function (name) {
1968
+ try {
1969
+ const r = await call("agents", "pipeline", { agent_name: name || "Content Creator Agent" }, ctxFrom(this));
1970
+ emit(this, r, (d) => {
1971
+ if (!d.agent) { process.stdout.write(`${d.error || "Agent not found"}\n`); return; }
1972
+ process.stdout.write(`${d.agent.name} — ${d.agent.status} (${d.agent.agent_type || "agent"})\n\n`);
1973
+ process.stdout.write(table(
1974
+ ["stage", "what it does"],
1975
+ (d.pipeline?.stages || []).map((s) => [s.id, truncate(s.detail, 80)]),
1976
+ ) + "\n");
1977
+ process.stdout.write(`Blocking gates: ${(d.pipeline?.blocking_gates || []).join(", ")}\n`);
1978
+ process.stdout.write(`Advisory gates: ${(d.pipeline?.advisory_gates || []).join(", ")}\n`);
1979
+ });
1980
+ } catch (e) { fail(e); }
1981
+ });
1982
+
1983
+ withCommonOpts(a.command("runs"))
1984
+ .description("List recent agent runs in this workspace")
1985
+ .option("--agent <name>", "filter by agent_name")
1986
+ .option("--limit <n>", "max rows", "25")
1987
+ .action(async function () {
1988
+ try {
1989
+ const o = this.optsWithGlobals();
1990
+ const r = await call("agents", "recent_runs", {
1991
+ agent_name: o.agent, limit: Number(o.limit),
1992
+ }, ctxFrom(this));
1993
+ emit(this, r, (d) => {
1994
+ process.stdout.write(table(
1995
+ ["created", "agent", "status", "results", "enriched"],
1996
+ (d.runs || []).map((rr) => [
1997
+ (rr.created_at || "").slice(0, 16).replace("T", " "),
1998
+ truncate(rr.agent_name, 28), rr.status, rr.results_count ?? 0, rr.enriched_count ?? 0,
1999
+ ]),
2000
+ ) + `\n${d.runs?.length ?? 0} rows\n`);
2001
+ });
2002
+ } catch (e) { fail(e); }
2003
+ });
2004
+
2005
+ withCommonOpts(a.command("delete <id>"))
2006
+ .description("Delete an agent")
2007
+ .option("-y, --yes", "skip the confirmation prompt")
2008
+ .action(async function (id) {
2009
+ try {
2010
+ await confirmDestructive(this, `Delete agent ${id}. This cannot be undone.`);
2011
+ const r = await call("agents", "delete", { id }, ctxFrom(this));
2012
+ emit(this, r, (d) => process.stdout.write(`✓ Deleted ${d.deleted}\n`));
2013
+ } catch (e) { fail(e); }
2014
+ });
2015
+ }
2016
+
2017
+ export function registerAuthority(program) {
2018
+ const a = program.command("authority").description("Authority Loop — queue, approve, dispatch, settings");
2019
+
2020
+ withCommonOpts(a.command("list"))
2021
+ .description("List authority touches (queued/approved/sent/failed/cancelled)")
2022
+ .option("--status <s>", "filter by status (queued|approved|sent|failed|cancelled|skipped)")
2023
+ .option("--channel <c>", "filter by channel (linkedin_dm|linkedin_invite)")
2024
+ .option("--lead <id>", "filter by lead_id")
2025
+ .option("--min-score <n>", "minimum score")
2026
+ .option("--limit <n>", "max rows", "50")
2027
+ .action(async function () {
2028
+ try {
2029
+ const o = this.optsWithGlobals();
2030
+ const r = await call("authority", "list", {
2031
+ status: o.status, channel: o.channel, lead_id: o.lead,
2032
+ min_score: o.minScore ? Number(o.minScore) : undefined,
2033
+ limit: Number(o.limit),
2034
+ }, ctxFrom(this));
2035
+ emit(this, r, (d) => {
2036
+ process.stdout.write(table(
2037
+ ["id", "status", "channel", "score", "asset", "scheduled"],
2038
+ (d.touches || []).map((t) => [
2039
+ t.id.slice(0, 8), t.status, t.channel, t.score ?? 0,
2040
+ truncate(t.asset_title || t.asset_url || "", 36),
2041
+ (t.scheduled_for || "").slice(0, 16).replace("T", " "),
2042
+ ]),
2043
+ ) + `\n${d.touches?.length ?? 0} rows\n`);
2044
+ });
2045
+ } catch (e) { fail(e); }
2046
+ });
2047
+
2048
+ withCommonOpts(a.command("show <id>"))
2049
+ .description("Full JSON for one authority touch")
2050
+ .action(async function (id) {
2051
+ try {
2052
+ const r = await call("authority", "get", { id }, ctxFrom(this));
2053
+ emit(this, r, (d) => process.stdout.write(JSON.stringify(d.touch, null, 2) + "\n"));
2054
+ } catch (e) { fail(e); }
2055
+ });
2056
+
2057
+ withCommonOpts(a.command("approve <id>"))
2058
+ .description("Approve a queued touch (sets status=approved)")
2059
+ .action(async function (id) {
2060
+ try {
2061
+ const r = await call("authority", "approve", { id }, ctxFrom(this));
2062
+ emit(this, r, (d) => process.stdout.write(`✓ Approved ${d.touch?.id} → ${d.touch?.status}\n`));
2063
+ } catch (e) { fail(e); }
2064
+ });
2065
+
2066
+ withCommonOpts(a.command("cancel <id>"))
2067
+ .description("Cancel a queued/approved touch")
2068
+ .option("--reason <text>", "skipped_reason note")
2069
+ .action(async function (id, opts) {
2070
+ try {
2071
+ const r = await call("authority", "cancel", { id, reason: opts.reason }, ctxFrom(this));
2072
+ emit(this, r, (d) => process.stdout.write(`✓ Cancelled ${d.touch?.id}\n`));
2073
+ } catch (e) { fail(e); }
2074
+ });
2075
+
2076
+ withCommonOpts(a.command("dispatch <id>"))
2077
+ .description("Dispatch a queued/approved touch via LinkedIn (calls authority-touch-dispatch)")
2078
+ .action(async function (id) {
2079
+ try {
2080
+ const r = await call("authority", "dispatch", { id }, ctxFrom(this));
2081
+ emit(this, r, (d) => {
2082
+ const ok = d.dispatch?.success;
2083
+ process.stdout.write(`${ok ? "✓" : "✖"} dispatch status=${d.status} ${JSON.stringify(d.dispatch)}\n`);
2084
+ });
2085
+ } catch (e) { fail(e); }
2086
+ });
2087
+
2088
+ const settings = a.command("settings").description("Authority Loop workspace settings");
2089
+
2090
+ withCommonOpts(settings.command("get"))
2091
+ .description("Read authority_loop_settings for this workspace")
2092
+ .action(async function () {
2093
+ try {
2094
+ const r = await call("authority", "settings_get", {}, ctxFrom(this));
2095
+ emit(this, r, (d) => process.stdout.write(JSON.stringify(d.settings, null, 2) + "\n"));
2096
+ } catch (e) { fail(e); }
2097
+ });
2098
+
2099
+ withCommonOpts(settings.command("set"))
2100
+ .description("Patch authority_loop_settings for this workspace (upsert)")
2101
+ .option("--enabled <bool>", "is_enabled (true|false)")
2102
+ .option("--auto-approve <bool>", "auto_approve (true|false)")
2103
+ .option("--daily-cap <n>", "max dispatches per day")
2104
+ .option("--min-relevance <n>", "min signal relevance_score (0-100)")
2105
+ .option("--min-match <n>", "min KB match score (0-100)")
2106
+ .option("--channels <csv>", "allowed_channels e.g. linkedin_dm,linkedin_invite")
2107
+ .option("--kb-categories <csv>", "allowed_kb_categories CSV")
2108
+ .option("--trigger-flags <csv>", "allowed_trigger_flags CSV")
2109
+ .option("--quiet-start <hhmm>", "quiet_hours_start (e.g. 22:00)")
2110
+ .option("--quiet-end <hhmm>", "quiet_hours_end (e.g. 07:00)")
2111
+ .action(async function (opts) {
2112
+ try {
2113
+ const asBool = (v) => v === undefined ? undefined : /^(1|true|yes|on)$/i.test(String(v));
2114
+ const asCsv = (v) => v === undefined ? undefined : String(v).split(",").map((s) => s.trim()).filter(Boolean);
2115
+ const params = {
2116
+ is_enabled: asBool(opts.enabled),
2117
+ auto_approve: asBool(opts.autoApprove),
2118
+ daily_cap: opts.dailyCap !== undefined ? Number(opts.dailyCap) : undefined,
2119
+ min_relevance_score: opts.minRelevance !== undefined ? Number(opts.minRelevance) : undefined,
2120
+ min_match_score: opts.minMatch !== undefined ? Number(opts.minMatch) : undefined,
2121
+ allowed_channels: asCsv(opts.channels),
2122
+ allowed_kb_categories: asCsv(opts.kbCategories),
2123
+ allowed_trigger_flags: asCsv(opts.triggerFlags),
2124
+ quiet_hours_start: opts.quietStart,
2125
+ quiet_hours_end: opts.quietEnd,
2126
+ };
2127
+ const r = await call("authority", "settings_set", params, ctxFrom(this));
2128
+ emit(this, r, (d) => process.stdout.write(`✓ Settings updated\n${JSON.stringify(d.settings, null, 2)}\n`));
2129
+ } catch (e) { fail(e); }
2130
+ });
2131
+ }
2132
+
2133
+ export function registerBriefs(program) {
2134
+ const b = program.command("briefs").description("Chief of Staff morning briefs — list, show, generate");
2135
+
2136
+ withCommonOpts(b.command("list"))
2137
+ .description("Recent intelligence_briefs for this workspace")
2138
+ .option("--user <id>", "filter by user_id")
2139
+ .option("--limit <n>", "max rows", "25")
2140
+ .action(async function () {
2141
+ try {
2142
+ const o = this.optsWithGlobals();
2143
+ const r = await call("briefs", "list", {
2144
+ user_id: o.user,
2145
+ limit: Number(o.limit),
2146
+ }, ctxFrom(this));
2147
+ emit(this, r, (d) => {
2148
+ process.stdout.write(table(
2149
+ ["id", "created", "signals", "actions", "sentiment", "headline"],
2150
+ (d.briefs || []).map((x) => [
2151
+ x.id.slice(0, 8),
2152
+ (x.created_at || "").slice(0, 16).replace("T", " "),
2153
+ x.signal_count ?? 0,
2154
+ x.top_actions_count ?? 0,
2155
+ x.sentiment ?? "—",
2156
+ truncate(x.headline || "", 48),
2157
+ ]),
2158
+ ) + `\n${d.briefs?.length ?? 0} rows\n`);
2159
+ });
2160
+ } catch (e) { fail(e); }
2161
+ });
2162
+
2163
+ withCommonOpts(b.command("show <id>"))
2164
+ .description("Full JSON for one brief (brief_data, talking points, top actions, risks)")
2165
+ .action(async function (id) {
2166
+ try {
2167
+ const r = await call("briefs", "get", { id }, ctxFrom(this));
2168
+ emit(this, r, (d) => process.stdout.write(JSON.stringify(d.brief, null, 2) + "\n"));
2169
+ } catch (e) { fail(e); }
2170
+ });
2171
+
2172
+ withCommonOpts(b.command("generate"))
2173
+ .description("Generate a fresh brief now — invokes agent-chief-of-staff for this workspace")
2174
+ .option("--user <id>", "attribute brief to a specific user_id (defaults to workspace owner)")
2175
+ .option("--job <id>", "existing agent_jobs.id to report progress against")
2176
+ .action(async function () {
2177
+ try {
2178
+ const o = this.optsWithGlobals();
2179
+ const r = await call("briefs", "generate", {
2180
+ user_id: o.user,
2181
+ job_id: o.job,
2182
+ }, ctxFrom(this));
2183
+ emit(this, r, (d) => {
2184
+ const g = d.generate || {};
2185
+ const ok = g.success !== false && d.status < 400;
2186
+ process.stdout.write(
2187
+ `${ok ? "✓" : "✖"} chief-of-staff status=${d.status}` +
2188
+ (g.brief_id ? ` brief_id=${g.brief_id}` : "") +
2189
+ (g.headline ? `\n ${g.headline}` : "") +
2190
+ (g.signal_count !== undefined ? `\n signals=${g.signal_count} actions=${g.actions_generated ?? "?"}` : "") +
2191
+ "\n",
2192
+ );
2193
+ });
2194
+ } catch (e) { fail(e); }
2195
+ });
2196
+ }