@series-a/cli 0.6.2 → 0.13.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 +39 -0
- package/README.md +62 -0
- package/package.json +1 -1
- package/src/api.mjs +26 -1
- package/src/commands/auth.mjs +45 -7
- package/src/commands/insights.mjs +194 -2
- package/src/commands/resources.mjs +346 -16
- package/src/commands/strategies.mjs +242 -0
- package/src/commands/voice.mjs +202 -0
- package/src/index.mjs +11 -3
- package/src/ui.mjs +47 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,45 @@ 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.13.0 — Full lifecycle + scheduling engine
|
|
8
|
+
|
|
9
|
+
- Campaign lifecycle is complete from the terminal: `campaigns update` (name, goal, sequence, sender accounts, daily send limit — no longer create-time-only), `launch`, `pause`, `resume`, `archive`, `clone`, and `delete` (dry-run preview by default, `--force` to execute, blocked when the campaign has dispatched activity unless forced).
|
|
10
|
+
- Sequence editing: `sequences update-step`, `delete-step`, and `replace-steps` — steps are no longer append-only. Graph validation flags disconnected nodes and cycles before launch.
|
|
11
|
+
- Scheduling engine awareness: calendar/schedule commands report whether a workspace will publish via Taplio (when connected) or the built-in cron scheduler, so automation targets the right engine.
|
|
12
|
+
- Live counts after writes: campaign and sequence mutations re-read the record and return live stats, clearly separated from the stored snapshot which may lag.
|
|
13
|
+
- Clearer errors under load: the API now returns a typed 429 with retry-after guidance instead of empty lists during heavy automation; the CLI surfaces this plainly.
|
|
14
|
+
|
|
15
|
+
## 0.12.0 — Agent audit trail
|
|
16
|
+
|
|
17
|
+
- `advisoros audit list/show/stats` — query the durable per-workspace audit log of every CLI/MCP/Agent API action (actor, surface, action, outcome, duration, correlation ID).
|
|
18
|
+
- CLI requests now send actor/client/version headers so audit records attribute actions to the right user and CLI version.
|
|
19
|
+
- `login` persists the profile email for audit attribution.
|
|
20
|
+
|
|
21
|
+
## 0.11.0 — Video management
|
|
22
|
+
|
|
23
|
+
- `videos` resource group: `list`, `show`, `create`, `generate`, `update`, `publish`, `unpublish`, `refresh`, `delete` against video projects, with workspace scoping and destructive-action confirmation.
|
|
24
|
+
|
|
25
|
+
## 0.10.0 — Calendar & Command Centre write access
|
|
26
|
+
|
|
27
|
+
- Full content calendar read/write/delete/schedule via CLI, with bulk operations and engine-aware scheduling (Taplio when connected, built-in cron otherwise).
|
|
28
|
+
- Command Centre content management: update, approve, schedule, unschedule, and publish-now actions.
|
|
29
|
+
|
|
30
|
+
## 0.9.0 — Tone of Voice
|
|
31
|
+
|
|
32
|
+
- Full Tone of Voice support: workspace configuration, writing-sample references, benchmark pinning, voice cards, scoring/redrafting, and report CRUD.
|
|
33
|
+
|
|
34
|
+
## 0.8.0 — Content Strategy
|
|
35
|
+
|
|
36
|
+
- Full content strategy read/write: create, list, show, update, delete strategies; manage strategy posts; retry/resume generation.
|
|
37
|
+
|
|
38
|
+
## 0.7.0 — Hardening & admin keys
|
|
39
|
+
|
|
40
|
+
- Shared typed errors with machine-readable codes across all commands.
|
|
41
|
+
- Cursor pagination up to 1,000 items per page.
|
|
42
|
+
- Destructive actions require explicit confirmation (`--confirm` / `--force`).
|
|
43
|
+
- `login --admin` for admin API keys, `--as-agent` impersonation flag, and `whoami` scope reporting.
|
|
44
|
+
- Admin keys (`advos_admin_…`) authorize from key scope alone and work across all workspaces.
|
|
45
|
+
|
|
7
46
|
## 0.6.1 — Campaign editing
|
|
8
47
|
|
|
9
48
|
- `campaigns` — added `update`/`edit` (name, description, goal, sequence, sender accounts, daily send limit, launch stage, status). Mirrors the new `update_campaign` MCP tool.
|
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
|
@@ -1,5 +1,15 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
1
3
|
import { getProfile, DEFAULTS } from "./config.mjs";
|
|
2
4
|
|
|
5
|
+
const CLI_VERSION = (() => {
|
|
6
|
+
try {
|
|
7
|
+
return createRequire(import.meta.url)("../package.json").version;
|
|
8
|
+
} catch {
|
|
9
|
+
return "unknown";
|
|
10
|
+
}
|
|
11
|
+
})();
|
|
12
|
+
|
|
3
13
|
export class AdvisorOSApiError extends Error {
|
|
4
14
|
constructor(message, { status, body } = {}) {
|
|
5
15
|
super(message);
|
|
@@ -26,7 +36,16 @@ export async function call(resource, action, params = {}, opts = {}) {
|
|
|
26
36
|
process.env.ADVISOROS_BASE_URL ||
|
|
27
37
|
stored?.baseUrl ||
|
|
28
38
|
DEFAULTS.baseUrl;
|
|
29
|
-
const agentName =
|
|
39
|
+
const agentName =
|
|
40
|
+
opts.agentName ||
|
|
41
|
+
process.env.ADVISOROS_AGENT_NAME ||
|
|
42
|
+
stored?.agentName ||
|
|
43
|
+
"advisoros-cli";
|
|
44
|
+
|
|
45
|
+
const actorEmail =
|
|
46
|
+
opts.actorEmail || process.env.ADVISOROS_ACTOR_EMAIL || stored?.email || null;
|
|
47
|
+
const actorUserId =
|
|
48
|
+
opts.actorUserId || process.env.ADVISOROS_ACTOR_USER_ID || stored?.userId || null;
|
|
30
49
|
|
|
31
50
|
if (!apiKey) {
|
|
32
51
|
throw new AdvisorOSApiError(
|
|
@@ -55,6 +74,12 @@ export async function call(resource, action, params = {}, opts = {}) {
|
|
|
55
74
|
headers: {
|
|
56
75
|
"Content-Type": "application/json",
|
|
57
76
|
"x-service-account-key": apiKey,
|
|
77
|
+
"x-surface": "cli",
|
|
78
|
+
"x-client-name": "advisoros-cli",
|
|
79
|
+
"x-client-version": CLI_VERSION,
|
|
80
|
+
"x-correlation-id": opts.correlationId || randomUUID(),
|
|
81
|
+
...(actorUserId ? { "x-actor-user-id": actorUserId } : {}),
|
|
82
|
+
...(actorEmail ? { "x-actor-email": actorEmail } : {}),
|
|
58
83
|
},
|
|
59
84
|
body: JSON.stringify(body),
|
|
60
85
|
});
|
package/src/commands/auth.mjs
CHANGED
|
@@ -10,8 +10,11 @@ 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")
|
|
17
|
+
.option("--email <email>", "your email — recorded on the audit trail for every action")
|
|
15
18
|
.action(async (opts) => {
|
|
16
19
|
try {
|
|
17
20
|
let { apiKey, workspace, baseUrl, profile } = opts;
|
|
@@ -19,13 +22,16 @@ export function registerAuth(program) {
|
|
|
19
22
|
const r = await prompts({
|
|
20
23
|
type: "password",
|
|
21
24
|
name: "v",
|
|
22
|
-
message: "API key (from Settings → API Keys)",
|
|
25
|
+
message: "API key (from Settings → API Keys; advos_admin_… for admin)",
|
|
23
26
|
validate: (v) => v && v.length > 10 ? true : "Required",
|
|
24
27
|
});
|
|
25
28
|
apiKey = r.v;
|
|
26
29
|
}
|
|
27
30
|
if (!apiKey) return fail(new Error("Aborted"));
|
|
28
|
-
|
|
31
|
+
|
|
32
|
+
const isAdmin = opts.admin || apiKey.startsWith("advos_admin_");
|
|
33
|
+
|
|
34
|
+
if (!isAdmin && !workspace) {
|
|
29
35
|
const r = await prompts({
|
|
30
36
|
type: "text",
|
|
31
37
|
name: "v",
|
|
@@ -34,14 +40,31 @@ export function registerAuth(program) {
|
|
|
34
40
|
});
|
|
35
41
|
workspace = r.v;
|
|
36
42
|
}
|
|
37
|
-
if (!workspace) return fail(new Error("Aborted"));
|
|
43
|
+
if (!isAdmin && !workspace) return fail(new Error("Aborted"));
|
|
44
|
+
|
|
45
|
+
const agentName = opts.asAgent || "advisoros-cli";
|
|
46
|
+
|
|
47
|
+
if (isAdmin) {
|
|
48
|
+
// Verify the admin key cross-workspace; no workspace slug needed.
|
|
49
|
+
const verify = await call("workspaces", "list", { limit: 1000 }, {
|
|
50
|
+
apiKey, baseUrl, agentName,
|
|
51
|
+
});
|
|
52
|
+
const count = verify?.pagination?.total ?? verify?.workspaces?.length ?? 0;
|
|
53
|
+
await setProfile(profile, { apiKey, baseUrl, admin: true, agentName, ...(opts.email ? { email: opts.email } : {}), ...(workspace ? { workspaceId: workspace } : {}) });
|
|
54
|
+
process.stdout.write(chalk.green(
|
|
55
|
+
`\n✓ Logged in as admin profile "${profile}" — ${count} workspaces reachable\n` +
|
|
56
|
+
` Config: ${CONFIG_PATH}\n` +
|
|
57
|
+
` Tip: pass --workspace <slug> per command, or set ADVISOROS_WORKSPACE_ID.\n`,
|
|
58
|
+
));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
38
61
|
|
|
39
62
|
// Verify the credentials with a cheap round-trip.
|
|
40
63
|
const verify = await call("agents", "list", {}, {
|
|
41
|
-
apiKey, workspaceId: workspace, baseUrl,
|
|
64
|
+
apiKey, workspaceId: workspace, baseUrl, agentName,
|
|
42
65
|
});
|
|
43
66
|
const count = verify?.agents?.length ?? 0;
|
|
44
|
-
await setProfile(profile, { apiKey, workspaceId: workspace, baseUrl });
|
|
67
|
+
await setProfile(profile, { apiKey, workspaceId: workspace, baseUrl, agentName, ...(opts.email ? { email: opts.email } : {}) });
|
|
45
68
|
process.stdout.write(chalk.green(
|
|
46
69
|
`\n✓ Logged in as profile "${profile}" — workspace ${workspace} (${count} agents visible)\n` +
|
|
47
70
|
` Config: ${CONFIG_PATH}\n`,
|
|
@@ -58,9 +81,24 @@ export function registerAuth(program) {
|
|
|
58
81
|
const p = await getProfile(opts.profile);
|
|
59
82
|
if (!p) return fail(new Error(`Profile "${opts.profile}" not found. Run \`advisoros login\`.`));
|
|
60
83
|
const masked = p.apiKey ? p.apiKey.slice(0, 8) + "…" + p.apiKey.slice(-4) : "(none)";
|
|
84
|
+
const isAdmin = Boolean(p.admin) || String(p.apiKey || "").startsWith("advos_admin_");
|
|
85
|
+
|
|
86
|
+
let reachable = null;
|
|
87
|
+
if (isAdmin) {
|
|
88
|
+
try {
|
|
89
|
+
const r = await call("workspaces", "list", { limit: 1000 }, {
|
|
90
|
+
apiKey: p.apiKey, baseUrl: p.baseUrl, agentName: p.agentName,
|
|
91
|
+
});
|
|
92
|
+
reachable = r?.pagination?.total ?? r?.workspaces?.length ?? null;
|
|
93
|
+
} catch { reachable = null; }
|
|
94
|
+
}
|
|
95
|
+
|
|
61
96
|
process.stdout.write(
|
|
62
97
|
`profile: ${opts.profile}\n` +
|
|
63
|
-
`
|
|
98
|
+
`scope: ${isAdmin ? "admin (cross-workspace)" : "workspace"}\n` +
|
|
99
|
+
`workspace: ${p.workspaceId || "(none — pass --workspace per command)"}\n` +
|
|
100
|
+
(isAdmin ? `reachable: ${reachable == null ? "(could not verify)" : `${reachable} workspaces`}\n` : "") +
|
|
101
|
+
`agent: ${p.agentName || "advisoros-cli"}\n` +
|
|
64
102
|
`base url: ${p.baseUrl || DEFAULTS.baseUrl}\n` +
|
|
65
103
|
`api key: ${masked}\n`,
|
|
66
104
|
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFile, writeFile } 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
|
const n = (v) => (v == null ? "" : String(v));
|
|
6
6
|
|
|
@@ -782,6 +782,115 @@ export function registerVideo(program) {
|
|
|
782
782
|
emit(this, r, (d) => process.stdout.write(`${d.share_url ?? "not shared"}\n`));
|
|
783
783
|
} catch (e) { fail(e); }
|
|
784
784
|
});
|
|
785
|
+
|
|
786
|
+
withCommonOpts(v.command("avatars"))
|
|
787
|
+
.description("List the avatars curated for this workspace")
|
|
788
|
+
.action(async function () {
|
|
789
|
+
try {
|
|
790
|
+
const r = await call("video", "avatars", {}, ctxFrom(this));
|
|
791
|
+
emit(this, r, (d) => {
|
|
792
|
+
process.stdout.write(table(
|
|
793
|
+
["avatar_id", "name", "gender", "voice", "default"],
|
|
794
|
+
(d.avatars || []).map((x) => [
|
|
795
|
+
truncate(x.avatar_id, 20), truncate(x.avatar_name, 24), n(x.gender),
|
|
796
|
+
truncate(n(x.default_voice_id), 20), x.is_default ? "yes" : "",
|
|
797
|
+
]),
|
|
798
|
+
) + `\n${d.avatars?.length ?? 0} rows\n`);
|
|
799
|
+
if (d.note) process.stdout.write(`${d.note}\n`);
|
|
800
|
+
});
|
|
801
|
+
} catch (e) { fail(e); }
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
withCommonOpts(v.command("create"))
|
|
805
|
+
.description("Generate a new AI video (talking head or explainer)")
|
|
806
|
+
.option("--prompt <text>", "brief or script context")
|
|
807
|
+
.option("--source-url <url>", "generate an explainer from a web page")
|
|
808
|
+
.option("--content-item <id>", "generate from a content item")
|
|
809
|
+
.option("--kind <k>", "talking_head | explainer", "talking_head")
|
|
810
|
+
.option("--avatar <id>", "avatar_id (see `video avatars`)")
|
|
811
|
+
.option("--voice <id>", "voice_id")
|
|
812
|
+
.option("--style <s>", "professional | casual | educational | inspirational")
|
|
813
|
+
.option("--format <f>", "platform format (e.g. linkedin, shorts)")
|
|
814
|
+
.option("--duration <n>", "target duration in seconds")
|
|
815
|
+
.option("--script-mode <m>", "verbatim to use --prompt as the exact script")
|
|
816
|
+
.action(async function () {
|
|
817
|
+
try {
|
|
818
|
+
const o = this.optsWithGlobals();
|
|
819
|
+
if (!o.prompt && !o.sourceUrl && !o.contentItem) {
|
|
820
|
+
throw new Error("Provide --prompt, --source-url or --content-item");
|
|
821
|
+
}
|
|
822
|
+
const r = await call("video", "create", {
|
|
823
|
+
prompt: o.prompt,
|
|
824
|
+
source_url: o.sourceUrl,
|
|
825
|
+
content_item_id: o.contentItem,
|
|
826
|
+
video_kind: o.kind,
|
|
827
|
+
avatar_id: o.avatar,
|
|
828
|
+
voice_id: o.voice,
|
|
829
|
+
style: o.style,
|
|
830
|
+
video_format: o.format,
|
|
831
|
+
target_duration: o.duration ? Number(o.duration) : undefined,
|
|
832
|
+
script_mode: o.scriptMode,
|
|
833
|
+
}, ctxFrom(this));
|
|
834
|
+
emit(this, r, (d) => process.stdout.write(JSON.stringify(d.video ?? d, null, 2) + "\n"));
|
|
835
|
+
} catch (e) { fail(e); }
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
withCommonOpts(v.command("update <id>"))
|
|
839
|
+
.description("Update video metadata (title, episode label, CTA)")
|
|
840
|
+
.option("--title <t>")
|
|
841
|
+
.option("--episode <label>")
|
|
842
|
+
.option("--cta-type <t>", "book_call | visit_site | subscribe | custom | none")
|
|
843
|
+
.option("--cta-label <l>")
|
|
844
|
+
.option("--cta-url <u>")
|
|
845
|
+
.option("--summary <s>")
|
|
846
|
+
.action(async function (id) {
|
|
847
|
+
try {
|
|
848
|
+
const o = this.optsWithGlobals();
|
|
849
|
+
const r = await call("video", "update", {
|
|
850
|
+
id, title: o.title, episode_label: o.episode,
|
|
851
|
+
cta_type: o.ctaType, cta_label: o.ctaLabel, cta_url: o.ctaUrl, summary: o.summary,
|
|
852
|
+
}, ctxFrom(this));
|
|
853
|
+
emit(this, r, (d) => process.stdout.write(JSON.stringify(d.video, null, 2) + "\n"));
|
|
854
|
+
} catch (e) { fail(e); }
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
withCommonOpts(v.command("publish <id>"))
|
|
858
|
+
.description("Make the video publicly shareable")
|
|
859
|
+
.action(async function (id) {
|
|
860
|
+
try {
|
|
861
|
+
const r = await call("video", "publish", { id }, ctxFrom(this));
|
|
862
|
+
emit(this, r, (d) => process.stdout.write(`${d.share_url ?? "published"}\n`));
|
|
863
|
+
} catch (e) { fail(e); }
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
withCommonOpts(v.command("unpublish <id>"))
|
|
867
|
+
.description("Revoke the public share link")
|
|
868
|
+
.action(async function (id) {
|
|
869
|
+
try {
|
|
870
|
+
const r = await call("video", "unpublish", { id }, ctxFrom(this));
|
|
871
|
+
emit(this, r, () => process.stdout.write("unpublished\n"));
|
|
872
|
+
} catch (e) { fail(e); }
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
withCommonOpts(v.command("refresh <id>"))
|
|
876
|
+
.description("Re-check the render status with the provider")
|
|
877
|
+
.action(async function (id) {
|
|
878
|
+
try {
|
|
879
|
+
const r = await call("video", "refresh", { id }, ctxFrom(this));
|
|
880
|
+
emit(this, r, (d) => process.stdout.write(JSON.stringify(d.video ?? d, null, 2) + "\n"));
|
|
881
|
+
} catch (e) { fail(e); }
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
withCommonOpts(v.command("delete <id>"))
|
|
885
|
+
.description("Delete a video project")
|
|
886
|
+
.option("--yes", "skip confirmation")
|
|
887
|
+
.action(async function (id) {
|
|
888
|
+
try {
|
|
889
|
+
await confirmDestructive(this, `Delete video project ${id}. This cannot be undone.`);
|
|
890
|
+
const r = await call("video", "delete", { id }, ctxFrom(this));
|
|
891
|
+
emit(this, r, (d) => process.stdout.write(`deleted ${d.deleted}\n`));
|
|
892
|
+
} catch (e) { fail(e); }
|
|
893
|
+
});
|
|
785
894
|
}
|
|
786
895
|
|
|
787
896
|
export function registerWatchlist(program) {
|
|
@@ -950,6 +1059,64 @@ export function registerActivity(program) {
|
|
|
950
1059
|
});
|
|
951
1060
|
}
|
|
952
1061
|
|
|
1062
|
+
export function registerAudit(program) {
|
|
1063
|
+
const a = program.command("audit").description("Audit trail of CLI / agent / assistant actions");
|
|
1064
|
+
|
|
1065
|
+
withCommonOpts(a.command("list"))
|
|
1066
|
+
.description("List audited actions (newest first)")
|
|
1067
|
+
.option("--status <s>", "ok | denied | error")
|
|
1068
|
+
.option("--surface <s>", "cli | mcp | api | internal")
|
|
1069
|
+
.option("--resource <r>", "e.g. leads, campaigns, content")
|
|
1070
|
+
.option("--actor <email>", "filter by person")
|
|
1071
|
+
.option("--writes", "only actions that changed data")
|
|
1072
|
+
.option("--since <iso>", "ISO timestamp lower bound")
|
|
1073
|
+
.option("--until <iso>", "ISO timestamp upper bound")
|
|
1074
|
+
.option("--limit <n>", "max rows", "50")
|
|
1075
|
+
.action(async function () {
|
|
1076
|
+
try {
|
|
1077
|
+
const o = this.optsWithGlobals();
|
|
1078
|
+
const r = await call("audit", "list", {
|
|
1079
|
+
status: o.status, surface: o.surface, resource: o.resource,
|
|
1080
|
+
actor_email: o.actor, writes_only: !!o.writes,
|
|
1081
|
+
since: o.since, until: o.until, limit: Number(o.limit),
|
|
1082
|
+
}, ctxFrom(this));
|
|
1083
|
+
emit(this, r, (d) => {
|
|
1084
|
+
process.stdout.write(table(
|
|
1085
|
+
["when", "who", "from", "action", "status", "ms"],
|
|
1086
|
+
(d.entries || []).map((x) => [
|
|
1087
|
+
n(x.created_at).replace("T", " ").slice(0, 19),
|
|
1088
|
+
truncate(x.actor_email || x.agent_name || "-", 24),
|
|
1089
|
+
n(x.surface),
|
|
1090
|
+
truncate(`${x.resource}.${x.action}`, 28),
|
|
1091
|
+
n(x.status),
|
|
1092
|
+
n(x.duration_ms),
|
|
1093
|
+
]),
|
|
1094
|
+
) + `\n${d.entries?.length ?? 0} rows\n`);
|
|
1095
|
+
});
|
|
1096
|
+
} catch (e) { fail(e); }
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
withCommonOpts(a.command("show <id>"))
|
|
1100
|
+
.description("Full request and outcome for one audited action")
|
|
1101
|
+
.action(async function (id) {
|
|
1102
|
+
try {
|
|
1103
|
+
const r = await call("audit", "get", { id }, ctxFrom(this));
|
|
1104
|
+
emit(this, r, (d) => process.stdout.write(JSON.stringify(d.entry, null, 2) + "\n"));
|
|
1105
|
+
} catch (e) { fail(e); }
|
|
1106
|
+
});
|
|
1107
|
+
|
|
1108
|
+
withCommonOpts(a.command("stats"))
|
|
1109
|
+
.description("Rolled-up audit summary")
|
|
1110
|
+
.option("--since <iso>", "ISO timestamp lower bound")
|
|
1111
|
+
.action(async function () {
|
|
1112
|
+
try {
|
|
1113
|
+
const o = this.optsWithGlobals();
|
|
1114
|
+
const r = await call("audit", "stats", { since: o.since }, ctxFrom(this));
|
|
1115
|
+
emit(this, r, (d) => process.stdout.write(JSON.stringify(d, null, 2) + "\n"));
|
|
1116
|
+
} catch (e) { fail(e); }
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
|
|
953
1120
|
export function registerWorkspacesExtras(program) {
|
|
954
1121
|
const w = program.command("workspaces").description("Workspaces (admin keys only)");
|
|
955
1122
|
|
|
@@ -1024,11 +1191,36 @@ export function registerIntegrations(program) {
|
|
|
1024
1191
|
});
|
|
1025
1192
|
|
|
1026
1193
|
withCommonOpts(i.command("check <platform>"))
|
|
1027
|
-
.description("Health-check one integration")
|
|
1194
|
+
.description("Health-check one integration (e.g. linkedin_tool, resend)")
|
|
1028
1195
|
.action(async function (platform) {
|
|
1029
1196
|
try {
|
|
1030
1197
|
const r = await call("integrations", "check", { platform }, ctxFrom(this));
|
|
1031
1198
|
emit(this, r, (d) => process.stdout.write(JSON.stringify(d, null, 2) + "\n"));
|
|
1032
1199
|
} catch (e) { fail(e); }
|
|
1033
1200
|
});
|
|
1201
|
+
|
|
1202
|
+
withCommonOpts(i.command("health"))
|
|
1203
|
+
.description("Live verdict: is Resend configured and are LinkedIn seats connected?")
|
|
1204
|
+
.action(async function () {
|
|
1205
|
+
try {
|
|
1206
|
+
const r = await call("integrations", "health", {}, ctxFrom(this));
|
|
1207
|
+
emit(this, r, (d) => {
|
|
1208
|
+
const flag = d.healthy ? "✔ healthy" : "✖ issues: " + (d.issues || []).join(", ");
|
|
1209
|
+
process.stdout.write(`${flag}\n`);
|
|
1210
|
+
process.stdout.write(table(
|
|
1211
|
+
["integration", "state", "detail"],
|
|
1212
|
+
[
|
|
1213
|
+
["resend", d.resend?.state ?? "unknown",
|
|
1214
|
+
d.resend?.configured ? `from=${d.resend?.from_email ?? "?"}` : (d.resend?.detail ?? "not configured")],
|
|
1215
|
+
...(d.linkedin?.seats?.length
|
|
1216
|
+
? d.linkedin.seats.map((s) => [
|
|
1217
|
+
`linkedin:${truncate(s.display_name || s.account_id || "?", 22)}`,
|
|
1218
|
+
s.state, s.connection_status ?? "",
|
|
1219
|
+
])
|
|
1220
|
+
: [["linkedin", d.linkedin?.verdict ?? "unknown", "no seats"]]),
|
|
1221
|
+
],
|
|
1222
|
+
) + "\n");
|
|
1223
|
+
});
|
|
1224
|
+
} catch (e) { fail(e); }
|
|
1225
|
+
});
|
|
1034
1226
|
}
|