@series-a/cli 0.1.0 → 0.6.0

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.
@@ -110,6 +110,77 @@ export function registerLeads(program) {
110
110
  } catch (e) { fail(e); }
111
111
  });
112
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")
115
+ .option("--query <q>", "search keywords (linkedin_search / company_search)")
116
+ .option("--post <url>", "LinkedIn post URL (post_reactors)")
117
+ .option("--file <path>", "CSV or pasted-profile text file (csv / paste_profiles / heyreach)")
118
+ .option("--audience <name>", "audience (segment) name or id to attach to")
119
+ .option("--campaign <id>", "campaign id (required for heyreach)")
120
+ .option("--limit <n>", "max prospects for LinkedIn sources", "50")
121
+ .option("--no-enrich", "skip the LinkedIn verify + enrich chain")
122
+ .action(async function (source) {
123
+ try {
124
+ const o = this.optsWithGlobals();
125
+ const params = {
126
+ source,
127
+ query: o.query,
128
+ post_url: o.post,
129
+ audience: o.audience,
130
+ campaign_id: o.campaign,
131
+ limit: Number(o.limit),
132
+ enrich: o.enrich !== false,
133
+ };
134
+ if (o.file) {
135
+ const text = await readFile(o.file, "utf8");
136
+ if (source === "paste_profiles") params.text = text;
137
+ else params.csv = text;
138
+ }
139
+ const r = await call("leads", "import", params, ctxFrom(this));
140
+ emit(this, r, (d) => process.stdout.write(
141
+ `✓ Imported ${d.imported ?? 0} leads (${d.duplicates ?? 0} duplicates, ${d.skipped ?? 0} skipped, ${d.failed ?? 0} failed)\n` +
142
+ (d.job_id ? ` job: ${d.job_id}\n` : "")));
143
+ } catch (e) { fail(e); }
144
+ });
145
+
146
+ withCommonOpts(leads.command("import-status <jobId>"))
147
+ .description("Poll a running lead import")
148
+ .action(async function (jobId) {
149
+ try {
150
+ const r = await call("leads", "import_status", { job_id: jobId }, ctxFrom(this));
151
+ emit(this, r, (d) => process.stdout.write(JSON.stringify(d.job, null, 2) + "\n"));
152
+ } catch (e) { fail(e); }
153
+ });
154
+
155
+ withCommonOpts(leads.command("update <id>"))
156
+ .description("Update a single lead")
157
+ .option("--name <v>").option("--title <v>").option("--company <v>")
158
+ .option("--email <v>").option("--linkedin <v>").option("--persona <v>")
159
+ .option("--score <n>").option("--status <v>").option("--notes <v>")
160
+ .action(async function (id) {
161
+ try {
162
+ const o = this.optsWithGlobals();
163
+ const params = { id };
164
+ for (const [k, v] of Object.entries({
165
+ name: o.name, title: o.title, company: o.company, email: o.email,
166
+ linkedin_url: o.linkedin, persona_tag: o.persona,
167
+ score: o.score !== undefined ? Number(o.score) : undefined,
168
+ status: o.status, notes: o.notes,
169
+ })) if (v !== undefined) params[k] = v;
170
+ const r = await call("leads", "update", params, ctxFrom(this));
171
+ emit(this, r, (d) => process.stdout.write(`✓ Updated ${d.lead?.name ?? id}\n`));
172
+ } catch (e) { fail(e); }
173
+ });
174
+
175
+ withCommonOpts(leads.command("delete <id>"))
176
+ .description("Delete a lead")
177
+ .action(async function (id) {
178
+ try {
179
+ const r = await call("leads", "delete", { id }, ctxFrom(this));
180
+ emit(this, r, () => process.stdout.write(`✓ Deleted ${id}\n`));
181
+ } catch (e) { fail(e); }
182
+ });
183
+
113
184
  withCommonOpts(leads.command("verify <id>"))
114
185
  .description("Run LinkedIn verification on a lead")
115
186
  .action(async function (id) {
@@ -222,7 +293,7 @@ export function registerCampaigns(program) {
222
293
  }
223
294
 
224
295
  export function registerSequences(program) {
225
- const s = program.command("sequences").description("View LinkedIn sequences");
296
+ const s = program.command("sequences").description("Manage LinkedIn sequences");
226
297
 
227
298
  withCommonOpts(s.command("list"))
228
299
  .description("List sequences")
@@ -234,7 +305,7 @@ export function registerSequences(program) {
234
305
  emit(this, r, (d) => {
235
306
  process.stdout.write(table(
236
307
  ["id", "name", "status"],
237
- (d.sequences || []).map((s) => [truncate(s.id, 8), truncate(s.name, 40), s.status]),
308
+ (d.sequences || []).map((s) => [s.id, truncate(s.name, 40), s.status]),
238
309
  ) + `\n${d.sequences?.length ?? 0} rows\n`);
239
310
  });
240
311
  } catch (e) { fail(e); }
@@ -257,10 +328,132 @@ export function registerSequences(program) {
257
328
  });
258
329
  } catch (e) { fail(e); }
259
330
  });
331
+
332
+ withCommonOpts(s.command("create"))
333
+ .description("Create a new (empty, draft) sequence")
334
+ .requiredOption("--name <name>", "sequence name")
335
+ .option("--description <text>", "description")
336
+ .option("--trigger <type>", "trigger_type", "manual")
337
+ .option("--stage <stage>", "launch_stage (liberate|architect|uncover|navigate|catalyse|harness|review|graduation)")
338
+ .action(async function (opts) {
339
+ try {
340
+ const r = await call("sequences", "create", {
341
+ name: opts.name,
342
+ description: opts.description,
343
+ trigger_type: opts.trigger,
344
+ launch_stage: opts.stage,
345
+ }, ctxFrom(this));
346
+ emit(this, r, (d) => {
347
+ process.stdout.write(`✓ Created sequence ${d.sequence?.id}\n name: ${d.sequence?.name}\n status: ${d.sequence?.status}\n`);
348
+ });
349
+ } catch (e) { fail(e); }
350
+ });
351
+
352
+ withCommonOpts(s.command("update <id>"))
353
+ .description("Update sequence metadata (name/description/status/stage)")
354
+ .option("--name <name>", "rename")
355
+ .option("--description <text>", "set description (pass empty string to clear)")
356
+ .option("--status <s>", "draft|active|paused|archived")
357
+ .option("--stage <stage>", "launch_stage (pass '' to clear)")
358
+ .action(async function (id, opts) {
359
+ try {
360
+ const params = { id };
361
+ if (opts.name != null) params.name = opts.name;
362
+ if (opts.description != null) params.description = opts.description || null;
363
+ if (opts.status != null) params.status = opts.status;
364
+ if (opts.stage != null) params.launch_stage = opts.stage || null;
365
+ const r = await call("sequences", "update", params, ctxFrom(this));
366
+ emit(this, r, (d) => {
367
+ process.stdout.write(`✓ Updated ${d.sequence?.id}\n name: ${d.sequence?.name}\n status: ${d.sequence?.status}\n stage: ${d.sequence?.launch_stage || "—"}\n`);
368
+ });
369
+ } catch (e) { fail(e); }
370
+ });
371
+
372
+ withCommonOpts(s.command("add-step <id>"))
373
+ .description("Append a step to a sequence")
374
+ .requiredOption(
375
+ "--action <action>",
376
+ "viewProfile|addReaction|addComment|sendInvitation|sendMessage|loopBack|createOpportunity|enrichLead|ifAccepted|ifReplied|ifConnection|ifNetworkDistance|stopOnReply|wait|wait_3h|wait_1d|wait_2d|wait_3d|wait_5d",
377
+ )
378
+ .option("--text <message>", "message_content (required for addComment/sendInvitation/sendMessage)")
379
+ .option("--delay <days>", "delay_days before this step", "0")
380
+ .option("--variant <v>", "variant label", "A")
381
+ .option("--step-type <t>", "action|condition|delay (defaults from --action)")
382
+ .option("--channel <c>", "channel (defaults from --action)")
383
+ .option("--branch <label>", "branch_label: yes|no (for steps under a condition)")
384
+ .option("--parent-step <id>", "parent_step_id (for branches)")
385
+ .option("--order <n>", "explicit step_order (defaults to max+1)")
386
+ .action(async function (id, opts) {
387
+ try {
388
+ const r = await call("sequences", "add_step", {
389
+ sequence_id: id,
390
+ action: opts.action,
391
+ message_content: opts.text,
392
+ delay_days: Number(opts.delay) || 0,
393
+ variant: opts.variant,
394
+ step_type: opts.stepType,
395
+ channel: opts.channel,
396
+ branch_label: opts.branch,
397
+ parent_step_id: opts.parentStep,
398
+ step_order: opts.order ? Number(opts.order) : undefined,
399
+ }, ctxFrom(this));
400
+ emit(this, r, (d) => {
401
+ const st = d.step || {};
402
+ process.stdout.write(`✓ Added step ${st.id}\n #${st.step_order} ${st.action} (${st.channel}) delay=${st.delay_days}d variant=${st.variant}\n`);
403
+ });
404
+ } catch (e) { fail(e); }
405
+ });
406
+
407
+ withCommonOpts(s.command("clone <id>"))
408
+ .description("Duplicate a sequence (and all its steps) as a new draft")
409
+ .option("--name <name>", "name for the clone (default: '<src> (copy)')")
410
+ .action(async function (id, opts) {
411
+ try {
412
+ const r = await call("sequences", "clone", { id, name: opts.name }, ctxFrom(this));
413
+ emit(this, r, (d) => {
414
+ process.stdout.write(`✓ Cloned to ${d.sequence?.id}\n name: ${d.sequence?.name}\n copied steps: ${d.cloned_steps}\n`);
415
+ });
416
+ } catch (e) { fail(e); }
417
+ });
418
+
419
+ withCommonOpts(s.command("validate <id>"))
420
+ .description("Run safety + char-limit validation on a sequence (same checks as 'campaigns launch')")
421
+ .action(async function (id) {
422
+ try {
423
+ const r = await call("sequences", "validate", { id }, ctxFrom(this));
424
+ emit(this, r, (d) => {
425
+ const status = d.ok ? "✓ PASS" : "✖ FAIL";
426
+ process.stdout.write(`${status} — ${d.step_count} step(s), ${d.errors.length} error(s), ${d.warnings.length} warning(s)\n`);
427
+ for (const e of d.errors) {
428
+ process.stdout.write(` ERROR #${e.step_order ?? "?"} ${e.action ?? ""} ${e.kind}` +
429
+ (e.field ? ` [${e.field}]` : "") +
430
+ (e.length != null ? ` ${e.length}/${e.limit} chars (+${e.excess})` : "") + "\n");
431
+ }
432
+ for (const w of d.warnings) {
433
+ process.stdout.write(` WARN #${w.step_order} ${w.action} ${w.kind}` +
434
+ (w.field ? ` [${w.field}]` : "") +
435
+ ` ${w.length}/${w.limit} chars (+${w.excess})\n`);
436
+ }
437
+ if (!d.ok) process.exitCode = 1;
438
+ });
439
+ } catch (e) { fail(e); }
440
+ });
441
+ }
442
+
443
+ function parseSnoozeDuration(s) {
444
+ // Accepts e.g. "30m", "2h", "1d", "45" (minutes). Returns minutes (int).
445
+ if (!s) return null;
446
+ const m = String(s).trim().match(/^(\d+)\s*([mhd]?)$/i);
447
+ if (!m) return null;
448
+ const n = parseInt(m[1], 10);
449
+ if (!Number.isFinite(n) || n <= 0) return null;
450
+ const unit = (m[2] || "m").toLowerCase();
451
+ const mult = unit === "h" ? 60 : unit === "d" ? 60 * 24 : 1;
452
+ return n * mult;
260
453
  }
261
454
 
262
455
  export function registerInbox(program) {
263
- const inbox = program.command("inbox").description("Inspect unified inbox");
456
+ const inbox = program.command("inbox").description("Inspect and act on the unified inbox");
264
457
 
265
458
  withCommonOpts(inbox.command("threads"))
266
459
  .description("List recent inbox messages")
@@ -290,46 +483,264 @@ export function registerInbox(program) {
290
483
  } catch (e) { fail(e); }
291
484
  });
292
485
 
486
+ const sendAction = async function (leadId, opts) {
487
+ try {
488
+ const text = String(opts.text || "").trim();
489
+ if (!text) { fail(new Error("--text cannot be empty")); return; }
490
+ if (text.length > 7900) {
491
+ fail(new Error(`--text is ${text.length} chars; hard cap is 7900`));
492
+ return;
493
+ }
494
+ if (!opts.confirm && !opts.yes) {
495
+ process.stderr.write(
496
+ `Refusing to send — re-run with --confirm to actually dispatch.\n` +
497
+ ` lead: ${leadId}\n` +
498
+ ` sender: ${opts.sender || "(auto)"}\n` +
499
+ ` chars: ${text.length}\n` +
500
+ ` text: ${truncate(text.replace(/\s+/g, " "), 120)}\n`,
501
+ );
502
+ process.exit(2);
503
+ }
504
+ const r = await call("inbox", "send_dm", {
505
+ lead_id: leadId,
506
+ text,
507
+ sender_account_id: opts.sender,
508
+ }, ctxFrom(this));
509
+ emit(this, r, (d) => {
510
+ const s = d.sent || {};
511
+ const b = s.budget || {};
512
+ process.stdout.write(
513
+ `✓ ${s.channel_kind === "linkedin_invite" ? "Invite note" : "DM"} sent to ${s.lead_name || leadId}\n` +
514
+ ` sender: ${s.sender_account_id}\n` +
515
+ ` chars: ${s.chars}\n` +
516
+ ` budget: invites ${b.invites_sent}/${b.invites_cap}, messages ${b.messages_sent}/${b.messages_cap}\n` +
517
+ (s.external_id ? ` msg id: ${s.external_id}\n` : ""),
518
+ );
519
+ });
520
+ } catch (e) { fail(e); }
521
+ };
522
+
293
523
  withCommonOpts(inbox.command("send <leadId>"))
294
524
  .description("Send a one-off LinkedIn DM (or first-touch invite note) to a lead")
295
525
  .requiredOption("--text <message>", "message body (auto-truncated to channel limit)")
296
526
  .option("--sender <accountId>", "sender_account_id (defaults to most-recently-updated linkedin_tool sender)")
297
- .action(async function (leadId, opts) {
527
+ .option("--confirm", "actually dispatch (otherwise prints a dry-run preview and exits)")
528
+ .action(sendAction);
529
+
530
+ // `reply` is a semantic alias for `send` — the server auto-resolves the most
531
+ // recent LinkedIn thread for (lead, sender) and routes via sendMessage.
532
+ withCommonOpts(inbox.command("reply <leadId>"))
533
+ .description("Reply on the most recent LinkedIn thread for a lead (alias of `send`; falls back to invite note if no thread)")
534
+ .requiredOption("--text <message>", "reply body (auto-truncated to channel limit)")
535
+ .option("--sender <accountId>", "sender_account_id (defaults to most-recently-updated linkedin_tool sender)")
536
+ .option("--confirm", "actually dispatch (otherwise prints a dry-run preview and exits)")
537
+ .action(sendAction);
538
+
539
+ withCommonOpts(inbox.command("mark-read <messageIds...>"))
540
+ .description("Mark one or more inbox messages as read")
541
+ .action(async function (messageIds) {
542
+ try {
543
+ if (!messageIds?.length) { fail(new Error("At least one messageId required")); return; }
544
+ const r = await call("inbox", "mark_read", { message_ids: messageIds }, ctxFrom(this));
545
+ emit(this, r, (d) => {
546
+ process.stdout.write(`✓ Marked ${d.marked_read}/${d.requested} message(s) read\n`);
547
+ });
548
+ } catch (e) { fail(e); }
549
+ });
550
+
551
+ withCommonOpts(inbox.command("snooze <messageId>"))
552
+ .description("Snooze a message until a future time")
553
+ .option("--for <duration>", "duration like 30m, 2h, 1d (default unit: minutes)")
554
+ .option("--until <iso>", "explicit ISO datetime (e.g. 2026-05-27T09:00:00Z)")
555
+ .option("--clear", "clear an existing snooze on this message")
556
+ .action(async function (messageId, opts) {
298
557
  try {
299
- const r = await call("inbox", "send_dm", {
300
- lead_id: leadId,
301
- text: opts.text,
302
- sender_account_id: opts.sender,
558
+ const params = { message_id: messageId };
559
+ if (opts.clear) {
560
+ params.clear = true;
561
+ } else if (opts.until) {
562
+ params.until = opts.until;
563
+ } else if (opts.for) {
564
+ const mins = parseSnoozeDuration(opts.for);
565
+ if (mins == null) { fail(new Error("Invalid --for value; use e.g. 30m, 2h, 1d")); return; }
566
+ params.minutes = mins;
567
+ } else {
568
+ fail(new Error("Provide --for <duration>, --until <iso>, or --clear"));
569
+ return;
570
+ }
571
+ const r = await call("inbox", "snooze", params, ctxFrom(this));
572
+ emit(this, r, (d) => {
573
+ if (opts.clear) process.stdout.write(`✓ Snooze cleared on ${d.snoozed?.id}\n`);
574
+ else process.stdout.write(`✓ Snoozed ${d.snoozed?.id} until ${d.snoozed?.snoozed_until}\n`);
575
+ });
576
+ } catch (e) { fail(e); }
577
+ });
578
+ }
579
+
580
+ export function registerKnowledge(program) {
581
+ const k = program.command("knowledge").description("Manage the workspace knowledge base");
582
+
583
+ withCommonOpts(k.command("list"))
584
+ .description("List active knowledge items")
585
+ .option("--category <c>", "filter by category (positioning, frameworks, case-studies, …)")
586
+ .option("--search <q>", "search name or summary")
587
+ .option("--limit <n>", "max rows", "50")
588
+ .action(async function () {
589
+ try {
590
+ const o = this.optsWithGlobals();
591
+ const r = await call("knowledge", "list", {
592
+ category: o.category, search: o.search, limit: Number(o.limit),
303
593
  }, ctxFrom(this));
304
594
  emit(this, r, (d) => {
305
- const s = d.sent || {};
306
- const b = s.budget || {};
595
+ process.stdout.write(table(
596
+ ["id", "type", "category", "name", "tags"],
597
+ (d.items || []).map((i) => [
598
+ truncate(i.id, 8), i.type, i.category,
599
+ truncate(i.name, 40),
600
+ truncate((i.tags || []).join(","), 30),
601
+ ]),
602
+ ) + `\n${d.items?.length ?? 0} rows\n`);
603
+ });
604
+ } catch (e) { fail(e); }
605
+ });
606
+
607
+ withCommonOpts(k.command("get <id>"))
608
+ .description("Show a knowledge item (full JSON, including extracted_text)")
609
+ .action(async function (id) {
610
+ try {
611
+ const r = await call("knowledge", "get", { id }, ctxFrom(this));
612
+ emit(this, r, (d) => process.stdout.write(JSON.stringify(d.item, null, 2) + "\n"));
613
+ } catch (e) { fail(e); }
614
+ });
615
+
616
+ // `add` mirrors the web UI flow: insert → (URL: firecrawl-scrape) → AI auto-tag.
617
+ // Choose exactly one source: --url, --text, or --file.
618
+ withCommonOpts(k.command("add"))
619
+ .description("Add a knowledge item (URL is auto-scraped + AI-classified, same as the web UI)")
620
+ .option("--url <url>", "scrape this URL via Firecrawl")
621
+ .option("--text <body>", "inline text body")
622
+ .option("--file <path>", "read body from a local text/markdown file")
623
+ .option("--name <title>", "display title (defaults to URL hostname or filename)")
624
+ .option("--category <c>", "seed category (AI may re-classify)")
625
+ .option("--tags <csv>", "seed tags, comma-separated (AI merges with classifier tags)")
626
+ .option("--visibility <v>", "workspace | private (default: workspace)")
627
+ .option("--no-summarize", "skip AI auto-tag/classify (raw insert only)")
628
+ .option("--no-scrape", "skip Firecrawl scrape for --url (URL stored as bookmark)")
629
+ .option("--style-reference", "flag as a Tone-of-Voice style reference (for known author documents)")
630
+ .option("--auto-style", "score as a style reference on arrival and auto-flag if it meets the threshold (default 70)")
631
+ .action(async function (opts) {
632
+ try {
633
+ const sources = [opts.url, opts.text, opts.file].filter(Boolean);
634
+ if (sources.length !== 1) {
635
+ return fail(new Error("Provide exactly one of --url, --text, or --file"));
636
+ }
637
+ let extracted_text;
638
+ let derivedName = opts.name;
639
+ if (opts.file) {
640
+ extracted_text = await readFile(opts.file, "utf8");
641
+ if (!derivedName) derivedName = opts.file.split(/[\\/]/).pop();
642
+ } else if (opts.text) {
643
+ extracted_text = opts.text;
644
+ }
645
+ const params = {
646
+ content_url: opts.url || undefined,
647
+ extracted_text,
648
+ name: derivedName,
649
+ category: opts.category,
650
+ tags: opts.tags ? opts.tags.split(",").map((s) => s.trim()).filter(Boolean) : undefined,
651
+ visibility: opts.visibility,
652
+ skip_summarize: opts.summarize === false ? true : undefined,
653
+ skip_scrape: opts.scrape === false ? true : undefined,
654
+ is_style_reference: opts.styleReference === true ? true : undefined,
655
+ auto_style_reference: opts.autoStyle === true ? true : undefined,
656
+ };
657
+ const r = await call("knowledge", "add", params, ctxFrom(this));
658
+ emit(this, r, (d) => {
659
+ const it = d.item || {};
660
+ process.stdout.write(
661
+ `✓ Added knowledge item ${it.id}\n` +
662
+ ` name: ${it.name}\n` +
663
+ ` type: ${it.type}\n` +
664
+ ` category: ${it.category}\n` +
665
+ ` tags: ${(it.tags || []).join(", ") || "(none)"}\n` +
666
+ (it.summary ? ` summary: ${truncate(it.summary, 240)}\n` : "") +
667
+ (it.is_style_reference ? ` ★ style reference\n` : "") +
668
+ (d.ai?.success === false ? ` ⚠ AI classify: ${d.ai.error}\n` : "") +
669
+ (d.style_reference_triage?.data?.results?.[0] && !it.is_style_reference
670
+ ? ` style triage: ${d.style_reference_triage.data.results[0].score}/100 — ${d.style_reference_triage.data.results[0].reason}\n`
671
+ : ""),
672
+ );
673
+ });
674
+ } catch (e) { fail(e); }
675
+ });
676
+
677
+ withCommonOpts(k.command("update <id>"))
678
+ .description("Patch a knowledge item")
679
+ .option("--name <s>", "rename")
680
+ .option("--summary <s>", "set summary")
681
+ .option("--category <c>", "set category")
682
+ .option("--tags <csv>", "replace tags (comma-separated)")
683
+ .option("--visibility <v>", "workspace | private")
684
+ .option("--style-reference <on|off>", "flag/unflag as a Tone-of-Voice style reference")
685
+ .action(async function (id, opts) {
686
+ try {
687
+ const patch = {
688
+ id,
689
+ name: opts.name,
690
+ summary: opts.summary,
691
+ category: opts.category,
692
+ tags: opts.tags ? opts.tags.split(",").map((s) => s.trim()).filter(Boolean) : undefined,
693
+ visibility: opts.visibility,
694
+ is_style_reference: opts.styleReference === "on" ? true : opts.styleReference === "off" ? false : undefined,
695
+ };
696
+ const r = await call("knowledge", "update", patch, ctxFrom(this));
697
+ emit(this, r, (d) => process.stdout.write(`✓ Updated ${d.item?.id}\n`));
698
+ } catch (e) { fail(e); }
699
+ });
700
+
701
+ withCommonOpts(k.command("reclassify <id>"))
702
+ .description("Re-run AI auto-tag/classify (same as the UI 'sparkles' button)")
703
+ .action(async function (id) {
704
+ try {
705
+ const r = await call("knowledge", "reclassify", { id }, ctxFrom(this));
706
+ emit(this, r, (d) => {
707
+ const it = d.item || {};
307
708
  process.stdout.write(
308
- `✓ ${s.channel_kind === "linkedin_invite" ? "Invite note" : "DM"} sent to ${s.lead_name || leadId}\n` +
309
- ` sender: ${s.sender_account_id}\n` +
310
- ` chars: ${s.chars}\n` +
311
- ` budget: invites ${b.invites_sent}/${b.invites_cap}, messages ${b.messages_sent}/${b.messages_cap}\n` +
312
- (s.external_id ? ` msg id: ${s.external_id}\n` : ""),
709
+ `✓ Reclassified ${it.id}\n` +
710
+ ` category: ${it.category}\n` +
711
+ ` tags: ${(it.tags || []).join(", ") || "(none)"}\n` +
712
+ (it.summary ? ` summary: ${truncate(it.summary, 240)}\n` : ""),
313
713
  );
314
714
  });
315
715
  } catch (e) { fail(e); }
316
716
  });
717
+
718
+ withCommonOpts(k.command("deactivate <id>"))
719
+ .description("Soft-delete (sets is_active = false)")
720
+ .action(async function (id) {
721
+ try {
722
+ const r = await call("knowledge", "deactivate", { id }, ctxFrom(this));
723
+ emit(this, r, (d) => process.stdout.write(`✓ Deactivated ${d.item?.id}\n`));
724
+ } catch (e) { fail(e); }
725
+ });
317
726
  }
318
727
 
319
728
  export function registerContent(program) {
320
- const c = program.command("content").description("Browse and publish content");
729
+ const c = program.command("content").description("Browse, create, schedule and publish content");
321
730
 
322
731
  withCommonOpts(c.command("list"))
323
732
  .description("List content items")
324
- .option("--status <s>", "filter by status")
325
- .option("--type <t>", "filter by type")
733
+ .option("--status <s>", "filter by status (draft|scheduled|published|…)")
734
+ .option("--type <t>", "filter by type (article|linkedin_post|…)")
735
+ .option("--approval <s>", "filter by approval_status")
326
736
  .option("--search <q>", "search title or body")
327
737
  .option("--limit <n>", "max rows", "50")
328
738
  .action(async function () {
329
739
  try {
330
740
  const o = this.optsWithGlobals();
331
741
  const r = await call("content", "list", {
332
- status: o.status, type: o.type, search: o.search, limit: Number(o.limit),
742
+ status: o.status, type: o.type, approval_status: o.approval,
743
+ search: o.search, limit: Number(o.limit),
333
744
  }, ctxFrom(this));
334
745
  emit(this, r, (d) => {
335
746
  process.stdout.write(table(
@@ -349,6 +760,72 @@ export function registerContent(program) {
349
760
  } catch (e) { fail(e); }
350
761
  });
351
762
 
763
+ withCommonOpts(c.command("create"))
764
+ .description("Create a new content item")
765
+ .requiredOption("--title <t>", "title")
766
+ .option("--type <t>", "article | linkedin_post | newsletter | …", "linkedin_post")
767
+ .option("--body <text>", "inline body")
768
+ .option("--body-file <path>", "read body from a local file")
769
+ .option("--platforms <csv>", "target platforms (linkedin,blog,…)")
770
+ .option("--tags <csv>", "tags")
771
+ .option("--scheduled <ts>", "ISO timestamp (sets status=scheduled)")
772
+ .option("--campaign <id>", "campaign_id")
773
+ .action(async function (opts) {
774
+ try {
775
+ let body = opts.body;
776
+ if (opts.bodyFile) body = await readFile(opts.bodyFile, "utf8");
777
+ const r = await call("content", "create", {
778
+ title: opts.title, type: opts.type, body: body ?? "",
779
+ target_platforms: opts.platforms ? opts.platforms.split(",").map((s) => s.trim()) : undefined,
780
+ tags: opts.tags ? opts.tags.split(",").map((s) => s.trim()) : undefined,
781
+ scheduled_date: opts.scheduled, status: opts.scheduled ? "scheduled" : undefined,
782
+ campaign_id: opts.campaign,
783
+ }, ctxFrom(this));
784
+ emit(this, r, (d) => process.stdout.write(`✓ Created content ${d.content?.id}\n`));
785
+ } catch (e) { fail(e); }
786
+ });
787
+
788
+ withCommonOpts(c.command("update <id>"))
789
+ .description("Patch a content item")
790
+ .option("--title <t>")
791
+ .option("--body <text>")
792
+ .option("--body-file <path>")
793
+ .option("--status <s>")
794
+ .option("--platforms <csv>")
795
+ .option("--tags <csv>")
796
+ .option("--scheduled <ts>")
797
+ .action(async function (id, opts) {
798
+ try {
799
+ let body = opts.body;
800
+ if (opts.bodyFile) body = await readFile(opts.bodyFile, "utf8");
801
+ const r = await call("content", "update", {
802
+ id, title: opts.title, body, status: opts.status,
803
+ target_platforms: opts.platforms ? opts.platforms.split(",").map((s) => s.trim()) : undefined,
804
+ tags: opts.tags ? opts.tags.split(",").map((s) => s.trim()) : undefined,
805
+ scheduled_date: opts.scheduled,
806
+ }, ctxFrom(this));
807
+ emit(this, r, (d) => process.stdout.write(`✓ Updated ${d.content?.id}\n`));
808
+ } catch (e) { fail(e); }
809
+ });
810
+
811
+ withCommonOpts(c.command("schedule <id> <isoTimestamp>"))
812
+ .description("Schedule a content item")
813
+ .action(async function (id, ts) {
814
+ try {
815
+ const r = await call("content", "schedule", { id, scheduled_date: ts }, ctxFrom(this));
816
+ emit(this, r, (d) => process.stdout.write(`✓ Scheduled ${d.content?.id} → ${d.content?.scheduled_date}\n`));
817
+ } catch (e) { fail(e); }
818
+ });
819
+
820
+ withCommonOpts(c.command("approve <id>"))
821
+ .description("Approve a content item (approval_status=approved)")
822
+ .action(async function (id) {
823
+ try {
824
+ const r = await call("content", "approve", { id }, ctxFrom(this));
825
+ emit(this, r, (d) => process.stdout.write(`✓ Approved ${d.content?.id}\n`));
826
+ } catch (e) { fail(e); }
827
+ });
828
+
352
829
  withCommonOpts(c.command("publish <id>"))
353
830
  .description("Dispatch publication via agent-content-posting")
354
831
  .action(async function (id) {
@@ -357,25 +834,535 @@ export function registerContent(program) {
357
834
  emit(this, r, (d) => process.stdout.write(JSON.stringify(d.publish, null, 2) + "\n"));
358
835
  } catch (e) { fail(e); }
359
836
  });
837
+
838
+ withCommonOpts(c.command("delete <id>"))
839
+ .description("Delete a content item")
840
+ .action(async function (id) {
841
+ try {
842
+ const r = await call("content", "delete", { id }, ctxFrom(this));
843
+ emit(this, r, (d) => process.stdout.write(`✓ Deleted ${d.deleted}\n`));
844
+ } catch (e) { fail(e); }
845
+ });
846
+
847
+ withCommonOpts(c.command("generate"))
848
+ .description("Generate a draft via agent-content-creator (KB + reference library grounded, URL-sanitised, citations recorded)")
849
+ .requiredOption("--angle <text>", "topic / angle for the draft")
850
+ .option("--format <f>", "blog | linkedin_post | video | explainer_video", "blog")
851
+ .option("--signal <id...>", "intelligence_signals ids to ground on (repeatable)")
852
+ .option("--reference <id...>", "knowledge_items ids to ground on (repeatable)")
853
+ .option("--grounding-text <text>", "extra freeform grounding to append")
854
+ .option("--wait", "block until the job completes (up to 4 min)")
855
+ .action(async function (opts) {
856
+ try {
857
+ const r = await call("content", "generate", {
858
+ angle: opts.angle,
859
+ format: opts.format,
860
+ signal_ids: opts.signal,
861
+ reference_item_ids: opts.reference,
862
+ grounding_text: opts.groundingText,
863
+ async: !opts.wait,
864
+ }, ctxFrom(this));
865
+ emit(this, r, (d) => {
866
+ if (d.content) {
867
+ process.stdout.write(`✓ Draft ${d.content.id} (${d.content.type}, ${d.citations?.length ?? 0} citations)\n`);
868
+ } else {
869
+ process.stdout.write(`⏳ Queued job ${d.job_id} — poll with: advisoros agents job-status ${d.job_id}\n`);
870
+ }
871
+ });
872
+ } catch (e) { fail(e); }
873
+ });
874
+
875
+ withCommonOpts(c.command("citations <id>"))
876
+ .description("List every source (signal, reference, transcript, published) recorded for a content item")
877
+ .action(async function (id) {
878
+ try {
879
+ const r = await call("content", "citations", { id }, ctxFrom(this));
880
+ emit(this, r, (d) => {
881
+ process.stdout.write(table(
882
+ ["kind", "id", "title", "url"],
883
+ (d.citations || []).map((c) => [c.source_kind, truncate(c.source_id ?? "", 8), truncate(c.title ?? "", 40), truncate(c.source_url ?? "", 60)]),
884
+ ) + `\n${d.citations?.length ?? 0} citations\n`);
885
+ });
886
+ } catch (e) { fail(e); }
887
+ });
888
+ }
889
+
890
+
891
+ export function registerCalendar(program) {
892
+ const cal = program.command("calendar").description("Manage the content calendar");
893
+
894
+ withCommonOpts(cal.command("list"))
895
+ .description("List scheduled calendar posts")
896
+ .option("--from <date>", "YYYY-MM-DD (inclusive)")
897
+ .option("--to <date>", "YYYY-MM-DD (inclusive)")
898
+ .option("--platform <p>", "filter by platform")
899
+ .option("--status <s>", "filter by status")
900
+ .option("--campaign <id>", "filter by campaign_id")
901
+ .option("--limit <n>", "max rows", "100")
902
+ .action(async function () {
903
+ try {
904
+ const o = this.optsWithGlobals();
905
+ const r = await call("calendar", "list", {
906
+ from: o.from, to: o.to, platform: o.platform, status: o.status,
907
+ campaign_id: o.campaign, limit: Number(o.limit),
908
+ }, ctxFrom(this));
909
+ emit(this, r, (d) => {
910
+ process.stdout.write(table(
911
+ ["id", "date", "time", "platform", "status", "title"],
912
+ (d.posts || []).map((p) => [
913
+ truncate(p.id, 8), p.scheduled_date, p.scheduled_time ?? "—",
914
+ p.platform, p.status, truncate(p.title, 50),
915
+ ]),
916
+ ) + `\n${d.posts?.length ?? 0} rows\n`);
917
+ });
918
+ } catch (e) { fail(e); }
919
+ });
920
+
921
+ withCommonOpts(cal.command("show <id>"))
922
+ .description("Show a calendar post")
923
+ .action(async function (id) {
924
+ try {
925
+ const r = await call("calendar", "get", { id }, ctxFrom(this));
926
+ emit(this, r, (d) => process.stdout.write(JSON.stringify(d.post, null, 2) + "\n"));
927
+ } catch (e) { fail(e); }
928
+ });
929
+
930
+ withCommonOpts(cal.command("create"))
931
+ .description("Create a calendar post")
932
+ .requiredOption("--title <t>", "title")
933
+ .requiredOption("--date <YYYY-MM-DD>", "scheduled date")
934
+ .option("--time <HH:MM>", "scheduled time")
935
+ .option("--platform <p>", "linkedin | blog | twitter | …", "linkedin")
936
+ .option("--content <text>", "post body")
937
+ .option("--content-file <path>", "read body from a local file")
938
+ .option("--hashtags <csv>", "hashtags (no #)")
939
+ .option("--cta <text>", "call to action")
940
+ .option("--campaign <id>", "campaign_id")
941
+ .option("--status <s>", "draft | scheduled | published", "draft")
942
+ .action(async function (opts) {
943
+ try {
944
+ let content = opts.content;
945
+ if (opts.contentFile) content = await readFile(opts.contentFile, "utf8");
946
+ const r = await call("calendar", "create", {
947
+ title: opts.title, scheduled_date: opts.date, scheduled_time: opts.time,
948
+ platform: opts.platform, content: content ?? "",
949
+ hashtags: opts.hashtags ? opts.hashtags.split(",").map((s) => s.trim()) : undefined,
950
+ cta: opts.cta, campaign_id: opts.campaign, status: opts.status,
951
+ }, ctxFrom(this));
952
+ emit(this, r, (d) => process.stdout.write(`✓ Created calendar post ${d.post?.id}\n`));
953
+ } catch (e) { fail(e); }
954
+ });
955
+
956
+ withCommonOpts(cal.command("update <id>"))
957
+ .description("Patch a calendar post")
958
+ .option("--title <t>")
959
+ .option("--date <YYYY-MM-DD>")
960
+ .option("--time <HH:MM>")
961
+ .option("--content <text>")
962
+ .option("--content-file <path>")
963
+ .option("--status <s>")
964
+ .option("--platform <p>")
965
+ .option("--hashtags <csv>")
966
+ .option("--cta <text>")
967
+ .action(async function (id, opts) {
968
+ try {
969
+ let content = opts.content;
970
+ if (opts.contentFile) content = await readFile(opts.contentFile, "utf8");
971
+ const r = await call("calendar", "update", {
972
+ id, title: opts.title, scheduled_date: opts.date, scheduled_time: opts.time,
973
+ content, status: opts.status, platform: opts.platform,
974
+ hashtags: opts.hashtags ? opts.hashtags.split(",").map((s) => s.trim()) : undefined,
975
+ cta: opts.cta,
976
+ }, ctxFrom(this));
977
+ emit(this, r, (d) => process.stdout.write(`✓ Updated ${d.post?.id}\n`));
978
+ } catch (e) { fail(e); }
979
+ });
980
+
981
+ withCommonOpts(cal.command("delete <id>"))
982
+ .description("Delete a calendar post")
983
+ .action(async function (id) {
984
+ try {
985
+ const r = await call("calendar", "delete", { id }, ctxFrom(this));
986
+ emit(this, r, (d) => process.stdout.write(`✓ Deleted ${d.deleted}\n`));
987
+ } catch (e) { fail(e); }
988
+ });
989
+ }
990
+
991
+ export function registerSignals(program) {
992
+ const s = program.command("signals").description("Read and manage market intelligence signals");
993
+
994
+ withCommonOpts(s.command("list"))
995
+ .description("List intelligence signals")
996
+ .option("--type <t>", "signal_type (news|funding|hiring|…)")
997
+ .option("--category <c>", "filter by category")
998
+ .option("--agent <name>", "filter by agent_name")
999
+ .option("--unread", "only unread")
1000
+ .option("--min-score <n>", "min relevance_score")
1001
+ .option("--search <q>", "search title/summary")
1002
+ .option("--limit <n>", "max rows", "50")
1003
+ .action(async function () {
1004
+ try {
1005
+ const o = this.optsWithGlobals();
1006
+ const r = await call("signals", "list", {
1007
+ signal_type: o.type, category: o.category, agent_name: o.agent,
1008
+ is_read: o.unread ? false : undefined, min_score: o.minScore,
1009
+ search: o.search, limit: Number(o.limit),
1010
+ }, ctxFrom(this));
1011
+ emit(this, r, (d) => {
1012
+ process.stdout.write(table(
1013
+ ["id", "score", "type", "agent", "title"],
1014
+ (d.signals || []).map((g) => [
1015
+ truncate(g.id, 8), g.relevance_score ?? "", g.signal_type,
1016
+ truncate(g.agent_name, 22), truncate(g.title, 60),
1017
+ ]),
1018
+ ) + `\n${d.signals?.length ?? 0} rows\n`);
1019
+ });
1020
+ } catch (e) { fail(e); }
1021
+ });
1022
+
1023
+ withCommonOpts(s.command("show <id>"))
1024
+ .description("Show a signal")
1025
+ .action(async function (id) {
1026
+ try {
1027
+ const r = await call("signals", "get", { id }, ctxFrom(this));
1028
+ emit(this, r, (d) => process.stdout.write(JSON.stringify(d.signal, null, 2) + "\n"));
1029
+ } catch (e) { fail(e); }
1030
+ });
1031
+
1032
+ withCommonOpts(s.command("create"))
1033
+ .description("Create a manual signal")
1034
+ .requiredOption("--title <t>", "title")
1035
+ .requiredOption("--agent <name>", "agent_name (source)")
1036
+ .option("--type <t>", "signal_type", "news")
1037
+ .option("--category <c>", "category", "general")
1038
+ .option("--url <url>", "source url")
1039
+ .option("--summary <text>", "short summary")
1040
+ .option("--why <text>", "why_it_matters")
1041
+ .option("--action <text>", "recommended_action")
1042
+ .option("--score <n>", "relevance_score 0-100", "50")
1043
+ .option("--flags <csv>", "trigger_flags (funding,hiring,…)")
1044
+ .action(async function (opts) {
1045
+ try {
1046
+ const r = await call("signals", "create", {
1047
+ title: opts.title, agent_name: opts.agent, signal_type: opts.type,
1048
+ category: opts.category, url: opts.url, summary: opts.summary,
1049
+ why_it_matters: opts.why, recommended_action: opts.action,
1050
+ relevance_score: Number(opts.score),
1051
+ trigger_flags: opts.flags ? opts.flags.split(",").map((s) => s.trim()) : undefined,
1052
+ }, ctxFrom(this));
1053
+ emit(this, r, (d) => process.stdout.write(`✓ Created signal ${d.signal?.id}\n`));
1054
+ } catch (e) { fail(e); }
1055
+ });
1056
+
1057
+ withCommonOpts(s.command("update <id>"))
1058
+ .description("Patch a signal")
1059
+ .option("--title <t>")
1060
+ .option("--summary <text>")
1061
+ .option("--why <text>")
1062
+ .option("--action <text>")
1063
+ .option("--score <n>")
1064
+ .option("--category <c>")
1065
+ .option("--sentiment <s>")
1066
+ .option("--flags <csv>")
1067
+ .action(async function (id, opts) {
1068
+ try {
1069
+ const r = await call("signals", "update", {
1070
+ id, title: opts.title, summary: opts.summary,
1071
+ why_it_matters: opts.why, recommended_action: opts.action,
1072
+ relevance_score: opts.score ? Number(opts.score) : undefined,
1073
+ category: opts.category, sentiment: opts.sentiment,
1074
+ trigger_flags: opts.flags ? opts.flags.split(",").map((s) => s.trim()) : undefined,
1075
+ }, ctxFrom(this));
1076
+ emit(this, r, (d) => process.stdout.write(`✓ Updated ${d.signal?.id}\n`));
1077
+ } catch (e) { fail(e); }
1078
+ });
1079
+
1080
+ withCommonOpts(s.command("read <id>"))
1081
+ .description("Mark a signal as read (use --unread to flip back)")
1082
+ .option("--unread", "mark as unread instead")
1083
+ .action(async function (id, opts) {
1084
+ try {
1085
+ const r = await call("signals", "mark_read", { id, is_read: !opts.unread }, ctxFrom(this));
1086
+ emit(this, r, (d) => process.stdout.write(`✓ ${d.signal?.id} → is_read=${d.signal?.is_read}\n`));
1087
+ } catch (e) { fail(e); }
1088
+ });
1089
+
1090
+ withCommonOpts(s.command("delete <id>"))
1091
+ .description("Delete a signal")
1092
+ .action(async function (id) {
1093
+ try {
1094
+ const r = await call("signals", "delete", { id }, ctxFrom(this));
1095
+ emit(this, r, (d) => process.stdout.write(`✓ Deleted ${d.deleted}\n`));
1096
+ } catch (e) { fail(e); }
1097
+ });
1098
+ }
1099
+
1100
+ export function registerPipelines(program) {
1101
+ const p = program.command("pipelines").description("Manage the opportunities / deal pipeline");
1102
+
1103
+ withCommonOpts(p.command("list"))
1104
+ .description("List opportunities (pipeline)")
1105
+ .option("--status <s>", "filter by status")
1106
+ .option("--stage <s>", "filter by pipeline_stage")
1107
+ .option("--outcome <s>", "qualification_outcome (pending|qualified_in|qualified_out)")
1108
+ .option("--urgency <s>", "low | medium | high")
1109
+ .option("--search <q>", "search title/account")
1110
+ .option("--limit <n>", "max rows", "50")
1111
+ .action(async function () {
1112
+ try {
1113
+ const o = this.optsWithGlobals();
1114
+ const r = await call("pipelines", "list", {
1115
+ status: o.status, pipeline_stage: o.stage,
1116
+ qualification_outcome: o.outcome, urgency: o.urgency,
1117
+ search: o.search, limit: Number(o.limit),
1118
+ }, ctxFrom(this));
1119
+ emit(this, r, (d) => {
1120
+ process.stdout.write(table(
1121
+ ["id", "stage", "score", "value", "account", "title"],
1122
+ (d.opportunities || []).map((op) => [
1123
+ truncate(op.id, 8), op.pipeline_stage, op.score ?? "",
1124
+ `${op.deal_currency ?? ""}${op.deal_value ?? 0}`,
1125
+ truncate(op.account, 22), truncate(op.title, 40),
1126
+ ]),
1127
+ ) + `\n${d.opportunities?.length ?? 0} rows\n`);
1128
+ });
1129
+ } catch (e) { fail(e); }
1130
+ });
1131
+
1132
+ withCommonOpts(p.command("show <id>"))
1133
+ .description("Show an opportunity")
1134
+ .action(async function (id) {
1135
+ try {
1136
+ const r = await call("pipelines", "get", { id }, ctxFrom(this));
1137
+ emit(this, r, (d) => process.stdout.write(JSON.stringify(d.opportunity, null, 2) + "\n"));
1138
+ } catch (e) { fail(e); }
1139
+ });
1140
+
1141
+ withCommonOpts(p.command("create"))
1142
+ .description("Create an opportunity")
1143
+ .requiredOption("--title <t>", "title")
1144
+ .option("--account <a>", "account / company")
1145
+ .option("--type <t>", "revenue | risk | …", "revenue")
1146
+ .option("--urgency <u>", "low | medium | high", "medium")
1147
+ .option("--stage <s>", "pipeline_stage", "engaged")
1148
+ .option("--value <n>", "deal_value", "0")
1149
+ .option("--currency <c>", "deal_currency", "USD")
1150
+ .option("--why <text>", "why_it_matters")
1151
+ .option("--action <text>", "recommended_action")
1152
+ .option("--contact <name>", "contact_name")
1153
+ .option("--email <e>", "contact_email")
1154
+ .option("--close <YYYY-MM-DD>", "close_date")
1155
+ .option("--notes <text>", "notes")
1156
+ .action(async function (opts) {
1157
+ try {
1158
+ const r = await call("pipelines", "create", {
1159
+ title: opts.title, account: opts.account, type: opts.type,
1160
+ urgency: opts.urgency, pipeline_stage: opts.stage,
1161
+ deal_value: Number(opts.value), deal_currency: opts.currency,
1162
+ why_it_matters: opts.why, recommended_action: opts.action,
1163
+ contact_name: opts.contact, contact_email: opts.email,
1164
+ close_date: opts.close, notes: opts.notes,
1165
+ }, ctxFrom(this));
1166
+ emit(this, r, (d) => process.stdout.write(`✓ Created opportunity ${d.opportunity?.id}\n`));
1167
+ } catch (e) { fail(e); }
1168
+ });
1169
+
1170
+ withCommonOpts(p.command("update <id>"))
1171
+ .description("Patch an opportunity")
1172
+ .option("--title <t>")
1173
+ .option("--account <a>")
1174
+ .option("--stage <s>", "pipeline_stage")
1175
+ .option("--status <s>")
1176
+ .option("--value <n>", "deal_value")
1177
+ .option("--currency <c>")
1178
+ .option("--urgency <u>")
1179
+ .option("--score <n>")
1180
+ .option("--why <text>")
1181
+ .option("--action <text>")
1182
+ .option("--contact <name>")
1183
+ .option("--email <e>")
1184
+ .option("--close <YYYY-MM-DD>")
1185
+ .option("--notes <text>")
1186
+ .option("--lost-reason <text>")
1187
+ .action(async function (id, opts) {
1188
+ try {
1189
+ const r = await call("pipelines", "update", {
1190
+ id, title: opts.title, account: opts.account,
1191
+ pipeline_stage: opts.stage, status: opts.status,
1192
+ deal_value: opts.value ? Number(opts.value) : undefined,
1193
+ deal_currency: opts.currency, urgency: opts.urgency,
1194
+ score: opts.score ? Number(opts.score) : undefined,
1195
+ why_it_matters: opts.why, recommended_action: opts.action,
1196
+ contact_name: opts.contact, contact_email: opts.email,
1197
+ close_date: opts.close, notes: opts.notes, lost_reason: opts.lostReason,
1198
+ }, ctxFrom(this));
1199
+ emit(this, r, (d) => process.stdout.write(`✓ Updated ${d.opportunity?.id}\n`));
1200
+ } catch (e) { fail(e); }
1201
+ });
1202
+
1203
+ withCommonOpts(p.command("stage <id> <pipelineStage>"))
1204
+ .description("Move an opportunity to a new pipeline_stage")
1205
+ .action(async function (id, stage) {
1206
+ try {
1207
+ const r = await call("pipelines", "update_stage", { id, pipeline_stage: stage }, ctxFrom(this));
1208
+ emit(this, r, (d) => process.stdout.write(`✓ ${d.opportunity?.id} → ${d.opportunity?.pipeline_stage}\n`));
1209
+ } catch (e) { fail(e); }
1210
+ });
1211
+
1212
+ withCommonOpts(p.command("qualify <id> <outcome>"))
1213
+ .description("Set qualification_outcome (pending|qualified_in|qualified_out)")
1214
+ .action(async function (id, outcome) {
1215
+ try {
1216
+ const r = await call("pipelines", "qualify", { id, qualification_outcome: outcome }, ctxFrom(this));
1217
+ emit(this, r, (d) => process.stdout.write(`✓ ${d.opportunity?.id} → ${d.opportunity?.qualification_outcome}\n`));
1218
+ } catch (e) { fail(e); }
1219
+ });
1220
+
1221
+ withCommonOpts(p.command("delete <id>"))
1222
+ .description("Delete an opportunity")
1223
+ .action(async function (id) {
1224
+ try {
1225
+ const r = await call("pipelines", "delete", { id }, ctxFrom(this));
1226
+ emit(this, r, (d) => process.stdout.write(`✓ Deleted ${d.deleted}\n`));
1227
+ } catch (e) { fail(e); }
1228
+ });
360
1229
  }
361
1230
 
362
1231
  export function registerAgents(program) {
363
- const a = program.command("agents").description("List and invoke agents");
1232
+ const a = program.command("agents").description("List, create, configure and invoke agents");
1233
+
1234
+ withCommonOpts(a.command("trigger <name>"))
1235
+ .description("Trigger an agent by name with optional JSON input")
1236
+ .option("--input <json>", "JSON input payload", "{}")
1237
+ .action(async function (name) {
1238
+ try {
1239
+ const o = this.optsWithGlobals();
1240
+ let input = {};
1241
+ try { input = JSON.parse(o.input); } catch { throw new Error("--input must be valid JSON"); }
1242
+ const r = await call("agents", "trigger", { agent_name: name, input }, ctxFrom(this));
1243
+ emit(this, r, (d) => process.stdout.write(JSON.stringify(d.run, null, 2) + "\n"));
1244
+ } catch (e) { fail(e); }
1245
+ });
1246
+
1247
+ withCommonOpts(a.command("history"))
1248
+ .description("Recent agent runs and jobs")
1249
+ .option("--agent <name>", "filter by agent name")
1250
+ .option("--days <n>", "look-back window", "30")
1251
+ .option("--limit <n>", "max rows", "50")
1252
+ .action(async function () {
1253
+ try {
1254
+ const o = this.optsWithGlobals();
1255
+ const r = await call("agents", "history", {
1256
+ agent_name: o.agent, days: Number(o.days), limit: Number(o.limit),
1257
+ }, ctxFrom(this));
1258
+ emit(this, r, (d) => {
1259
+ process.stdout.write(table(
1260
+ ["when", "agent", "status", "results"],
1261
+ (d.runs || []).map((x) => [
1262
+ String(x.created_at ?? "").slice(0, 16), truncate(x.agent_name, 30),
1263
+ x.status ?? "", String(x.results_count ?? ""),
1264
+ ]),
1265
+ ) + `\n${d.runs?.length ?? 0} runs, ${d.jobs?.length ?? 0} jobs\n`);
1266
+ });
1267
+ } catch (e) { fail(e); }
1268
+ });
364
1269
 
365
1270
  withCommonOpts(a.command("list"))
366
1271
  .description("List available agents")
1272
+ .option("--scope <s>", "workspace | global | both (default: both)")
1273
+ .option("--status <s>", "filter by status")
1274
+ .option("--layer <l>", "filter by layer")
367
1275
  .action(async function () {
368
1276
  try {
369
- const r = await call("agents", "list", {}, ctxFrom(this));
1277
+ const o = this.optsWithGlobals();
1278
+ const r = await call("agents", "list", {
1279
+ scope: o.scope, status: o.status, layer: o.layer,
1280
+ }, ctxFrom(this));
370
1281
  emit(this, r, (d) => {
371
1282
  process.stdout.write(table(
372
- ["name", "status", "model"],
373
- (d.agents || []).map((a) => [truncate(a.name, 32), a.status ?? "", truncate(a.model, 30)]),
1283
+ ["name", "status", "layer", "schedule"],
1284
+ (d.agents || []).map((a) => [
1285
+ truncate(a.name, 32), a.status ?? "", a.layer ?? "", truncate(a.schedule, 20),
1286
+ ]),
374
1287
  ) + `\n${d.agents?.length ?? 0} rows\n`);
375
1288
  });
376
1289
  } catch (e) { fail(e); }
377
1290
  });
378
1291
 
1292
+ withCommonOpts(a.command("show <nameOrId>"))
1293
+ .description("Show an agent (by id or name)")
1294
+ .action(async function (key) {
1295
+ try {
1296
+ const isUuid = /^[0-9a-f-]{36}$/i.test(key);
1297
+ const r = await call("agents", "get", isUuid ? { id: key } : { name: key }, ctxFrom(this));
1298
+ emit(this, r, (d) => process.stdout.write(JSON.stringify(d.agent, null, 2) + "\n"));
1299
+ } catch (e) { fail(e); }
1300
+ });
1301
+
1302
+ withCommonOpts(a.command("create"))
1303
+ .description("Create a custom agent")
1304
+ .requiredOption("--name <name>", "agent name (unique per workspace)")
1305
+ .requiredOption("--layer <l>", "layer (intelligence | content | sales | …)")
1306
+ .option("--description <text>")
1307
+ .option("--status <s>", "planned | active | paused", "active")
1308
+ .option("--schedule <cron>", "cron schedule")
1309
+ .option("--type <t>", "agent_type")
1310
+ .option("--icon <icon>", "icon name", "bot")
1311
+ .option("--tools <text>", "tool descriptor")
1312
+ .option("--outputs <csv>", "expected outputs")
1313
+ .option("--scope <s>", "workspace | global (default: workspace)")
1314
+ .option("--pipeline <json>", "pipeline JSON")
1315
+ .action(async function (opts) {
1316
+ try {
1317
+ let pipeline; if (opts.pipeline) try { pipeline = JSON.parse(opts.pipeline); }
1318
+ catch { return fail(new Error("--pipeline must be valid JSON")); }
1319
+ const r = await call("agents", "create", {
1320
+ name: opts.name, layer: opts.layer, description: opts.description,
1321
+ status: opts.status, schedule: opts.schedule, agent_type: opts.type,
1322
+ icon: opts.icon, tools: opts.tools, scope: opts.scope,
1323
+ outputs: opts.outputs ? opts.outputs.split(",").map((s) => s.trim()) : undefined,
1324
+ pipeline,
1325
+ }, ctxFrom(this));
1326
+ emit(this, r, (d) => process.stdout.write(`✓ Created agent ${d.agent?.name} (${d.agent?.id})\n`));
1327
+ } catch (e) { fail(e); }
1328
+ });
1329
+
1330
+ withCommonOpts(a.command("update <id>"))
1331
+ .description("Patch an agent")
1332
+ .option("--name <name>")
1333
+ .option("--description <text>")
1334
+ .option("--status <s>")
1335
+ .option("--schedule <cron>")
1336
+ .option("--layer <l>")
1337
+ .option("--type <t>")
1338
+ .option("--icon <icon>")
1339
+ .option("--tools <text>")
1340
+ .option("--outputs <csv>")
1341
+ .option("--pipeline <json>")
1342
+ .action(async function (id, opts) {
1343
+ try {
1344
+ let pipeline; if (opts.pipeline) try { pipeline = JSON.parse(opts.pipeline); }
1345
+ catch { return fail(new Error("--pipeline must be valid JSON")); }
1346
+ const r = await call("agents", "update", {
1347
+ id, name: opts.name, description: opts.description, status: opts.status,
1348
+ schedule: opts.schedule, layer: opts.layer, agent_type: opts.type,
1349
+ icon: opts.icon, tools: opts.tools,
1350
+ outputs: opts.outputs ? opts.outputs.split(",").map((s) => s.trim()) : undefined,
1351
+ pipeline,
1352
+ }, ctxFrom(this));
1353
+ emit(this, r, (d) => process.stdout.write(`✓ Updated agent ${d.agent?.id}\n`));
1354
+ } catch (e) { fail(e); }
1355
+ });
1356
+
1357
+ withCommonOpts(a.command("set-status <id> <status>"))
1358
+ .description("Change an agent's status (active|paused|planned|…)")
1359
+ .action(async function (id, status) {
1360
+ try {
1361
+ const r = await call("agents", "set_status", { id, status }, ctxFrom(this));
1362
+ emit(this, r, (d) => process.stdout.write(`✓ ${d.agent?.name ?? id} → ${d.agent?.status}\n`));
1363
+ } catch (e) { fail(e); }
1364
+ });
1365
+
379
1366
  withCommonOpts(a.command("run <name>"))
380
1367
  .description("Invoke an agent")
381
1368
  .option("--input <json>", "JSON input payload", "{}")
@@ -397,4 +1384,216 @@ export function registerAgents(program) {
397
1384
  emit(this, r, (d) => process.stdout.write(JSON.stringify(d.job, null, 2) + "\n"));
398
1385
  } catch (e) { fail(e); }
399
1386
  });
1387
+
1388
+ withCommonOpts(a.command("runs"))
1389
+ .description("List recent agent runs in this workspace")
1390
+ .option("--agent <name>", "filter by agent_name")
1391
+ .option("--limit <n>", "max rows", "25")
1392
+ .action(async function () {
1393
+ try {
1394
+ const o = this.optsWithGlobals();
1395
+ const r = await call("agents", "recent_runs", {
1396
+ agent_name: o.agent, limit: Number(o.limit),
1397
+ }, ctxFrom(this));
1398
+ emit(this, r, (d) => {
1399
+ process.stdout.write(table(
1400
+ ["created", "agent", "status", "results", "enriched"],
1401
+ (d.runs || []).map((rr) => [
1402
+ (rr.created_at || "").slice(0, 16).replace("T", " "),
1403
+ truncate(rr.agent_name, 28), rr.status, rr.results_count ?? 0, rr.enriched_count ?? 0,
1404
+ ]),
1405
+ ) + `\n${d.runs?.length ?? 0} rows\n`);
1406
+ });
1407
+ } catch (e) { fail(e); }
1408
+ });
1409
+
1410
+ withCommonOpts(a.command("delete <id>"))
1411
+ .description("Delete an agent")
1412
+ .action(async function (id) {
1413
+ try {
1414
+ const r = await call("agents", "delete", { id }, ctxFrom(this));
1415
+ emit(this, r, (d) => process.stdout.write(`✓ Deleted ${d.deleted}\n`));
1416
+ } catch (e) { fail(e); }
1417
+ });
1418
+ }
1419
+
1420
+ export function registerAuthority(program) {
1421
+ const a = program.command("authority").description("Authority Loop — queue, approve, dispatch, settings");
1422
+
1423
+ withCommonOpts(a.command("list"))
1424
+ .description("List authority touches (queued/approved/sent/failed/cancelled)")
1425
+ .option("--status <s>", "filter by status (queued|approved|sent|failed|cancelled|skipped)")
1426
+ .option("--channel <c>", "filter by channel (linkedin_dm|linkedin_invite)")
1427
+ .option("--lead <id>", "filter by lead_id")
1428
+ .option("--min-score <n>", "minimum score")
1429
+ .option("--limit <n>", "max rows", "50")
1430
+ .action(async function () {
1431
+ try {
1432
+ const o = this.optsWithGlobals();
1433
+ const r = await call("authority", "list", {
1434
+ status: o.status, channel: o.channel, lead_id: o.lead,
1435
+ min_score: o.minScore ? Number(o.minScore) : undefined,
1436
+ limit: Number(o.limit),
1437
+ }, ctxFrom(this));
1438
+ emit(this, r, (d) => {
1439
+ process.stdout.write(table(
1440
+ ["id", "status", "channel", "score", "asset", "scheduled"],
1441
+ (d.touches || []).map((t) => [
1442
+ t.id.slice(0, 8), t.status, t.channel, t.score ?? 0,
1443
+ truncate(t.asset_title || t.asset_url || "", 36),
1444
+ (t.scheduled_for || "").slice(0, 16).replace("T", " "),
1445
+ ]),
1446
+ ) + `\n${d.touches?.length ?? 0} rows\n`);
1447
+ });
1448
+ } catch (e) { fail(e); }
1449
+ });
1450
+
1451
+ withCommonOpts(a.command("show <id>"))
1452
+ .description("Full JSON for one authority touch")
1453
+ .action(async function (id) {
1454
+ try {
1455
+ const r = await call("authority", "get", { id }, ctxFrom(this));
1456
+ emit(this, r, (d) => process.stdout.write(JSON.stringify(d.touch, null, 2) + "\n"));
1457
+ } catch (e) { fail(e); }
1458
+ });
1459
+
1460
+ withCommonOpts(a.command("approve <id>"))
1461
+ .description("Approve a queued touch (sets status=approved)")
1462
+ .action(async function (id) {
1463
+ try {
1464
+ const r = await call("authority", "approve", { id }, ctxFrom(this));
1465
+ emit(this, r, (d) => process.stdout.write(`✓ Approved ${d.touch?.id} → ${d.touch?.status}\n`));
1466
+ } catch (e) { fail(e); }
1467
+ });
1468
+
1469
+ withCommonOpts(a.command("cancel <id>"))
1470
+ .description("Cancel a queued/approved touch")
1471
+ .option("--reason <text>", "skipped_reason note")
1472
+ .action(async function (id, opts) {
1473
+ try {
1474
+ const r = await call("authority", "cancel", { id, reason: opts.reason }, ctxFrom(this));
1475
+ emit(this, r, (d) => process.stdout.write(`✓ Cancelled ${d.touch?.id}\n`));
1476
+ } catch (e) { fail(e); }
1477
+ });
1478
+
1479
+ withCommonOpts(a.command("dispatch <id>"))
1480
+ .description("Dispatch a queued/approved touch via LinkedIn (calls authority-touch-dispatch)")
1481
+ .action(async function (id) {
1482
+ try {
1483
+ const r = await call("authority", "dispatch", { id }, ctxFrom(this));
1484
+ emit(this, r, (d) => {
1485
+ const ok = d.dispatch?.success;
1486
+ process.stdout.write(`${ok ? "✓" : "✖"} dispatch status=${d.status} ${JSON.stringify(d.dispatch)}\n`);
1487
+ });
1488
+ } catch (e) { fail(e); }
1489
+ });
1490
+
1491
+ const settings = a.command("settings").description("Authority Loop workspace settings");
1492
+
1493
+ withCommonOpts(settings.command("get"))
1494
+ .description("Read authority_loop_settings for this workspace")
1495
+ .action(async function () {
1496
+ try {
1497
+ const r = await call("authority", "settings_get", {}, ctxFrom(this));
1498
+ emit(this, r, (d) => process.stdout.write(JSON.stringify(d.settings, null, 2) + "\n"));
1499
+ } catch (e) { fail(e); }
1500
+ });
1501
+
1502
+ withCommonOpts(settings.command("set"))
1503
+ .description("Patch authority_loop_settings for this workspace (upsert)")
1504
+ .option("--enabled <bool>", "is_enabled (true|false)")
1505
+ .option("--auto-approve <bool>", "auto_approve (true|false)")
1506
+ .option("--daily-cap <n>", "max dispatches per day")
1507
+ .option("--min-relevance <n>", "min signal relevance_score (0-100)")
1508
+ .option("--min-match <n>", "min KB match score (0-100)")
1509
+ .option("--channels <csv>", "allowed_channels e.g. linkedin_dm,linkedin_invite")
1510
+ .option("--kb-categories <csv>", "allowed_kb_categories CSV")
1511
+ .option("--trigger-flags <csv>", "allowed_trigger_flags CSV")
1512
+ .option("--quiet-start <hhmm>", "quiet_hours_start (e.g. 22:00)")
1513
+ .option("--quiet-end <hhmm>", "quiet_hours_end (e.g. 07:00)")
1514
+ .action(async function (opts) {
1515
+ try {
1516
+ const asBool = (v) => v === undefined ? undefined : /^(1|true|yes|on)$/i.test(String(v));
1517
+ const asCsv = (v) => v === undefined ? undefined : String(v).split(",").map((s) => s.trim()).filter(Boolean);
1518
+ const params = {
1519
+ is_enabled: asBool(opts.enabled),
1520
+ auto_approve: asBool(opts.autoApprove),
1521
+ daily_cap: opts.dailyCap !== undefined ? Number(opts.dailyCap) : undefined,
1522
+ min_relevance_score: opts.minRelevance !== undefined ? Number(opts.minRelevance) : undefined,
1523
+ min_match_score: opts.minMatch !== undefined ? Number(opts.minMatch) : undefined,
1524
+ allowed_channels: asCsv(opts.channels),
1525
+ allowed_kb_categories: asCsv(opts.kbCategories),
1526
+ allowed_trigger_flags: asCsv(opts.triggerFlags),
1527
+ quiet_hours_start: opts.quietStart,
1528
+ quiet_hours_end: opts.quietEnd,
1529
+ };
1530
+ const r = await call("authority", "settings_set", params, ctxFrom(this));
1531
+ emit(this, r, (d) => process.stdout.write(`✓ Settings updated\n${JSON.stringify(d.settings, null, 2)}\n`));
1532
+ } catch (e) { fail(e); }
1533
+ });
1534
+ }
1535
+
1536
+ export function registerBriefs(program) {
1537
+ const b = program.command("briefs").description("Chief of Staff morning briefs — list, show, generate");
1538
+
1539
+ withCommonOpts(b.command("list"))
1540
+ .description("Recent intelligence_briefs for this workspace")
1541
+ .option("--user <id>", "filter by user_id")
1542
+ .option("--limit <n>", "max rows", "25")
1543
+ .action(async function () {
1544
+ try {
1545
+ const o = this.optsWithGlobals();
1546
+ const r = await call("briefs", "list", {
1547
+ user_id: o.user,
1548
+ limit: Number(o.limit),
1549
+ }, ctxFrom(this));
1550
+ emit(this, r, (d) => {
1551
+ process.stdout.write(table(
1552
+ ["id", "created", "signals", "actions", "sentiment", "headline"],
1553
+ (d.briefs || []).map((x) => [
1554
+ x.id.slice(0, 8),
1555
+ (x.created_at || "").slice(0, 16).replace("T", " "),
1556
+ x.signal_count ?? 0,
1557
+ x.top_actions_count ?? 0,
1558
+ x.sentiment ?? "—",
1559
+ truncate(x.headline || "", 48),
1560
+ ]),
1561
+ ) + `\n${d.briefs?.length ?? 0} rows\n`);
1562
+ });
1563
+ } catch (e) { fail(e); }
1564
+ });
1565
+
1566
+ withCommonOpts(b.command("show <id>"))
1567
+ .description("Full JSON for one brief (brief_data, talking points, top actions, risks)")
1568
+ .action(async function (id) {
1569
+ try {
1570
+ const r = await call("briefs", "get", { id }, ctxFrom(this));
1571
+ emit(this, r, (d) => process.stdout.write(JSON.stringify(d.brief, null, 2) + "\n"));
1572
+ } catch (e) { fail(e); }
1573
+ });
1574
+
1575
+ withCommonOpts(b.command("generate"))
1576
+ .description("Generate a fresh brief now — invokes agent-chief-of-staff for this workspace")
1577
+ .option("--user <id>", "attribute brief to a specific user_id (defaults to workspace owner)")
1578
+ .option("--job <id>", "existing agent_jobs.id to report progress against")
1579
+ .action(async function () {
1580
+ try {
1581
+ const o = this.optsWithGlobals();
1582
+ const r = await call("briefs", "generate", {
1583
+ user_id: o.user,
1584
+ job_id: o.job,
1585
+ }, ctxFrom(this));
1586
+ emit(this, r, (d) => {
1587
+ const g = d.generate || {};
1588
+ const ok = g.success !== false && d.status < 400;
1589
+ process.stdout.write(
1590
+ `${ok ? "✓" : "✖"} chief-of-staff status=${d.status}` +
1591
+ (g.brief_id ? ` brief_id=${g.brief_id}` : "") +
1592
+ (g.headline ? `\n ${g.headline}` : "") +
1593
+ (g.signal_count !== undefined ? `\n signals=${g.signal_count} actions=${g.actions_generated ?? "?"}` : "") +
1594
+ "\n",
1595
+ );
1596
+ });
1597
+ } catch (e) { fail(e); }
1598
+ });
400
1599
  }