@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 +4 -0
- package/README.md +99 -2
- package/package.json +2 -3
- package/src/api.mjs +5 -1
- package/src/commands/auth.mjs +44 -7
- package/src/commands/resources.mjs +261 -11
- package/src/index.mjs +3 -2
- package/src/ui.mjs +47 -1
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
|
|
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.
|
|
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 =
|
|
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); }
|
|
@@ -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("
|
|
268
|
-
.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)")
|
|
269
319
|
.action(async function (id) {
|
|
270
320
|
try {
|
|
271
|
-
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
|
+
);
|
|
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
|
-
.
|
|
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
|
-
|
|
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|
|
|
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
|
|
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 (
|
|
384
|
-
.option("--parent-step <id>", "parent_step_id (
|
|
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.
|
|
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
|
+
}
|