@series-a/cli 0.7.1 → 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/package.json +1 -1
- package/src/api.mjs +21 -0
- package/src/commands/auth.mjs +3 -2
- package/src/commands/insights.mjs +194 -2
- package/src/commands/resources.mjs +153 -10
- package/src/commands/strategies.mjs +242 -0
- package/src/commands/voice.mjs +202 -0
- package/src/index.mjs +9 -2
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/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);
|
|
@@ -32,6 +42,11 @@ export async function call(resource, action, params = {}, opts = {}) {
|
|
|
32
42
|
stored?.agentName ||
|
|
33
43
|
"advisoros-cli";
|
|
34
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;
|
|
49
|
+
|
|
35
50
|
if (!apiKey) {
|
|
36
51
|
throw new AdvisorOSApiError(
|
|
37
52
|
"No API key configured. Run `advisoros login` or set ADVISOROS_API_KEY.",
|
|
@@ -59,6 +74,12 @@ export async function call(resource, action, params = {}, opts = {}) {
|
|
|
59
74
|
headers: {
|
|
60
75
|
"Content-Type": "application/json",
|
|
61
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 } : {}),
|
|
62
83
|
},
|
|
63
84
|
body: JSON.stringify(body),
|
|
64
85
|
});
|
package/src/commands/auth.mjs
CHANGED
|
@@ -14,6 +14,7 @@ export function registerAuth(program) {
|
|
|
14
14
|
.option("--admin", "admin key login — verify cross-workspace, no workspace required", false)
|
|
15
15
|
.option("--base-url <url>", "agent-api base URL", DEFAULTS.baseUrl)
|
|
16
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")
|
|
17
18
|
.action(async (opts) => {
|
|
18
19
|
try {
|
|
19
20
|
let { apiKey, workspace, baseUrl, profile } = opts;
|
|
@@ -49,7 +50,7 @@ export function registerAuth(program) {
|
|
|
49
50
|
apiKey, baseUrl, agentName,
|
|
50
51
|
});
|
|
51
52
|
const count = verify?.pagination?.total ?? verify?.workspaces?.length ?? 0;
|
|
52
|
-
await setProfile(profile, { apiKey, baseUrl, admin: true, agentName, ...(workspace ? { workspaceId: workspace } : {}) });
|
|
53
|
+
await setProfile(profile, { apiKey, baseUrl, admin: true, agentName, ...(opts.email ? { email: opts.email } : {}), ...(workspace ? { workspaceId: workspace } : {}) });
|
|
53
54
|
process.stdout.write(chalk.green(
|
|
54
55
|
`\n✓ Logged in as admin profile "${profile}" — ${count} workspaces reachable\n` +
|
|
55
56
|
` Config: ${CONFIG_PATH}\n` +
|
|
@@ -63,7 +64,7 @@ export function registerAuth(program) {
|
|
|
63
64
|
apiKey, workspaceId: workspace, baseUrl, agentName,
|
|
64
65
|
});
|
|
65
66
|
const count = verify?.agents?.length ?? 0;
|
|
66
|
-
await setProfile(profile, { apiKey, workspaceId: workspace, baseUrl, agentName });
|
|
67
|
+
await setProfile(profile, { apiKey, workspaceId: workspace, baseUrl, agentName, ...(opts.email ? { email: opts.email } : {}) });
|
|
67
68
|
process.stdout.write(chalk.green(
|
|
68
69
|
`\n✓ Logged in as profile "${profile}" — workspace ${workspace} (${count} agents visible)\n` +
|
|
69
70
|
` Config: ${CONFIG_PATH}\n`,
|
|
@@ -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
|
}
|
|
@@ -1049,11 +1049,27 @@ export function registerContent(program) {
|
|
|
1049
1049
|
});
|
|
1050
1050
|
|
|
1051
1051
|
withCommonOpts(c.command("schedule <id> <isoTimestamp>"))
|
|
1052
|
-
.description("Schedule a content item")
|
|
1053
|
-
.
|
|
1052
|
+
.description("Schedule a content item (Taplio when connected, otherwise built-in cron)")
|
|
1053
|
+
.option("--channel <c>", "linkedin | blog | newsletter")
|
|
1054
|
+
.option("--publish-as <p>", "personal | company")
|
|
1055
|
+
.option("--company-page <id>", "company page id when publishing as company")
|
|
1056
|
+
.option("--timezone <tz>", "IANA timezone label to store")
|
|
1057
|
+
.action(async function (id, ts, opts) {
|
|
1058
|
+
try {
|
|
1059
|
+
const r = await call("content", "schedule", {
|
|
1060
|
+
id, scheduled_for: ts, channel: opts.channel,
|
|
1061
|
+
publish_as: opts.publishAs, company_page_id: opts.companyPage, timezone: opts.timezone,
|
|
1062
|
+
}, ctxFrom(this));
|
|
1063
|
+
emit(this, r, (d) => process.stdout.write(`✓ Scheduled via ${d.engine} for ${d.scheduled_for}\n`));
|
|
1064
|
+
} catch (e) { fail(e); }
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
withCommonOpts(c.command("unschedule <id>"))
|
|
1068
|
+
.description("Cancel a scheduled content item on whichever engine holds it")
|
|
1069
|
+
.action(async function (id) {
|
|
1054
1070
|
try {
|
|
1055
|
-
const r = await call("content", "
|
|
1056
|
-
emit(this, r, (d) => process.stdout.write(`✓
|
|
1071
|
+
const r = await call("content", "unschedule", { id }, ctxFrom(this));
|
|
1072
|
+
emit(this, r, (d) => process.stdout.write(`✓ Unscheduled ${d.unscheduled ?? id}${d.engine ? ` (${d.engine})` : ""}\n`));
|
|
1057
1073
|
} catch (e) { fail(e); }
|
|
1058
1074
|
});
|
|
1059
1075
|
|
|
@@ -1066,15 +1082,58 @@ export function registerContent(program) {
|
|
|
1066
1082
|
} catch (e) { fail(e); }
|
|
1067
1083
|
});
|
|
1068
1084
|
|
|
1085
|
+
withCommonOpts(c.command("bulk-approve [ids...]"))
|
|
1086
|
+
.description("Approve many content items (all pending when no ids given)")
|
|
1087
|
+
.option("--campaign <id>", "only pending items in this campaign")
|
|
1088
|
+
.option("-y, --yes", "skip the confirmation prompt")
|
|
1089
|
+
.action(async function (ids, opts) {
|
|
1090
|
+
try {
|
|
1091
|
+
if (!ids?.length) await confirmDestructive(this, "Approve every pending content item in this workspace.");
|
|
1092
|
+
const r = await call("content", "bulk_approve", {
|
|
1093
|
+
ids: ids?.length ? ids : undefined, campaign_id: opts.campaign,
|
|
1094
|
+
}, ctxFrom(this));
|
|
1095
|
+
emit(this, r, (d) => process.stdout.write(`✓ Approved ${d.approved} items\n`));
|
|
1096
|
+
} catch (e) { fail(e); }
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
withCommonOpts(c.command("bulk-schedule <ids...>"))
|
|
1100
|
+
.description("Schedule several content items on a cadence via the live engine")
|
|
1101
|
+
.option("--start <iso>", "first slot (default: in 1 hour)")
|
|
1102
|
+
.option("--interval-hours <n>", "hours between posts", "24")
|
|
1103
|
+
.option("--channel <c>", "linkedin | blog | newsletter")
|
|
1104
|
+
.option("--publish-as <p>", "personal | company")
|
|
1105
|
+
.action(async function (ids, opts) {
|
|
1106
|
+
try {
|
|
1107
|
+
const r = await call("content", "bulk_schedule", {
|
|
1108
|
+
ids, start: opts.start, interval_hours: Number(opts.intervalHours),
|
|
1109
|
+
channel: opts.channel, publish_as: opts.publishAs,
|
|
1110
|
+
}, ctxFrom(this));
|
|
1111
|
+
emit(this, r, (d) => {
|
|
1112
|
+
process.stdout.write(table(
|
|
1113
|
+
["id", "ok", "engine", "when / error"],
|
|
1114
|
+
(d.results || []).map((x) => [truncate(x.id, 8), x.ok ? "yes" : "no", x.engine ?? "—", x.scheduled_for ?? truncate(x.error ?? "", 50)]),
|
|
1115
|
+
) + `\n${d.scheduled} scheduled, ${d.failed} failed\n`);
|
|
1116
|
+
});
|
|
1117
|
+
} catch (e) { fail(e); }
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1069
1120
|
withCommonOpts(c.command("publish <id>"))
|
|
1070
|
-
.description("
|
|
1071
|
-
.
|
|
1121
|
+
.description("Publish a content item immediately")
|
|
1122
|
+
.option("--channel <c>", "linkedin | blog | newsletter")
|
|
1123
|
+
.option("--publish-as <p>", "personal | company")
|
|
1124
|
+
.option("--company-page <id>", "company page id when publishing as company")
|
|
1125
|
+
.option("-y, --yes", "skip the confirmation prompt")
|
|
1126
|
+
.action(async function (id, opts) {
|
|
1072
1127
|
try {
|
|
1073
|
-
|
|
1074
|
-
|
|
1128
|
+
await confirmDestructive(this, `Publish content item ${id} right now.`);
|
|
1129
|
+
const r = await call("content", "publish_now", {
|
|
1130
|
+
id, channel: opts.channel, publish_as: opts.publishAs, company_page_id: opts.companyPage,
|
|
1131
|
+
}, ctxFrom(this));
|
|
1132
|
+
emit(this, r, (d) => process.stdout.write(`✓ Published via ${d.worker}\n`));
|
|
1075
1133
|
} catch (e) { fail(e); }
|
|
1076
1134
|
});
|
|
1077
1135
|
|
|
1136
|
+
|
|
1078
1137
|
withCommonOpts(c.command("delete <id>"))
|
|
1079
1138
|
.description("Delete a content item")
|
|
1080
1139
|
.option("-y, --yes", "skip the confirmation prompt")
|
|
@@ -1181,11 +1240,13 @@ export function registerCalendar(program) {
|
|
|
1181
1240
|
.option("--cta <text>", "call to action")
|
|
1182
1241
|
.option("--campaign <id>", "campaign_id")
|
|
1183
1242
|
.option("--status <s>", "draft | scheduled | published", "draft")
|
|
1243
|
+
.option("--at <iso>", "ISO timestamp — schedules the post immediately via the live engine")
|
|
1184
1244
|
.action(async function (opts) {
|
|
1185
1245
|
try {
|
|
1186
1246
|
let content = opts.content;
|
|
1187
1247
|
if (opts.contentFile) content = await readFile(opts.contentFile, "utf8");
|
|
1188
1248
|
const r = await call("calendar", "create", {
|
|
1249
|
+
scheduled_for: opts.at,
|
|
1189
1250
|
title: opts.title, scheduled_date: opts.date, scheduled_time: opts.time,
|
|
1190
1251
|
platform: opts.platform, content: content ?? "",
|
|
1191
1252
|
hashtags: opts.hashtags ? opts.hashtags.split(",").map((s) => s.trim()) : undefined,
|
|
@@ -1221,17 +1282,99 @@ export function registerCalendar(program) {
|
|
|
1221
1282
|
});
|
|
1222
1283
|
|
|
1223
1284
|
withCommonOpts(cal.command("delete <id>"))
|
|
1224
|
-
.description("Delete a calendar post")
|
|
1285
|
+
.description("Delete a calendar post (cancels it on Taplio first when applicable)")
|
|
1225
1286
|
.option("-y, --yes", "skip the confirmation prompt")
|
|
1226
1287
|
.action(async function (id) {
|
|
1227
1288
|
try {
|
|
1228
1289
|
await confirmDestructive(this, `Delete calendar post ${id}. This cannot be undone.`);
|
|
1229
1290
|
const r = await call("calendar", "delete", { id }, ctxFrom(this));
|
|
1230
|
-
emit(this, r, (d) => process.stdout.write(`✓ Deleted ${d.deleted}\n`));
|
|
1291
|
+
emit(this, r, (d) => process.stdout.write(`✓ Deleted ${d.deleted ?? d.unscheduled ?? id}\n`));
|
|
1292
|
+
} catch (e) { fail(e); }
|
|
1293
|
+
});
|
|
1294
|
+
|
|
1295
|
+
withCommonOpts(cal.command("engine"))
|
|
1296
|
+
.description("Show which scheduler is live for this workspace (Taplio or built-in cron)")
|
|
1297
|
+
.option("--channel <c>", "linkedin | blog | newsletter", "linkedin")
|
|
1298
|
+
.action(async function (opts) {
|
|
1299
|
+
try {
|
|
1300
|
+
const r = await call("calendar", "engine", { channel: opts.channel }, ctxFrom(this));
|
|
1301
|
+
emit(this, r, (d) => {
|
|
1302
|
+
process.stdout.write(
|
|
1303
|
+
`Engine: ${d.engine}\n` +
|
|
1304
|
+
`Channel: ${d.channel}\n` +
|
|
1305
|
+
`Taplio connected: ${d.taplio_connected ? "yes" : "no"}\n` +
|
|
1306
|
+
`Dispatch worker: ${d.dispatch_worker}\n` +
|
|
1307
|
+
`Reason: ${d.reason}\n` +
|
|
1308
|
+
`Pending scheduled: ${d.scheduled_pending}\n` +
|
|
1309
|
+
(d.recent_failures?.length
|
|
1310
|
+
? `\nRecent failures:\n` + table(
|
|
1311
|
+
["id", "when", "attempts", "error"],
|
|
1312
|
+
d.recent_failures.map((f) => [truncate(f.id, 8), f.scheduled_for ?? "—", f.dispatch_attempts ?? 0, truncate(f.dispatch_error ?? "", 60)]),
|
|
1313
|
+
) + "\n"
|
|
1314
|
+
: ""),
|
|
1315
|
+
);
|
|
1316
|
+
});
|
|
1317
|
+
} catch (e) { fail(e); }
|
|
1318
|
+
});
|
|
1319
|
+
|
|
1320
|
+
withCommonOpts(cal.command("schedule <id>"))
|
|
1321
|
+
.description("Schedule (or reschedule) a calendar post via the live engine")
|
|
1322
|
+
.requiredOption("--at <iso>", "ISO timestamp, e.g. 2026-09-10T09:00:00Z")
|
|
1323
|
+
.option("--channel <c>", "linkedin | blog | newsletter")
|
|
1324
|
+
.option("--publish-as <p>", "personal | company")
|
|
1325
|
+
.option("--company-page <id>", "company page id when publishing as company")
|
|
1326
|
+
.option("--timezone <tz>", "IANA timezone label to store")
|
|
1327
|
+
.action(async function (id, opts) {
|
|
1328
|
+
try {
|
|
1329
|
+
const r = await call("calendar", "schedule", {
|
|
1330
|
+
id, scheduled_for: opts.at, channel: opts.channel,
|
|
1331
|
+
publish_as: opts.publishAs, company_page_id: opts.companyPage, timezone: opts.timezone,
|
|
1332
|
+
}, ctxFrom(this));
|
|
1333
|
+
emit(this, r, (d) => process.stdout.write(`✓ Scheduled via ${d.engine} for ${d.scheduled_for}\n`));
|
|
1334
|
+
} catch (e) { fail(e); }
|
|
1335
|
+
});
|
|
1336
|
+
|
|
1337
|
+
withCommonOpts(cal.command("unschedule <id>"))
|
|
1338
|
+
.description("Cancel a scheduled calendar post (cancels on Taplio when it owns it)")
|
|
1339
|
+
.action(async function (id) {
|
|
1340
|
+
try {
|
|
1341
|
+
const r = await call("calendar", "unschedule", { id }, ctxFrom(this));
|
|
1342
|
+
emit(this, r, (d) => process.stdout.write(`✓ Unscheduled ${d.unscheduled} (${d.engine})\n`));
|
|
1343
|
+
} catch (e) { fail(e); }
|
|
1344
|
+
});
|
|
1345
|
+
|
|
1346
|
+
withCommonOpts(cal.command("publish-now <id>"))
|
|
1347
|
+
.description("Publish a calendar post immediately")
|
|
1348
|
+
.option("-y, --yes", "skip the confirmation prompt")
|
|
1349
|
+
.action(async function (id) {
|
|
1350
|
+
try {
|
|
1351
|
+
await confirmDestructive(this, `Publish calendar post ${id} right now.`);
|
|
1352
|
+
const r = await call("calendar", "publish_now", { id }, ctxFrom(this));
|
|
1353
|
+
emit(this, r, (d) => process.stdout.write(`✓ Published via ${d.worker}\n`));
|
|
1354
|
+
} catch (e) { fail(e); }
|
|
1355
|
+
});
|
|
1356
|
+
|
|
1357
|
+
withCommonOpts(cal.command("bulk-schedule <ids...>"))
|
|
1358
|
+
.description("Schedule several calendar posts on a cadence")
|
|
1359
|
+
.option("--start <iso>", "first slot (default: in 1 hour)")
|
|
1360
|
+
.option("--interval-hours <n>", "hours between posts", "24")
|
|
1361
|
+
.option("--channel <c>", "linkedin | blog | newsletter")
|
|
1362
|
+
.action(async function (ids, opts) {
|
|
1363
|
+
try {
|
|
1364
|
+
const r = await call("calendar", "bulk_schedule", {
|
|
1365
|
+
ids, start: opts.start, interval_hours: Number(opts.intervalHours), channel: opts.channel,
|
|
1366
|
+
}, ctxFrom(this));
|
|
1367
|
+
emit(this, r, (d) => {
|
|
1368
|
+
process.stdout.write(table(
|
|
1369
|
+
["id", "ok", "engine", "when / error"],
|
|
1370
|
+
(d.results || []).map((x) => [truncate(x.id, 8), x.ok ? "yes" : "no", x.engine ?? "—", x.scheduled_for ?? truncate(x.error ?? "", 50)]),
|
|
1371
|
+
) + `\n${d.scheduled} scheduled, ${d.failed} failed\n`);
|
|
1372
|
+
});
|
|
1231
1373
|
} catch (e) { fail(e); }
|
|
1232
1374
|
});
|
|
1233
1375
|
}
|
|
1234
1376
|
|
|
1377
|
+
|
|
1235
1378
|
export function registerSignals(program) {
|
|
1236
1379
|
const s = program.command("signals").description("Read and manage market intelligence signals");
|
|
1237
1380
|
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { call } from "../api.mjs";
|
|
2
|
+
import { emit, fail, ctxFrom, withCommonOpts, table, truncate, confirmDestructive } from "../ui.mjs";
|
|
3
|
+
|
|
4
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
5
|
+
|
|
6
|
+
export function registerStrategies(program) {
|
|
7
|
+
const s = program
|
|
8
|
+
.command("strategy")
|
|
9
|
+
.alias("strategies")
|
|
10
|
+
.description("Build and manage AI content strategies (multi-week content arcs)");
|
|
11
|
+
|
|
12
|
+
withCommonOpts(s.command("ideate"))
|
|
13
|
+
.description("Propose 3 grounded strategy directions from KB + signals")
|
|
14
|
+
.option("--hint <text>", "steer the ideation (theme, audience, launch…)")
|
|
15
|
+
.option("--no-tone", "do not apply the workspace tone of voice")
|
|
16
|
+
.action(async function () {
|
|
17
|
+
try {
|
|
18
|
+
const o = this.optsWithGlobals();
|
|
19
|
+
const r = await call("strategies", "ideate", {
|
|
20
|
+
hint: o.hint, apply_tone_of_voice: o.tone !== false,
|
|
21
|
+
}, ctxFrom(this));
|
|
22
|
+
emit(this, r, (d) => {
|
|
23
|
+
(d.strategies || []).forEach((st, i) => {
|
|
24
|
+
process.stdout.write(
|
|
25
|
+
`\n${i + 1}. ${st.name}\n audience: ${st.audience}\n outcome: ${st.outcome}\n pillars: ${(st.pillars || []).join(", ")}\n ${st.narrative}\n`,
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
process.stdout.write("\n");
|
|
29
|
+
});
|
|
30
|
+
} catch (e) { fail(e); }
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
withCommonOpts(s.command("create"))
|
|
34
|
+
.description("Build a full strategy: plans the arc and queues every post for drafting")
|
|
35
|
+
.requiredOption("--name <name>", "strategy name")
|
|
36
|
+
.option("--narrative <text>", "the story arc")
|
|
37
|
+
.option("--audience <text>", "who it targets")
|
|
38
|
+
.option("--outcome <text>", "what it should achieve")
|
|
39
|
+
.option("--pillars <list>", "comma-separated content pillars")
|
|
40
|
+
.option("--months <n>", "duration in months", "1")
|
|
41
|
+
.option("--per-week <n>", "posts per week (1-5)", "3")
|
|
42
|
+
.option("--format <f>", "linkedin | blog | mixed", "linkedin")
|
|
43
|
+
.option("--no-tone", "do not apply the workspace tone of voice")
|
|
44
|
+
.option("--wait", "poll resume until every post is drafted")
|
|
45
|
+
.action(async function (opts) {
|
|
46
|
+
try {
|
|
47
|
+
const ctx = ctxFrom(this);
|
|
48
|
+
const r = await call("strategies", "create", {
|
|
49
|
+
strategy: {
|
|
50
|
+
name: opts.name, narrative: opts.narrative, audience: opts.audience,
|
|
51
|
+
outcome: opts.outcome,
|
|
52
|
+
pillars: opts.pillars ? String(opts.pillars).split(",").map((p) => p.trim()) : [],
|
|
53
|
+
},
|
|
54
|
+
duration_months: Number(opts.months),
|
|
55
|
+
posts_per_week: Number(opts.perWeek),
|
|
56
|
+
format_mix: opts.format,
|
|
57
|
+
apply_tone_of_voice: opts.tone !== false,
|
|
58
|
+
}, ctx);
|
|
59
|
+
const id = r?.data?.strategy?.id ?? r?.strategy?.id;
|
|
60
|
+
if (opts.wait && id) {
|
|
61
|
+
for (let i = 0; i < 90; i++) {
|
|
62
|
+
await sleep(8000);
|
|
63
|
+
const p = await call("strategies", "resume", { id }, ctx);
|
|
64
|
+
const d = p?.data ?? p;
|
|
65
|
+
process.stderr.write(` … ${d.progress?.done ?? 0}/${d.progress?.total ?? "?"} done\n`);
|
|
66
|
+
if (d.status === "ready" || d.status === "failed") break;
|
|
67
|
+
}
|
|
68
|
+
const final = await call("strategies", "get", { id, include_posts: false }, ctx);
|
|
69
|
+
return emit(this, final, (d) => process.stdout.write(`✓ ${d.strategy?.name} — ${d.strategy?.status}\n`));
|
|
70
|
+
}
|
|
71
|
+
emit(this, r, (d) => process.stdout.write(
|
|
72
|
+
`✓ Created strategy ${d.strategy?.id} (${d.total_posts ?? "?"} posts queued)\n Run: advisoros strategy resume ${d.strategy?.id}\n`,
|
|
73
|
+
));
|
|
74
|
+
} catch (e) { fail(e); }
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
withCommonOpts(s.command("list"))
|
|
78
|
+
.description("List content strategies")
|
|
79
|
+
.option("--status <s>", "planning | generating | ready | failed | archived")
|
|
80
|
+
.option("--search <q>", "search by name")
|
|
81
|
+
.option("--limit <n>", "max rows", "25")
|
|
82
|
+
.action(async function () {
|
|
83
|
+
try {
|
|
84
|
+
const o = this.optsWithGlobals();
|
|
85
|
+
const r = await call("strategies", "list", {
|
|
86
|
+
status: o.status, search: o.search, limit: Number(o.limit),
|
|
87
|
+
}, ctxFrom(this));
|
|
88
|
+
emit(this, r, (d) => {
|
|
89
|
+
process.stdout.write(table(
|
|
90
|
+
["id", "status", "posts", "wk", "name"],
|
|
91
|
+
(d.strategies || []).map((st) => [
|
|
92
|
+
truncate(st.id, 8), st.status, st.total_posts ?? "", st.posts_per_week ?? "",
|
|
93
|
+
truncate(st.name, 46),
|
|
94
|
+
]),
|
|
95
|
+
) + `\n${d.strategies?.length ?? 0} rows\n`);
|
|
96
|
+
});
|
|
97
|
+
} catch (e) { fail(e); }
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
withCommonOpts(s.command("show <id>"))
|
|
101
|
+
.description("Show a strategy with its planned posts")
|
|
102
|
+
.option("--no-posts", "omit the post list")
|
|
103
|
+
.action(async function (id, opts) {
|
|
104
|
+
try {
|
|
105
|
+
const r = await call("strategies", "get", { id, include_posts: opts.posts !== false }, ctxFrom(this));
|
|
106
|
+
emit(this, r, (d) => {
|
|
107
|
+
const st = d.strategy || {};
|
|
108
|
+
process.stdout.write(`${st.name} [${st.status}]\n${st.narrative ?? ""}\n\n`);
|
|
109
|
+
if (d.posts?.length) {
|
|
110
|
+
process.stdout.write(table(
|
|
111
|
+
["#", "wk", "status", "format", "date", "hook"],
|
|
112
|
+
d.posts.map((p) => [p.position, p.week_no, p.status, p.format, p.planned_date ?? "", truncate(p.hook, 44)]),
|
|
113
|
+
) + "\n");
|
|
114
|
+
}
|
|
115
|
+
if (d.progress) process.stdout.write(`\n${JSON.stringify(d.progress)}\n`);
|
|
116
|
+
});
|
|
117
|
+
} catch (e) { fail(e); }
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
withCommonOpts(s.command("resume <id>"))
|
|
121
|
+
.description("Push the drafting queue along (repeat until status is ready)")
|
|
122
|
+
.option("--wait", "keep polling until done")
|
|
123
|
+
.action(async function (id, opts) {
|
|
124
|
+
try {
|
|
125
|
+
const ctx = ctxFrom(this);
|
|
126
|
+
let r = await call("strategies", "resume", { id }, ctx);
|
|
127
|
+
if (opts.wait) {
|
|
128
|
+
for (let i = 0; i < 90; i++) {
|
|
129
|
+
const d = r?.data ?? r;
|
|
130
|
+
if (d.status === "ready" || d.status === "failed") break;
|
|
131
|
+
process.stderr.write(` … ${d.progress?.done ?? 0}/${d.progress?.total ?? "?"} done\n`);
|
|
132
|
+
await sleep(8000);
|
|
133
|
+
r = await call("strategies", "resume", { id }, ctx);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
emit(this, r, (d) => process.stdout.write(`${d.status} — ${JSON.stringify(d.progress)}\n`));
|
|
137
|
+
} catch (e) { fail(e); }
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
withCommonOpts(s.command("update <id>"))
|
|
141
|
+
.description("Patch a strategy")
|
|
142
|
+
.option("--name <n>")
|
|
143
|
+
.option("--narrative <text>")
|
|
144
|
+
.option("--audience <text>")
|
|
145
|
+
.option("--outcome <text>")
|
|
146
|
+
.option("--pillars <list>", "comma-separated")
|
|
147
|
+
.option("--status <s>", "planning | generating | ready | failed | archived")
|
|
148
|
+
.option("--campaign <id>", "link to a campaign")
|
|
149
|
+
.action(async function (id, opts) {
|
|
150
|
+
try {
|
|
151
|
+
const r = await call("strategies", "update", {
|
|
152
|
+
id, name: opts.name, narrative: opts.narrative, audience: opts.audience,
|
|
153
|
+
outcome: opts.outcome, status: opts.status, campaign_id: opts.campaign,
|
|
154
|
+
pillars: opts.pillars ? String(opts.pillars).split(",").map((p) => p.trim()) : undefined,
|
|
155
|
+
}, ctxFrom(this));
|
|
156
|
+
emit(this, r, (d) => process.stdout.write(`✓ Updated ${d.strategy?.id}\n`));
|
|
157
|
+
} catch (e) { fail(e); }
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
withCommonOpts(s.command("delete <id>"))
|
|
161
|
+
.description("Delete a strategy and its planned posts")
|
|
162
|
+
.option("-y, --yes", "skip the confirmation prompt")
|
|
163
|
+
.action(async function (id) {
|
|
164
|
+
try {
|
|
165
|
+
await confirmDestructive(this, `Delete strategy ${id} and every planned post. This cannot be undone.`);
|
|
166
|
+
const r = await call("strategies", "delete", { id }, ctxFrom(this));
|
|
167
|
+
emit(this, r, (d) => process.stdout.write(`✓ Deleted ${d.deleted}\n`));
|
|
168
|
+
} catch (e) { fail(e); }
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
withCommonOpts(s.command("posts <id>"))
|
|
172
|
+
.description("List the planned posts of a strategy")
|
|
173
|
+
.option("--status <s>", "queued | claimed | done | failed | skipped")
|
|
174
|
+
.option("--limit <n>", "max rows", "200")
|
|
175
|
+
.action(async function (id) {
|
|
176
|
+
try {
|
|
177
|
+
const o = this.optsWithGlobals();
|
|
178
|
+
const r = await call("strategies", "posts", { id, status: o.status, limit: Number(o.limit) }, ctxFrom(this));
|
|
179
|
+
emit(this, r, (d) => {
|
|
180
|
+
process.stdout.write(table(
|
|
181
|
+
["post_id", "#", "wk", "status", "format", "date", "hook"],
|
|
182
|
+
(d.posts || []).map((p) => [
|
|
183
|
+
truncate(p.id, 8), p.position, p.week_no, p.status, p.format,
|
|
184
|
+
p.planned_date ?? "", truncate(p.hook, 38),
|
|
185
|
+
]),
|
|
186
|
+
) + `\n${JSON.stringify(d.progress)}\n`);
|
|
187
|
+
});
|
|
188
|
+
} catch (e) { fail(e); }
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
withCommonOpts(s.command("post <postId>"))
|
|
192
|
+
.description("Show one planned post")
|
|
193
|
+
.action(async function (postId) {
|
|
194
|
+
try {
|
|
195
|
+
const r = await call("strategies", "get_post", { post_id: postId }, ctxFrom(this));
|
|
196
|
+
emit(this, r, (d) => process.stdout.write(JSON.stringify(d.post, null, 2) + "\n"));
|
|
197
|
+
} catch (e) { fail(e); }
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
withCommonOpts(s.command("update-post <postId>"))
|
|
201
|
+
.description("Patch a planned post before it is drafted")
|
|
202
|
+
.option("--pillar <p>")
|
|
203
|
+
.option("--hook <text>")
|
|
204
|
+
.option("--objective <text>")
|
|
205
|
+
.option("--title <text>", "title_seed")
|
|
206
|
+
.option("--brief <text>")
|
|
207
|
+
.option("--format <f>", "linkedin | blog")
|
|
208
|
+
.option("--date <YYYY-MM-DD>", "planned_date")
|
|
209
|
+
.option("--week <n>", "week_no")
|
|
210
|
+
.option("--status <s>", "queued | claimed | done | failed | skipped")
|
|
211
|
+
.action(async function (postId, opts) {
|
|
212
|
+
try {
|
|
213
|
+
const r = await call("strategies", "update_post", {
|
|
214
|
+
post_id: postId, pillar: opts.pillar, hook: opts.hook, objective: opts.objective,
|
|
215
|
+
title_seed: opts.title, brief: opts.brief, format: opts.format,
|
|
216
|
+
planned_date: opts.date, week_no: opts.week ? Number(opts.week) : undefined,
|
|
217
|
+
status: opts.status,
|
|
218
|
+
}, ctxFrom(this));
|
|
219
|
+
emit(this, r, (d) => process.stdout.write(`✓ Updated post ${d.post?.id}\n`));
|
|
220
|
+
} catch (e) { fail(e); }
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
withCommonOpts(s.command("retry-post <postId>"))
|
|
224
|
+
.description("Re-queue a failed post for drafting")
|
|
225
|
+
.action(async function (postId) {
|
|
226
|
+
try {
|
|
227
|
+
const r = await call("strategies", "retry_post", { post_id: postId }, ctxFrom(this));
|
|
228
|
+
emit(this, r, (d) => process.stdout.write(`✓ Re-queued ${d.post?.id}\n`));
|
|
229
|
+
} catch (e) { fail(e); }
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
withCommonOpts(s.command("delete-post <postId>"))
|
|
233
|
+
.description("Remove a planned post")
|
|
234
|
+
.option("-y, --yes", "skip the confirmation prompt")
|
|
235
|
+
.action(async function (postId) {
|
|
236
|
+
try {
|
|
237
|
+
await confirmDestructive(this, `Delete planned post ${postId}.`);
|
|
238
|
+
const r = await call("strategies", "delete_post", { post_id: postId }, ctxFrom(this));
|
|
239
|
+
emit(this, r, (d) => process.stdout.write(`✓ Deleted ${d.deleted}\n`));
|
|
240
|
+
} catch (e) { fail(e); }
|
|
241
|
+
});
|
|
242
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { call } from "../api.mjs";
|
|
2
|
+
import { emit, fail, ctxFrom, withCommonOpts, table, truncate, confirmDestructive } from "../ui.mjs";
|
|
3
|
+
|
|
4
|
+
export function registerVoice(program) {
|
|
5
|
+
const v = program
|
|
6
|
+
.command("voice")
|
|
7
|
+
.alias("tone")
|
|
8
|
+
.description("Read, write and delete the workspace Tone of Voice (directive, samples, scores)");
|
|
9
|
+
|
|
10
|
+
withCommonOpts(v.command("show"))
|
|
11
|
+
.alias("get")
|
|
12
|
+
.description("Show the workspace tone of voice configuration")
|
|
13
|
+
.action(async function () {
|
|
14
|
+
try {
|
|
15
|
+
const r = await call("voice", "get", {}, ctxFrom(this));
|
|
16
|
+
emit(this, r, (d) => {
|
|
17
|
+
const t = d.voice || {};
|
|
18
|
+
process.stdout.write(
|
|
19
|
+
`Directive: ${t.has_directive ? "set" : "(none)"}${t.source ? ` [source: ${t.source}]` : ""}\n` +
|
|
20
|
+
`Applied by default: ${t.apply_by_default ? "yes" : "no"}\n` +
|
|
21
|
+
`Writing samples: ${t.style_reference_count}${t.primary_style_reference_id ? ` (pinned: ${t.primary_style_reference_id})` : ""}\n` +
|
|
22
|
+
(t.synced_at ? `Last synced: ${t.synced_at}\n` : "") +
|
|
23
|
+
(t.directive ? `\n${t.directive}\n` : ""),
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
} catch (e) { fail(e); }
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
withCommonOpts(v.command("set"))
|
|
30
|
+
.description("Write the tone of voice directive and/or the apply-by-default toggle")
|
|
31
|
+
.option("--directive <text>", "the tone of voice directive")
|
|
32
|
+
.option("--source <text>", "where the directive came from", undefined)
|
|
33
|
+
.option("--apply-default <yes|no>", "apply tone of voice to new content by default")
|
|
34
|
+
.action(async function (opts) {
|
|
35
|
+
try {
|
|
36
|
+
const params = { directive: opts.directive, source: opts.source };
|
|
37
|
+
if (opts.applyDefault !== undefined) {
|
|
38
|
+
params.apply_by_default = /^(y|yes|true|1|on)$/i.test(String(opts.applyDefault));
|
|
39
|
+
}
|
|
40
|
+
const r = await call("voice", "set", params, ctxFrom(this));
|
|
41
|
+
emit(this, r, (d) => process.stdout.write(
|
|
42
|
+
`✓ Tone of voice updated (directive ${d.voice?.has_directive ? "set" : "empty"}, default ${d.voice?.apply_by_default ? "on" : "off"})\n`,
|
|
43
|
+
));
|
|
44
|
+
} catch (e) { fail(e); }
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
withCommonOpts(v.command("delete"))
|
|
48
|
+
.alias("clear")
|
|
49
|
+
.description("Delete the tone of voice directive")
|
|
50
|
+
.option("--with-references", "also unmark every writing sample")
|
|
51
|
+
.option("-y, --yes", "skip the confirmation prompt")
|
|
52
|
+
.action(async function (opts) {
|
|
53
|
+
try {
|
|
54
|
+
await confirmDestructive(this, "Delete this workspace's tone of voice directive.");
|
|
55
|
+
const r = await call("voice", "delete", { clear_references: !!opts.withReferences }, ctxFrom(this));
|
|
56
|
+
emit(this, r, () => process.stdout.write("✓ Tone of voice cleared\n"));
|
|
57
|
+
} catch (e) { fail(e); }
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
withCommonOpts(v.command("sync"))
|
|
61
|
+
.description("Re-pull the directive from Business Build")
|
|
62
|
+
.option("--email <email>", "identity to resolve the client by")
|
|
63
|
+
.action(async function (opts) {
|
|
64
|
+
try {
|
|
65
|
+
const r = await call("voice", "sync", { email: opts.email }, ctxFrom(this));
|
|
66
|
+
emit(this, r, (d) => process.stdout.write(
|
|
67
|
+
`${d.voice?.has_directive ? "✓ Directive synced" : "No directive found"}\n`,
|
|
68
|
+
));
|
|
69
|
+
} catch (e) { fail(e); }
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// ── writing samples (benchmark corpus) ──
|
|
73
|
+
withCommonOpts(v.command("references"))
|
|
74
|
+
.alias("samples")
|
|
75
|
+
.description("List the knowledge base items used as writing samples")
|
|
76
|
+
.option("--limit <n>", "max rows", "50")
|
|
77
|
+
.action(async function (opts) {
|
|
78
|
+
try {
|
|
79
|
+
const r = await call("voice", "references", { limit: Number(opts.limit) }, ctxFrom(this));
|
|
80
|
+
emit(this, r, (d) => process.stdout.write(
|
|
81
|
+
table(
|
|
82
|
+
["id", "primary", "name", "category"],
|
|
83
|
+
(d.references || []).map((x) => [truncate(x.id, 8), x.is_primary ? "★" : "", truncate(x.name, 44), x.category ?? ""]),
|
|
84
|
+
) + `\n${d.references?.length ?? 0} rows\n`,
|
|
85
|
+
));
|
|
86
|
+
} catch (e) { fail(e); }
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
withCommonOpts(v.command("add-reference <knowledgeItemId>"))
|
|
90
|
+
.description("Mark a knowledge base item as a writing sample")
|
|
91
|
+
.option("--primary", "also pin it as the benchmark sample")
|
|
92
|
+
.action(async function (id, opts) {
|
|
93
|
+
try {
|
|
94
|
+
const r = await call("voice", "add_reference", { knowledge_item_id: id, set_primary: !!opts.primary }, ctxFrom(this));
|
|
95
|
+
emit(this, r, () => process.stdout.write(`✓ ${id} is now a writing sample\n`));
|
|
96
|
+
} catch (e) { fail(e); }
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
withCommonOpts(v.command("remove-reference <knowledgeItemId>"))
|
|
100
|
+
.description("Unmark a writing sample")
|
|
101
|
+
.action(async function (id) {
|
|
102
|
+
try {
|
|
103
|
+
const r = await call("voice", "remove_reference", { knowledge_item_id: id }, ctxFrom(this));
|
|
104
|
+
emit(this, r, () => process.stdout.write(`✓ ${id} removed from the writing samples\n`));
|
|
105
|
+
} catch (e) { fail(e); }
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
withCommonOpts(v.command("set-primary [knowledgeItemId]"))
|
|
109
|
+
.description("Pin (or clear, when no id is given) the benchmark writing sample")
|
|
110
|
+
.action(async function (id) {
|
|
111
|
+
try {
|
|
112
|
+
const r = await call("voice", "set_primary", { knowledge_item_id: id ?? null }, ctxFrom(this));
|
|
113
|
+
emit(this, r, (d) => process.stdout.write(
|
|
114
|
+
d.primary_style_reference_id ? `✓ Pinned ${d.primary_style_reference_id}\n` : "✓ Benchmark unpinned\n",
|
|
115
|
+
));
|
|
116
|
+
} catch (e) { fail(e); }
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// ── derived card + scoring ──
|
|
120
|
+
withCommonOpts(v.command("card"))
|
|
121
|
+
.description("Show the measured voice card (house rules + stylometric targets)")
|
|
122
|
+
.option("--rebuild", "rebuild it from the writing samples")
|
|
123
|
+
.action(async function (opts) {
|
|
124
|
+
try {
|
|
125
|
+
const r = await call("voice", "card", { rebuild: !!opts.rebuild }, ctxFrom(this));
|
|
126
|
+
emit(this, r, (d) => {
|
|
127
|
+
if (!d.card) return process.stdout.write("No voice card — add a writing sample first.\n");
|
|
128
|
+
process.stdout.write(
|
|
129
|
+
`Built: ${d.card.built_at}\nReferences: ${(d.card.reference_titles || []).join(", ")}\n\nRules:\n` +
|
|
130
|
+
(d.card.rules || []).map((x) => ` - ${x}`).join("\n") +
|
|
131
|
+
`\n\nTargets:\n` + (d.targets || []).map((x) => ` - ${x}`).join("\n") + "\n",
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
} catch (e) { fail(e); }
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
withCommonOpts(v.command("score"))
|
|
138
|
+
.description("Score a piece of writing against the workspace voice")
|
|
139
|
+
.option("--text <text>", "text to score")
|
|
140
|
+
.option("--content <id>", "content item to score")
|
|
141
|
+
.option("--reference <id>", "knowledge item to score against (defaults to the pinned sample)")
|
|
142
|
+
.option("--no-persist", "do not stamp the score onto the content item")
|
|
143
|
+
.action(async function (opts) {
|
|
144
|
+
try {
|
|
145
|
+
const r = await call("voice", "score", {
|
|
146
|
+
text: opts.text, content_item_id: opts.content,
|
|
147
|
+
reference_item_id: opts.reference, persist: opts.persist !== false,
|
|
148
|
+
}, ctxFrom(this));
|
|
149
|
+
emit(this, r, (d) => process.stdout.write(
|
|
150
|
+
`Match: ${d.report?.overall_score ?? d.overall_score ?? "?"}%\n${d.report?.rationale ?? d.rationale ?? ""}\n`,
|
|
151
|
+
));
|
|
152
|
+
} catch (e) { fail(e); }
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
withCommonOpts(v.command("redraft"))
|
|
156
|
+
.description("Rewrite a piece in the author's voice (returns text, saves nothing)")
|
|
157
|
+
.option("--text <text>", "text to rewrite")
|
|
158
|
+
.option("--content <id>", "content item to rewrite")
|
|
159
|
+
.action(async function (opts) {
|
|
160
|
+
try {
|
|
161
|
+
const r = await call("voice", "redraft", { text: opts.text, content_item_id: opts.content }, ctxFrom(this));
|
|
162
|
+
emit(this, r, (d) => process.stdout.write(`${d.text ?? d.redraft ?? JSON.stringify(d)}\n`));
|
|
163
|
+
} catch (e) { fail(e); }
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// ── reports ──
|
|
167
|
+
withCommonOpts(v.command("reports"))
|
|
168
|
+
.description("List voice match reports")
|
|
169
|
+
.option("--content <id>", "only reports for this content item")
|
|
170
|
+
.option("--limit <n>", "max rows", "25")
|
|
171
|
+
.action(async function (opts) {
|
|
172
|
+
try {
|
|
173
|
+
const r = await call("voice", "reports", { content_item_id: opts.content, limit: Number(opts.limit) }, ctxFrom(this));
|
|
174
|
+
emit(this, r, (d) => process.stdout.write(
|
|
175
|
+
table(
|
|
176
|
+
["id", "score", "created", "subject"],
|
|
177
|
+
(d.reports || []).map((x) => [truncate(x.id, 8), x.overall_score, (x.created_at || "").slice(0, 16), truncate(x.subject_title, 40)]),
|
|
178
|
+
) + `\n${d.reports?.length ?? 0} rows\n`,
|
|
179
|
+
));
|
|
180
|
+
} catch (e) { fail(e); }
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
withCommonOpts(v.command("report <id>"))
|
|
184
|
+
.description("Show one voice match report")
|
|
185
|
+
.action(async function (id) {
|
|
186
|
+
try {
|
|
187
|
+
const r = await call("voice", "get_report", { report_id: id }, ctxFrom(this));
|
|
188
|
+
emit(this, r, (d) => process.stdout.write(JSON.stringify(d.report, null, 2) + "\n"));
|
|
189
|
+
} catch (e) { fail(e); }
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
withCommonOpts(v.command("delete-report <id>"))
|
|
193
|
+
.description("Delete a voice match report")
|
|
194
|
+
.option("-y, --yes", "skip the confirmation prompt")
|
|
195
|
+
.action(async function (id) {
|
|
196
|
+
try {
|
|
197
|
+
await confirmDestructive(this, `Delete voice match report ${id}. This cannot be undone.`);
|
|
198
|
+
const r = await call("voice", "delete_report", { report_id: id }, ctxFrom(this));
|
|
199
|
+
emit(this, r, (d) => process.stdout.write(`✓ Deleted ${d.deleted}\n`));
|
|
200
|
+
} catch (e) { fail(e); }
|
|
201
|
+
});
|
|
202
|
+
}
|
package/src/index.mjs
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { registerStrategies } from "./commands/strategies.mjs";
|
|
2
|
+
import { registerVoice } from "./commands/voice.mjs";
|
|
1
3
|
import { Command } from "commander";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
2
5
|
import { registerAuth } from "./commands/auth.mjs";
|
|
3
6
|
import {
|
|
4
7
|
registerLeads, registerCampaigns, registerSequences,
|
|
@@ -10,17 +13,18 @@ import {
|
|
|
10
13
|
registerCampaignAnalytics, registerAnalytics, registerSafety, registerCtas,
|
|
11
14
|
registerNotifications, registerAudiences, registerNewsletter, registerVideo,
|
|
12
15
|
registerWatchlist, registerMeetings, registerActivity, registerWorkspacesExtras,
|
|
13
|
-
registerExport, registerIntegrations,
|
|
16
|
+
registerExport, registerIntegrations, registerAudit,
|
|
14
17
|
} from "./commands/insights.mjs";
|
|
15
18
|
import { registerFlow } from "./commands/flow.mjs";
|
|
16
19
|
import { registerVaultSync } from "./commands/knowledge-sync.mjs";
|
|
17
20
|
|
|
18
21
|
const program = new Command();
|
|
22
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
19
23
|
|
|
20
24
|
program
|
|
21
25
|
.name("advisoros")
|
|
22
26
|
.description("AdvisorPro CLI — drive leads, campaigns, sequences and agents from your terminal.")
|
|
23
|
-
.version(
|
|
27
|
+
.version(pkg.version)
|
|
24
28
|
.option("--json", "machine-readable JSON output (applies to all subcommands)")
|
|
25
29
|
.option("--as-agent <name>", "caller identity sent as agent_name (audit label only)");
|
|
26
30
|
|
|
@@ -34,6 +38,8 @@ registerCalendar(program);
|
|
|
34
38
|
registerKnowledge(program);
|
|
35
39
|
registerSignals(program);
|
|
36
40
|
registerPipelines(program);
|
|
41
|
+
registerStrategies(program);
|
|
42
|
+
registerVoice(program);
|
|
37
43
|
registerAuthority(program);
|
|
38
44
|
registerBriefs(program);
|
|
39
45
|
registerAgents(program);
|
|
@@ -51,6 +57,7 @@ registerVideo(program);
|
|
|
51
57
|
registerWatchlist(program);
|
|
52
58
|
registerMeetings(program);
|
|
53
59
|
registerActivity(program);
|
|
60
|
+
registerAudit(program);
|
|
54
61
|
registerWorkspacesExtras(program);
|
|
55
62
|
registerExport(program);
|
|
56
63
|
|