@series-a/cli 0.24.0 → 1.1.0
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 +33 -148
- package/README.md +79 -260
- package/bin/setu.mjs +587 -0
- package/lib/api.mjs +84 -0
- package/lib/config.mjs +42 -0
- package/lib/ui.mjs +68 -0
- package/package.json +15 -50
- package/LICENSE +0 -7
- package/SKILLS.md +0 -325
- package/bin/advisoros.mjs +0 -2
- package/src/api.mjs +0 -102
- package/src/commands/auth.mjs +0 -193
- package/src/commands/brand.mjs +0 -439
- package/src/commands/flow.mjs +0 -175
- package/src/commands/insights.mjs +0 -1758
- package/src/commands/knowledge-sync.mjs +0 -519
- package/src/commands/resources.mjs +0 -2151
- package/src/commands/strategies.mjs +0 -242
- package/src/commands/voice.mjs +0 -212
- package/src/config.mjs +0 -82
- package/src/index.mjs +0 -76
- package/src/ui.mjs +0 -104
|
@@ -1,1758 +0,0 @@
|
|
|
1
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
-
import { call } from "../api.mjs";
|
|
3
|
-
import { emit, fail, ctxFrom, withCommonOpts, table, truncate, confirmDestructive } from "../ui.mjs";
|
|
4
|
-
|
|
5
|
-
const n = (v) => (v == null ? "" : String(v));
|
|
6
|
-
|
|
7
|
-
/** Minimal CSV reader: header row + quoted fields. */
|
|
8
|
-
function parseCsvLite(text) {
|
|
9
|
-
const lines = String(text).split(/\r?\n/).filter((l) => l.trim().length);
|
|
10
|
-
if (!lines.length) return [];
|
|
11
|
-
const split = (line) => {
|
|
12
|
-
const out = []; let cur = ""; let q = false;
|
|
13
|
-
for (let i = 0; i < line.length; i++) {
|
|
14
|
-
const c = line[i];
|
|
15
|
-
if (q) {
|
|
16
|
-
if (c === '"' && line[i + 1] === '"') { cur += '"'; i++; }
|
|
17
|
-
else if (c === '"') q = false;
|
|
18
|
-
else cur += c;
|
|
19
|
-
} else if (c === ",") { out.push(cur); cur = ""; }
|
|
20
|
-
else if (c === '"') q = true;
|
|
21
|
-
else cur += c;
|
|
22
|
-
}
|
|
23
|
-
out.push(cur);
|
|
24
|
-
return out.map((x) => x.trim());
|
|
25
|
-
};
|
|
26
|
-
const headers = split(lines[0]).map((h) => h.toLowerCase());
|
|
27
|
-
return lines.slice(1).map((line) => {
|
|
28
|
-
const cells = split(line);
|
|
29
|
-
const row = {};
|
|
30
|
-
headers.forEach((h, i) => { row[h] = cells[i] ?? ""; });
|
|
31
|
-
return row;
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
function findGroup(program, name) {
|
|
37
|
-
return program.commands.find((c) => c.name() === name);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function toCsv(rows) {
|
|
41
|
-
if (!rows.length) return "";
|
|
42
|
-
const headers = Array.from(
|
|
43
|
-
rows.reduce((set, r) => {
|
|
44
|
-
Object.keys(r).forEach((k) => set.add(k));
|
|
45
|
-
return set;
|
|
46
|
-
}, new Set()),
|
|
47
|
-
);
|
|
48
|
-
const esc = (v) => {
|
|
49
|
-
if (v == null) return "";
|
|
50
|
-
const s = typeof v === "object" ? JSON.stringify(v) : String(v);
|
|
51
|
-
return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
|
52
|
-
};
|
|
53
|
-
return [
|
|
54
|
-
headers.join(","),
|
|
55
|
-
...rows.map((r) => headers.map((h) => esc(r[h])).join(",")),
|
|
56
|
-
].join("\n") + "\n";
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** Campaign analytics subcommands, attached to the existing `campaigns` group. */
|
|
60
|
-
export function registerCampaignAnalytics(program) {
|
|
61
|
-
const c = findGroup(program, "campaigns");
|
|
62
|
-
if (!c) return;
|
|
63
|
-
|
|
64
|
-
withCommonOpts(c.command("stats <id>"))
|
|
65
|
-
.description("Campaign funnel: invites, accepts, messages, replies, meetings")
|
|
66
|
-
.option("--days <n>", "look-back window in days", "30")
|
|
67
|
-
.action(async function (id) {
|
|
68
|
-
try {
|
|
69
|
-
const o = this.optsWithGlobals();
|
|
70
|
-
const r = await call("campaigns", "stats", { id, days: Number(o.days) }, ctxFrom(this));
|
|
71
|
-
emit(this, r, (d) => {
|
|
72
|
-
process.stdout.write(
|
|
73
|
-
`\n${d.campaign?.name ?? id} (${d.campaign?.status ?? "?"}) — last ${o.days}d\n\n`,
|
|
74
|
-
);
|
|
75
|
-
process.stdout.write(table(
|
|
76
|
-
["metric", "value"],
|
|
77
|
-
[
|
|
78
|
-
["Leads enrolled", d.funnel.leads_enrolled],
|
|
79
|
-
["Invites sent", d.funnel.invites_sent],
|
|
80
|
-
["Accepted", `${d.funnel.accepts} (${d.rates.accept_rate}%)`],
|
|
81
|
-
["Messages sent", d.funnel.messages_sent],
|
|
82
|
-
["Replies", `${d.funnel.replies} (${d.rates.reply_rate}%)`],
|
|
83
|
-
["Positive replies", `${d.funnel.positive_replies} (${d.rates.positive_rate}%)`],
|
|
84
|
-
["Meetings booked", `${d.funnel.meetings_booked} (${d.rates.meeting_rate}%)`],
|
|
85
|
-
["Failures", d.funnel.failures],
|
|
86
|
-
],
|
|
87
|
-
) + "\n");
|
|
88
|
-
});
|
|
89
|
-
} catch (e) { fail(e); }
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
withCommonOpts(c.command("events <id>"))
|
|
93
|
-
.description("Campaign event log")
|
|
94
|
-
.option("--type <t>", "filter by event_type")
|
|
95
|
-
.option("--since <iso>", "only events after this timestamp")
|
|
96
|
-
.option("--limit <n>", "max rows", "100")
|
|
97
|
-
.action(async function (id) {
|
|
98
|
-
try {
|
|
99
|
-
const o = this.optsWithGlobals();
|
|
100
|
-
const r = await call(
|
|
101
|
-
"campaigns", "events",
|
|
102
|
-
{ id, type: o.type, since: o.since, limit: Number(o.limit) },
|
|
103
|
-
ctxFrom(this),
|
|
104
|
-
);
|
|
105
|
-
emit(this, r, (d) => {
|
|
106
|
-
process.stdout.write(table(
|
|
107
|
-
["when", "event", "step", "lead", "sender"],
|
|
108
|
-
(d.events || []).map((e) => [
|
|
109
|
-
n(e.created_at).replace("T", " ").slice(0, 19),
|
|
110
|
-
e.event_type, n(e.step_order), truncate(e.lead_id, 8), truncate(e.sender_account, 14),
|
|
111
|
-
]),
|
|
112
|
-
) + `\n${d.events?.length ?? 0} rows\n`);
|
|
113
|
-
});
|
|
114
|
-
} catch (e) { fail(e); }
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
withCommonOpts(c.command("health <id>"))
|
|
118
|
-
.description("Execution health: run state, stalls, retries, watchdog findings")
|
|
119
|
-
.action(async function (id) {
|
|
120
|
-
try {
|
|
121
|
-
const r = await call("campaigns", "health", { id }, ctxFrom(this));
|
|
122
|
-
emit(this, r, (d) => {
|
|
123
|
-
const h = d.health;
|
|
124
|
-
process.stdout.write(
|
|
125
|
-
h
|
|
126
|
-
? `\nStatus: ${h.health_status} ${h.reason ? `(${h.reason})` : ""}\n` +
|
|
127
|
-
`Last progress: ${n(h.last_progress_at) || "never"}\n` +
|
|
128
|
-
`Next attempt: ${n(h.next_attempt_at) || "—"}\n` +
|
|
129
|
-
`Retries: ${h.retry_count ?? 0}${h.last_error ? `\nLast error: ${h.last_error}` : ""}\n\n`
|
|
130
|
-
: "\nNo health record yet.\n\n",
|
|
131
|
-
);
|
|
132
|
-
process.stdout.write(table(
|
|
133
|
-
["run", "status", "step", "pause reason", "fails", "next attempt"],
|
|
134
|
-
(d.runs || []).map((r2) => [
|
|
135
|
-
truncate(r2.id, 8), r2.status, `${r2.current_step ?? 0}/${r2.total_steps ?? 0}`,
|
|
136
|
-
truncate(r2.pause_reason, 24), r2.consecutive_failure_count ?? 0,
|
|
137
|
-
n(r2.next_attempt_at).replace("T", " ").slice(0, 19),
|
|
138
|
-
]),
|
|
139
|
-
) + "\n");
|
|
140
|
-
if (d.watchdog_findings?.length) {
|
|
141
|
-
process.stdout.write("\nWatchdog findings:\n" + table(
|
|
142
|
-
["severity", "title", "seen", "resolved"],
|
|
143
|
-
d.watchdog_findings.map((f) => [
|
|
144
|
-
f.severity, truncate(f.title, 40), f.occurrences,
|
|
145
|
-
f.resolved_at ? "yes" : "no",
|
|
146
|
-
]),
|
|
147
|
-
) + "\n");
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
} catch (e) { fail(e); }
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
withCommonOpts(c.command("leads <id>"))
|
|
154
|
-
.description("Leads enrolled in a campaign with their current state")
|
|
155
|
-
.option("--status <s>", "filter by enrolment status")
|
|
156
|
-
.option("--limit <n>", "max rows", "100")
|
|
157
|
-
.action(async function (id) {
|
|
158
|
-
try {
|
|
159
|
-
const o = this.optsWithGlobals();
|
|
160
|
-
const r = await call(
|
|
161
|
-
"campaigns", "leads",
|
|
162
|
-
{ id, status: o.status, limit: Number(o.limit) },
|
|
163
|
-
ctxFrom(this),
|
|
164
|
-
);
|
|
165
|
-
emit(this, r, (d) => {
|
|
166
|
-
process.stdout.write(table(
|
|
167
|
-
["lead", "name", "company", "enrolment", "conversation"],
|
|
168
|
-
(d.leads || []).map((l) => [
|
|
169
|
-
truncate(l.lead_id, 8), truncate(l.lead?.name, 24), truncate(l.lead?.company, 22),
|
|
170
|
-
l.status, n(l.lead?.conversation_stage),
|
|
171
|
-
]),
|
|
172
|
-
) + `\n${d.leads?.length ?? 0} rows\n`);
|
|
173
|
-
});
|
|
174
|
-
} catch (e) { fail(e); }
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
export function registerAnalytics(program) {
|
|
179
|
-
const a = program.command("analytics").description("Workspace and campaign analytics");
|
|
180
|
-
|
|
181
|
-
withCommonOpts(a.command("overview"))
|
|
182
|
-
.description("Outreach, content and pipeline results for the workspace")
|
|
183
|
-
.option("--days <n>", "look-back window", "30")
|
|
184
|
-
.action(async function () {
|
|
185
|
-
try {
|
|
186
|
-
const o = this.optsWithGlobals();
|
|
187
|
-
const r = await call("analytics", "overview", { days: Number(o.days) }, ctxFrom(this));
|
|
188
|
-
emit(this, r, (d) => {
|
|
189
|
-
const m = d.outreach;
|
|
190
|
-
process.stdout.write(`\nLast ${d.period_days} days\n\n`);
|
|
191
|
-
process.stdout.write(table(["outreach", "value"], [
|
|
192
|
-
["Touches", m.touches], ["Invites sent", m.invites_sent],
|
|
193
|
-
["Accepts", `${m.accepts} (${m.rates.accept_rate}%)`],
|
|
194
|
-
["Messages sent", m.messages_sent],
|
|
195
|
-
["Replies", `${m.replies} (${m.rates.reply_rate}%)`],
|
|
196
|
-
["Positive replies", `${m.positive_replies} (${m.rates.positive_rate}%)`],
|
|
197
|
-
["Meetings booked", m.meetings_booked], ["Meetings held", m.meetings_held],
|
|
198
|
-
]) + "\n");
|
|
199
|
-
process.stdout.write(table(["content", "value"], [
|
|
200
|
-
["Posts distributed", d.content.posts_distributed],
|
|
201
|
-
["Impressions", d.content.impressions],
|
|
202
|
-
["Engagement", `${d.content.engagement} (${d.content.engagement_rate}%)`],
|
|
203
|
-
["Link clicks", d.content.link_clicks],
|
|
204
|
-
]) + "\n");
|
|
205
|
-
process.stdout.write(table(["pipeline", "value"], [
|
|
206
|
-
["Opportunities created", d.pipeline.opportunities_created],
|
|
207
|
-
["Pipeline value", d.pipeline.pipeline_value],
|
|
208
|
-
["Closed won", `${d.pipeline.closed_won_count} / ${d.pipeline.closed_won_value}`],
|
|
209
|
-
]) + "\n");
|
|
210
|
-
});
|
|
211
|
-
} catch (e) { fail(e); }
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
withCommonOpts(a.command("campaigns"))
|
|
215
|
-
.description("Compare every campaign on the same funnel")
|
|
216
|
-
.option("--days <n>", "look-back window", "30")
|
|
217
|
-
.option("--limit <n>", "max campaigns", "25")
|
|
218
|
-
.action(async function () {
|
|
219
|
-
try {
|
|
220
|
-
const o = this.optsWithGlobals();
|
|
221
|
-
const r = await call(
|
|
222
|
-
"analytics", "campaigns",
|
|
223
|
-
{ days: Number(o.days), limit: Number(o.limit) },
|
|
224
|
-
ctxFrom(this),
|
|
225
|
-
);
|
|
226
|
-
emit(this, r, (d) => {
|
|
227
|
-
process.stdout.write(table(
|
|
228
|
-
["campaign", "status", "leads", "invites", "accepts", "msgs", "replies", "pos", "mtgs"],
|
|
229
|
-
(d.campaigns || []).map((c) => [
|
|
230
|
-
truncate(c.name, 26), c.status, n(c.leads_enrolled), n(c.invites_sent),
|
|
231
|
-
n(c.accepts), n(c.messages_sent), n(c.replies), n(c.positive_replies),
|
|
232
|
-
n(c.meetings_booked),
|
|
233
|
-
]),
|
|
234
|
-
) + "\n");
|
|
235
|
-
});
|
|
236
|
-
} catch (e) { fail(e); }
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
withCommonOpts(a.command("content"))
|
|
240
|
-
.description("Per-post performance: impressions, engagement, clicks")
|
|
241
|
-
.option("--days <n>", "look-back window", "30")
|
|
242
|
-
.option("--limit <n>", "max rows", "50")
|
|
243
|
-
.action(async function () {
|
|
244
|
-
try {
|
|
245
|
-
const o = this.optsWithGlobals();
|
|
246
|
-
const r = await call(
|
|
247
|
-
"analytics", "content",
|
|
248
|
-
{ days: Number(o.days), limit: Number(o.limit) },
|
|
249
|
-
ctxFrom(this),
|
|
250
|
-
);
|
|
251
|
-
emit(this, r, (d) => {
|
|
252
|
-
process.stdout.write(table(
|
|
253
|
-
["title", "platform", "impressions", "eng", "eng %", "clicks"],
|
|
254
|
-
(d.posts || []).map((p) => [
|
|
255
|
-
truncate(p.title, 34), p.platform, n(p.impressions), n(p.engagement),
|
|
256
|
-
n(p.engagement_rate), n(p.reference_clicks),
|
|
257
|
-
]),
|
|
258
|
-
) + `\n${d.posts?.length ?? 0} rows\n`);
|
|
259
|
-
});
|
|
260
|
-
} catch (e) { fail(e); }
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
withCommonOpts(a.command("report"))
|
|
264
|
-
.description("Full branded performance report (summary, funnel, content, pipeline)")
|
|
265
|
-
.option("--days <n>", "look-back window", "30")
|
|
266
|
-
.option("--format <fmt>", "md | json", "md")
|
|
267
|
-
.option("--out <file>", "write the report to a file instead of stdout")
|
|
268
|
-
.action(async function () {
|
|
269
|
-
try {
|
|
270
|
-
const o = this.optsWithGlobals();
|
|
271
|
-
const r = await call("analytics", "report", { days: Number(o.days) }, ctxFrom(this));
|
|
272
|
-
if (o.json && !o.out) return emit(this, r, () => {});
|
|
273
|
-
const body = o.format === "json"
|
|
274
|
-
? JSON.stringify(r, null, 2)
|
|
275
|
-
: reportMarkdown(r);
|
|
276
|
-
if (o.out) {
|
|
277
|
-
await writeFile(o.out, body, "utf8");
|
|
278
|
-
process.stdout.write(`✔ Wrote report to ${o.out}\n`);
|
|
279
|
-
} else {
|
|
280
|
-
process.stdout.write(body + "\n");
|
|
281
|
-
}
|
|
282
|
-
} catch (e) { fail(e); }
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
withCommonOpts(a.command("export"))
|
|
286
|
-
.description("Export workspace analytics as CSV or JSON (overview | campaigns | content | leads | pipeline)")
|
|
287
|
-
.option("--dataset <name>", "overview | campaigns | content | leads | pipeline", "overview")
|
|
288
|
-
.option("--format <fmt>", "csv | json", "csv")
|
|
289
|
-
.option("--days <n>", "look-back window", "30")
|
|
290
|
-
.option("--limit <n>", "max rows for row-level datasets")
|
|
291
|
-
.option("--out <file>", "write to a file instead of stdout")
|
|
292
|
-
.action(async function () {
|
|
293
|
-
try {
|
|
294
|
-
const o = this.optsWithGlobals();
|
|
295
|
-
const r = await call("analytics", "export", {
|
|
296
|
-
dataset: o.dataset,
|
|
297
|
-
format: o.format,
|
|
298
|
-
days: Number(o.days),
|
|
299
|
-
...(o.limit ? { limit: Number(o.limit) } : {}),
|
|
300
|
-
}, ctxFrom(this));
|
|
301
|
-
const out = o.out === true ? r.filename : o.out;
|
|
302
|
-
if (out) {
|
|
303
|
-
await writeFile(out, r.content ?? "", "utf8");
|
|
304
|
-
process.stdout.write(`✔ Wrote ${r.row_count} ${r.dataset} row(s) to ${out}\n`);
|
|
305
|
-
} else {
|
|
306
|
-
process.stdout.write((r.content ?? "") + "\n");
|
|
307
|
-
}
|
|
308
|
-
} catch (e) { fail(e); }
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
function reportMarkdown(d) {
|
|
314
|
-
const m = d.outreach ?? {};
|
|
315
|
-
const c = d.content ?? {};
|
|
316
|
-
const p = d.pipeline ?? {};
|
|
317
|
-
const rows = (hdrs, data) => [
|
|
318
|
-
`| ${hdrs.join(" | ")} |`,
|
|
319
|
-
`| ${hdrs.map(() => "---").join(" | ")} |`,
|
|
320
|
-
...data.map((r) => `| ${r.map((v) => n(v)).join(" | ")} |`),
|
|
321
|
-
].join("\n");
|
|
322
|
-
|
|
323
|
-
return [
|
|
324
|
-
`# AdvisorPro — Performance Report`,
|
|
325
|
-
``,
|
|
326
|
-
`**Workspace:** ${d.workspace?.name ?? d.workspace?.id} `,
|
|
327
|
-
`**Period:** last ${d.period_days} days (since ${n(d.since).slice(0, 10)}) `,
|
|
328
|
-
`**Generated:** ${n(d.generated_at).slice(0, 16).replace("T", " ")} UTC`,
|
|
329
|
-
``,
|
|
330
|
-
`## Performance summary`,
|
|
331
|
-
``,
|
|
332
|
-
...(d.summary ?? []).map((s) => `- ${s}`),
|
|
333
|
-
``,
|
|
334
|
-
`## Outreach`,
|
|
335
|
-
``,
|
|
336
|
-
rows(["Metric", "Value"], [
|
|
337
|
-
["Touches", m.touches], ["Invites sent", m.invites_sent],
|
|
338
|
-
["Accepts", `${n(m.accepts)} (${n(m.rates?.accept_rate)}%)`],
|
|
339
|
-
["Messages sent", m.messages_sent],
|
|
340
|
-
["Replies", `${n(m.replies)} (${n(m.rates?.reply_rate)}%)`],
|
|
341
|
-
["Positive replies", `${n(m.positive_replies)} (${n(m.rates?.positive_rate)}%)`],
|
|
342
|
-
["Meetings booked", m.meetings_booked], ["Meetings held", m.meetings_held],
|
|
343
|
-
]),
|
|
344
|
-
``,
|
|
345
|
-
`## Campaigns`,
|
|
346
|
-
``,
|
|
347
|
-
rows(["Campaign", "Status", "Leads", "Invites", "Accepts", "Messages", "Replies", "Positive", "Meetings"],
|
|
348
|
-
(d.campaigns ?? []).map((x) => [
|
|
349
|
-
x.name, x.status, x.leads_enrolled, x.invites_sent, x.accepts,
|
|
350
|
-
x.messages_sent, x.replies, x.positive_replies, x.meetings_booked,
|
|
351
|
-
])),
|
|
352
|
-
``,
|
|
353
|
-
`## Content`,
|
|
354
|
-
``,
|
|
355
|
-
rows(["Metric", "Value"], [
|
|
356
|
-
["Posts distributed", c.posts_distributed], ["Impressions", c.impressions],
|
|
357
|
-
["Engagement", `${n(c.engagement)} (${n(c.engagement_rate)}%)`],
|
|
358
|
-
["Link clicks", c.link_clicks],
|
|
359
|
-
]),
|
|
360
|
-
``,
|
|
361
|
-
rows(["Top posts", "Platform", "Impressions", "Engagement", "Clicks"],
|
|
362
|
-
(d.top_content ?? []).map((x) => [
|
|
363
|
-
x.title ?? x.content_item_id, x.platform, x.impressions, x.engagement, x.reference_clicks,
|
|
364
|
-
])),
|
|
365
|
-
``,
|
|
366
|
-
`## Revenue impact`,
|
|
367
|
-
``,
|
|
368
|
-
rows(["Metric", "Value"], [
|
|
369
|
-
["Opportunities created", p.opportunities_created],
|
|
370
|
-
["Pipeline value", p.pipeline_value],
|
|
371
|
-
["Closed won", `${n(p.closed_won_count)} / ${n(p.closed_won_value)}`],
|
|
372
|
-
]),
|
|
373
|
-
``,
|
|
374
|
-
`---`,
|
|
375
|
-
`Generated by AdvisorPro CLI.`,
|
|
376
|
-
].join("\n");
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
export function registerSafety(program) {
|
|
381
|
-
const s = program.command("safety").description("LinkedIn safety guardrails (read-only)");
|
|
382
|
-
|
|
383
|
-
withCommonOpts(s.command("limits"))
|
|
384
|
-
.description("Per-seat caps, usage and headroom")
|
|
385
|
-
.action(async function () {
|
|
386
|
-
try {
|
|
387
|
-
const r = await call("safety", "limits", {}, ctxFrom(this));
|
|
388
|
-
emit(this, r, (d) => {
|
|
389
|
-
process.stdout.write(`\nGovernor: ${d.governor} · resets ${d.resets_at}\n\n`);
|
|
390
|
-
process.stdout.write(table(
|
|
391
|
-
["account", "cooldown", "invites today", "messages today", "resume at"],
|
|
392
|
-
(d.seats || []).map((x) => [
|
|
393
|
-
truncate(x.account_name || x.account_id, 26),
|
|
394
|
-
x.cooldown_active ? (x.cooldown_reason || "yes") : "clear",
|
|
395
|
-
x.sent_today.invites, x.sent_today.messages, n(x.resume_at),
|
|
396
|
-
]),
|
|
397
|
-
) + "\n");
|
|
398
|
-
});
|
|
399
|
-
} catch (e) { fail(e); }
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
withCommonOpts(s.command("reads"))
|
|
403
|
-
.description("Read budgets and the safe bulk-import size")
|
|
404
|
-
.option("--account <id>", "include a specific LinkedIn account")
|
|
405
|
-
.action(async function () {
|
|
406
|
-
try {
|
|
407
|
-
const o = this.optsWithGlobals();
|
|
408
|
-
const r = await call("safety", "reads", { account_id: o.account }, ctxFrom(this));
|
|
409
|
-
emit(this, r, (d) => {
|
|
410
|
-
const b = d.budgets;
|
|
411
|
-
const rows = [
|
|
412
|
-
["Workspace daily", b.workspace_daily.used, b.workspace_daily.limit, b.workspace_daily.remaining],
|
|
413
|
-
["Workspace bulk", b.workspace_bulk.used, b.workspace_bulk.limit, b.workspace_bulk.remaining],
|
|
414
|
-
["Shared pool (bulk)", b.shared_pool.used, b.shared_pool.limit, b.shared_pool.bulk_remaining],
|
|
415
|
-
];
|
|
416
|
-
if (b.account) {
|
|
417
|
-
rows.push(["Account reads", b.account.reads.used, b.account.reads.limit, b.account.reads.remaining]);
|
|
418
|
-
rows.push(["Account bulk", b.account.bulk.used, b.account.bulk.limit, b.account.bulk.remaining]);
|
|
419
|
-
}
|
|
420
|
-
process.stdout.write(table(["budget", "used", "limit", "remaining"], rows) + "\n");
|
|
421
|
-
process.stdout.write(`\nSafe bulk import size right now: ${d.safe_bulk_import_size} leads\nResets ${d.resets_at}\n`);
|
|
422
|
-
});
|
|
423
|
-
} catch (e) { fail(e); }
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
withCommonOpts(s.command("seats"))
|
|
427
|
-
.description("Connected LinkedIn seats and their health")
|
|
428
|
-
.action(async function () {
|
|
429
|
-
try {
|
|
430
|
-
const r = await call("safety", "seats", {}, ctxFrom(this));
|
|
431
|
-
emit(this, r, (d) => {
|
|
432
|
-
process.stdout.write(table(
|
|
433
|
-
["account", "id", "cooldown", "reason", "resume at", "strikes"],
|
|
434
|
-
(d.seats || []).map((x) => [
|
|
435
|
-
truncate(x.account_name, 22), truncate(x.account_id, 12),
|
|
436
|
-
x.cooldown_active ? "active" : "clear", truncate(x.cooldown_reason, 22),
|
|
437
|
-
n(x.resume_at), n(x.backoff?.strikes),
|
|
438
|
-
]),
|
|
439
|
-
) + "\n");
|
|
440
|
-
});
|
|
441
|
-
} catch (e) { fail(e); }
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
withCommonOpts(s.command("cooldowns"))
|
|
445
|
-
.description("Active cooldowns and holds")
|
|
446
|
-
.action(async function () {
|
|
447
|
-
try {
|
|
448
|
-
const r = await call("safety", "cooldowns", {}, ctxFrom(this));
|
|
449
|
-
emit(this, r, (d) => {
|
|
450
|
-
process.stdout.write(table(
|
|
451
|
-
["account", "source", "class", "reason", "clears at"],
|
|
452
|
-
(d.active || []).map((x) => [
|
|
453
|
-
truncate(x.account_name || x.account_id, 24), x.source, n(x.class),
|
|
454
|
-
truncate(x.reason, 30), n(x.resume_at),
|
|
455
|
-
]),
|
|
456
|
-
) + `\n${d.active?.length ?? 0} active\n`);
|
|
457
|
-
});
|
|
458
|
-
} catch (e) { fail(e); }
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
withCommonOpts(s.command("check"))
|
|
462
|
-
.description("Preflight a LinkedIn action against current guardrails")
|
|
463
|
-
.requiredOption("--account <id>", "LinkedIn account id")
|
|
464
|
-
.option("--action <name>", "action name", "sendMessage")
|
|
465
|
-
.action(async function () {
|
|
466
|
-
try {
|
|
467
|
-
const o = this.optsWithGlobals();
|
|
468
|
-
const r = await call(
|
|
469
|
-
"safety", "check",
|
|
470
|
-
{ account_id: o.account, action_name: o.action },
|
|
471
|
-
ctxFrom(this),
|
|
472
|
-
);
|
|
473
|
-
emit(this, r, (d) => {
|
|
474
|
-
process.stdout.write(
|
|
475
|
-
`\n${d.allowed ? "ALLOWED" : "BLOCKED"} — ${d.action} on ${d.account_id}\n` +
|
|
476
|
-
(d.allowed ? "" : `Reason: ${d.blocked_reason}\nRetry at: ${n(d.retry_at)}\n`) +
|
|
477
|
-
`\n${d.note}\n`,
|
|
478
|
-
);
|
|
479
|
-
});
|
|
480
|
-
} catch (e) { fail(e); }
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
withCommonOpts(s.command("policy"))
|
|
484
|
-
.description("Governor policy and enforcement mode")
|
|
485
|
-
.action(async function () {
|
|
486
|
-
try {
|
|
487
|
-
const r = await call("safety", "policy", {}, ctxFrom(this));
|
|
488
|
-
emit(this, r, (d) => process.stdout.write(JSON.stringify(d, null, 2) + "\n"));
|
|
489
|
-
} catch (e) { fail(e); }
|
|
490
|
-
});
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
export function registerCtas(program) {
|
|
494
|
-
const c = program.command("ctas").description("Knowledge-base CTAs used in content and sequences");
|
|
495
|
-
|
|
496
|
-
withCommonOpts(c.command("list"))
|
|
497
|
-
.description("List CTAs")
|
|
498
|
-
.option("--all", "include inactive")
|
|
499
|
-
.action(async function () {
|
|
500
|
-
try {
|
|
501
|
-
const o = this.optsWithGlobals();
|
|
502
|
-
const r = await call("ctas", "list", { active_only: !o.all }, ctxFrom(this));
|
|
503
|
-
emit(this, r, (d) => {
|
|
504
|
-
process.stdout.write(table(
|
|
505
|
-
["id", "label", "url", "type", "primary", "active"],
|
|
506
|
-
(d.ctas || []).map((x) => [
|
|
507
|
-
truncate(x.id, 8), truncate(x.label, 26), truncate(x.url, 40),
|
|
508
|
-
n(x.type), x.is_primary ? "yes" : "", x.is_active ? "yes" : "no",
|
|
509
|
-
]),
|
|
510
|
-
) + `\n${d.ctas?.length ?? 0} rows\n`);
|
|
511
|
-
});
|
|
512
|
-
} catch (e) { fail(e); }
|
|
513
|
-
});
|
|
514
|
-
|
|
515
|
-
withCommonOpts(c.command("create"))
|
|
516
|
-
.description("Create a CTA")
|
|
517
|
-
.requiredOption("--label <text>", "button/link label")
|
|
518
|
-
.requiredOption("--url <url>", "destination URL")
|
|
519
|
-
.option("--type <t>", "cta type", "link")
|
|
520
|
-
.option("--primary", "mark as primary CTA")
|
|
521
|
-
.option("--sort <n>", "sort order", "0")
|
|
522
|
-
.action(async function () {
|
|
523
|
-
try {
|
|
524
|
-
const o = this.optsWithGlobals();
|
|
525
|
-
const r = await call("ctas", "create", {
|
|
526
|
-
label: o.label, url: o.url, type: o.type,
|
|
527
|
-
is_primary: !!o.primary, sort_order: Number(o.sort),
|
|
528
|
-
}, ctxFrom(this));
|
|
529
|
-
emit(this, r, (d) => process.stdout.write(`✔ CTA created: ${d.cta?.id}\n`));
|
|
530
|
-
} catch (e) { fail(e); }
|
|
531
|
-
});
|
|
532
|
-
|
|
533
|
-
withCommonOpts(c.command("update <id>"))
|
|
534
|
-
.description("Update a CTA")
|
|
535
|
-
.option("--label <text>", "label")
|
|
536
|
-
.option("--url <url>", "url")
|
|
537
|
-
.option("--type <t>", "type")
|
|
538
|
-
.option("--sort <n>", "sort order")
|
|
539
|
-
.option("--primary <bool>", "primary flag (true/false)")
|
|
540
|
-
.action(async function (id) {
|
|
541
|
-
try {
|
|
542
|
-
const o = this.optsWithGlobals();
|
|
543
|
-
const patch = { id };
|
|
544
|
-
if (o.label) patch.label = o.label;
|
|
545
|
-
if (o.url) patch.url = o.url;
|
|
546
|
-
if (o.type) patch.type = o.type;
|
|
547
|
-
if (o.sort !== undefined) patch.sort_order = Number(o.sort);
|
|
548
|
-
if (o.primary !== undefined) patch.is_primary = o.primary === "true";
|
|
549
|
-
const r = await call("ctas", "update", patch, ctxFrom(this));
|
|
550
|
-
emit(this, r, (d) => process.stdout.write(`✔ CTA updated: ${d.cta?.id}\n`));
|
|
551
|
-
} catch (e) { fail(e); }
|
|
552
|
-
});
|
|
553
|
-
|
|
554
|
-
withCommonOpts(c.command("deactivate <id>"))
|
|
555
|
-
.description("Deactivate a CTA")
|
|
556
|
-
.action(async function (id) {
|
|
557
|
-
try {
|
|
558
|
-
const r = await call("ctas", "deactivate", { id }, ctxFrom(this));
|
|
559
|
-
emit(this, r, () => process.stdout.write(`✔ CTA deactivated: ${id}\n`));
|
|
560
|
-
} catch (e) { fail(e); }
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
export function registerNotifications(program) {
|
|
565
|
-
const nCmd = program.command("notifications").description("Workspace notifications");
|
|
566
|
-
|
|
567
|
-
withCommonOpts(nCmd.command("list"))
|
|
568
|
-
.description("List notifications")
|
|
569
|
-
.option("--unread", "unread only")
|
|
570
|
-
.option("--type <t>", "filter by type")
|
|
571
|
-
.option("--limit <n>", "max rows", "50")
|
|
572
|
-
.action(async function () {
|
|
573
|
-
try {
|
|
574
|
-
const o = this.optsWithGlobals();
|
|
575
|
-
const r = await call(
|
|
576
|
-
"notifications", "list",
|
|
577
|
-
{ unread_only: !!o.unread, type: o.type, limit: Number(o.limit) },
|
|
578
|
-
ctxFrom(this),
|
|
579
|
-
);
|
|
580
|
-
emit(this, r, (d) => {
|
|
581
|
-
process.stdout.write(table(
|
|
582
|
-
["when", "type", "title", "read"],
|
|
583
|
-
(d.notifications || []).map((x) => [
|
|
584
|
-
n(x.created_at).replace("T", " ").slice(0, 16), truncate(x.type, 22),
|
|
585
|
-
truncate(x.title, 42), x.is_read ? "yes" : "no",
|
|
586
|
-
]),
|
|
587
|
-
) + `\n${d.notifications?.length ?? 0} rows\n`);
|
|
588
|
-
});
|
|
589
|
-
} catch (e) { fail(e); }
|
|
590
|
-
});
|
|
591
|
-
|
|
592
|
-
withCommonOpts(nCmd.command("read <ids...>"))
|
|
593
|
-
.description("Mark notifications as read")
|
|
594
|
-
.action(async function (ids) {
|
|
595
|
-
try {
|
|
596
|
-
const r = await call("notifications", "mark_read", { ids }, ctxFrom(this));
|
|
597
|
-
emit(this, r, (d) => process.stdout.write(`✔ ${d.updated} marked read\n`));
|
|
598
|
-
} catch (e) { fail(e); }
|
|
599
|
-
});
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
export function registerAudiences(program) {
|
|
603
|
-
const a = program.command("audiences").description("Smart audiences / lead segments");
|
|
604
|
-
|
|
605
|
-
withCommonOpts(a.command("list"))
|
|
606
|
-
.description("List audiences")
|
|
607
|
-
.action(async function () {
|
|
608
|
-
try {
|
|
609
|
-
const r = await call("audiences", "list", {}, ctxFrom(this));
|
|
610
|
-
emit(this, r, (d) => {
|
|
611
|
-
process.stdout.write(table(
|
|
612
|
-
["id", "name", "type", "leads", "updated"],
|
|
613
|
-
(d.audiences || []).map((x) => [
|
|
614
|
-
truncate(x.id, 8), truncate(x.name, 30), n(x.segment_type),
|
|
615
|
-
n(x.lead_count), n(x.updated_at).slice(0, 10),
|
|
616
|
-
]),
|
|
617
|
-
) + `\n${d.audiences?.length ?? 0} rows\n`);
|
|
618
|
-
});
|
|
619
|
-
} catch (e) { fail(e); }
|
|
620
|
-
});
|
|
621
|
-
|
|
622
|
-
withCommonOpts(a.command("members <id>"))
|
|
623
|
-
.description("List the leads in an audience")
|
|
624
|
-
.option("--limit <n>", "max rows", "100")
|
|
625
|
-
.action(async function (id) {
|
|
626
|
-
try {
|
|
627
|
-
const o = this.optsWithGlobals();
|
|
628
|
-
const r = await call("audiences", "members", { id, limit: Number(o.limit) }, ctxFrom(this));
|
|
629
|
-
emit(this, r, (d) => {
|
|
630
|
-
process.stdout.write(table(
|
|
631
|
-
["id", "name", "title", "company"],
|
|
632
|
-
(d.leads || []).map((l) => [
|
|
633
|
-
truncate(l.id, 8), truncate(l.name, 24), truncate(l.title, 26), truncate(l.company, 22),
|
|
634
|
-
]),
|
|
635
|
-
) + `\n${d.count} members\n`);
|
|
636
|
-
});
|
|
637
|
-
} catch (e) { fail(e); }
|
|
638
|
-
});
|
|
639
|
-
|
|
640
|
-
withCommonOpts(a.command("create"))
|
|
641
|
-
.description("Create a smart audience (lead segment)")
|
|
642
|
-
.requiredOption("--name <name>", "audience name")
|
|
643
|
-
.option("--description <text>", "description")
|
|
644
|
-
.option("--type <t>", "segment type", "manual")
|
|
645
|
-
.action(async function () {
|
|
646
|
-
try {
|
|
647
|
-
const o = this.optsWithGlobals();
|
|
648
|
-
const r = await call("audiences", "create", {
|
|
649
|
-
name: o.name, description: o.description, segment_type: o.type,
|
|
650
|
-
}, ctxFrom(this));
|
|
651
|
-
emit(this, r, (d) => process.stdout.write(`\u2714 Created audience ${d.audience?.id}\n`));
|
|
652
|
-
} catch (e) { fail(e); }
|
|
653
|
-
});
|
|
654
|
-
|
|
655
|
-
withCommonOpts(a.command("edit <id>"))
|
|
656
|
-
.description("Rename or re-describe an audience")
|
|
657
|
-
.option("--name <name>").option("--description <text>").option("--type <t>")
|
|
658
|
-
.action(async function (id) {
|
|
659
|
-
try {
|
|
660
|
-
const o = this.optsWithGlobals();
|
|
661
|
-
const params = { id };
|
|
662
|
-
if (o.name !== undefined) params.name = o.name;
|
|
663
|
-
if (o.description !== undefined) params.description = o.description;
|
|
664
|
-
if (o.type !== undefined) params.segment_type = o.type;
|
|
665
|
-
const r = await call("audiences", "update", params, ctxFrom(this));
|
|
666
|
-
emit(this, r, (d) => process.stdout.write(`\u2714 Updated ${d.audience?.id ?? id}\n`));
|
|
667
|
-
} catch (e) { fail(e); }
|
|
668
|
-
});
|
|
669
|
-
|
|
670
|
-
withCommonOpts(a.command("delete <id>"))
|
|
671
|
-
.description("Delete an audience and its membership rows")
|
|
672
|
-
.option("-y, --yes", "skip the confirmation prompt")
|
|
673
|
-
.action(async function (id) {
|
|
674
|
-
try {
|
|
675
|
-
await confirmDestructive(this, `Delete audience ${id} and its membership rows.`);
|
|
676
|
-
const r = await call("audiences", "delete", { id }, ctxFrom(this));
|
|
677
|
-
emit(this, r, () => process.stdout.write(`\u2714 Deleted ${id}\n`));
|
|
678
|
-
} catch (e) { fail(e); }
|
|
679
|
-
});
|
|
680
|
-
|
|
681
|
-
withCommonOpts(a.command("add-leads <id> <leadIds...>"))
|
|
682
|
-
.description("Add leads to an audience")
|
|
683
|
-
.action(async function (id, leadIds) {
|
|
684
|
-
try {
|
|
685
|
-
const r = await call("audiences", "add_members", { id, lead_ids: leadIds }, ctxFrom(this));
|
|
686
|
-
emit(this, r, (d) => process.stdout.write(`\u2714 Added ${d.added ?? 0} of ${d.candidates ?? leadIds.length}\n`));
|
|
687
|
-
} catch (e) { fail(e); }
|
|
688
|
-
});
|
|
689
|
-
|
|
690
|
-
withCommonOpts(a.command("remove-leads <id> <leadIds...>"))
|
|
691
|
-
.description("Remove leads from an audience")
|
|
692
|
-
.action(async function (id, leadIds) {
|
|
693
|
-
try {
|
|
694
|
-
const r = await call("audiences", "remove_members", { id, lead_ids: leadIds }, ctxFrom(this));
|
|
695
|
-
emit(this, r, (d) => process.stdout.write(`\u2714 Removed ${d.removed ?? 0}\n`));
|
|
696
|
-
} catch (e) { fail(e); }
|
|
697
|
-
});
|
|
698
|
-
|
|
699
|
-
withCommonOpts(a.command("add-to-campaign <id> <campaignId>"))
|
|
700
|
-
.description("Enrol every lead in an audience into a campaign")
|
|
701
|
-
.action(async function (id, campaignId) {
|
|
702
|
-
try {
|
|
703
|
-
const r = await call("audiences", "add_to_campaign", { id, campaign_id: campaignId }, ctxFrom(this));
|
|
704
|
-
emit(this, r, (d) => process.stdout.write(`✔ Added ${d.added} of ${d.candidates} leads\n`));
|
|
705
|
-
} catch (e) { fail(e); }
|
|
706
|
-
});
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
export function registerNewsletter(program) {
|
|
710
|
-
const nl = program.command("newsletter").description("Newsletters, audiences and contacts");
|
|
711
|
-
|
|
712
|
-
withCommonOpts(nl.command("list"))
|
|
713
|
-
.description("List newsletters")
|
|
714
|
-
.option("--limit <n>", "max rows", "50")
|
|
715
|
-
.action(async function () {
|
|
716
|
-
try {
|
|
717
|
-
const o = this.optsWithGlobals();
|
|
718
|
-
const r = await call("newsletter", "list", { limit: Number(o.limit) }, ctxFrom(this));
|
|
719
|
-
emit(this, r, (d) => {
|
|
720
|
-
process.stdout.write(table(
|
|
721
|
-
["id", "title", "status", "scheduled", "published"],
|
|
722
|
-
(d.newsletters || []).map((x) => [
|
|
723
|
-
truncate(x.id, 8), truncate(x.title, 36), x.status,
|
|
724
|
-
n(x.scheduled_date).slice(0, 16), n(x.published_at).slice(0, 10),
|
|
725
|
-
]),
|
|
726
|
-
) + `\n${d.newsletters?.length ?? 0} rows\n`);
|
|
727
|
-
});
|
|
728
|
-
} catch (e) { fail(e); }
|
|
729
|
-
});
|
|
730
|
-
|
|
731
|
-
withCommonOpts(nl.command("show <id>"))
|
|
732
|
-
.description("Show one newsletter")
|
|
733
|
-
.action(async function (id) {
|
|
734
|
-
try {
|
|
735
|
-
const r = await call("newsletter", "get", { id }, ctxFrom(this));
|
|
736
|
-
emit(this, r, (d) => process.stdout.write(JSON.stringify(d.newsletter, null, 2) + "\n"));
|
|
737
|
-
} catch (e) { fail(e); }
|
|
738
|
-
});
|
|
739
|
-
|
|
740
|
-
withCommonOpts(nl.command("create"))
|
|
741
|
-
.description("Create a newsletter draft")
|
|
742
|
-
.requiredOption("--subject <s>", "email subject")
|
|
743
|
-
.option("--preview <s>", "preview text")
|
|
744
|
-
.option("--body <html>", "HTML body")
|
|
745
|
-
.option("--file <path>", "read the HTML body from a file")
|
|
746
|
-
.action(async function () {
|
|
747
|
-
try {
|
|
748
|
-
const o = this.optsWithGlobals();
|
|
749
|
-
const body = o.file ? await readFile(o.file, "utf8") : (o.body ?? "");
|
|
750
|
-
const r = await call("newsletter", "create", {
|
|
751
|
-
subject: o.subject, preview_text: o.preview, body,
|
|
752
|
-
}, ctxFrom(this));
|
|
753
|
-
emit(this, r, (d) => process.stdout.write(`✔ Created draft ${d.newsletter?.id}\n`));
|
|
754
|
-
} catch (e) { fail(e); }
|
|
755
|
-
});
|
|
756
|
-
|
|
757
|
-
withCommonOpts(nl.command("edit <id>"))
|
|
758
|
-
.description("Update a newsletter draft")
|
|
759
|
-
.option("--subject <s>").option("--preview <s>").option("--body <html>")
|
|
760
|
-
.option("--file <path>", "read the HTML body from a file")
|
|
761
|
-
.option("--status <s>", "draft | scheduled")
|
|
762
|
-
.action(async function (id) {
|
|
763
|
-
try {
|
|
764
|
-
const o = this.optsWithGlobals();
|
|
765
|
-
const params = { id };
|
|
766
|
-
if (o.subject !== undefined) params.subject = o.subject;
|
|
767
|
-
if (o.preview !== undefined) params.preview_text = o.preview;
|
|
768
|
-
if (o.status !== undefined) params.status = o.status;
|
|
769
|
-
if (o.file) params.body = await readFile(o.file, "utf8");
|
|
770
|
-
else if (o.body !== undefined) params.body = o.body;
|
|
771
|
-
const r = await call("newsletter", "update", params, ctxFrom(this));
|
|
772
|
-
emit(this, r, (d) => process.stdout.write(`✔ Updated ${d.newsletter?.id ?? id}\n`));
|
|
773
|
-
} catch (e) { fail(e); }
|
|
774
|
-
});
|
|
775
|
-
|
|
776
|
-
withCommonOpts(nl.command("delete <id>"))
|
|
777
|
-
.description("Delete a newsletter draft")
|
|
778
|
-
.action(async function (id) {
|
|
779
|
-
try {
|
|
780
|
-
const r = await call("newsletter", "delete", { id }, ctxFrom(this));
|
|
781
|
-
emit(this, r, () => process.stdout.write(`✔ Deleted ${id}\n`));
|
|
782
|
-
} catch (e) { fail(e); }
|
|
783
|
-
});
|
|
784
|
-
|
|
785
|
-
withCommonOpts(nl.command("schedule <id> <isoTimestamp>"))
|
|
786
|
-
.description("Schedule a newsletter")
|
|
787
|
-
.action(async function (id, ts) {
|
|
788
|
-
try {
|
|
789
|
-
const r = await call("newsletter", "schedule", { id, scheduled_date: ts }, ctxFrom(this));
|
|
790
|
-
emit(this, r, (d) => process.stdout.write(`✔ Scheduled for ${d.newsletter?.scheduled_date}\n`));
|
|
791
|
-
} catch (e) { fail(e); }
|
|
792
|
-
});
|
|
793
|
-
|
|
794
|
-
withCommonOpts(nl.command("audiences"))
|
|
795
|
-
.description("List newsletter audiences")
|
|
796
|
-
.action(async function () {
|
|
797
|
-
try {
|
|
798
|
-
const r = await call("newsletter", "audiences", {}, ctxFrom(this));
|
|
799
|
-
emit(this, r, (d) => {
|
|
800
|
-
process.stdout.write(table(
|
|
801
|
-
["id", "name", "description"],
|
|
802
|
-
(d.audiences || []).map((x) => [truncate(x.id, 8), truncate(x.name, 28), truncate(x.description, 40)]),
|
|
803
|
-
) + "\n");
|
|
804
|
-
});
|
|
805
|
-
} catch (e) { fail(e); }
|
|
806
|
-
});
|
|
807
|
-
|
|
808
|
-
withCommonOpts(nl.command("contacts"))
|
|
809
|
-
.description("List newsletter contacts")
|
|
810
|
-
.option("--audience <id>", "filter by audience")
|
|
811
|
-
.option("--limit <n>", "max rows", "100")
|
|
812
|
-
.action(async function () {
|
|
813
|
-
try {
|
|
814
|
-
const o = this.optsWithGlobals();
|
|
815
|
-
const r = await call(
|
|
816
|
-
"newsletter", "contacts",
|
|
817
|
-
{ audience_id: o.audience, limit: Number(o.limit) },
|
|
818
|
-
ctxFrom(this),
|
|
819
|
-
);
|
|
820
|
-
emit(this, r, (d) => {
|
|
821
|
-
process.stdout.write(table(
|
|
822
|
-
["email", "name", "unsubscribed", "source"],
|
|
823
|
-
(d.contacts || []).map((x) => [
|
|
824
|
-
truncate(x.email, 34), truncate(`${n(x.first_name)} ${n(x.last_name)}`.trim(), 24),
|
|
825
|
-
x.unsubscribed ? "yes" : "", n(x.source),
|
|
826
|
-
]),
|
|
827
|
-
) + `\n${d.contacts?.length ?? 0} rows\n`);
|
|
828
|
-
});
|
|
829
|
-
} catch (e) { fail(e); }
|
|
830
|
-
});
|
|
831
|
-
|
|
832
|
-
withCommonOpts(nl.command("audience-show <audienceId>"))
|
|
833
|
-
.description("Show one mailing audience with contact counts")
|
|
834
|
-
.action(async function (audienceId) {
|
|
835
|
-
try {
|
|
836
|
-
const r = await call("newsletter", "audience_get", { audience_id: audienceId }, ctxFrom(this));
|
|
837
|
-
emit(this, r, (d) => process.stdout.write(
|
|
838
|
-
`${d.audience?.name}\n${d.stats?.contacts ?? 0} contacts (${d.stats?.subscribed ?? 0} subscribed, ${d.stats?.unsubscribed ?? 0} unsubscribed)\n` +
|
|
839
|
-
`Resend mirror: ${d.audience?.resend_audience_id || "(none — local only)"}\n`,
|
|
840
|
-
));
|
|
841
|
-
} catch (e) { fail(e); }
|
|
842
|
-
});
|
|
843
|
-
|
|
844
|
-
withCommonOpts(nl.command("audience-create"))
|
|
845
|
-
.description("Create a mailing audience (mirrored to Resend when connected)")
|
|
846
|
-
.requiredOption("--name <name>", "audience name")
|
|
847
|
-
.option("--description <text>", "description")
|
|
848
|
-
.action(async function () {
|
|
849
|
-
try {
|
|
850
|
-
const o = this.optsWithGlobals();
|
|
851
|
-
const r = await call("newsletter", "audience_create", {
|
|
852
|
-
name: o.name, description: o.description,
|
|
853
|
-
}, ctxFrom(this));
|
|
854
|
-
emit(this, r, (d) => process.stdout.write(
|
|
855
|
-
`\u2714 Created audience ${d.audience?.id}${d.resend_mirrored ? " (mirrored to Resend)" : " (local only — Resend not connected)"}\n`,
|
|
856
|
-
));
|
|
857
|
-
} catch (e) { fail(e); }
|
|
858
|
-
});
|
|
859
|
-
|
|
860
|
-
withCommonOpts(nl.command("audience-edit <audienceId>"))
|
|
861
|
-
.description("Rename or re-describe a mailing audience")
|
|
862
|
-
.option("--name <name>").option("--description <text>")
|
|
863
|
-
.action(async function (audienceId) {
|
|
864
|
-
try {
|
|
865
|
-
const o = this.optsWithGlobals();
|
|
866
|
-
const params = { audience_id: audienceId };
|
|
867
|
-
if (o.name !== undefined) params.name = o.name;
|
|
868
|
-
if (o.description !== undefined) params.description = o.description;
|
|
869
|
-
const r = await call("newsletter", "audience_update", params, ctxFrom(this));
|
|
870
|
-
emit(this, r, (d) => process.stdout.write(`\u2714 Updated ${d.audience?.id ?? audienceId}\n`));
|
|
871
|
-
} catch (e) { fail(e); }
|
|
872
|
-
});
|
|
873
|
-
|
|
874
|
-
withCommonOpts(nl.command("audience-delete <audienceId>"))
|
|
875
|
-
.description("Delete a mailing audience and its contacts")
|
|
876
|
-
.option("-y, --yes", "skip the confirmation prompt")
|
|
877
|
-
.action(async function (audienceId) {
|
|
878
|
-
try {
|
|
879
|
-
await confirmDestructive(this, `Delete mailing audience ${audienceId} and every contact in it.`);
|
|
880
|
-
const r = await call("newsletter", "audience_delete", { audience_id: audienceId }, ctxFrom(this));
|
|
881
|
-
emit(this, r, () => process.stdout.write(`\u2714 Deleted ${audienceId}\n`));
|
|
882
|
-
} catch (e) { fail(e); }
|
|
883
|
-
});
|
|
884
|
-
|
|
885
|
-
withCommonOpts(nl.command("contact-add <audienceId> <email>"))
|
|
886
|
-
.description("Add one contact to a mailing audience")
|
|
887
|
-
.option("--first <name>").option("--last <name>")
|
|
888
|
-
.action(async function (audienceId, email) {
|
|
889
|
-
try {
|
|
890
|
-
const o = this.optsWithGlobals();
|
|
891
|
-
const r = await call("newsletter", "contact_add", {
|
|
892
|
-
audience_id: audienceId, email, first_name: o.first, last_name: o.last,
|
|
893
|
-
}, ctxFrom(this));
|
|
894
|
-
emit(this, r, (d) => process.stdout.write(`\u2714 Added ${d.added} contact(s)\n`));
|
|
895
|
-
} catch (e) { fail(e); }
|
|
896
|
-
});
|
|
897
|
-
|
|
898
|
-
withCommonOpts(nl.command("contacts-import <audienceId>"))
|
|
899
|
-
.description("Bulk import contacts from a CSV file (email,first_name,last_name)")
|
|
900
|
-
.requiredOption("--file <path>", "CSV file path")
|
|
901
|
-
.action(async function (audienceId) {
|
|
902
|
-
try {
|
|
903
|
-
const o = this.optsWithGlobals();
|
|
904
|
-
const rows = parseCsvLite(await readFile(o.file, "utf8"));
|
|
905
|
-
const contacts = rows.map((r) => ({
|
|
906
|
-
email: r.email || r["email address"] || "",
|
|
907
|
-
first_name: r.first_name || r.first || undefined,
|
|
908
|
-
last_name: r.last_name || r.last || undefined,
|
|
909
|
-
})).filter((c) => c.email);
|
|
910
|
-
if (!contacts.length) throw new Error("No email column found in the CSV");
|
|
911
|
-
const r = await call("newsletter", "contacts_import", {
|
|
912
|
-
audience_id: audienceId, contacts, source: "cli:import",
|
|
913
|
-
}, ctxFrom(this));
|
|
914
|
-
emit(this, r, (d) => process.stdout.write(
|
|
915
|
-
`\u2714 Imported ${d.added} of ${d.candidates} (Resend mirrored: ${d.resend_mirrored})\n`,
|
|
916
|
-
));
|
|
917
|
-
} catch (e) { fail(e); }
|
|
918
|
-
});
|
|
919
|
-
|
|
920
|
-
withCommonOpts(nl.command("contact-remove <email>"))
|
|
921
|
-
.description("Delete a contact (optionally only from one audience)")
|
|
922
|
-
.option("--audience <id>", "restrict to one audience")
|
|
923
|
-
.option("-y, --yes", "skip the confirmation prompt")
|
|
924
|
-
.action(async function (email) {
|
|
925
|
-
try {
|
|
926
|
-
const o = this.optsWithGlobals();
|
|
927
|
-
await confirmDestructive(this, `Delete contact ${email}.`);
|
|
928
|
-
const r = await call("newsletter", "contact_remove", {
|
|
929
|
-
email, audience_id: o.audience,
|
|
930
|
-
}, ctxFrom(this));
|
|
931
|
-
emit(this, r, (d) => process.stdout.write(`\u2714 Removed ${d.removed}\n`));
|
|
932
|
-
} catch (e) { fail(e); }
|
|
933
|
-
});
|
|
934
|
-
|
|
935
|
-
withCommonOpts(nl.command("contact-unsubscribe <email>"))
|
|
936
|
-
.description("Mark a contact unsubscribed")
|
|
937
|
-
.option("--audience <id>", "restrict to one audience")
|
|
938
|
-
.action(async function (email) {
|
|
939
|
-
try {
|
|
940
|
-
const o = this.optsWithGlobals();
|
|
941
|
-
const r = await call("newsletter", "contact_unsubscribe", {
|
|
942
|
-
email, audience_id: o.audience,
|
|
943
|
-
}, ctxFrom(this));
|
|
944
|
-
emit(this, r, (d) => process.stdout.write(`\u2714 Unsubscribed ${d.unsubscribed}\n`));
|
|
945
|
-
} catch (e) { fail(e); }
|
|
946
|
-
});
|
|
947
|
-
|
|
948
|
-
withCommonOpts(nl.command("sync-segment <segmentId> <audienceId>"))
|
|
949
|
-
.description("Push every lead in a smart audience into a mailing audience")
|
|
950
|
-
.option("--include-unverified", "include leads without a verified email")
|
|
951
|
-
.action(async function (segmentId, audienceId) {
|
|
952
|
-
try {
|
|
953
|
-
const o = this.optsWithGlobals();
|
|
954
|
-
const r = await call("newsletter", "sync_segment", {
|
|
955
|
-
segment_id: segmentId, audience_id: audienceId, include_unverified: !!o.includeUnverified,
|
|
956
|
-
}, ctxFrom(this));
|
|
957
|
-
emit(this, r, (d) => process.stdout.write(
|
|
958
|
-
`\u2714 Synced: ${d.added ?? 0} added, ${d.skipped ?? 0} skipped, ${d.failed ?? 0} failed\n`,
|
|
959
|
-
));
|
|
960
|
-
} catch (e) { fail(e); }
|
|
961
|
-
});
|
|
962
|
-
|
|
963
|
-
withCommonOpts(nl.command("send <id>"))
|
|
964
|
-
.description("Send a newsletter now, or schedule it, to one or more mailing audiences")
|
|
965
|
-
.option("--audience <ids>", "comma-separated mailing audience ids")
|
|
966
|
-
.option("--at <isoTimestamp>", "schedule instead of sending now")
|
|
967
|
-
.option("--from <email>", "override the from address")
|
|
968
|
-
.option("-y, --yes", "skip the confirmation prompt")
|
|
969
|
-
.action(async function (id) {
|
|
970
|
-
try {
|
|
971
|
-
const o = this.optsWithGlobals();
|
|
972
|
-
const audienceIds = String(o.audience || "").split(",").map((x) => x.trim()).filter(Boolean);
|
|
973
|
-
if (!audienceIds.length) throw new Error("--audience is required (comma-separated ids)");
|
|
974
|
-
await confirmDestructive(this, o.at
|
|
975
|
-
? `Schedule newsletter ${id} for ${o.at} to ${audienceIds.length} audience(s).`
|
|
976
|
-
: `Send newsletter ${id} now to ${audienceIds.length} audience(s).`);
|
|
977
|
-
const r = await call("newsletter", "send", {
|
|
978
|
-
id, audience_ids: audienceIds, mode: o.at ? "schedule" : "now",
|
|
979
|
-
scheduled_for: o.at, from: o.from,
|
|
980
|
-
}, ctxFrom(this));
|
|
981
|
-
emit(this, r, (d) => process.stdout.write(`\u2714 ${d.mode === "schedule" ? "Scheduled" : "Sent"} ${id}\n`));
|
|
982
|
-
} catch (e) { fail(e); }
|
|
983
|
-
});
|
|
984
|
-
|
|
985
|
-
withCommonOpts(nl.command("test <id> <email>"))
|
|
986
|
-
.description("Send a test copy of a newsletter to one address")
|
|
987
|
-
.action(async function (id, email) {
|
|
988
|
-
try {
|
|
989
|
-
const r = await call("newsletter", "send_test", { id, test_email: email }, ctxFrom(this));
|
|
990
|
-
emit(this, r, () => process.stdout.write(`\u2714 Test sent to ${email}\n`));
|
|
991
|
-
} catch (e) { fail(e); }
|
|
992
|
-
});
|
|
993
|
-
|
|
994
|
-
withCommonOpts(nl.command("render <id>"))
|
|
995
|
-
.description("Render the final email HTML without sending")
|
|
996
|
-
.action(async function (id) {
|
|
997
|
-
try {
|
|
998
|
-
const r = await call("newsletter", "render", { id }, ctxFrom(this));
|
|
999
|
-
emit(this, r, (d) => process.stdout.write(
|
|
1000
|
-
(d.result?.html || d.result?.body_html || JSON.stringify(d.result, null, 2)) + "\n",
|
|
1001
|
-
));
|
|
1002
|
-
} catch (e) { fail(e); }
|
|
1003
|
-
});
|
|
1004
|
-
|
|
1005
|
-
withCommonOpts(nl.command("metrics"))
|
|
1006
|
-
.description("Delivery metrics and sending health for the workspace")
|
|
1007
|
-
.option("--id <newsletterId>", "restrict to one newsletter")
|
|
1008
|
-
.option("--limit <n>", "events to scan", "500")
|
|
1009
|
-
.action(async function () {
|
|
1010
|
-
try {
|
|
1011
|
-
const o = this.optsWithGlobals();
|
|
1012
|
-
const r = await call("newsletter", "metrics", { id: o.id, limit: Number(o.limit) }, ctxFrom(this));
|
|
1013
|
-
emit(this, r, (d) => {
|
|
1014
|
-
const t = d.totals || {};
|
|
1015
|
-
const s = d.send_state;
|
|
1016
|
-
process.stdout.write(
|
|
1017
|
-
Object.keys(t).length
|
|
1018
|
-
? table(["event", "count"], Object.entries(t).map(([k, v]) => [k, String(v)])) + "\n"
|
|
1019
|
-
: "No delivery events yet\n",
|
|
1020
|
-
);
|
|
1021
|
-
if (d.rates) {
|
|
1022
|
-
process.stdout.write(`Open ${d.rates.open_rate}% Click ${d.rates.click_rate}% Bounce ${d.rates.bounce_rate}%\n`);
|
|
1023
|
-
}
|
|
1024
|
-
if (s) {
|
|
1025
|
-
process.stdout.write(
|
|
1026
|
-
`Daily ${s.daily_sent}/${s.daily_cap} warmup stage ${s.warmup_stage}` +
|
|
1027
|
-
(s.paused_reason ? ` PAUSED: ${s.paused_reason}` : "") + "\n",
|
|
1028
|
-
);
|
|
1029
|
-
}
|
|
1030
|
-
});
|
|
1031
|
-
} catch (e) { fail(e); }
|
|
1032
|
-
});
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
export function registerVideo(program) {
|
|
1036
|
-
const v = program.command("video").description("Video projects");
|
|
1037
|
-
|
|
1038
|
-
withCommonOpts(v.command("list"))
|
|
1039
|
-
.description("List video projects")
|
|
1040
|
-
.option("--status <s>", "filter by status")
|
|
1041
|
-
.option("--limit <n>", "max rows", "50")
|
|
1042
|
-
.action(async function () {
|
|
1043
|
-
try {
|
|
1044
|
-
const o = this.optsWithGlobals();
|
|
1045
|
-
const r = await call("video", "list", { status: o.status, limit: Number(o.limit) }, ctxFrom(this));
|
|
1046
|
-
emit(this, r, (d) => {
|
|
1047
|
-
process.stdout.write(table(
|
|
1048
|
-
["id", "title", "status", "progress", "published"],
|
|
1049
|
-
(d.videos || []).map((x) => [
|
|
1050
|
-
truncate(x.id, 8), truncate(x.title, 32), x.status,
|
|
1051
|
-
n(x.progress), n(x.published_at).slice(0, 10),
|
|
1052
|
-
]),
|
|
1053
|
-
) + `\n${d.videos?.length ?? 0} rows\n`);
|
|
1054
|
-
});
|
|
1055
|
-
} catch (e) { fail(e); }
|
|
1056
|
-
});
|
|
1057
|
-
|
|
1058
|
-
withCommonOpts(v.command("show <id>"))
|
|
1059
|
-
.description("Show a video project and its render status")
|
|
1060
|
-
.action(async function (id) {
|
|
1061
|
-
try {
|
|
1062
|
-
const r = await call("video", "get", { id }, ctxFrom(this));
|
|
1063
|
-
emit(this, r, (d) => process.stdout.write(JSON.stringify(d.video, null, 2) + "\n"));
|
|
1064
|
-
} catch (e) { fail(e); }
|
|
1065
|
-
});
|
|
1066
|
-
|
|
1067
|
-
withCommonOpts(v.command("share <id>"))
|
|
1068
|
-
.description("Get the public share link for a video")
|
|
1069
|
-
.action(async function (id) {
|
|
1070
|
-
try {
|
|
1071
|
-
const r = await call("video", "share", { id }, ctxFrom(this));
|
|
1072
|
-
emit(this, r, (d) => process.stdout.write(`${d.share_url ?? "not shared"}\n`));
|
|
1073
|
-
} catch (e) { fail(e); }
|
|
1074
|
-
});
|
|
1075
|
-
|
|
1076
|
-
withCommonOpts(v.command("avatars"))
|
|
1077
|
-
.description("List the avatars curated for this workspace")
|
|
1078
|
-
.action(async function () {
|
|
1079
|
-
try {
|
|
1080
|
-
const r = await call("video", "avatars", {}, ctxFrom(this));
|
|
1081
|
-
emit(this, r, (d) => {
|
|
1082
|
-
process.stdout.write(table(
|
|
1083
|
-
["avatar_id", "name", "gender", "voice", "default"],
|
|
1084
|
-
(d.avatars || []).map((x) => [
|
|
1085
|
-
truncate(x.avatar_id, 20), truncate(x.avatar_name, 24), n(x.gender),
|
|
1086
|
-
truncate(n(x.default_voice_id), 20), x.is_default ? "yes" : "",
|
|
1087
|
-
]),
|
|
1088
|
-
) + `\n${d.avatars?.length ?? 0} rows\n`);
|
|
1089
|
-
if (d.note) process.stdout.write(`${d.note}\n`);
|
|
1090
|
-
});
|
|
1091
|
-
} catch (e) { fail(e); }
|
|
1092
|
-
});
|
|
1093
|
-
|
|
1094
|
-
withCommonOpts(v.command("create"))
|
|
1095
|
-
.description("Generate a new AI video (talking head or explainer)")
|
|
1096
|
-
.option("--prompt <text>", "brief or script context")
|
|
1097
|
-
.option("--source-url <url>", "generate an explainer from a web page")
|
|
1098
|
-
.option("--content-item <id>", "generate from a content item")
|
|
1099
|
-
.option("--kind <k>", "talking_head | explainer", "talking_head")
|
|
1100
|
-
.option("--avatar <id>", "avatar_id (see `video avatars`)")
|
|
1101
|
-
.option("--voice <id>", "voice_id")
|
|
1102
|
-
.option("--style <s>", "professional | casual | educational | inspirational")
|
|
1103
|
-
.option("--format <f>", "platform format (e.g. linkedin, shorts)")
|
|
1104
|
-
.option("--duration <n>", "target duration in seconds")
|
|
1105
|
-
.option("--script-mode <m>", "verbatim to use --prompt as the exact script")
|
|
1106
|
-
.action(async function () {
|
|
1107
|
-
try {
|
|
1108
|
-
const o = this.optsWithGlobals();
|
|
1109
|
-
if (!o.prompt && !o.sourceUrl && !o.contentItem) {
|
|
1110
|
-
throw new Error("Provide --prompt, --source-url or --content-item");
|
|
1111
|
-
}
|
|
1112
|
-
const r = await call("video", "create", {
|
|
1113
|
-
prompt: o.prompt,
|
|
1114
|
-
source_url: o.sourceUrl,
|
|
1115
|
-
content_item_id: o.contentItem,
|
|
1116
|
-
video_kind: o.kind,
|
|
1117
|
-
avatar_id: o.avatar,
|
|
1118
|
-
voice_id: o.voice,
|
|
1119
|
-
style: o.style,
|
|
1120
|
-
video_format: o.format,
|
|
1121
|
-
target_duration: o.duration ? Number(o.duration) : undefined,
|
|
1122
|
-
script_mode: o.scriptMode,
|
|
1123
|
-
}, ctxFrom(this));
|
|
1124
|
-
emit(this, r, (d) => process.stdout.write(JSON.stringify(d.video ?? d, null, 2) + "\n"));
|
|
1125
|
-
} catch (e) { fail(e); }
|
|
1126
|
-
});
|
|
1127
|
-
|
|
1128
|
-
withCommonOpts(v.command("update <id>"))
|
|
1129
|
-
.description("Update video metadata (title, episode label, CTA)")
|
|
1130
|
-
.option("--title <t>")
|
|
1131
|
-
.option("--episode <label>")
|
|
1132
|
-
.option("--cta-type <t>", "book_call | visit_site | subscribe | custom | none")
|
|
1133
|
-
.option("--cta-label <l>")
|
|
1134
|
-
.option("--cta-url <u>")
|
|
1135
|
-
.option("--summary <s>")
|
|
1136
|
-
.action(async function (id) {
|
|
1137
|
-
try {
|
|
1138
|
-
const o = this.optsWithGlobals();
|
|
1139
|
-
const r = await call("video", "update", {
|
|
1140
|
-
id, title: o.title, episode_label: o.episode,
|
|
1141
|
-
cta_type: o.ctaType, cta_label: o.ctaLabel, cta_url: o.ctaUrl, summary: o.summary,
|
|
1142
|
-
}, ctxFrom(this));
|
|
1143
|
-
emit(this, r, (d) => process.stdout.write(JSON.stringify(d.video, null, 2) + "\n"));
|
|
1144
|
-
} catch (e) { fail(e); }
|
|
1145
|
-
});
|
|
1146
|
-
|
|
1147
|
-
withCommonOpts(v.command("publish <id>"))
|
|
1148
|
-
.description("Make the video publicly shareable")
|
|
1149
|
-
.action(async function (id) {
|
|
1150
|
-
try {
|
|
1151
|
-
const r = await call("video", "publish", { id }, ctxFrom(this));
|
|
1152
|
-
emit(this, r, (d) => process.stdout.write(`${d.share_url ?? "published"}\n`));
|
|
1153
|
-
} catch (e) { fail(e); }
|
|
1154
|
-
});
|
|
1155
|
-
|
|
1156
|
-
withCommonOpts(v.command("unpublish <id>"))
|
|
1157
|
-
.description("Revoke the public share link")
|
|
1158
|
-
.action(async function (id) {
|
|
1159
|
-
try {
|
|
1160
|
-
const r = await call("video", "unpublish", { id }, ctxFrom(this));
|
|
1161
|
-
emit(this, r, () => process.stdout.write("unpublished\n"));
|
|
1162
|
-
} catch (e) { fail(e); }
|
|
1163
|
-
});
|
|
1164
|
-
|
|
1165
|
-
withCommonOpts(v.command("refresh <id>"))
|
|
1166
|
-
.description("Re-check the render status with the provider")
|
|
1167
|
-
.action(async function (id) {
|
|
1168
|
-
try {
|
|
1169
|
-
const r = await call("video", "refresh", { id }, ctxFrom(this));
|
|
1170
|
-
emit(this, r, (d) => process.stdout.write(JSON.stringify(d.video ?? d, null, 2) + "\n"));
|
|
1171
|
-
} catch (e) { fail(e); }
|
|
1172
|
-
});
|
|
1173
|
-
|
|
1174
|
-
withCommonOpts(v.command("delete <id>"))
|
|
1175
|
-
.description("Delete a video project")
|
|
1176
|
-
.option("--yes", "skip confirmation")
|
|
1177
|
-
.action(async function (id) {
|
|
1178
|
-
try {
|
|
1179
|
-
await confirmDestructive(this, `Delete video project ${id}. This cannot be undone.`);
|
|
1180
|
-
const r = await call("video", "delete", { id }, ctxFrom(this));
|
|
1181
|
-
emit(this, r, (d) => process.stdout.write(`deleted ${d.deleted}\n`));
|
|
1182
|
-
} catch (e) { fail(e); }
|
|
1183
|
-
});
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
export function registerWatchlist(program) {
|
|
1187
|
-
const w = program.command("watchlist").description("Intelligence watchlist (P0–P3 priorities)");
|
|
1188
|
-
|
|
1189
|
-
withCommonOpts(w.command("list"))
|
|
1190
|
-
.description("List watchlist items")
|
|
1191
|
-
.option("--all", "include inactive")
|
|
1192
|
-
.action(async function () {
|
|
1193
|
-
try {
|
|
1194
|
-
const o = this.optsWithGlobals();
|
|
1195
|
-
const r = await call("watchlist", "list", { active_only: !o.all }, ctxFrom(this));
|
|
1196
|
-
emit(this, r, (d) => {
|
|
1197
|
-
process.stdout.write(table(
|
|
1198
|
-
["id", "name", "type", "priority", "matches", "last match"],
|
|
1199
|
-
(d.items || []).map((x) => [
|
|
1200
|
-
truncate(x.id, 8), truncate(x.name, 28), n(x.type), n(x.priority),
|
|
1201
|
-
n(x.match_count), n(x.last_match_at).slice(0, 10),
|
|
1202
|
-
]),
|
|
1203
|
-
) + `\n${d.items?.length ?? 0} rows\n`);
|
|
1204
|
-
});
|
|
1205
|
-
} catch (e) { fail(e); }
|
|
1206
|
-
});
|
|
1207
|
-
|
|
1208
|
-
withCommonOpts(w.command("add"))
|
|
1209
|
-
.description("Add a watchlist item")
|
|
1210
|
-
.requiredOption("--name <name>", "company, person or topic")
|
|
1211
|
-
.option("--type <t>", "company|person|topic|competitor", "company")
|
|
1212
|
-
.option("--priority <p>", "P0|P1|P2|P3", "P2")
|
|
1213
|
-
.option("--keywords <list>", "comma-separated keywords")
|
|
1214
|
-
.action(async function () {
|
|
1215
|
-
try {
|
|
1216
|
-
const o = this.optsWithGlobals();
|
|
1217
|
-
const r = await call("watchlist", "add", {
|
|
1218
|
-
name: o.name, type: o.type, priority: o.priority,
|
|
1219
|
-
keywords: o.keywords ? o.keywords.split(",").map((s) => s.trim()) : [],
|
|
1220
|
-
}, ctxFrom(this));
|
|
1221
|
-
emit(this, r, (d) => process.stdout.write(`✔ Added: ${d.item?.id}\n`));
|
|
1222
|
-
} catch (e) { fail(e); }
|
|
1223
|
-
});
|
|
1224
|
-
|
|
1225
|
-
withCommonOpts(w.command("priority <id> <priority>"))
|
|
1226
|
-
.description("Set the priority tier (P0–P3)")
|
|
1227
|
-
.action(async function (id, priority) {
|
|
1228
|
-
try {
|
|
1229
|
-
const r = await call("watchlist", "update", { id, priority }, ctxFrom(this));
|
|
1230
|
-
emit(this, r, (d) => process.stdout.write(`✔ ${d.item?.name} → ${d.item?.priority}\n`));
|
|
1231
|
-
} catch (e) { fail(e); }
|
|
1232
|
-
});
|
|
1233
|
-
|
|
1234
|
-
withCommonOpts(w.command("show <id>"))
|
|
1235
|
-
.description("Show one watchlist item")
|
|
1236
|
-
.action(async function (id) {
|
|
1237
|
-
try {
|
|
1238
|
-
const r = await call("watchlist", "get", { id }, ctxFrom(this));
|
|
1239
|
-
emit(this, r, (d) => {
|
|
1240
|
-
const x = d.item || {};
|
|
1241
|
-
process.stdout.write(
|
|
1242
|
-
`${x.name}\n id: ${x.id}\n type: ${n(x.type)}\n priority: ${n(x.priority_label || x.priority)}\n` +
|
|
1243
|
-
` keywords: ${(x.keywords || []).join(", ") || "(none)"}\n active: ${x.is_active}\n matches: ${n(x.match_count)}\n`,
|
|
1244
|
-
);
|
|
1245
|
-
});
|
|
1246
|
-
} catch (e) { fail(e); }
|
|
1247
|
-
});
|
|
1248
|
-
|
|
1249
|
-
withCommonOpts(w.command("update <id>"))
|
|
1250
|
-
.description("Update a watchlist item")
|
|
1251
|
-
.option("--name <name>", "new name")
|
|
1252
|
-
.option("--type <t>", "company|person|topic|competitor|keyword")
|
|
1253
|
-
.option("--priority <p>", "P0|P1|P2|P3")
|
|
1254
|
-
.option("--keywords <list>", "comma-separated keywords (replaces the list)")
|
|
1255
|
-
.action(async function (id) {
|
|
1256
|
-
try {
|
|
1257
|
-
const o = this.optsWithGlobals();
|
|
1258
|
-
const body = { id };
|
|
1259
|
-
if (o.name) body.name = o.name;
|
|
1260
|
-
if (o.type) body.type = o.type;
|
|
1261
|
-
if (o.priority) body.priority = o.priority;
|
|
1262
|
-
if (o.keywords !== undefined) body.keywords = o.keywords.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1263
|
-
const r = await call("watchlist", "update", body, ctxFrom(this));
|
|
1264
|
-
emit(this, r, (d) => process.stdout.write(`✔ Updated ${d.item?.name}\n`));
|
|
1265
|
-
} catch (e) { fail(e); }
|
|
1266
|
-
});
|
|
1267
|
-
|
|
1268
|
-
withCommonOpts(w.command("tag <id> <keywords>"))
|
|
1269
|
-
.description("Add keyword tags (comma-separated) to a watchlist item")
|
|
1270
|
-
.action(async function (id, keywords) {
|
|
1271
|
-
try {
|
|
1272
|
-
const r = await call("watchlist", "add_keywords", {
|
|
1273
|
-
id, keywords: keywords.split(",").map((s) => s.trim()).filter(Boolean),
|
|
1274
|
-
}, ctxFrom(this));
|
|
1275
|
-
emit(this, r, (d) => process.stdout.write(`✔ ${d.item?.name}: ${(d.item?.keywords || []).join(", ")}\n`));
|
|
1276
|
-
} catch (e) { fail(e); }
|
|
1277
|
-
});
|
|
1278
|
-
|
|
1279
|
-
withCommonOpts(w.command("untag <id> <keywords>"))
|
|
1280
|
-
.description("Remove keyword tags (comma-separated) from a watchlist item")
|
|
1281
|
-
.action(async function (id, keywords) {
|
|
1282
|
-
try {
|
|
1283
|
-
const r = await call("watchlist", "remove_keywords", {
|
|
1284
|
-
id, keywords: keywords.split(",").map((s) => s.trim()).filter(Boolean),
|
|
1285
|
-
}, ctxFrom(this));
|
|
1286
|
-
emit(this, r, (d) => process.stdout.write(`✔ ${d.item?.name}: ${(d.item?.keywords || []).join(", ") || "(none)"}\n`));
|
|
1287
|
-
} catch (e) { fail(e); }
|
|
1288
|
-
});
|
|
1289
|
-
|
|
1290
|
-
withCommonOpts(w.command("toggle <id>"))
|
|
1291
|
-
.description("Activate or deactivate a watchlist item")
|
|
1292
|
-
.option("--on", "force active")
|
|
1293
|
-
.option("--off", "force inactive")
|
|
1294
|
-
.action(async function (id) {
|
|
1295
|
-
try {
|
|
1296
|
-
const o = this.optsWithGlobals();
|
|
1297
|
-
const body = { id };
|
|
1298
|
-
if (o.on) body.is_active = true;
|
|
1299
|
-
if (o.off) body.is_active = false;
|
|
1300
|
-
const r = await call("watchlist", "toggle", body, ctxFrom(this));
|
|
1301
|
-
emit(this, r, (d) => process.stdout.write(
|
|
1302
|
-
`✔ ${d.item?.name ?? id} → ${d.item?.is_active ? "active" : "inactive"} (id ${d.item?.id ?? id}, workspace ${d.workspace_id || "?"}, ${d.status || "ok"})\n`,
|
|
1303
|
-
));
|
|
1304
|
-
} catch (e) { fail(e); }
|
|
1305
|
-
});
|
|
1306
|
-
|
|
1307
|
-
withCommonOpts(w.command("suggest"))
|
|
1308
|
-
.description("Generate watchlist suggestions from the knowledge base")
|
|
1309
|
-
.action(async function () {
|
|
1310
|
-
try {
|
|
1311
|
-
const r = await call("watchlist", "suggest", {}, ctxFrom(this));
|
|
1312
|
-
emit(this, r, (d) => process.stdout.write(`✔ Added ${d.added ?? 0} suggested items\n`));
|
|
1313
|
-
} catch (e) { fail(e); }
|
|
1314
|
-
});
|
|
1315
|
-
|
|
1316
|
-
withCommonOpts(w.command("sync-prompts"))
|
|
1317
|
-
.description("Resync the agent intelligence prompts derived from the watchlist")
|
|
1318
|
-
.action(async function () {
|
|
1319
|
-
try {
|
|
1320
|
-
const r = await call("watchlist", "sync_prompts", {}, ctxFrom(this));
|
|
1321
|
-
emit(this, r, (d) => process.stdout.write(
|
|
1322
|
-
`✔ Prompts resynced — ${d.count ?? (d.prompts || []).length} prompt(s), workspace ${d.workspace_id || "?"} (${d.status || "ok"})\n` +
|
|
1323
|
-
(d.prompts || []).map((x) => ` - ${x.prompt_type || x.type || x.id}`).join("\n") + "\n",
|
|
1324
|
-
));
|
|
1325
|
-
} catch (e) { fail(e); }
|
|
1326
|
-
});
|
|
1327
|
-
|
|
1328
|
-
withCommonOpts(w.command("remove <id>"))
|
|
1329
|
-
.description("Remove a watchlist item (suppressed by default so it is not re-suggested)")
|
|
1330
|
-
.option("--hard", "permanently delete the row")
|
|
1331
|
-
.action(async function (id) {
|
|
1332
|
-
try {
|
|
1333
|
-
const o = this.optsWithGlobals();
|
|
1334
|
-
const r = await call("watchlist", "remove", { id, hard: !!o.hard }, ctxFrom(this));
|
|
1335
|
-
emit(this, r, () => process.stdout.write(`✔ Removed ${id}\n`));
|
|
1336
|
-
} catch (e) { fail(e); }
|
|
1337
|
-
});
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
export function registerIntelligencePrompts(program) {
|
|
1341
|
-
const p = program.command("intel-prompts")
|
|
1342
|
-
.description("Agent intelligence prompts (news, competitive, market, leads, content)");
|
|
1343
|
-
|
|
1344
|
-
withCommonOpts(p.command("list"))
|
|
1345
|
-
.description("List the workspace intelligence prompts")
|
|
1346
|
-
.action(async function () {
|
|
1347
|
-
try {
|
|
1348
|
-
const r = await call("intelligence_prompts", "list", {}, ctxFrom(this));
|
|
1349
|
-
emit(this, r, (d) => {
|
|
1350
|
-
process.stdout.write(table(
|
|
1351
|
-
["type", "source", "active", "prompt"],
|
|
1352
|
-
(d.prompts || []).map((x) => [
|
|
1353
|
-
n(x.prompt_type), x.is_auto === false ? "manual" : "auto",
|
|
1354
|
-
String(!!x.is_active), truncate(n(x.prompt_text).replace(/\s+/g, " "), 70),
|
|
1355
|
-
]),
|
|
1356
|
-
) + "\n");
|
|
1357
|
-
});
|
|
1358
|
-
} catch (e) { fail(e); }
|
|
1359
|
-
});
|
|
1360
|
-
|
|
1361
|
-
withCommonOpts(p.command("show <type>"))
|
|
1362
|
-
.description("Show one prompt in full")
|
|
1363
|
-
.action(async function (type) {
|
|
1364
|
-
try {
|
|
1365
|
-
const r = await call("intelligence_prompts", "get", { prompt_type: type }, ctxFrom(this));
|
|
1366
|
-
emit(this, r, (d) => process.stdout.write(`${n(d.prompt?.prompt_text)}\n`));
|
|
1367
|
-
} catch (e) { fail(e); }
|
|
1368
|
-
});
|
|
1369
|
-
|
|
1370
|
-
withCommonOpts(p.command("set <type>"))
|
|
1371
|
-
.description("Override a prompt with custom text (marks it manual)")
|
|
1372
|
-
.option("--text <text>", "prompt text")
|
|
1373
|
-
.option("--file <path>", "read prompt text from a file")
|
|
1374
|
-
.action(async function (type) {
|
|
1375
|
-
try {
|
|
1376
|
-
const o = this.optsWithGlobals();
|
|
1377
|
-
const text = o.file ? await readFile(o.file, "utf8") : o.text;
|
|
1378
|
-
if (!text) throw new Error("--text or --file required");
|
|
1379
|
-
const r = await call("intelligence_prompts", "set", { prompt_type: type, prompt_text: text }, ctxFrom(this));
|
|
1380
|
-
emit(this, r, (d) => process.stdout.write(`✔ ${d.prompt?.prompt_type} updated (manual override)\n`));
|
|
1381
|
-
} catch (e) { fail(e); }
|
|
1382
|
-
});
|
|
1383
|
-
|
|
1384
|
-
withCommonOpts(p.command("toggle <type>"))
|
|
1385
|
-
.description("Activate or deactivate a prompt")
|
|
1386
|
-
.option("--on", "force active")
|
|
1387
|
-
.option("--off", "force inactive")
|
|
1388
|
-
.action(async function (type) {
|
|
1389
|
-
try {
|
|
1390
|
-
const o = this.optsWithGlobals();
|
|
1391
|
-
const body = { prompt_type: type };
|
|
1392
|
-
if (o.on) body.is_active = true;
|
|
1393
|
-
if (o.off) body.is_active = false;
|
|
1394
|
-
const r = await call("intelligence_prompts", "toggle", body, ctxFrom(this));
|
|
1395
|
-
emit(this, r, (d) => process.stdout.write(`✔ ${d.prompt?.prompt_type} → ${d.prompt?.is_active ? "active" : "inactive"}\n`));
|
|
1396
|
-
} catch (e) { fail(e); }
|
|
1397
|
-
});
|
|
1398
|
-
|
|
1399
|
-
withCommonOpts(p.command("reset <type>"))
|
|
1400
|
-
.description("Reset a prompt back to auto (regenerated from the watchlist)")
|
|
1401
|
-
.action(async function (type) {
|
|
1402
|
-
try {
|
|
1403
|
-
const r = await call("intelligence_prompts", "reset", { prompt_type: type }, ctxFrom(this));
|
|
1404
|
-
emit(this, r, () => process.stdout.write(`✔ ${type} reset to auto\n`));
|
|
1405
|
-
} catch (e) { fail(e); }
|
|
1406
|
-
});
|
|
1407
|
-
|
|
1408
|
-
withCommonOpts(p.command("resync"))
|
|
1409
|
-
.description("Regenerate all auto prompts from the current watchlist")
|
|
1410
|
-
.action(async function () {
|
|
1411
|
-
try {
|
|
1412
|
-
const r = await call("intelligence_prompts", "resync", {}, ctxFrom(this));
|
|
1413
|
-
emit(this, r, (d) => process.stdout.write(
|
|
1414
|
-
`✔ Prompts resynced — ${d.count ?? (d.prompts || []).length} prompt(s), workspace ${d.workspace_id || "?"} (${d.status || "ok"})\n` +
|
|
1415
|
-
(d.prompts || []).map((x) => ` - ${x.prompt_type || x.type || x.id}`).join("\n") + "\n",
|
|
1416
|
-
));
|
|
1417
|
-
} catch (e) { fail(e); }
|
|
1418
|
-
});
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
export function registerMeetings(program) {
|
|
1422
|
-
const m = program.command("meetings").description("Meeting transcripts (anonymised)");
|
|
1423
|
-
|
|
1424
|
-
withCommonOpts(m.command("list"))
|
|
1425
|
-
.description("List transcripts")
|
|
1426
|
-
.option("--limit <n>", "max rows", "50")
|
|
1427
|
-
.action(async function () {
|
|
1428
|
-
try {
|
|
1429
|
-
const o = this.optsWithGlobals();
|
|
1430
|
-
const r = await call("meetings", "list", { limit: Number(o.limit) }, ctxFrom(this));
|
|
1431
|
-
emit(this, r, (d) => {
|
|
1432
|
-
process.stdout.write(table(
|
|
1433
|
-
["id", "name", "when"],
|
|
1434
|
-
(d.meetings || []).map((x) => [
|
|
1435
|
-
truncate(x.id, 8), truncate(x.name, 44), n(x.created_at).slice(0, 10),
|
|
1436
|
-
]),
|
|
1437
|
-
) + `\n${d.meetings?.length ?? 0} rows\n`);
|
|
1438
|
-
});
|
|
1439
|
-
} catch (e) { fail(e); }
|
|
1440
|
-
});
|
|
1441
|
-
|
|
1442
|
-
withCommonOpts(m.command("show <id>"))
|
|
1443
|
-
.description("Show a transcript (redacted unless anonymised)")
|
|
1444
|
-
.action(async function (id) {
|
|
1445
|
-
try {
|
|
1446
|
-
const r = await call("meetings", "get", { id }, ctxFrom(this));
|
|
1447
|
-
emit(this, r, (d) => process.stdout.write(JSON.stringify(d.meeting, null, 2) + "\n"));
|
|
1448
|
-
} catch (e) { fail(e); }
|
|
1449
|
-
});
|
|
1450
|
-
|
|
1451
|
-
withCommonOpts(m.command("transcripts"))
|
|
1452
|
-
.description("List meeting transcripts from the transcript store")
|
|
1453
|
-
.option("--provider <p>", "filter by provider")
|
|
1454
|
-
.option("--since <iso>", "only meetings on/after this date")
|
|
1455
|
-
.option("--limit <n>", "max rows", "50")
|
|
1456
|
-
.action(async function () {
|
|
1457
|
-
try {
|
|
1458
|
-
const o = this.optsWithGlobals();
|
|
1459
|
-
const r = await call("meetings", "transcripts", {
|
|
1460
|
-
provider: o.provider, since: o.since, limit: Number(o.limit),
|
|
1461
|
-
}, ctxFrom(this));
|
|
1462
|
-
emit(this, r, (d) => {
|
|
1463
|
-
process.stdout.write(table(
|
|
1464
|
-
["id", "title", "provider", "date", "mins", "state"],
|
|
1465
|
-
(d.transcripts || []).map((x) => [
|
|
1466
|
-
truncate(x.id, 8), truncate(x.title, 34), x.provider,
|
|
1467
|
-
n(x.meeting_date).slice(0, 10), String(x.duration_minutes ?? ""),
|
|
1468
|
-
x.anonymised_status ?? "",
|
|
1469
|
-
]),
|
|
1470
|
-
) + `\n${d.transcripts?.length ?? 0} rows\n`);
|
|
1471
|
-
});
|
|
1472
|
-
} catch (e) { fail(e); }
|
|
1473
|
-
});
|
|
1474
|
-
|
|
1475
|
-
withCommonOpts(m.command("transcript <id>"))
|
|
1476
|
-
.description("Show one transcript. Anonymised by default; --original needs the meetings:original permission")
|
|
1477
|
-
.option("--original", "return the original text and speaker identities (permission-gated, audited)")
|
|
1478
|
-
.option("--mode <mode>", "anonymised | original")
|
|
1479
|
-
.action(async function (id) {
|
|
1480
|
-
try {
|
|
1481
|
-
const o = this.optsWithGlobals();
|
|
1482
|
-
const mode = o.original ? "original" : (o.mode || "anonymised");
|
|
1483
|
-
const r = await call("meetings", "transcript", { id, mode }, ctxFrom(this));
|
|
1484
|
-
emit(this, r, (d) => process.stdout.write(JSON.stringify(d.transcript, null, 2) + "\n"));
|
|
1485
|
-
} catch (e) { fail(e); }
|
|
1486
|
-
});
|
|
1487
|
-
|
|
1488
|
-
withCommonOpts(m.command("sync"))
|
|
1489
|
-
.description("Trigger a provider transcript sync")
|
|
1490
|
-
.action(async function () {
|
|
1491
|
-
try {
|
|
1492
|
-
const r = await call("meetings", "sync", {}, ctxFrom(this));
|
|
1493
|
-
emit(this, r, (d) => process.stdout.write(d.triggered ? "✔ Sync triggered\n" : "✖ Sync failed\n"));
|
|
1494
|
-
} catch (e) { fail(e); }
|
|
1495
|
-
});
|
|
1496
|
-
|
|
1497
|
-
withCommonOpts(m.command("integrations"))
|
|
1498
|
-
.description("List connected meeting providers")
|
|
1499
|
-
.action(async function () {
|
|
1500
|
-
try {
|
|
1501
|
-
const r = await call("meetings", "integrations", {}, ctxFrom(this));
|
|
1502
|
-
emit(this, r, (d) => {
|
|
1503
|
-
process.stdout.write(table(
|
|
1504
|
-
["provider", "active", "last sync"],
|
|
1505
|
-
(d.integrations || []).map((x) => [x.provider, x.is_active ? "yes" : "no", n(x.last_synced_at)]),
|
|
1506
|
-
) + "\n");
|
|
1507
|
-
});
|
|
1508
|
-
} catch (e) { fail(e); }
|
|
1509
|
-
});
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
export function registerActivity(program) {
|
|
1513
|
-
withCommonOpts(program.command("activity"))
|
|
1514
|
-
.description("Recent activity across agents, campaigns and content")
|
|
1515
|
-
.option("--days <n>", "look-back window", "7")
|
|
1516
|
-
.option("--limit <n>", "max rows", "50")
|
|
1517
|
-
.action(async function () {
|
|
1518
|
-
try {
|
|
1519
|
-
const o = this.optsWithGlobals();
|
|
1520
|
-
const r = await call("activity", "list", { days: Number(o.days), limit: Number(o.limit) }, ctxFrom(this));
|
|
1521
|
-
emit(this, r, (d) => {
|
|
1522
|
-
process.stdout.write(table(
|
|
1523
|
-
["when", "kind", "type", "source"],
|
|
1524
|
-
(d.activity || []).map((x) => [
|
|
1525
|
-
n(x.at).replace("T", " ").slice(0, 19), x.kind, truncate(x.type, 28), truncate(x.source, 24),
|
|
1526
|
-
]),
|
|
1527
|
-
) + `\n${d.activity?.length ?? 0} rows\n`);
|
|
1528
|
-
});
|
|
1529
|
-
} catch (e) { fail(e); }
|
|
1530
|
-
});
|
|
1531
|
-
}
|
|
1532
|
-
|
|
1533
|
-
export function registerAudit(program) {
|
|
1534
|
-
const a = program.command("audit").description("Audit trail of CLI / agent / assistant actions");
|
|
1535
|
-
|
|
1536
|
-
withCommonOpts(a.command("list"))
|
|
1537
|
-
.description("List audited actions (newest first)")
|
|
1538
|
-
.option("--status <s>", "ok | denied | error")
|
|
1539
|
-
.option("--surface <s>", "cli | mcp | api | internal")
|
|
1540
|
-
.option("--resource <r>", "e.g. leads, campaigns, content")
|
|
1541
|
-
.option("--actor <email>", "filter by person")
|
|
1542
|
-
.option("--writes", "only actions that changed data")
|
|
1543
|
-
.option("--since <iso>", "ISO timestamp lower bound")
|
|
1544
|
-
.option("--until <iso>", "ISO timestamp upper bound")
|
|
1545
|
-
.option("--limit <n>", "max rows", "50")
|
|
1546
|
-
.action(async function () {
|
|
1547
|
-
try {
|
|
1548
|
-
const o = this.optsWithGlobals();
|
|
1549
|
-
const r = await call("audit", "list", {
|
|
1550
|
-
status: o.status, surface: o.surface, resource: o.resource,
|
|
1551
|
-
actor_email: o.actor, writes_only: !!o.writes,
|
|
1552
|
-
since: o.since, until: o.until, limit: Number(o.limit),
|
|
1553
|
-
}, ctxFrom(this));
|
|
1554
|
-
emit(this, r, (d) => {
|
|
1555
|
-
process.stdout.write(table(
|
|
1556
|
-
["when", "who", "from", "action", "status", "ms"],
|
|
1557
|
-
(d.entries || []).map((x) => [
|
|
1558
|
-
n(x.created_at).replace("T", " ").slice(0, 19),
|
|
1559
|
-
truncate(x.actor_email || x.agent_name || "-", 24),
|
|
1560
|
-
n(x.surface),
|
|
1561
|
-
truncate(`${x.resource}.${x.action}`, 28),
|
|
1562
|
-
n(x.status),
|
|
1563
|
-
n(x.duration_ms),
|
|
1564
|
-
]),
|
|
1565
|
-
) + `\n${d.entries?.length ?? 0} rows\n`);
|
|
1566
|
-
});
|
|
1567
|
-
} catch (e) { fail(e); }
|
|
1568
|
-
});
|
|
1569
|
-
|
|
1570
|
-
withCommonOpts(a.command("show <id>"))
|
|
1571
|
-
.description("Full request and outcome for one audited action")
|
|
1572
|
-
.action(async function (id) {
|
|
1573
|
-
try {
|
|
1574
|
-
const r = await call("audit", "get", { id }, ctxFrom(this));
|
|
1575
|
-
emit(this, r, (d) => process.stdout.write(JSON.stringify(d.entry, null, 2) + "\n"));
|
|
1576
|
-
} catch (e) { fail(e); }
|
|
1577
|
-
});
|
|
1578
|
-
|
|
1579
|
-
withCommonOpts(a.command("stats"))
|
|
1580
|
-
.description("Rolled-up audit summary")
|
|
1581
|
-
.option("--since <iso>", "ISO timestamp lower bound")
|
|
1582
|
-
.action(async function () {
|
|
1583
|
-
try {
|
|
1584
|
-
const o = this.optsWithGlobals();
|
|
1585
|
-
const r = await call("audit", "stats", { since: o.since }, ctxFrom(this));
|
|
1586
|
-
emit(this, r, (d) => process.stdout.write(JSON.stringify(d, null, 2) + "\n"));
|
|
1587
|
-
} catch (e) { fail(e); }
|
|
1588
|
-
});
|
|
1589
|
-
}
|
|
1590
|
-
|
|
1591
|
-
export function registerWorkspacesExtras(program) {
|
|
1592
|
-
const w = program.command("workspaces").description("Workspaces (admin keys only)");
|
|
1593
|
-
|
|
1594
|
-
withCommonOpts(w.command("list"))
|
|
1595
|
-
.description("List workspaces this key can reach")
|
|
1596
|
-
.action(async function () {
|
|
1597
|
-
try {
|
|
1598
|
-
const r = await call("workspaces", "list", {}, ctxFrom(this));
|
|
1599
|
-
emit(this, r, (d) => {
|
|
1600
|
-
process.stdout.write(table(
|
|
1601
|
-
["id", "name", "created"],
|
|
1602
|
-
(d.workspaces || []).map((x) => [x.id, truncate(x.name, 34), n(x.created_at).slice(0, 10)]),
|
|
1603
|
-
) + `\n${d.workspaces?.length ?? 0} rows\n`);
|
|
1604
|
-
});
|
|
1605
|
-
} catch (e) { fail(e); }
|
|
1606
|
-
});
|
|
1607
|
-
|
|
1608
|
-
withCommonOpts(w.command("members <id>"))
|
|
1609
|
-
.description("List members and roles of a workspace")
|
|
1610
|
-
.action(async function (id) {
|
|
1611
|
-
try {
|
|
1612
|
-
const r = await call("workspaces", "members", { id }, ctxFrom(this));
|
|
1613
|
-
emit(this, r, (d) => {
|
|
1614
|
-
process.stdout.write(table(
|
|
1615
|
-
["user_id", "role", "joined"],
|
|
1616
|
-
(d.members || []).map((x) => [x.user_id, x.role, n(x.created_at).slice(0, 10)]),
|
|
1617
|
-
) + `\n${d.members?.length ?? 0} members\n`);
|
|
1618
|
-
});
|
|
1619
|
-
} catch (e) { fail(e); }
|
|
1620
|
-
});
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
export function registerExport(program) {
|
|
1624
|
-
withCommonOpts(program.command("export <resource> <action>"))
|
|
1625
|
-
.description("Run any list action and write the rows to CSV")
|
|
1626
|
-
.option("--out <file>", "output file", "export.csv")
|
|
1627
|
-
.option("--params <json>", "extra params as JSON", "{}")
|
|
1628
|
-
.action(async function (resource, action) {
|
|
1629
|
-
try {
|
|
1630
|
-
const o = this.optsWithGlobals();
|
|
1631
|
-
let params = {};
|
|
1632
|
-
try { params = JSON.parse(o.params); } catch { throw new Error("--params must be valid JSON"); }
|
|
1633
|
-
const r = await call(resource, action, params, ctxFrom(this));
|
|
1634
|
-
const arrayKey = Object.keys(r).find((k) => Array.isArray(r[k]));
|
|
1635
|
-
if (!arrayKey) throw new Error("That action did not return a list of rows");
|
|
1636
|
-
const csv = toCsv(r[arrayKey]);
|
|
1637
|
-
await writeFile(o.out, csv, "utf8");
|
|
1638
|
-
process.stdout.write(`✔ Wrote ${r[arrayKey].length} rows to ${o.out}\n`);
|
|
1639
|
-
} catch (e) { fail(e); }
|
|
1640
|
-
});
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
export function registerIntegrations(program) {
|
|
1645
|
-
const i = program.command("integrations").description("Integration status (read-only)");
|
|
1646
|
-
|
|
1647
|
-
withCommonOpts(i.command("status"))
|
|
1648
|
-
.description("List every configured integration and its state")
|
|
1649
|
-
.action(async function () {
|
|
1650
|
-
try {
|
|
1651
|
-
const r = await call("integrations", "list", {}, ctxFrom(this));
|
|
1652
|
-
emit(this, r, (d) => {
|
|
1653
|
-
process.stdout.write(table(
|
|
1654
|
-
["kind", "platform", "name", "state", "last activity"],
|
|
1655
|
-
(d.integrations || []).map((x) => [
|
|
1656
|
-
x.kind, truncate(x.platform, 20), truncate(x.display_name, 24), x.state,
|
|
1657
|
-
n(x.last_synced_at || x.last_tested_at).slice(0, 16),
|
|
1658
|
-
]),
|
|
1659
|
-
) + `\n${d.connected ?? 0}/${d.total ?? 0} connected\n`);
|
|
1660
|
-
});
|
|
1661
|
-
} catch (e) { fail(e); }
|
|
1662
|
-
});
|
|
1663
|
-
|
|
1664
|
-
withCommonOpts(i.command("check <platform>"))
|
|
1665
|
-
.description("Health-check one integration (e.g. linkedin_tool, resend)")
|
|
1666
|
-
.action(async function (platform) {
|
|
1667
|
-
try {
|
|
1668
|
-
const r = await call("integrations", "check", { platform }, ctxFrom(this));
|
|
1669
|
-
emit(this, r, (d) => process.stdout.write(JSON.stringify(d, null, 2) + "\n"));
|
|
1670
|
-
} catch (e) { fail(e); }
|
|
1671
|
-
});
|
|
1672
|
-
|
|
1673
|
-
withCommonOpts(i.command("health"))
|
|
1674
|
-
.description("Live verdict: is Resend configured and are LinkedIn seats connected?")
|
|
1675
|
-
.action(async function () {
|
|
1676
|
-
try {
|
|
1677
|
-
const r = await call("integrations", "health", {}, ctxFrom(this));
|
|
1678
|
-
emit(this, r, (d) => {
|
|
1679
|
-
const flag = d.healthy ? "✔ healthy" : "✖ issues: " + (d.issues || []).join(", ");
|
|
1680
|
-
process.stdout.write(`${flag}\n`);
|
|
1681
|
-
process.stdout.write(table(
|
|
1682
|
-
["integration", "state", "detail"],
|
|
1683
|
-
[
|
|
1684
|
-
["resend", d.resend?.state ?? "unknown",
|
|
1685
|
-
d.resend?.configured ? `from=${d.resend?.from_email ?? "?"}` : (d.resend?.detail ?? "not configured")],
|
|
1686
|
-
...(d.linkedin?.seats?.length
|
|
1687
|
-
? d.linkedin.seats.map((s) => [
|
|
1688
|
-
`linkedin:${truncate(s.display_name || s.account_id || "?", 22)}`,
|
|
1689
|
-
s.state, s.connection_status ?? "",
|
|
1690
|
-
])
|
|
1691
|
-
: [["linkedin", d.linkedin?.verdict ?? "unknown", "no seats"]]),
|
|
1692
|
-
],
|
|
1693
|
-
) + "\n");
|
|
1694
|
-
});
|
|
1695
|
-
} catch (e) { fail(e); }
|
|
1696
|
-
});
|
|
1697
|
-
}
|
|
1698
|
-
|
|
1699
|
-
export function registerReports(program) {
|
|
1700
|
-
const r = program.command("reports").description("Daily workspace reports (history + on-demand generation)");
|
|
1701
|
-
|
|
1702
|
-
withCommonOpts(r.command("list"))
|
|
1703
|
-
.description("List past daily reports (newest first)")
|
|
1704
|
-
.option("--since <date>", "report date lower bound (YYYY-MM-DD)")
|
|
1705
|
-
.option("--until <date>", "report date upper bound (YYYY-MM-DD)")
|
|
1706
|
-
.option("--limit <n>", "max rows", "25")
|
|
1707
|
-
.action(async function () {
|
|
1708
|
-
try {
|
|
1709
|
-
const o = this.optsWithGlobals();
|
|
1710
|
-
const res = await call("daily_reports", "list", {
|
|
1711
|
-
since: o.since, until: o.until, limit: Number(o.limit),
|
|
1712
|
-
}, ctxFrom(this));
|
|
1713
|
-
emit(this, res, (d) => {
|
|
1714
|
-
process.stdout.write(table(
|
|
1715
|
-
["date", "status", "activity", "recipients", "sent", "report id"],
|
|
1716
|
-
(d.reports || []).map((x) => [
|
|
1717
|
-
n(x.report_date), n(x.status), n(x.activity_total),
|
|
1718
|
-
n(x.recipient_count), n(x.sent_count), truncate(x.id, 13),
|
|
1719
|
-
]),
|
|
1720
|
-
) + `\n${d.reports?.length ?? 0} rows\n`);
|
|
1721
|
-
});
|
|
1722
|
-
} catch (e) { fail(e); }
|
|
1723
|
-
});
|
|
1724
|
-
|
|
1725
|
-
withCommonOpts(r.command("show <idOrDate>"))
|
|
1726
|
-
.description("Show one daily report by id or date (YYYY-MM-DD)")
|
|
1727
|
-
.action(async function (idOrDate) {
|
|
1728
|
-
try {
|
|
1729
|
-
const isDate = /^\d{4}-\d{2}-\d{2}$/.test(idOrDate);
|
|
1730
|
-
const res = await call("daily_reports", "get", isDate ? { date: idOrDate } : { id: idOrDate }, ctxFrom(this));
|
|
1731
|
-
emit(this, res, (d) => {
|
|
1732
|
-
const x = d.report || {};
|
|
1733
|
-
process.stdout.write(`${x.london_date_label ?? x.report_date} — ${x.status}\n`);
|
|
1734
|
-
process.stdout.write(`activity: ${x.activity_total ?? 0} · recipients: ${x.recipient_count ?? 0} · sent: ${x.sent_count ?? 0}\n`);
|
|
1735
|
-
process.stdout.write(`url: ${x.report_url ?? "-"}\n`);
|
|
1736
|
-
process.stdout.write(JSON.stringify(x.stats ?? {}, null, 2) + "\n");
|
|
1737
|
-
});
|
|
1738
|
-
} catch (e) { fail(e); }
|
|
1739
|
-
});
|
|
1740
|
-
|
|
1741
|
-
withCommonOpts(r.command("generate"))
|
|
1742
|
-
.description("Generate today's report now (dry-run; --send to email workspace members)")
|
|
1743
|
-
.option("--send", "actually email workspace members (default: generate only)")
|
|
1744
|
-
.option("--to <email>", "send only to this test address")
|
|
1745
|
-
.action(async function () {
|
|
1746
|
-
try {
|
|
1747
|
-
const o = this.optsWithGlobals();
|
|
1748
|
-
const res = await call("daily_reports", "generate", {
|
|
1749
|
-
send: !!o.send, test_to: o.to,
|
|
1750
|
-
}, ctxFrom(this));
|
|
1751
|
-
emit(this, res, (d) => {
|
|
1752
|
-
const x = d.report || {};
|
|
1753
|
-
process.stdout.write(`report ${x.report_id ?? x.id ?? "?"} — sent: ${d.sent ?? 0}\n`);
|
|
1754
|
-
if (x.report_url) process.stdout.write(`url: ${x.report_url}\n`);
|
|
1755
|
-
});
|
|
1756
|
-
} catch (e) { fail(e); }
|
|
1757
|
-
});
|
|
1758
|
-
}
|