@series-a/cli 0.7.1 → 0.15.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.
@@ -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
- .action(async function (id, ts) {
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", "schedule", { id, scheduled_date: ts }, ctxFrom(this));
1056
- emit(this, r, (d) => process.stdout.write(`✓ Scheduled ${d.content?.id} → ${d.content?.scheduled_date}\n`));
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("Dispatch publication via agent-content-posting")
1071
- .action(async function (id) {
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
- const r = await call("content", "publish", { id }, ctxFrom(this));
1074
- emit(this, r, (d) => process.stdout.write(JSON.stringify(d.publish, null, 2) + "\n"));
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
+ }