@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 +62 -0
- package/package.json +1 -1
- package/src/api.mjs +5 -1
- package/src/commands/auth.mjs +44 -7
- package/src/commands/resources.mjs +193 -6
- package/src/index.mjs +3 -2
- package/src/ui.mjs +47 -1
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
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 =
|
|
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(
|
package/src/commands/auth.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
`
|
|
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("
|
|
295
|
-
.description("
|
|
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", "
|
|
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
|
-
.
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|