@series-a/cli 0.6.0 → 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/CHANGELOG.md CHANGED
@@ -4,6 +4,10 @@ All notable changes to this package are documented here. This project follows
4
4
  [Semantic Versioning](https://semver.org/) and uses
5
5
  [Changesets](https://github.com/changesets/changesets) to manage releases.
6
6
 
7
+ ## 0.6.1 — Campaign editing
8
+
9
+ - `campaigns` — added `update`/`edit` (name, description, goal, sequence, sender accounts, daily send limit, launch stage, status). Mirrors the new `update_campaign` MCP tool.
10
+
7
11
  ## 0.2.0 — Write coverage expansion
8
12
 
9
13
  - `inbox` — added `send`, `reply` (dry-run by default, `--confirm` to dispatch, 7,900 char guard), `mark-read` (bulk), `snooze` (`--for`, `--until`, `--clear`).
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
@@ -73,11 +86,28 @@ advisoros leads list [--status new] [--persona founder] [--search acme] [--limit
73
86
  advisoros leads get <id>
74
87
  advisoros leads set-status <id> <new|engaged|won|lost>
75
88
  advisoros leads import --file leads.csv [--persona founder]
89
+ advisoros leads import-from <source> [--query ...] [--post <url>] [--file <path>] [--audience <name>] [--campaign <id>] [--limit 50] [--no-enrich]
90
+ advisoros leads import-status <jobId>
91
+ advisoros leads update <id> [--name ...] [--title ...] [--company ...] [--email ...] [--linkedin ...] [--persona ...] [--score ...] [--status ...] [--notes ...]
92
+ advisoros leads delete <id>
76
93
  advisoros leads verify <id>
77
94
  ```
78
95
 
79
96
  `leads import` accepts CSV (header row), JSON array, or JSONL. Upserts on `(workspace_id, linkedin_url)`. Bulk requests are auto-chunked at 200/call (server cap: 500/call).
80
97
 
98
+ `leads import-from` runs any of the six import sources in **Get Leads → Import Leads**:
99
+
100
+ | source | required params | what it does |
101
+ |---|---|---|
102
+ | `linkedin_search` | `--query` | keyword / title / company people search via Unipile |
103
+ | `company_search` | `--query` | LinkedIn company search |
104
+ | `post_reactors` | `--post <url>` | everyone who reacted to a LinkedIn post |
105
+ | `paste_profiles` | `--file <path>` | name + LinkedIn URL lines; rest is enriched |
106
+ | `csv` | `--file <path>` | same CSV parser the UI uses |
107
+ | `heyreach` | `--file <path>` `--campaign <id>` | HeyReach campaign CSV, statuses preserved |
108
+
109
+ All sources accept `--audience` to attach leads to a segment (created if missing). LinkedIn-backed sources run at bulk read priority so live campaigns are not starved. Long-running imports return a `job_id`; poll with `advisoros leads import-status <jobId>`.
110
+
81
111
  ### Campaigns
82
112
 
83
113
  ```bash
@@ -96,7 +126,7 @@ advisoros campaigns resume <id>
96
126
 
97
127
  `launch` flips status to `active` — the `enforce_campaign_char_limits` trigger validates every step's copy against LinkedIn limits before allowing the transition.
98
128
 
99
- ### Sequences / Inbox / Content / Agents
129
+ ### Sequences / Inbox / Content / Agents / Meetings / Newsletter / Integrations
100
130
 
101
131
  ```bash
102
132
  advisoros sequences list
@@ -110,11 +140,29 @@ advisoros content publish <id>
110
140
 
111
141
  advisoros agents list
112
142
  advisoros agents run <name> --input '{"foo":"bar"}'
143
+ advisoros agents trigger <name> --input '{"foo":"bar"}'
144
+ advisoros agents history [--agent <name>] [--days 30] [--limit 50]
113
145
  advisoros agents job <jobId>
146
+
147
+ advisoros meetings list [--limit 50]
148
+ advisoros meetings transcripts [--provider fireflies|roam] [--since 2026-01-01] [--limit 50]
149
+ advisoros meetings transcript <transcriptId>
150
+
151
+ advisoros newsletter list
152
+ advisoros newsletter create --subject "Weekly roundup" --body "<html>..." [--preview-text "..."]
153
+ advisoros newsletter edit <id> [--subject ...] [--body ...] [--preview-text ...] [--status draft|scheduled]
154
+ advisoros newsletter delete <id>
155
+
156
+ advisoros integrations status
157
+ advisoros integrations check <platform>
114
158
  ```
115
159
 
116
160
  > **Heads-up:** `inbox send`/`reply` are intentionally **not** in v1. Outbound goes through `campaigns launch` (sender resolution + char limits + per-sender daily budget). For one-off DMs, use the web inbox.
117
161
 
162
+ `meetings transcript` only returns full text once the transcript has been anonymised; otherwise metadata is returned with the body redacted.
163
+
164
+ `integrations status` is read-only and never returns credentials. `integrations check <platform>` runs a live health probe for the requested platform (e.g. `linkedin_tool`).
165
+
118
166
  ### Obsidian vault mirror (`kb`) — one-way
119
167
 
120
168
  ```bash
@@ -153,6 +201,35 @@ JOB=$(advisoros agents run lead-intelligence --input '{}' --json | jq -r '.run.j
153
201
  watch -n 5 "advisoros agents job $JOB --json | jq '{status,progress}'"
154
202
  ```
155
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
+
156
233
  ## Exit codes
157
234
 
158
235
  | Code | Meaning |
@@ -183,7 +260,7 @@ const { campaigns } = await call("campaigns", "list", { status: "active" }, {
183
260
  ```bash
184
261
  cd tools/cli
185
262
  npx changeset # describe the change
186
- npm run version # bump package.json + CHANGELOG.md
263
+ npm run version-packages # bump package.json + CHANGELOG.md
187
264
  git commit -am "chore(cli): release v$(node -p "require('./package.json').version")"
188
265
  git tag "cli-v$(node -p "require('./package.json').version")"
189
266
  git push --follow-tags
@@ -203,3 +280,23 @@ git push --follow-tags
203
280
 
204
281
  See [`docs/agent-api.md`](../../docs/agent-api.md) for the underlying `/agent-api` envelope.
205
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.0",
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": {
@@ -21,7 +21,7 @@
21
21
  "smoke": "node bin/advisoros.mjs --help >/dev/null && node bin/advisoros.mjs --version >/dev/null && node -e \"import('node:fs').then(({readFileSync})=>{const s=readFileSync('bin/advisoros.mjs','utf8');if(!s.startsWith('#!/usr/bin/env node')){console.error('Missing shebang in bin/advisoros.mjs');process.exit(1)}})\"",
22
22
  "prepublishOnly": "npm run smoke",
23
23
  "changeset": "changeset",
24
- "version": "changeset version",
24
+ "version-packages": "changeset version",
25
25
  "release": "npm run smoke && changeset publish --access public"
26
26
  },
27
27
  "dependencies": {
@@ -58,4 +58,3 @@
58
58
  "author": "AdvisorPro",
59
59
  "license": "UNLICENSED"
60
60
  }
61
-
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); }
@@ -246,6 +248,33 @@ export function registerCampaigns(program) {
246
248
  } catch (e) { fail(e); }
247
249
  });
248
250
 
251
+ withCommonOpts(c.command("update <campaignId>"))
252
+ .description("Edit an existing campaign")
253
+ .option("--name <name>", "rename campaign")
254
+ .option("--description <text>", "description")
255
+ .option("--goal <text>", "campaign goal")
256
+ .option("--sequence <id>", "attach a sequence")
257
+ .option("--senders <csv>", "comma-separated sender_account_ids")
258
+ .option("--daily-limit <n>", "daily send cap")
259
+ .option("--status <status>", "draft|active|paused|completed|archived")
260
+ .action(async function (campaignId, opts) {
261
+ try {
262
+ const payload = { id: campaignId };
263
+ if (opts.name !== undefined) payload.name = opts.name;
264
+ if (opts.description !== undefined) payload.description = opts.description;
265
+ if (opts.goal !== undefined) payload.goal = opts.goal;
266
+ if (opts.sequence !== undefined) payload.sequence_id = opts.sequence;
267
+ if (opts.senders !== undefined) {
268
+ payload.sender_accounts = opts.senders.split(",").map((s) => s.trim()).filter(Boolean);
269
+ }
270
+ if (opts.dailyLimit !== undefined) payload.daily_send_limit = Number(opts.dailyLimit);
271
+ if (opts.status !== undefined) payload.status = opts.status;
272
+ const r = await call("campaigns", "update", payload, ctxFrom(this));
273
+ emit(this, r, (d) => process.stdout.write(`✓ Updated campaign ${d.campaign?.id}\n`));
274
+ } catch (e) { fail(e); }
275
+ });
276
+
277
+
249
278
  withCommonOpts(c.command("add-leads <campaignId>"))
250
279
  .description("Attach leads to a campaign")
251
280
  .option("--lead-ids <csv>", "comma-separated lead UUIDs")
@@ -264,11 +293,56 @@ export function registerCampaigns(program) {
264
293
  } catch (e) { fail(e); }
265
294
  });
266
295
 
267
- withCommonOpts(c.command("launch <id>"))
268
- .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)")
269
319
  .action(async function (id) {
270
320
  try {
271
- 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
+ );
272
346
  emit(this, r, (d) => process.stdout.write(`✓ ${d.campaign?.name ?? id} → ${d.campaign?.status}\n`));
273
347
  } catch (e) { fail(e); }
274
348
  });
@@ -284,14 +358,84 @@ export function registerCampaigns(program) {
284
358
 
285
359
  withCommonOpts(c.command("resume <id>"))
286
360
  .description("Resume a paused campaign")
287
- .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) {
288
364
  try {
289
- 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));
290
373
  emit(this, r, (d) => process.stdout.write(`✓ ${d.campaign?.name ?? id} → ${d.campaign?.status}\n`));
291
374
  } catch (e) { fail(e); }
292
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
+ });
293
423
  }
294
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`);
436
+ }
437
+
438
+
295
439
  export function registerSequences(program) {
296
440
  const s = program.command("sequences").description("Manage LinkedIn sequences");
297
441
 
@@ -373,15 +517,17 @@ export function registerSequences(program) {
373
517
  .description("Append a step to a sequence")
374
518
  .requiredOption(
375
519
  "--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",
520
+ "viewProfile|addReaction|sendInvitation|sendMessage|loopBack|createOpportunity|enrichLead|ifAccepted|ifReplied|ifConnection|ifNetworkDistance|stopOnReply|wait|wait_3h|wait_1d|wait_2d|wait_3d|wait_5d",
377
521
  )
378
- .option("--text <message>", "message_content (required for addComment/sendInvitation/sendMessage)")
522
+ .option("--text <message>", "message_content (required for sendInvitation/sendMessage)")
379
523
  .option("--delay <days>", "delay_days before this step", "0")
380
524
  .option("--variant <v>", "variant label", "A")
381
525
  .option("--step-type <t>", "action|condition|delay (defaults from --action)")
382
526
  .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)")
527
+ .option("--branch <label>", "branch_label: yes|no|first|second_or_third|follower|out_of_network (children of a condition)")
528
+ .option("--parent-step <id>", "parent_step_id (defaults to the tail of the sequence)")
529
+ .option("--root", "start a new root step instead of appending to the tail (rarely correct)")
530
+
385
531
  .option("--order <n>", "explicit step_order (defaults to max+1)")
386
532
  .action(async function (id, opts) {
387
533
  try {
@@ -395,15 +541,49 @@ export function registerSequences(program) {
395
541
  channel: opts.channel,
396
542
  branch_label: opts.branch,
397
543
  parent_step_id: opts.parentStep,
544
+ root: opts.root === true,
398
545
  step_order: opts.order ? Number(opts.order) : undefined,
399
546
  }, ctxFrom(this));
400
547
  emit(this, r, (d) => {
401
548
  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`);
549
+ process.stdout.write(`✓ Added step ${st.id}\n #${st.step_order} ${st.action} (${st.channel}) delay=${st.delay_days}d variant=${st.variant}\n parent=${st.parent_step_id || "root"}${st.branch_label ? ` branch=${st.branch_label}` : ""}\n`);
550
+ for (const w of d.graph?.warnings || []) process.stdout.write(` ⚠ ${w.message}\n`);
403
551
  });
404
552
  } catch (e) { fail(e); }
553
+
405
554
  });
406
555
 
556
+ withCommonOpts(s.command("replace-steps <id>"))
557
+ .description("Replace every step of a sequence with a validated tree from a JSON file (atomic)")
558
+ .requiredOption("--file <path>", "JSON file: [{ temp_id, parent, branch_label, action, message_content, delay_days }]")
559
+ .option("--dry-run", "validate the graph without writing")
560
+ .action(async function (id, opts) {
561
+ try {
562
+ const raw = await readFile(opts.file, "utf8");
563
+ const parsed = JSON.parse(raw);
564
+ const steps = Array.isArray(parsed) ? parsed : parsed.steps;
565
+ if (!Array.isArray(steps)) throw new Error("file must contain an array of steps or { steps: [...] }");
566
+ const r = await call("sequences", "replace_steps", {
567
+ sequence_id: id,
568
+ steps,
569
+ dry_run: opts.dryRun === true,
570
+ }, ctxFrom(this));
571
+ emit(this, r, (d) => {
572
+ if (d.dry_run) {
573
+ process.stdout.write(`✓ Graph valid — ${d.planned_steps} step(s), nothing written\n`);
574
+ return;
575
+ }
576
+ process.stdout.write(`✓ Replaced steps on ${d.sequence_id} — ${d.replaced_steps} step(s)\n`);
577
+ for (const st of d.steps || []) {
578
+ process.stdout.write(` #${st.step_order} ${st.action}${st.branch_label ? ` [${st.branch_label}]` : ""} parent=${st.parent_step_id || "root"}\n`);
579
+ }
580
+ for (const w of d.graph?.warnings || []) process.stdout.write(` ⚠ ${w.message}\n`);
581
+ });
582
+ } catch (e) { fail(e); }
583
+ });
584
+
585
+
586
+
407
587
  withCommonOpts(s.command("clone <id>"))
408
588
  .description("Duplicate a sequence (and all its steps) as a new draft")
409
589
  .option("--name <name>", "name for the clone (default: '<src> (copy)')")
@@ -438,8 +618,68 @@ export function registerSequences(program) {
438
618
  });
439
619
  } catch (e) { fail(e); }
440
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
+ });
441
680
  }
442
681
 
682
+
443
683
  function parseSnoozeDuration(s) {
444
684
  // Accepts e.g. "30m", "2h", "1d", "45" (minutes). Returns minutes (int).
445
685
  if (!s) return null;
@@ -837,8 +1077,10 @@ export function registerContent(program) {
837
1077
 
838
1078
  withCommonOpts(c.command("delete <id>"))
839
1079
  .description("Delete a content item")
1080
+ .option("-y, --yes", "skip the confirmation prompt")
840
1081
  .action(async function (id) {
841
1082
  try {
1083
+ await confirmDestructive(this, `Delete content item ${id}. This cannot be undone.`);
842
1084
  const r = await call("content", "delete", { id }, ctxFrom(this));
843
1085
  emit(this, r, (d) => process.stdout.write(`✓ Deleted ${d.deleted}\n`));
844
1086
  } catch (e) { fail(e); }
@@ -980,8 +1222,10 @@ export function registerCalendar(program) {
980
1222
 
981
1223
  withCommonOpts(cal.command("delete <id>"))
982
1224
  .description("Delete a calendar post")
1225
+ .option("-y, --yes", "skip the confirmation prompt")
983
1226
  .action(async function (id) {
984
1227
  try {
1228
+ await confirmDestructive(this, `Delete calendar post ${id}. This cannot be undone.`);
985
1229
  const r = await call("calendar", "delete", { id }, ctxFrom(this));
986
1230
  emit(this, r, (d) => process.stdout.write(`✓ Deleted ${d.deleted}\n`));
987
1231
  } catch (e) { fail(e); }
@@ -1089,8 +1333,10 @@ export function registerSignals(program) {
1089
1333
 
1090
1334
  withCommonOpts(s.command("delete <id>"))
1091
1335
  .description("Delete a signal")
1336
+ .option("-y, --yes", "skip the confirmation prompt")
1092
1337
  .action(async function (id) {
1093
1338
  try {
1339
+ await confirmDestructive(this, `Delete signal ${id}. This cannot be undone.`);
1094
1340
  const r = await call("signals", "delete", { id }, ctxFrom(this));
1095
1341
  emit(this, r, (d) => process.stdout.write(`✓ Deleted ${d.deleted}\n`));
1096
1342
  } catch (e) { fail(e); }
@@ -1220,8 +1466,10 @@ export function registerPipelines(program) {
1220
1466
 
1221
1467
  withCommonOpts(p.command("delete <id>"))
1222
1468
  .description("Delete an opportunity")
1469
+ .option("-y, --yes", "skip the confirmation prompt")
1223
1470
  .action(async function (id) {
1224
1471
  try {
1472
+ await confirmDestructive(this, `Delete pipeline record ${id}. This cannot be undone.`);
1225
1473
  const r = await call("pipelines", "delete", { id }, ctxFrom(this));
1226
1474
  emit(this, r, (d) => process.stdout.write(`✓ Deleted ${d.deleted}\n`));
1227
1475
  } catch (e) { fail(e); }
@@ -1409,8 +1657,10 @@ export function registerAgents(program) {
1409
1657
 
1410
1658
  withCommonOpts(a.command("delete <id>"))
1411
1659
  .description("Delete an agent")
1660
+ .option("-y, --yes", "skip the confirmation prompt")
1412
1661
  .action(async function (id) {
1413
1662
  try {
1663
+ await confirmDestructive(this, `Delete agent ${id}. This cannot be undone.`);
1414
1664
  const r = await call("agents", "delete", { id }, ctxFrom(this));
1415
1665
  emit(this, r, (d) => process.stdout.write(`✓ Deleted ${d.deleted}\n`));
1416
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
+ }