@series-a/cli 0.6.2 → 0.7.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.
package/README.md CHANGED
@@ -40,6 +40,19 @@ npx @series-a/cli@latest login
40
40
  # Prompts for API key + workspace ID (the 6-char slug, e.g. dmw9wj)
41
41
  ```
42
42
 
43
+ ### Admin login
44
+
45
+ Admin keys (`advos_admin_…`) are detected automatically — no workspace prompt:
46
+
47
+ ```bash
48
+ advisoros login --admin
49
+ # ✓ Logged in as admin profile "default" — N workspaces reachable
50
+
51
+ advisoros workspaces list # admin-only, no workspace needed
52
+ advisoros campaigns list --workspace dmw9wj # pass workspace per command
53
+ advisoros whoami # shows scope: admin (cross-workspace)
54
+ ```
55
+
43
56
  Multiple workspaces?
44
57
 
45
58
  ```bash
@@ -188,6 +201,35 @@ JOB=$(advisoros agents run lead-intelligence --input '{}' --json | jq -r '.run.j
188
201
  watch -n 5 "advisoros agents job $JOB --json | jq '{status,progress}'"
189
202
  ```
190
203
 
204
+ ## Safety rails
205
+
206
+ Anything that stops sending, un-enrols leads or deletes data now asks first.
207
+
208
+ ```bash
209
+ # Launch is a dry run until you confirm — preflight runs server-side.
210
+ advisoros campaigns preflight 9884b9ec
211
+ advisoros campaigns launch 9884b9ec # shows blockers, changes nothing
212
+ advisoros campaigns launch 9884b9ec --confirm # actually launches
213
+
214
+ # Destructive commands prompt; scripts must pass --yes.
215
+ advisoros campaigns delete 9884b9ec --dry-run
216
+ advisoros campaigns delete 9884b9ec --yes
217
+ advisoros sequences delete-step 6b60d6ad --yes
218
+ ```
219
+
220
+ A campaign with no leads, no sequence, no steps or no sender account is
221
+ **refused**, not silently activated. `--force` overrides once you know why.
222
+
223
+ New commands: `campaigns preflight|archive|clone|delete|remove-leads`,
224
+ `sequences delete|update-step|delete-step`.
225
+
226
+ ## Reading more than 200 rows
227
+
228
+ List commands accept `--limit` up to 1000 and every list response carries
229
+ `pagination` (`total`, `limit`, `offset`, `next_cursor`, `has_more`). If you ask
230
+ for more than the maximum, the response says it was clamped instead of quietly
231
+ truncating.
232
+
191
233
  ## Exit codes
192
234
 
193
235
  | Code | Meaning |
@@ -238,3 +280,23 @@ git push --follow-tags
238
280
 
239
281
  See [`docs/agent-api.md`](../../docs/agent-api.md) for the underlying `/agent-api` envelope.
240
282
 
283
+
284
+
285
+ ## Admin keys
286
+
287
+ Admin keys (`advos_admin_…`) are authorised by the key itself — they do **not** need an
288
+ `agent_workspace_bindings` row. `agent_name` is an audit label only.
289
+
290
+ ```bash
291
+ advisoros login --admin --profile ops # no workspace prompt; prints reachable workspace count
292
+ advisoros whoami --profile ops # scope: admin (cross-workspace) + reachable count
293
+ advisoros workspaces list --profile ops # every reachable workspace (paginated, up to 1000)
294
+ advisoros analytics overview --workspace dmw9wj --profile ops
295
+ ```
296
+
297
+ Caller identity is overridable: `--as-agent <name>` on any command, `ADVISOROS_AGENT_NAME`
298
+ in the environment, or the `agentName` stored on the profile at login (default `advisoros-cli`).
299
+
300
+ Workspace-scoped keys are unchanged: they can only reach their own workspace (403 otherwise)
301
+ and cannot use the `workspaces` resource. An agent without bindings now gets a typed 403
302
+ explaining to use an admin key, instead of a 500.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@series-a/cli",
3
- "version": "0.6.2",
3
+ "version": "0.7.1",
4
4
  "description": "AdvisorPro command-line interface — drive leads, campaigns, sequences and agents from your terminal.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/api.mjs CHANGED
@@ -26,7 +26,11 @@ export async function call(resource, action, params = {}, opts = {}) {
26
26
  process.env.ADVISOROS_BASE_URL ||
27
27
  stored?.baseUrl ||
28
28
  DEFAULTS.baseUrl;
29
- const agentName = opts.agentName || stored?.agentName || "advisoros-cli";
29
+ const agentName =
30
+ opts.agentName ||
31
+ process.env.ADVISOROS_AGENT_NAME ||
32
+ stored?.agentName ||
33
+ "advisoros-cli";
30
34
 
31
35
  if (!apiKey) {
32
36
  throw new AdvisorOSApiError(
@@ -10,8 +10,10 @@ export function registerAuth(program) {
10
10
  .description("Save an API key + workspace to ~/.advisoros/config.json")
11
11
  .option("-p, --profile <name>", "profile name", "default")
12
12
  .option("--api-key <key>", "API key (otherwise prompted)")
13
- .option("--workspace <id>", "workspace id (otherwise prompted)")
13
+ .option("--workspace <id>", "workspace id (otherwise prompted, unless --admin)")
14
+ .option("--admin", "admin key login — verify cross-workspace, no workspace required", false)
14
15
  .option("--base-url <url>", "agent-api base URL", DEFAULTS.baseUrl)
16
+ .option("--as-agent <name>", "caller identity to send as agent_name", "advisoros-cli")
15
17
  .action(async (opts) => {
16
18
  try {
17
19
  let { apiKey, workspace, baseUrl, profile } = opts;
@@ -19,13 +21,16 @@ export function registerAuth(program) {
19
21
  const r = await prompts({
20
22
  type: "password",
21
23
  name: "v",
22
- message: "API key (from Settings → API Keys)",
24
+ message: "API key (from Settings → API Keys; advos_admin_… for admin)",
23
25
  validate: (v) => v && v.length > 10 ? true : "Required",
24
26
  });
25
27
  apiKey = r.v;
26
28
  }
27
29
  if (!apiKey) return fail(new Error("Aborted"));
28
- if (!workspace) {
30
+
31
+ const isAdmin = opts.admin || apiKey.startsWith("advos_admin_");
32
+
33
+ if (!isAdmin && !workspace) {
29
34
  const r = await prompts({
30
35
  type: "text",
31
36
  name: "v",
@@ -34,14 +39,31 @@ export function registerAuth(program) {
34
39
  });
35
40
  workspace = r.v;
36
41
  }
37
- if (!workspace) return fail(new Error("Aborted"));
42
+ if (!isAdmin && !workspace) return fail(new Error("Aborted"));
43
+
44
+ const agentName = opts.asAgent || "advisoros-cli";
45
+
46
+ if (isAdmin) {
47
+ // Verify the admin key cross-workspace; no workspace slug needed.
48
+ const verify = await call("workspaces", "list", { limit: 1000 }, {
49
+ apiKey, baseUrl, agentName,
50
+ });
51
+ const count = verify?.pagination?.total ?? verify?.workspaces?.length ?? 0;
52
+ await setProfile(profile, { apiKey, baseUrl, admin: true, agentName, ...(workspace ? { workspaceId: workspace } : {}) });
53
+ process.stdout.write(chalk.green(
54
+ `\n✓ Logged in as admin profile "${profile}" — ${count} workspaces reachable\n` +
55
+ ` Config: ${CONFIG_PATH}\n` +
56
+ ` Tip: pass --workspace <slug> per command, or set ADVISOROS_WORKSPACE_ID.\n`,
57
+ ));
58
+ return;
59
+ }
38
60
 
39
61
  // Verify the credentials with a cheap round-trip.
40
62
  const verify = await call("agents", "list", {}, {
41
- apiKey, workspaceId: workspace, baseUrl,
63
+ apiKey, workspaceId: workspace, baseUrl, agentName,
42
64
  });
43
65
  const count = verify?.agents?.length ?? 0;
44
- await setProfile(profile, { apiKey, workspaceId: workspace, baseUrl });
66
+ await setProfile(profile, { apiKey, workspaceId: workspace, baseUrl, agentName });
45
67
  process.stdout.write(chalk.green(
46
68
  `\n✓ Logged in as profile "${profile}" — workspace ${workspace} (${count} agents visible)\n` +
47
69
  ` Config: ${CONFIG_PATH}\n`,
@@ -58,9 +80,24 @@ export function registerAuth(program) {
58
80
  const p = await getProfile(opts.profile);
59
81
  if (!p) return fail(new Error(`Profile "${opts.profile}" not found. Run \`advisoros login\`.`));
60
82
  const masked = p.apiKey ? p.apiKey.slice(0, 8) + "…" + p.apiKey.slice(-4) : "(none)";
83
+ const isAdmin = Boolean(p.admin) || String(p.apiKey || "").startsWith("advos_admin_");
84
+
85
+ let reachable = null;
86
+ if (isAdmin) {
87
+ try {
88
+ const r = await call("workspaces", "list", { limit: 1000 }, {
89
+ apiKey: p.apiKey, baseUrl: p.baseUrl, agentName: p.agentName,
90
+ });
91
+ reachable = r?.pagination?.total ?? r?.workspaces?.length ?? null;
92
+ } catch { reachable = null; }
93
+ }
94
+
61
95
  process.stdout.write(
62
96
  `profile: ${opts.profile}\n` +
63
- `workspace: ${p.workspaceId}\n` +
97
+ `scope: ${isAdmin ? "admin (cross-workspace)" : "workspace"}\n` +
98
+ `workspace: ${p.workspaceId || "(none — pass --workspace per command)"}\n` +
99
+ (isAdmin ? `reachable: ${reachable == null ? "(could not verify)" : `${reachable} workspaces`}\n` : "") +
100
+ `agent: ${p.agentName || "advisoros-cli"}\n` +
64
101
  `base url: ${p.baseUrl || DEFAULTS.baseUrl}\n` +
65
102
  `api key: ${masked}\n`,
66
103
  );
@@ -1,6 +1,6 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { call } from "../api.mjs";
3
- import { emit, fail, ctxFrom, withCommonOpts, table, truncate } from "../ui.mjs";
3
+ import { emit, fail, ctxFrom, withCommonOpts, table, truncate, confirmDestructive } from "../ui.mjs";
4
4
 
5
5
  function parseCsv(text) {
6
6
  // Tiny CSV parser: header row + comma-separated values, quoted fields supported.
@@ -174,8 +174,10 @@ export function registerLeads(program) {
174
174
 
175
175
  withCommonOpts(leads.command("delete <id>"))
176
176
  .description("Delete a lead")
177
+ .option("-y, --yes", "skip the confirmation prompt")
177
178
  .action(async function (id) {
178
179
  try {
180
+ await confirmDestructive(this, `Delete lead ${id}. This cannot be undone.`);
179
181
  const r = await call("leads", "delete", { id }, ctxFrom(this));
180
182
  emit(this, r, () => process.stdout.write(`✓ Deleted ${id}\n`));
181
183
  } catch (e) { fail(e); }
@@ -291,11 +293,56 @@ export function registerCampaigns(program) {
291
293
  } catch (e) { fail(e); }
292
294
  });
293
295
 
294
- withCommonOpts(c.command("launch <id>"))
295
- .description("Activate a campaign (status → active)")
296
+ withCommonOpts(c.command("remove-leads <campaignId>"))
297
+ .description("Un-enrol leads from a campaign")
298
+ .option("--lead-ids <ids>", "comma-separated lead ids")
299
+ .option("--all", "remove every enrolled lead")
300
+ .option("-y, --yes", "skip the confirmation prompt")
301
+ .option("--dry-run", "show what would be removed")
302
+ .action(async function (campaignId, opts) {
303
+ try {
304
+ const ids = opts.leadIds ? opts.leadIds.split(",").map((s) => s.trim()).filter(Boolean) : [];
305
+ if (!ids.length && !opts.all) return fail(new Error("--lead-ids or --all required"));
306
+ const params = { campaign_id: campaignId, lead_ids: ids, all: !!opts.all };
307
+ if (opts.dryRun) {
308
+ const d = await call("campaigns", "remove_leads", { ...params, dry_run: true }, ctxFrom(this));
309
+ return emit(this, d, (x) => process.stdout.write(`Would remove ${x.would_remove} lead(s) from ${campaignId}\n`));
310
+ }
311
+ await confirmDestructive(this, `Remove ${opts.all ? "ALL enrolled leads" : `${ids.length} lead(s)`} from campaign ${campaignId}.`);
312
+ const r = await call("campaigns", "remove_leads", params, ctxFrom(this));
313
+ emit(this, r, (d) => process.stdout.write(`✓ Removed ${d.removed} lead(s)\n`));
314
+ } catch (e) { fail(e); }
315
+ });
316
+
317
+ withCommonOpts(c.command("preflight <id>"))
318
+ .description("Check whether a campaign is safe to launch (read-only)")
296
319
  .action(async function (id) {
297
320
  try {
298
- const r = await call("campaigns", "launch", { campaign_id: id }, ctxFrom(this));
321
+ const r = await call("campaigns", "preflight", { campaign_id: id }, ctxFrom(this));
322
+ emit(this, r, renderPreflight);
323
+ } catch (e) { fail(e); }
324
+ });
325
+
326
+ withCommonOpts(c.command("launch <id>"))
327
+ .description("Activate a campaign — runs preflight first, then requires --confirm")
328
+ .option("--confirm", "actually launch (without it, this is a dry run)")
329
+ .option("--force", "launch even if preflight found blockers")
330
+ .action(async function (id, opts) {
331
+ try {
332
+ if (!opts.confirm) {
333
+ const d = await call("campaigns", "launch", { campaign_id: id, dry_run: true }, ctxFrom(this));
334
+ return emit(this, d, (x) => {
335
+ renderPreflight(x.preflight ?? x);
336
+ process.stdout.write(
337
+ x.preflight?.ok
338
+ ? `\nDry run only. Re-run with --confirm to launch.\n`
339
+ : `\nDry run only. Fix the blockers above, then re-run with --confirm.\n`,
340
+ );
341
+ });
342
+ }
343
+ const r = await call(
344
+ "campaigns", "launch", { campaign_id: id, force: !!opts.force }, ctxFrom(this),
345
+ );
299
346
  emit(this, r, (d) => process.stdout.write(`✓ ${d.campaign?.name ?? id} → ${d.campaign?.status}\n`));
300
347
  } catch (e) { fail(e); }
301
348
  });
@@ -311,14 +358,84 @@ export function registerCampaigns(program) {
311
358
 
312
359
  withCommonOpts(c.command("resume <id>"))
313
360
  .description("Resume a paused campaign")
314
- .action(async function (id) {
361
+ .option("--confirm", "required — resuming restarts sending")
362
+ .option("--force", "resume even if preflight found blockers")
363
+ .action(async function (id, opts) {
315
364
  try {
316
- const r = await call("campaigns", "resume", { campaign_id: id }, ctxFrom(this));
365
+ if (!opts.confirm) {
366
+ const d = await call("campaigns", "resume", { campaign_id: id, dry_run: true }, ctxFrom(this));
367
+ return emit(this, d, (x) => {
368
+ renderPreflight(x.preflight ?? x);
369
+ process.stdout.write(`\nDry run only. Re-run with --confirm to resume.\n`);
370
+ });
371
+ }
372
+ const r = await call("campaigns", "resume", { campaign_id: id, force: !!opts.force }, ctxFrom(this));
317
373
  emit(this, r, (d) => process.stdout.write(`✓ ${d.campaign?.name ?? id} → ${d.campaign?.status}\n`));
318
374
  } catch (e) { fail(e); }
319
375
  });
376
+
377
+ withCommonOpts(c.command("archive <id>"))
378
+ .description("Archive a campaign (reversible, keeps history)")
379
+ .option("-y, --yes", "skip the confirmation prompt")
380
+ .action(async function (id) {
381
+ try {
382
+ await confirmDestructive(this, `Archive campaign ${id}. It will stop sending.`);
383
+ const r = await call("campaigns", "archive", { campaign_id: id }, ctxFrom(this));
384
+ emit(this, r, (d) => process.stdout.write(`✓ ${d.campaign?.name ?? id} → archived\n`));
385
+ } catch (e) { fail(e); }
386
+ });
387
+
388
+ withCommonOpts(c.command("clone <id>"))
389
+ .description("Duplicate a campaign as a fresh draft (no leads, no history)")
390
+ .option("--name <name>", "name for the copy")
391
+ .action(async function (id, opts) {
392
+ try {
393
+ const r = await call("campaigns", "clone", { campaign_id: id, name: opts.name }, ctxFrom(this));
394
+ emit(this, r, (d) => process.stdout.write(`✓ Cloned to ${d.campaign?.id} — ${d.campaign?.name}\n`));
395
+ } catch (e) { fail(e); }
396
+ });
397
+
398
+ withCommonOpts(c.command("delete <id>"))
399
+ .description("Permanently delete a campaign and its enrolments")
400
+ .option("-y, --yes", "skip the confirmation prompt")
401
+ .option("--dry-run", "show what would be deleted")
402
+ .option("--force", "delete even when the campaign has dispatch history")
403
+ .action(async function (id, opts) {
404
+ try {
405
+ if (opts.dryRun) {
406
+ const d = await call("campaigns", "delete", { campaign_id: id, dry_run: true }, ctxFrom(this));
407
+ return emit(this, d, (x) =>
408
+ process.stdout.write(
409
+ `Would delete campaign ${x.campaign?.name ?? id} (${x.dispatches} dispatched actions)\n`,
410
+ ));
411
+ }
412
+ await confirmDestructive(
413
+ this,
414
+ `Permanently delete campaign ${id} and every lead enrolment on it. This cannot be undone.`,
415
+ { expect: id.slice(0, 8) },
416
+ );
417
+ const r = await call(
418
+ "campaigns", "delete", { campaign_id: id, force: !!opts.force }, ctxFrom(this),
419
+ );
420
+ emit(this, r, (d) => process.stdout.write(`✓ Deleted ${d.name ?? id}\n`));
421
+ } catch (e) { fail(e); }
422
+ });
423
+ }
424
+
425
+ function renderPreflight(p) {
426
+ if (!p) return;
427
+ process.stdout.write(
428
+ `${p.ok ? "✓ Ready to launch" : "✖ Not ready"} — ${p.name ?? p.campaign_id} (${p.status})\n`,
429
+ );
430
+ const c = p.checks ?? {};
431
+ process.stdout.write(
432
+ ` leads: ${c.leads_enrolled} steps: ${c.sequence_steps} senders: ${c.sender_accounts} daily limit: ${c.daily_send_limit ?? "—"}\n`,
433
+ );
434
+ for (const b of p.blockers ?? []) process.stdout.write(` ✖ ${b}\n`);
435
+ for (const w of p.warnings ?? []) process.stdout.write(` ! ${w}\n`);
320
436
  }
321
437
 
438
+
322
439
  export function registerSequences(program) {
323
440
  const s = program.command("sequences").description("Manage LinkedIn sequences");
324
441
 
@@ -501,8 +618,68 @@ export function registerSequences(program) {
501
618
  });
502
619
  } catch (e) { fail(e); }
503
620
  });
621
+
622
+ withCommonOpts(s.command("update-step <stepId>"))
623
+ .description("Patch one step in place (message, delay, variant, channel, action)")
624
+ .option("--action <action>", "step action")
625
+ .option("--message <text>", "message_content")
626
+ .option("--delay <days>", "delay_days")
627
+ .option("--variant <v>", "variant")
628
+ .option("--channel <c>", "channel")
629
+ .action(async function (stepId, opts) {
630
+ try {
631
+ const params = { step_id: stepId };
632
+ if (opts.action !== undefined) params.action = opts.action;
633
+ if (opts.message !== undefined) params.message_content = opts.message;
634
+ if (opts.delay !== undefined) params.delay_days = Number(opts.delay);
635
+ if (opts.variant !== undefined) params.variant = opts.variant;
636
+ if (opts.channel !== undefined) params.channel = opts.channel;
637
+ const r = await call("sequences", "update_step", params, ctxFrom(this));
638
+ emit(this, r, (d) => process.stdout.write(`✓ Updated step ${d.step?.id ?? stepId} (${d.step?.action})\n`));
639
+ } catch (e) { fail(e); }
640
+ });
641
+
642
+ withCommonOpts(s.command("delete-step <stepId>"))
643
+ .description("Delete one step; its children are re-parented")
644
+ .option("-y, --yes", "skip the confirmation prompt")
645
+ .option("--dry-run", "show what would change")
646
+ .action(async function (stepId, opts) {
647
+ try {
648
+ if (opts.dryRun) {
649
+ const d = await call("sequences", "delete_step", { step_id: stepId, dry_run: true }, ctxFrom(this));
650
+ return emit(this, d, (x) =>
651
+ process.stdout.write(`Would delete step ${stepId}; ${x.reparented_children} child step(s) re-parented\n`));
652
+ }
653
+ await confirmDestructive(this, `Delete sequence step ${stepId}. Child steps will be re-parented.`);
654
+ const r = await call("sequences", "delete_step", { step_id: stepId }, ctxFrom(this));
655
+ emit(this, r, (d) =>
656
+ process.stdout.write(`✓ Deleted step ${stepId} (${d.reparented_children} child step(s) re-parented)\n`));
657
+ } catch (e) { fail(e); }
658
+ });
659
+
660
+ withCommonOpts(s.command("delete <id>"))
661
+ .description("Delete a sequence and all its steps")
662
+ .option("-y, --yes", "skip the confirmation prompt")
663
+ .option("--dry-run", "show what would be deleted")
664
+ .option("--force", "delete even if campaigns still use it (they are detached)")
665
+ .action(async function (id, opts) {
666
+ try {
667
+ if (opts.dryRun) {
668
+ const d = await call("sequences", "delete", { id, dry_run: true }, ctxFrom(this));
669
+ return emit(this, d, (x) =>
670
+ process.stdout.write(
671
+ `Would delete sequence ${x.sequence?.name ?? id}; used by ${x.used_by_campaigns?.length ?? 0} campaign(s)\n`,
672
+ ));
673
+ }
674
+ await confirmDestructive(this, `Delete sequence ${id} and every step on it. This cannot be undone.`);
675
+ const r = await call("sequences", "delete", { id, force: !!opts.force }, ctxFrom(this));
676
+ emit(this, r, (d) =>
677
+ process.stdout.write(`✓ Deleted sequence ${d.sequence_id} (${d.detached_campaigns} campaign(s) detached)\n`));
678
+ } catch (e) { fail(e); }
679
+ });
504
680
  }
505
681
 
682
+
506
683
  function parseSnoozeDuration(s) {
507
684
  // Accepts e.g. "30m", "2h", "1d", "45" (minutes). Returns minutes (int).
508
685
  if (!s) return null;
@@ -900,8 +1077,10 @@ export function registerContent(program) {
900
1077
 
901
1078
  withCommonOpts(c.command("delete <id>"))
902
1079
  .description("Delete a content item")
1080
+ .option("-y, --yes", "skip the confirmation prompt")
903
1081
  .action(async function (id) {
904
1082
  try {
1083
+ await confirmDestructive(this, `Delete content item ${id}. This cannot be undone.`);
905
1084
  const r = await call("content", "delete", { id }, ctxFrom(this));
906
1085
  emit(this, r, (d) => process.stdout.write(`✓ Deleted ${d.deleted}\n`));
907
1086
  } catch (e) { fail(e); }
@@ -1043,8 +1222,10 @@ export function registerCalendar(program) {
1043
1222
 
1044
1223
  withCommonOpts(cal.command("delete <id>"))
1045
1224
  .description("Delete a calendar post")
1225
+ .option("-y, --yes", "skip the confirmation prompt")
1046
1226
  .action(async function (id) {
1047
1227
  try {
1228
+ await confirmDestructive(this, `Delete calendar post ${id}. This cannot be undone.`);
1048
1229
  const r = await call("calendar", "delete", { id }, ctxFrom(this));
1049
1230
  emit(this, r, (d) => process.stdout.write(`✓ Deleted ${d.deleted}\n`));
1050
1231
  } catch (e) { fail(e); }
@@ -1152,8 +1333,10 @@ export function registerSignals(program) {
1152
1333
 
1153
1334
  withCommonOpts(s.command("delete <id>"))
1154
1335
  .description("Delete a signal")
1336
+ .option("-y, --yes", "skip the confirmation prompt")
1155
1337
  .action(async function (id) {
1156
1338
  try {
1339
+ await confirmDestructive(this, `Delete signal ${id}. This cannot be undone.`);
1157
1340
  const r = await call("signals", "delete", { id }, ctxFrom(this));
1158
1341
  emit(this, r, (d) => process.stdout.write(`✓ Deleted ${d.deleted}\n`));
1159
1342
  } catch (e) { fail(e); }
@@ -1283,8 +1466,10 @@ export function registerPipelines(program) {
1283
1466
 
1284
1467
  withCommonOpts(p.command("delete <id>"))
1285
1468
  .description("Delete an opportunity")
1469
+ .option("-y, --yes", "skip the confirmation prompt")
1286
1470
  .action(async function (id) {
1287
1471
  try {
1472
+ await confirmDestructive(this, `Delete pipeline record ${id}. This cannot be undone.`);
1288
1473
  const r = await call("pipelines", "delete", { id }, ctxFrom(this));
1289
1474
  emit(this, r, (d) => process.stdout.write(`✓ Deleted ${d.deleted}\n`));
1290
1475
  } catch (e) { fail(e); }
@@ -1472,8 +1657,10 @@ export function registerAgents(program) {
1472
1657
 
1473
1658
  withCommonOpts(a.command("delete <id>"))
1474
1659
  .description("Delete an agent")
1660
+ .option("-y, --yes", "skip the confirmation prompt")
1475
1661
  .action(async function (id) {
1476
1662
  try {
1663
+ await confirmDestructive(this, `Delete agent ${id}. This cannot be undone.`);
1477
1664
  const r = await call("agents", "delete", { id }, ctxFrom(this));
1478
1665
  emit(this, r, (d) => process.stdout.write(`✓ Deleted ${d.deleted}\n`));
1479
1666
  } catch (e) { fail(e); }
package/src/index.mjs CHANGED
@@ -20,8 +20,9 @@ const program = new Command();
20
20
  program
21
21
  .name("advisoros")
22
22
  .description("AdvisorPro CLI — drive leads, campaigns, sequences and agents from your terminal.")
23
- .version("0.5.0")
24
- .option("--json", "machine-readable JSON output (applies to all subcommands)");
23
+ .version("0.7.0")
24
+ .option("--json", "machine-readable JSON output (applies to all subcommands)")
25
+ .option("--as-agent <name>", "caller identity sent as agent_name (audit label only)");
25
26
 
26
27
  registerAuth(program);
27
28
  registerLeads(program);
package/src/ui.mjs CHANGED
@@ -45,7 +45,8 @@ export function withCommonOpts(cmd) {
45
45
  .option("--json", "output raw JSON")
46
46
  .option("-w, --workspace <id>", "override workspace_id")
47
47
  .option("-p, --profile <name>", "use a named profile")
48
- .option("--api-key <key>", "override API key");
48
+ .option("--api-key <key>", "override API key")
49
+ .option("--as-agent <name>", "caller identity sent as agent_name (audit label)");
49
50
  }
50
51
 
51
52
  export function ctxFrom(cmd) {
@@ -54,5 +55,50 @@ export function ctxFrom(cmd) {
54
55
  apiKey: opts.apiKey,
55
56
  workspaceId: opts.workspace,
56
57
  profile: opts.profile,
58
+ agentName: opts.asAgent,
57
59
  };
58
60
  }
61
+
62
+ /**
63
+ * Gate a destructive command. Returns true when it may proceed.
64
+ * Non-interactive shells must pass --yes; --json implies scripted use, so it
65
+ * also requires --yes rather than hanging on a prompt.
66
+ */
67
+ export async function confirmDestructive(cmd, summary, { expect } = {}) {
68
+ const opts = cmd.optsWithGlobals();
69
+ if (opts.yes) return true;
70
+ if (isJsonMode(cmd) || !process.stdin.isTTY) {
71
+ process.stderr.write(
72
+ chalk.red(`✖ Refusing to run without confirmation.\n`) +
73
+ chalk.gray(` ${summary}\n Re-run with --yes to confirm (or --dry-run to preview).\n`),
74
+ );
75
+ process.exit(1);
76
+ }
77
+ const { default: prompts } = await import("prompts");
78
+ process.stdout.write(chalk.yellow(`\n${summary}\n`));
79
+ if (expect) {
80
+ const r = await prompts({
81
+ type: "text",
82
+ name: "v",
83
+ message: `Type "${expect}" to confirm`,
84
+ });
85
+ if (r.v !== expect) {
86
+ process.stdout.write("Aborted.\n");
87
+ process.exit(1);
88
+ }
89
+ return true;
90
+ }
91
+ const r = await prompts({ type: "confirm", name: "v", message: "Proceed?", initial: false });
92
+ if (!r.v) {
93
+ process.stdout.write("Aborted.\n");
94
+ process.exit(1);
95
+ }
96
+ return true;
97
+ }
98
+
99
+ /** Common flags for destructive commands. */
100
+ export function withDestructiveOpts(cmd) {
101
+ return cmd
102
+ .option("-y, --yes", "skip the confirmation prompt")
103
+ .option("--dry-run", "show what would happen without changing anything");
104
+ }