@series-a/cli 0.2.1 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/LICENSE +3 -2
- package/README.md +54 -4
- package/package.json +4 -4
- package/src/api.mjs +2 -1
- package/src/commands/insights.mjs +1034 -0
- package/src/commands/knowledge-sync.mjs +519 -0
- package/src/commands/resources.mjs +234 -8
- package/src/index.mjs +29 -2
|
@@ -0,0 +1,1034 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { call } from "../api.mjs";
|
|
3
|
+
import { emit, fail, ctxFrom, withCommonOpts, table, truncate } from "../ui.mjs";
|
|
4
|
+
|
|
5
|
+
const n = (v) => (v == null ? "" : String(v));
|
|
6
|
+
|
|
7
|
+
function findGroup(program, name) {
|
|
8
|
+
return program.commands.find((c) => c.name() === name);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function toCsv(rows) {
|
|
12
|
+
if (!rows.length) return "";
|
|
13
|
+
const headers = Array.from(
|
|
14
|
+
rows.reduce((set, r) => {
|
|
15
|
+
Object.keys(r).forEach((k) => set.add(k));
|
|
16
|
+
return set;
|
|
17
|
+
}, new Set()),
|
|
18
|
+
);
|
|
19
|
+
const esc = (v) => {
|
|
20
|
+
if (v == null) return "";
|
|
21
|
+
const s = typeof v === "object" ? JSON.stringify(v) : String(v);
|
|
22
|
+
return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
|
23
|
+
};
|
|
24
|
+
return [
|
|
25
|
+
headers.join(","),
|
|
26
|
+
...rows.map((r) => headers.map((h) => esc(r[h])).join(",")),
|
|
27
|
+
].join("\n") + "\n";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Campaign analytics subcommands, attached to the existing `campaigns` group. */
|
|
31
|
+
export function registerCampaignAnalytics(program) {
|
|
32
|
+
const c = findGroup(program, "campaigns");
|
|
33
|
+
if (!c) return;
|
|
34
|
+
|
|
35
|
+
withCommonOpts(c.command("stats <id>"))
|
|
36
|
+
.description("Campaign funnel: invites, accepts, messages, replies, meetings")
|
|
37
|
+
.option("--days <n>", "look-back window in days", "30")
|
|
38
|
+
.action(async function (id) {
|
|
39
|
+
try {
|
|
40
|
+
const o = this.optsWithGlobals();
|
|
41
|
+
const r = await call("campaigns", "stats", { id, days: Number(o.days) }, ctxFrom(this));
|
|
42
|
+
emit(this, r, (d) => {
|
|
43
|
+
process.stdout.write(
|
|
44
|
+
`\n${d.campaign?.name ?? id} (${d.campaign?.status ?? "?"}) — last ${o.days}d\n\n`,
|
|
45
|
+
);
|
|
46
|
+
process.stdout.write(table(
|
|
47
|
+
["metric", "value"],
|
|
48
|
+
[
|
|
49
|
+
["Leads enrolled", d.funnel.leads_enrolled],
|
|
50
|
+
["Invites sent", d.funnel.invites_sent],
|
|
51
|
+
["Accepted", `${d.funnel.accepts} (${d.rates.accept_rate}%)`],
|
|
52
|
+
["Messages sent", d.funnel.messages_sent],
|
|
53
|
+
["Replies", `${d.funnel.replies} (${d.rates.reply_rate}%)`],
|
|
54
|
+
["Positive replies", `${d.funnel.positive_replies} (${d.rates.positive_rate}%)`],
|
|
55
|
+
["Meetings booked", `${d.funnel.meetings_booked} (${d.rates.meeting_rate}%)`],
|
|
56
|
+
["Failures", d.funnel.failures],
|
|
57
|
+
],
|
|
58
|
+
) + "\n");
|
|
59
|
+
});
|
|
60
|
+
} catch (e) { fail(e); }
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
withCommonOpts(c.command("events <id>"))
|
|
64
|
+
.description("Campaign event log")
|
|
65
|
+
.option("--type <t>", "filter by event_type")
|
|
66
|
+
.option("--since <iso>", "only events after this timestamp")
|
|
67
|
+
.option("--limit <n>", "max rows", "100")
|
|
68
|
+
.action(async function (id) {
|
|
69
|
+
try {
|
|
70
|
+
const o = this.optsWithGlobals();
|
|
71
|
+
const r = await call(
|
|
72
|
+
"campaigns", "events",
|
|
73
|
+
{ id, type: o.type, since: o.since, limit: Number(o.limit) },
|
|
74
|
+
ctxFrom(this),
|
|
75
|
+
);
|
|
76
|
+
emit(this, r, (d) => {
|
|
77
|
+
process.stdout.write(table(
|
|
78
|
+
["when", "event", "step", "lead", "sender"],
|
|
79
|
+
(d.events || []).map((e) => [
|
|
80
|
+
n(e.created_at).replace("T", " ").slice(0, 19),
|
|
81
|
+
e.event_type, n(e.step_order), truncate(e.lead_id, 8), truncate(e.sender_account, 14),
|
|
82
|
+
]),
|
|
83
|
+
) + `\n${d.events?.length ?? 0} rows\n`);
|
|
84
|
+
});
|
|
85
|
+
} catch (e) { fail(e); }
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
withCommonOpts(c.command("health <id>"))
|
|
89
|
+
.description("Execution health: run state, stalls, retries, watchdog findings")
|
|
90
|
+
.action(async function (id) {
|
|
91
|
+
try {
|
|
92
|
+
const r = await call("campaigns", "health", { id }, ctxFrom(this));
|
|
93
|
+
emit(this, r, (d) => {
|
|
94
|
+
const h = d.health;
|
|
95
|
+
process.stdout.write(
|
|
96
|
+
h
|
|
97
|
+
? `\nStatus: ${h.health_status} ${h.reason ? `(${h.reason})` : ""}\n` +
|
|
98
|
+
`Last progress: ${n(h.last_progress_at) || "never"}\n` +
|
|
99
|
+
`Next attempt: ${n(h.next_attempt_at) || "—"}\n` +
|
|
100
|
+
`Retries: ${h.retry_count ?? 0}${h.last_error ? `\nLast error: ${h.last_error}` : ""}\n\n`
|
|
101
|
+
: "\nNo health record yet.\n\n",
|
|
102
|
+
);
|
|
103
|
+
process.stdout.write(table(
|
|
104
|
+
["run", "status", "step", "pause reason", "fails", "next attempt"],
|
|
105
|
+
(d.runs || []).map((r2) => [
|
|
106
|
+
truncate(r2.id, 8), r2.status, `${r2.current_step ?? 0}/${r2.total_steps ?? 0}`,
|
|
107
|
+
truncate(r2.pause_reason, 24), r2.consecutive_failure_count ?? 0,
|
|
108
|
+
n(r2.next_attempt_at).replace("T", " ").slice(0, 19),
|
|
109
|
+
]),
|
|
110
|
+
) + "\n");
|
|
111
|
+
if (d.watchdog_findings?.length) {
|
|
112
|
+
process.stdout.write("\nWatchdog findings:\n" + table(
|
|
113
|
+
["severity", "title", "seen", "resolved"],
|
|
114
|
+
d.watchdog_findings.map((f) => [
|
|
115
|
+
f.severity, truncate(f.title, 40), f.occurrences,
|
|
116
|
+
f.resolved_at ? "yes" : "no",
|
|
117
|
+
]),
|
|
118
|
+
) + "\n");
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
} catch (e) { fail(e); }
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
withCommonOpts(c.command("leads <id>"))
|
|
125
|
+
.description("Leads enrolled in a campaign with their current state")
|
|
126
|
+
.option("--status <s>", "filter by enrolment status")
|
|
127
|
+
.option("--limit <n>", "max rows", "100")
|
|
128
|
+
.action(async function (id) {
|
|
129
|
+
try {
|
|
130
|
+
const o = this.optsWithGlobals();
|
|
131
|
+
const r = await call(
|
|
132
|
+
"campaigns", "leads",
|
|
133
|
+
{ id, status: o.status, limit: Number(o.limit) },
|
|
134
|
+
ctxFrom(this),
|
|
135
|
+
);
|
|
136
|
+
emit(this, r, (d) => {
|
|
137
|
+
process.stdout.write(table(
|
|
138
|
+
["lead", "name", "company", "enrolment", "conversation"],
|
|
139
|
+
(d.leads || []).map((l) => [
|
|
140
|
+
truncate(l.lead_id, 8), truncate(l.lead?.name, 24), truncate(l.lead?.company, 22),
|
|
141
|
+
l.status, n(l.lead?.conversation_stage),
|
|
142
|
+
]),
|
|
143
|
+
) + `\n${d.leads?.length ?? 0} rows\n`);
|
|
144
|
+
});
|
|
145
|
+
} catch (e) { fail(e); }
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function registerAnalytics(program) {
|
|
150
|
+
const a = program.command("analytics").description("Workspace and campaign analytics");
|
|
151
|
+
|
|
152
|
+
withCommonOpts(a.command("overview"))
|
|
153
|
+
.description("Outreach, content and pipeline results for the workspace")
|
|
154
|
+
.option("--days <n>", "look-back window", "30")
|
|
155
|
+
.action(async function () {
|
|
156
|
+
try {
|
|
157
|
+
const o = this.optsWithGlobals();
|
|
158
|
+
const r = await call("analytics", "overview", { days: Number(o.days) }, ctxFrom(this));
|
|
159
|
+
emit(this, r, (d) => {
|
|
160
|
+
const m = d.outreach;
|
|
161
|
+
process.stdout.write(`\nLast ${d.period_days} days\n\n`);
|
|
162
|
+
process.stdout.write(table(["outreach", "value"], [
|
|
163
|
+
["Touches", m.touches], ["Invites sent", m.invites_sent],
|
|
164
|
+
["Accepts", `${m.accepts} (${m.rates.accept_rate}%)`],
|
|
165
|
+
["Messages sent", m.messages_sent],
|
|
166
|
+
["Replies", `${m.replies} (${m.rates.reply_rate}%)`],
|
|
167
|
+
["Positive replies", `${m.positive_replies} (${m.rates.positive_rate}%)`],
|
|
168
|
+
["Meetings booked", m.meetings_booked], ["Meetings held", m.meetings_held],
|
|
169
|
+
]) + "\n");
|
|
170
|
+
process.stdout.write(table(["content", "value"], [
|
|
171
|
+
["Posts distributed", d.content.posts_distributed],
|
|
172
|
+
["Impressions", d.content.impressions],
|
|
173
|
+
["Engagement", `${d.content.engagement} (${d.content.engagement_rate}%)`],
|
|
174
|
+
["Link clicks", d.content.link_clicks],
|
|
175
|
+
]) + "\n");
|
|
176
|
+
process.stdout.write(table(["pipeline", "value"], [
|
|
177
|
+
["Opportunities created", d.pipeline.opportunities_created],
|
|
178
|
+
["Pipeline value", d.pipeline.pipeline_value],
|
|
179
|
+
["Closed won", `${d.pipeline.closed_won_count} / ${d.pipeline.closed_won_value}`],
|
|
180
|
+
]) + "\n");
|
|
181
|
+
});
|
|
182
|
+
} catch (e) { fail(e); }
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
withCommonOpts(a.command("campaigns"))
|
|
186
|
+
.description("Compare every campaign on the same funnel")
|
|
187
|
+
.option("--days <n>", "look-back window", "30")
|
|
188
|
+
.option("--limit <n>", "max campaigns", "25")
|
|
189
|
+
.action(async function () {
|
|
190
|
+
try {
|
|
191
|
+
const o = this.optsWithGlobals();
|
|
192
|
+
const r = await call(
|
|
193
|
+
"analytics", "campaigns",
|
|
194
|
+
{ days: Number(o.days), limit: Number(o.limit) },
|
|
195
|
+
ctxFrom(this),
|
|
196
|
+
);
|
|
197
|
+
emit(this, r, (d) => {
|
|
198
|
+
process.stdout.write(table(
|
|
199
|
+
["campaign", "status", "leads", "invites", "accepts", "msgs", "replies", "pos", "mtgs"],
|
|
200
|
+
(d.campaigns || []).map((c) => [
|
|
201
|
+
truncate(c.name, 26), c.status, n(c.leads_enrolled), n(c.invites_sent),
|
|
202
|
+
n(c.accepts), n(c.messages_sent), n(c.replies), n(c.positive_replies),
|
|
203
|
+
n(c.meetings_booked),
|
|
204
|
+
]),
|
|
205
|
+
) + "\n");
|
|
206
|
+
});
|
|
207
|
+
} catch (e) { fail(e); }
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
withCommonOpts(a.command("content"))
|
|
211
|
+
.description("Per-post performance: impressions, engagement, clicks")
|
|
212
|
+
.option("--days <n>", "look-back window", "30")
|
|
213
|
+
.option("--limit <n>", "max rows", "50")
|
|
214
|
+
.action(async function () {
|
|
215
|
+
try {
|
|
216
|
+
const o = this.optsWithGlobals();
|
|
217
|
+
const r = await call(
|
|
218
|
+
"analytics", "content",
|
|
219
|
+
{ days: Number(o.days), limit: Number(o.limit) },
|
|
220
|
+
ctxFrom(this),
|
|
221
|
+
);
|
|
222
|
+
emit(this, r, (d) => {
|
|
223
|
+
process.stdout.write(table(
|
|
224
|
+
["title", "platform", "impressions", "eng", "eng %", "clicks"],
|
|
225
|
+
(d.posts || []).map((p) => [
|
|
226
|
+
truncate(p.title, 34), p.platform, n(p.impressions), n(p.engagement),
|
|
227
|
+
n(p.engagement_rate), n(p.reference_clicks),
|
|
228
|
+
]),
|
|
229
|
+
) + `\n${d.posts?.length ?? 0} rows\n`);
|
|
230
|
+
});
|
|
231
|
+
} catch (e) { fail(e); }
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
withCommonOpts(a.command("report"))
|
|
235
|
+
.description("Full branded performance report (summary, funnel, content, pipeline)")
|
|
236
|
+
.option("--days <n>", "look-back window", "30")
|
|
237
|
+
.option("--format <fmt>", "md | json", "md")
|
|
238
|
+
.option("--out <file>", "write the report to a file instead of stdout")
|
|
239
|
+
.action(async function () {
|
|
240
|
+
try {
|
|
241
|
+
const o = this.optsWithGlobals();
|
|
242
|
+
const r = await call("analytics", "report", { days: Number(o.days) }, ctxFrom(this));
|
|
243
|
+
if (o.json && !o.out) return emit(this, r, () => {});
|
|
244
|
+
const body = o.format === "json"
|
|
245
|
+
? JSON.stringify(r, null, 2)
|
|
246
|
+
: reportMarkdown(r);
|
|
247
|
+
if (o.out) {
|
|
248
|
+
await writeFile(o.out, body, "utf8");
|
|
249
|
+
process.stdout.write(`✔ Wrote report to ${o.out}\n`);
|
|
250
|
+
} else {
|
|
251
|
+
process.stdout.write(body + "\n");
|
|
252
|
+
}
|
|
253
|
+
} catch (e) { fail(e); }
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
withCommonOpts(a.command("export"))
|
|
257
|
+
.description("Export workspace analytics as CSV or JSON (overview | campaigns | content | leads | pipeline)")
|
|
258
|
+
.option("--dataset <name>", "overview | campaigns | content | leads | pipeline", "overview")
|
|
259
|
+
.option("--format <fmt>", "csv | json", "csv")
|
|
260
|
+
.option("--days <n>", "look-back window", "30")
|
|
261
|
+
.option("--limit <n>", "max rows for row-level datasets")
|
|
262
|
+
.option("--out <file>", "write to a file instead of stdout")
|
|
263
|
+
.action(async function () {
|
|
264
|
+
try {
|
|
265
|
+
const o = this.optsWithGlobals();
|
|
266
|
+
const r = await call("analytics", "export", {
|
|
267
|
+
dataset: o.dataset,
|
|
268
|
+
format: o.format,
|
|
269
|
+
days: Number(o.days),
|
|
270
|
+
...(o.limit ? { limit: Number(o.limit) } : {}),
|
|
271
|
+
}, ctxFrom(this));
|
|
272
|
+
const out = o.out === true ? r.filename : o.out;
|
|
273
|
+
if (out) {
|
|
274
|
+
await writeFile(out, r.content ?? "", "utf8");
|
|
275
|
+
process.stdout.write(`✔ Wrote ${r.row_count} ${r.dataset} row(s) to ${out}\n`);
|
|
276
|
+
} else {
|
|
277
|
+
process.stdout.write((r.content ?? "") + "\n");
|
|
278
|
+
}
|
|
279
|
+
} catch (e) { fail(e); }
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
function reportMarkdown(d) {
|
|
285
|
+
const m = d.outreach ?? {};
|
|
286
|
+
const c = d.content ?? {};
|
|
287
|
+
const p = d.pipeline ?? {};
|
|
288
|
+
const rows = (hdrs, data) => [
|
|
289
|
+
`| ${hdrs.join(" | ")} |`,
|
|
290
|
+
`| ${hdrs.map(() => "---").join(" | ")} |`,
|
|
291
|
+
...data.map((r) => `| ${r.map((v) => n(v)).join(" | ")} |`),
|
|
292
|
+
].join("\n");
|
|
293
|
+
|
|
294
|
+
return [
|
|
295
|
+
`# AdvisorPro — Performance Report`,
|
|
296
|
+
``,
|
|
297
|
+
`**Workspace:** ${d.workspace?.name ?? d.workspace?.id} `,
|
|
298
|
+
`**Period:** last ${d.period_days} days (since ${n(d.since).slice(0, 10)}) `,
|
|
299
|
+
`**Generated:** ${n(d.generated_at).slice(0, 16).replace("T", " ")} UTC`,
|
|
300
|
+
``,
|
|
301
|
+
`## Performance summary`,
|
|
302
|
+
``,
|
|
303
|
+
...(d.summary ?? []).map((s) => `- ${s}`),
|
|
304
|
+
``,
|
|
305
|
+
`## Outreach`,
|
|
306
|
+
``,
|
|
307
|
+
rows(["Metric", "Value"], [
|
|
308
|
+
["Touches", m.touches], ["Invites sent", m.invites_sent],
|
|
309
|
+
["Accepts", `${n(m.accepts)} (${n(m.rates?.accept_rate)}%)`],
|
|
310
|
+
["Messages sent", m.messages_sent],
|
|
311
|
+
["Replies", `${n(m.replies)} (${n(m.rates?.reply_rate)}%)`],
|
|
312
|
+
["Positive replies", `${n(m.positive_replies)} (${n(m.rates?.positive_rate)}%)`],
|
|
313
|
+
["Meetings booked", m.meetings_booked], ["Meetings held", m.meetings_held],
|
|
314
|
+
]),
|
|
315
|
+
``,
|
|
316
|
+
`## Campaigns`,
|
|
317
|
+
``,
|
|
318
|
+
rows(["Campaign", "Status", "Leads", "Invites", "Accepts", "Messages", "Replies", "Positive", "Meetings"],
|
|
319
|
+
(d.campaigns ?? []).map((x) => [
|
|
320
|
+
x.name, x.status, x.leads_enrolled, x.invites_sent, x.accepts,
|
|
321
|
+
x.messages_sent, x.replies, x.positive_replies, x.meetings_booked,
|
|
322
|
+
])),
|
|
323
|
+
``,
|
|
324
|
+
`## Content`,
|
|
325
|
+
``,
|
|
326
|
+
rows(["Metric", "Value"], [
|
|
327
|
+
["Posts distributed", c.posts_distributed], ["Impressions", c.impressions],
|
|
328
|
+
["Engagement", `${n(c.engagement)} (${n(c.engagement_rate)}%)`],
|
|
329
|
+
["Link clicks", c.link_clicks],
|
|
330
|
+
]),
|
|
331
|
+
``,
|
|
332
|
+
rows(["Top posts", "Platform", "Impressions", "Engagement", "Clicks"],
|
|
333
|
+
(d.top_content ?? []).map((x) => [
|
|
334
|
+
x.title ?? x.content_item_id, x.platform, x.impressions, x.engagement, x.reference_clicks,
|
|
335
|
+
])),
|
|
336
|
+
``,
|
|
337
|
+
`## Revenue impact`,
|
|
338
|
+
``,
|
|
339
|
+
rows(["Metric", "Value"], [
|
|
340
|
+
["Opportunities created", p.opportunities_created],
|
|
341
|
+
["Pipeline value", p.pipeline_value],
|
|
342
|
+
["Closed won", `${n(p.closed_won_count)} / ${n(p.closed_won_value)}`],
|
|
343
|
+
]),
|
|
344
|
+
``,
|
|
345
|
+
`---`,
|
|
346
|
+
`Generated by AdvisorPro CLI.`,
|
|
347
|
+
].join("\n");
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
export function registerSafety(program) {
|
|
352
|
+
const s = program.command("safety").description("LinkedIn safety guardrails (read-only)");
|
|
353
|
+
|
|
354
|
+
withCommonOpts(s.command("limits"))
|
|
355
|
+
.description("Per-seat caps, usage and headroom")
|
|
356
|
+
.action(async function () {
|
|
357
|
+
try {
|
|
358
|
+
const r = await call("safety", "limits", {}, ctxFrom(this));
|
|
359
|
+
emit(this, r, (d) => {
|
|
360
|
+
process.stdout.write(`\nGovernor: ${d.governor} · resets ${d.resets_at}\n\n`);
|
|
361
|
+
process.stdout.write(table(
|
|
362
|
+
["account", "cooldown", "invites today", "messages today", "resume at"],
|
|
363
|
+
(d.seats || []).map((x) => [
|
|
364
|
+
truncate(x.account_name || x.account_id, 26),
|
|
365
|
+
x.cooldown_active ? (x.cooldown_reason || "yes") : "clear",
|
|
366
|
+
x.sent_today.invites, x.sent_today.messages, n(x.resume_at),
|
|
367
|
+
]),
|
|
368
|
+
) + "\n");
|
|
369
|
+
});
|
|
370
|
+
} catch (e) { fail(e); }
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
withCommonOpts(s.command("reads"))
|
|
374
|
+
.description("Read budgets and the safe bulk-import size")
|
|
375
|
+
.option("--account <id>", "include a specific LinkedIn account")
|
|
376
|
+
.action(async function () {
|
|
377
|
+
try {
|
|
378
|
+
const o = this.optsWithGlobals();
|
|
379
|
+
const r = await call("safety", "reads", { account_id: o.account }, ctxFrom(this));
|
|
380
|
+
emit(this, r, (d) => {
|
|
381
|
+
const b = d.budgets;
|
|
382
|
+
const rows = [
|
|
383
|
+
["Workspace daily", b.workspace_daily.used, b.workspace_daily.limit, b.workspace_daily.remaining],
|
|
384
|
+
["Workspace bulk", b.workspace_bulk.used, b.workspace_bulk.limit, b.workspace_bulk.remaining],
|
|
385
|
+
["Shared pool (bulk)", b.shared_pool.used, b.shared_pool.limit, b.shared_pool.bulk_remaining],
|
|
386
|
+
];
|
|
387
|
+
if (b.account) {
|
|
388
|
+
rows.push(["Account reads", b.account.reads.used, b.account.reads.limit, b.account.reads.remaining]);
|
|
389
|
+
rows.push(["Account bulk", b.account.bulk.used, b.account.bulk.limit, b.account.bulk.remaining]);
|
|
390
|
+
}
|
|
391
|
+
process.stdout.write(table(["budget", "used", "limit", "remaining"], rows) + "\n");
|
|
392
|
+
process.stdout.write(`\nSafe bulk import size right now: ${d.safe_bulk_import_size} leads\nResets ${d.resets_at}\n`);
|
|
393
|
+
});
|
|
394
|
+
} catch (e) { fail(e); }
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
withCommonOpts(s.command("seats"))
|
|
398
|
+
.description("Connected LinkedIn seats and their health")
|
|
399
|
+
.action(async function () {
|
|
400
|
+
try {
|
|
401
|
+
const r = await call("safety", "seats", {}, ctxFrom(this));
|
|
402
|
+
emit(this, r, (d) => {
|
|
403
|
+
process.stdout.write(table(
|
|
404
|
+
["account", "id", "cooldown", "reason", "resume at", "strikes"],
|
|
405
|
+
(d.seats || []).map((x) => [
|
|
406
|
+
truncate(x.account_name, 22), truncate(x.account_id, 12),
|
|
407
|
+
x.cooldown_active ? "active" : "clear", truncate(x.cooldown_reason, 22),
|
|
408
|
+
n(x.resume_at), n(x.backoff?.strikes),
|
|
409
|
+
]),
|
|
410
|
+
) + "\n");
|
|
411
|
+
});
|
|
412
|
+
} catch (e) { fail(e); }
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
withCommonOpts(s.command("cooldowns"))
|
|
416
|
+
.description("Active cooldowns and holds")
|
|
417
|
+
.action(async function () {
|
|
418
|
+
try {
|
|
419
|
+
const r = await call("safety", "cooldowns", {}, ctxFrom(this));
|
|
420
|
+
emit(this, r, (d) => {
|
|
421
|
+
process.stdout.write(table(
|
|
422
|
+
["account", "source", "class", "reason", "clears at"],
|
|
423
|
+
(d.active || []).map((x) => [
|
|
424
|
+
truncate(x.account_name || x.account_id, 24), x.source, n(x.class),
|
|
425
|
+
truncate(x.reason, 30), n(x.resume_at),
|
|
426
|
+
]),
|
|
427
|
+
) + `\n${d.active?.length ?? 0} active\n`);
|
|
428
|
+
});
|
|
429
|
+
} catch (e) { fail(e); }
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
withCommonOpts(s.command("check"))
|
|
433
|
+
.description("Preflight a LinkedIn action against current guardrails")
|
|
434
|
+
.requiredOption("--account <id>", "LinkedIn account id")
|
|
435
|
+
.option("--action <name>", "action name", "sendMessage")
|
|
436
|
+
.action(async function () {
|
|
437
|
+
try {
|
|
438
|
+
const o = this.optsWithGlobals();
|
|
439
|
+
const r = await call(
|
|
440
|
+
"safety", "check",
|
|
441
|
+
{ account_id: o.account, action_name: o.action },
|
|
442
|
+
ctxFrom(this),
|
|
443
|
+
);
|
|
444
|
+
emit(this, r, (d) => {
|
|
445
|
+
process.stdout.write(
|
|
446
|
+
`\n${d.allowed ? "ALLOWED" : "BLOCKED"} — ${d.action} on ${d.account_id}\n` +
|
|
447
|
+
(d.allowed ? "" : `Reason: ${d.blocked_reason}\nRetry at: ${n(d.retry_at)}\n`) +
|
|
448
|
+
`\n${d.note}\n`,
|
|
449
|
+
);
|
|
450
|
+
});
|
|
451
|
+
} catch (e) { fail(e); }
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
withCommonOpts(s.command("policy"))
|
|
455
|
+
.description("Governor policy and enforcement mode")
|
|
456
|
+
.action(async function () {
|
|
457
|
+
try {
|
|
458
|
+
const r = await call("safety", "policy", {}, ctxFrom(this));
|
|
459
|
+
emit(this, r, (d) => process.stdout.write(JSON.stringify(d, null, 2) + "\n"));
|
|
460
|
+
} catch (e) { fail(e); }
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export function registerCtas(program) {
|
|
465
|
+
const c = program.command("ctas").description("Knowledge-base CTAs used in content and sequences");
|
|
466
|
+
|
|
467
|
+
withCommonOpts(c.command("list"))
|
|
468
|
+
.description("List CTAs")
|
|
469
|
+
.option("--all", "include inactive")
|
|
470
|
+
.action(async function () {
|
|
471
|
+
try {
|
|
472
|
+
const o = this.optsWithGlobals();
|
|
473
|
+
const r = await call("ctas", "list", { active_only: !o.all }, ctxFrom(this));
|
|
474
|
+
emit(this, r, (d) => {
|
|
475
|
+
process.stdout.write(table(
|
|
476
|
+
["id", "label", "url", "type", "primary", "active"],
|
|
477
|
+
(d.ctas || []).map((x) => [
|
|
478
|
+
truncate(x.id, 8), truncate(x.label, 26), truncate(x.url, 40),
|
|
479
|
+
n(x.type), x.is_primary ? "yes" : "", x.is_active ? "yes" : "no",
|
|
480
|
+
]),
|
|
481
|
+
) + `\n${d.ctas?.length ?? 0} rows\n`);
|
|
482
|
+
});
|
|
483
|
+
} catch (e) { fail(e); }
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
withCommonOpts(c.command("create"))
|
|
487
|
+
.description("Create a CTA")
|
|
488
|
+
.requiredOption("--label <text>", "button/link label")
|
|
489
|
+
.requiredOption("--url <url>", "destination URL")
|
|
490
|
+
.option("--type <t>", "cta type", "link")
|
|
491
|
+
.option("--primary", "mark as primary CTA")
|
|
492
|
+
.option("--sort <n>", "sort order", "0")
|
|
493
|
+
.action(async function () {
|
|
494
|
+
try {
|
|
495
|
+
const o = this.optsWithGlobals();
|
|
496
|
+
const r = await call("ctas", "create", {
|
|
497
|
+
label: o.label, url: o.url, type: o.type,
|
|
498
|
+
is_primary: !!o.primary, sort_order: Number(o.sort),
|
|
499
|
+
}, ctxFrom(this));
|
|
500
|
+
emit(this, r, (d) => process.stdout.write(`✔ CTA created: ${d.cta?.id}\n`));
|
|
501
|
+
} catch (e) { fail(e); }
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
withCommonOpts(c.command("update <id>"))
|
|
505
|
+
.description("Update a CTA")
|
|
506
|
+
.option("--label <text>", "label")
|
|
507
|
+
.option("--url <url>", "url")
|
|
508
|
+
.option("--type <t>", "type")
|
|
509
|
+
.option("--sort <n>", "sort order")
|
|
510
|
+
.option("--primary <bool>", "primary flag (true/false)")
|
|
511
|
+
.action(async function (id) {
|
|
512
|
+
try {
|
|
513
|
+
const o = this.optsWithGlobals();
|
|
514
|
+
const patch = { id };
|
|
515
|
+
if (o.label) patch.label = o.label;
|
|
516
|
+
if (o.url) patch.url = o.url;
|
|
517
|
+
if (o.type) patch.type = o.type;
|
|
518
|
+
if (o.sort !== undefined) patch.sort_order = Number(o.sort);
|
|
519
|
+
if (o.primary !== undefined) patch.is_primary = o.primary === "true";
|
|
520
|
+
const r = await call("ctas", "update", patch, ctxFrom(this));
|
|
521
|
+
emit(this, r, (d) => process.stdout.write(`✔ CTA updated: ${d.cta?.id}\n`));
|
|
522
|
+
} catch (e) { fail(e); }
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
withCommonOpts(c.command("deactivate <id>"))
|
|
526
|
+
.description("Deactivate a CTA")
|
|
527
|
+
.action(async function (id) {
|
|
528
|
+
try {
|
|
529
|
+
const r = await call("ctas", "deactivate", { id }, ctxFrom(this));
|
|
530
|
+
emit(this, r, () => process.stdout.write(`✔ CTA deactivated: ${id}\n`));
|
|
531
|
+
} catch (e) { fail(e); }
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
export function registerNotifications(program) {
|
|
536
|
+
const nCmd = program.command("notifications").description("Workspace notifications");
|
|
537
|
+
|
|
538
|
+
withCommonOpts(nCmd.command("list"))
|
|
539
|
+
.description("List notifications")
|
|
540
|
+
.option("--unread", "unread only")
|
|
541
|
+
.option("--type <t>", "filter by type")
|
|
542
|
+
.option("--limit <n>", "max rows", "50")
|
|
543
|
+
.action(async function () {
|
|
544
|
+
try {
|
|
545
|
+
const o = this.optsWithGlobals();
|
|
546
|
+
const r = await call(
|
|
547
|
+
"notifications", "list",
|
|
548
|
+
{ unread_only: !!o.unread, type: o.type, limit: Number(o.limit) },
|
|
549
|
+
ctxFrom(this),
|
|
550
|
+
);
|
|
551
|
+
emit(this, r, (d) => {
|
|
552
|
+
process.stdout.write(table(
|
|
553
|
+
["when", "type", "title", "read"],
|
|
554
|
+
(d.notifications || []).map((x) => [
|
|
555
|
+
n(x.created_at).replace("T", " ").slice(0, 16), truncate(x.type, 22),
|
|
556
|
+
truncate(x.title, 42), x.is_read ? "yes" : "no",
|
|
557
|
+
]),
|
|
558
|
+
) + `\n${d.notifications?.length ?? 0} rows\n`);
|
|
559
|
+
});
|
|
560
|
+
} catch (e) { fail(e); }
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
withCommonOpts(nCmd.command("read <ids...>"))
|
|
564
|
+
.description("Mark notifications as read")
|
|
565
|
+
.action(async function (ids) {
|
|
566
|
+
try {
|
|
567
|
+
const r = await call("notifications", "mark_read", { ids }, ctxFrom(this));
|
|
568
|
+
emit(this, r, (d) => process.stdout.write(`✔ ${d.updated} marked read\n`));
|
|
569
|
+
} catch (e) { fail(e); }
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
export function registerAudiences(program) {
|
|
574
|
+
const a = program.command("audiences").description("Smart audiences / lead segments");
|
|
575
|
+
|
|
576
|
+
withCommonOpts(a.command("list"))
|
|
577
|
+
.description("List audiences")
|
|
578
|
+
.action(async function () {
|
|
579
|
+
try {
|
|
580
|
+
const r = await call("audiences", "list", {}, ctxFrom(this));
|
|
581
|
+
emit(this, r, (d) => {
|
|
582
|
+
process.stdout.write(table(
|
|
583
|
+
["id", "name", "type", "leads", "updated"],
|
|
584
|
+
(d.audiences || []).map((x) => [
|
|
585
|
+
truncate(x.id, 8), truncate(x.name, 30), n(x.segment_type),
|
|
586
|
+
n(x.lead_count), n(x.updated_at).slice(0, 10),
|
|
587
|
+
]),
|
|
588
|
+
) + `\n${d.audiences?.length ?? 0} rows\n`);
|
|
589
|
+
});
|
|
590
|
+
} catch (e) { fail(e); }
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
withCommonOpts(a.command("members <id>"))
|
|
594
|
+
.description("List the leads in an audience")
|
|
595
|
+
.option("--limit <n>", "max rows", "100")
|
|
596
|
+
.action(async function (id) {
|
|
597
|
+
try {
|
|
598
|
+
const o = this.optsWithGlobals();
|
|
599
|
+
const r = await call("audiences", "members", { id, limit: Number(o.limit) }, ctxFrom(this));
|
|
600
|
+
emit(this, r, (d) => {
|
|
601
|
+
process.stdout.write(table(
|
|
602
|
+
["id", "name", "title", "company"],
|
|
603
|
+
(d.leads || []).map((l) => [
|
|
604
|
+
truncate(l.id, 8), truncate(l.name, 24), truncate(l.title, 26), truncate(l.company, 22),
|
|
605
|
+
]),
|
|
606
|
+
) + `\n${d.count} members\n`);
|
|
607
|
+
});
|
|
608
|
+
} catch (e) { fail(e); }
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
withCommonOpts(a.command("add-to-campaign <id> <campaignId>"))
|
|
612
|
+
.description("Enrol every lead in an audience into a campaign")
|
|
613
|
+
.action(async function (id, campaignId) {
|
|
614
|
+
try {
|
|
615
|
+
const r = await call("audiences", "add_to_campaign", { id, campaign_id: campaignId }, ctxFrom(this));
|
|
616
|
+
emit(this, r, (d) => process.stdout.write(`✔ Added ${d.added} of ${d.candidates} leads\n`));
|
|
617
|
+
} catch (e) { fail(e); }
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
export function registerNewsletter(program) {
|
|
622
|
+
const nl = program.command("newsletter").description("Newsletters, audiences and contacts");
|
|
623
|
+
|
|
624
|
+
withCommonOpts(nl.command("list"))
|
|
625
|
+
.description("List newsletters")
|
|
626
|
+
.option("--limit <n>", "max rows", "50")
|
|
627
|
+
.action(async function () {
|
|
628
|
+
try {
|
|
629
|
+
const o = this.optsWithGlobals();
|
|
630
|
+
const r = await call("newsletter", "list", { limit: Number(o.limit) }, ctxFrom(this));
|
|
631
|
+
emit(this, r, (d) => {
|
|
632
|
+
process.stdout.write(table(
|
|
633
|
+
["id", "title", "status", "scheduled", "published"],
|
|
634
|
+
(d.newsletters || []).map((x) => [
|
|
635
|
+
truncate(x.id, 8), truncate(x.title, 36), x.status,
|
|
636
|
+
n(x.scheduled_date).slice(0, 16), n(x.published_at).slice(0, 10),
|
|
637
|
+
]),
|
|
638
|
+
) + `\n${d.newsletters?.length ?? 0} rows\n`);
|
|
639
|
+
});
|
|
640
|
+
} catch (e) { fail(e); }
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
withCommonOpts(nl.command("show <id>"))
|
|
644
|
+
.description("Show one newsletter")
|
|
645
|
+
.action(async function (id) {
|
|
646
|
+
try {
|
|
647
|
+
const r = await call("newsletter", "get", { id }, ctxFrom(this));
|
|
648
|
+
emit(this, r, (d) => process.stdout.write(JSON.stringify(d.newsletter, null, 2) + "\n"));
|
|
649
|
+
} catch (e) { fail(e); }
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
withCommonOpts(nl.command("create"))
|
|
653
|
+
.description("Create a newsletter draft")
|
|
654
|
+
.requiredOption("--subject <s>", "email subject")
|
|
655
|
+
.option("--preview <s>", "preview text")
|
|
656
|
+
.option("--body <html>", "HTML body")
|
|
657
|
+
.option("--file <path>", "read the HTML body from a file")
|
|
658
|
+
.action(async function () {
|
|
659
|
+
try {
|
|
660
|
+
const o = this.optsWithGlobals();
|
|
661
|
+
const body = o.file ? await readFile(o.file, "utf8") : (o.body ?? "");
|
|
662
|
+
const r = await call("newsletter", "create", {
|
|
663
|
+
subject: o.subject, preview_text: o.preview, body,
|
|
664
|
+
}, ctxFrom(this));
|
|
665
|
+
emit(this, r, (d) => process.stdout.write(`✔ Created draft ${d.newsletter?.id}\n`));
|
|
666
|
+
} catch (e) { fail(e); }
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
withCommonOpts(nl.command("edit <id>"))
|
|
670
|
+
.description("Update a newsletter draft")
|
|
671
|
+
.option("--subject <s>").option("--preview <s>").option("--body <html>")
|
|
672
|
+
.option("--file <path>", "read the HTML body from a file")
|
|
673
|
+
.option("--status <s>", "draft | scheduled")
|
|
674
|
+
.action(async function (id) {
|
|
675
|
+
try {
|
|
676
|
+
const o = this.optsWithGlobals();
|
|
677
|
+
const params = { id };
|
|
678
|
+
if (o.subject !== undefined) params.subject = o.subject;
|
|
679
|
+
if (o.preview !== undefined) params.preview_text = o.preview;
|
|
680
|
+
if (o.status !== undefined) params.status = o.status;
|
|
681
|
+
if (o.file) params.body = await readFile(o.file, "utf8");
|
|
682
|
+
else if (o.body !== undefined) params.body = o.body;
|
|
683
|
+
const r = await call("newsletter", "update", params, ctxFrom(this));
|
|
684
|
+
emit(this, r, (d) => process.stdout.write(`✔ Updated ${d.newsletter?.id ?? id}\n`));
|
|
685
|
+
} catch (e) { fail(e); }
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
withCommonOpts(nl.command("delete <id>"))
|
|
689
|
+
.description("Delete a newsletter draft")
|
|
690
|
+
.action(async function (id) {
|
|
691
|
+
try {
|
|
692
|
+
const r = await call("newsletter", "delete", { id }, ctxFrom(this));
|
|
693
|
+
emit(this, r, () => process.stdout.write(`✔ Deleted ${id}\n`));
|
|
694
|
+
} catch (e) { fail(e); }
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
withCommonOpts(nl.command("schedule <id> <isoTimestamp>"))
|
|
698
|
+
.description("Schedule a newsletter")
|
|
699
|
+
.action(async function (id, ts) {
|
|
700
|
+
try {
|
|
701
|
+
const r = await call("newsletter", "schedule", { id, scheduled_date: ts }, ctxFrom(this));
|
|
702
|
+
emit(this, r, (d) => process.stdout.write(`✔ Scheduled for ${d.newsletter?.scheduled_date}\n`));
|
|
703
|
+
} catch (e) { fail(e); }
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
withCommonOpts(nl.command("audiences"))
|
|
707
|
+
.description("List newsletter audiences")
|
|
708
|
+
.action(async function () {
|
|
709
|
+
try {
|
|
710
|
+
const r = await call("newsletter", "audiences", {}, ctxFrom(this));
|
|
711
|
+
emit(this, r, (d) => {
|
|
712
|
+
process.stdout.write(table(
|
|
713
|
+
["id", "name", "description"],
|
|
714
|
+
(d.audiences || []).map((x) => [truncate(x.id, 8), truncate(x.name, 28), truncate(x.description, 40)]),
|
|
715
|
+
) + "\n");
|
|
716
|
+
});
|
|
717
|
+
} catch (e) { fail(e); }
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
withCommonOpts(nl.command("contacts"))
|
|
721
|
+
.description("List newsletter contacts")
|
|
722
|
+
.option("--audience <id>", "filter by audience")
|
|
723
|
+
.option("--limit <n>", "max rows", "100")
|
|
724
|
+
.action(async function () {
|
|
725
|
+
try {
|
|
726
|
+
const o = this.optsWithGlobals();
|
|
727
|
+
const r = await call(
|
|
728
|
+
"newsletter", "contacts",
|
|
729
|
+
{ audience_id: o.audience, limit: Number(o.limit) },
|
|
730
|
+
ctxFrom(this),
|
|
731
|
+
);
|
|
732
|
+
emit(this, r, (d) => {
|
|
733
|
+
process.stdout.write(table(
|
|
734
|
+
["email", "name", "unsubscribed", "source"],
|
|
735
|
+
(d.contacts || []).map((x) => [
|
|
736
|
+
truncate(x.email, 34), truncate(`${n(x.first_name)} ${n(x.last_name)}`.trim(), 24),
|
|
737
|
+
x.unsubscribed ? "yes" : "", n(x.source),
|
|
738
|
+
]),
|
|
739
|
+
) + `\n${d.contacts?.length ?? 0} rows\n`);
|
|
740
|
+
});
|
|
741
|
+
} catch (e) { fail(e); }
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
export function registerVideo(program) {
|
|
746
|
+
const v = program.command("video").description("Video projects");
|
|
747
|
+
|
|
748
|
+
withCommonOpts(v.command("list"))
|
|
749
|
+
.description("List video projects")
|
|
750
|
+
.option("--status <s>", "filter by status")
|
|
751
|
+
.option("--limit <n>", "max rows", "50")
|
|
752
|
+
.action(async function () {
|
|
753
|
+
try {
|
|
754
|
+
const o = this.optsWithGlobals();
|
|
755
|
+
const r = await call("video", "list", { status: o.status, limit: Number(o.limit) }, ctxFrom(this));
|
|
756
|
+
emit(this, r, (d) => {
|
|
757
|
+
process.stdout.write(table(
|
|
758
|
+
["id", "title", "status", "progress", "published"],
|
|
759
|
+
(d.videos || []).map((x) => [
|
|
760
|
+
truncate(x.id, 8), truncate(x.title, 32), x.status,
|
|
761
|
+
n(x.progress), n(x.published_at).slice(0, 10),
|
|
762
|
+
]),
|
|
763
|
+
) + `\n${d.videos?.length ?? 0} rows\n`);
|
|
764
|
+
});
|
|
765
|
+
} catch (e) { fail(e); }
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
withCommonOpts(v.command("show <id>"))
|
|
769
|
+
.description("Show a video project and its render status")
|
|
770
|
+
.action(async function (id) {
|
|
771
|
+
try {
|
|
772
|
+
const r = await call("video", "get", { id }, ctxFrom(this));
|
|
773
|
+
emit(this, r, (d) => process.stdout.write(JSON.stringify(d.video, null, 2) + "\n"));
|
|
774
|
+
} catch (e) { fail(e); }
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
withCommonOpts(v.command("share <id>"))
|
|
778
|
+
.description("Get the public share link for a video")
|
|
779
|
+
.action(async function (id) {
|
|
780
|
+
try {
|
|
781
|
+
const r = await call("video", "share", { id }, ctxFrom(this));
|
|
782
|
+
emit(this, r, (d) => process.stdout.write(`${d.share_url ?? "not shared"}\n`));
|
|
783
|
+
} catch (e) { fail(e); }
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
export function registerWatchlist(program) {
|
|
788
|
+
const w = program.command("watchlist").description("Intelligence watchlist (P0–P3 priorities)");
|
|
789
|
+
|
|
790
|
+
withCommonOpts(w.command("list"))
|
|
791
|
+
.description("List watchlist items")
|
|
792
|
+
.option("--all", "include inactive")
|
|
793
|
+
.action(async function () {
|
|
794
|
+
try {
|
|
795
|
+
const o = this.optsWithGlobals();
|
|
796
|
+
const r = await call("watchlist", "list", { active_only: !o.all }, ctxFrom(this));
|
|
797
|
+
emit(this, r, (d) => {
|
|
798
|
+
process.stdout.write(table(
|
|
799
|
+
["id", "name", "type", "priority", "matches", "last match"],
|
|
800
|
+
(d.items || []).map((x) => [
|
|
801
|
+
truncate(x.id, 8), truncate(x.name, 28), n(x.type), n(x.priority),
|
|
802
|
+
n(x.match_count), n(x.last_match_at).slice(0, 10),
|
|
803
|
+
]),
|
|
804
|
+
) + `\n${d.items?.length ?? 0} rows\n`);
|
|
805
|
+
});
|
|
806
|
+
} catch (e) { fail(e); }
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
withCommonOpts(w.command("add"))
|
|
810
|
+
.description("Add a watchlist item")
|
|
811
|
+
.requiredOption("--name <name>", "company, person or topic")
|
|
812
|
+
.option("--type <t>", "company|person|topic|competitor", "company")
|
|
813
|
+
.option("--priority <p>", "P0|P1|P2|P3", "P2")
|
|
814
|
+
.option("--keywords <list>", "comma-separated keywords")
|
|
815
|
+
.action(async function () {
|
|
816
|
+
try {
|
|
817
|
+
const o = this.optsWithGlobals();
|
|
818
|
+
const r = await call("watchlist", "add", {
|
|
819
|
+
name: o.name, type: o.type, priority: o.priority,
|
|
820
|
+
keywords: o.keywords ? o.keywords.split(",").map((s) => s.trim()) : [],
|
|
821
|
+
}, ctxFrom(this));
|
|
822
|
+
emit(this, r, (d) => process.stdout.write(`✔ Added: ${d.item?.id}\n`));
|
|
823
|
+
} catch (e) { fail(e); }
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
withCommonOpts(w.command("priority <id> <priority>"))
|
|
827
|
+
.description("Set the priority tier (P0–P3)")
|
|
828
|
+
.action(async function (id, priority) {
|
|
829
|
+
try {
|
|
830
|
+
const r = await call("watchlist", "update", { id, priority }, ctxFrom(this));
|
|
831
|
+
emit(this, r, (d) => process.stdout.write(`✔ ${d.item?.name} → ${d.item?.priority}\n`));
|
|
832
|
+
} catch (e) { fail(e); }
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
withCommonOpts(w.command("remove <id>"))
|
|
836
|
+
.description("Remove a watchlist item")
|
|
837
|
+
.action(async function (id) {
|
|
838
|
+
try {
|
|
839
|
+
const r = await call("watchlist", "remove", { id }, ctxFrom(this));
|
|
840
|
+
emit(this, r, () => process.stdout.write(`✔ Removed ${id}\n`));
|
|
841
|
+
} catch (e) { fail(e); }
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
export function registerMeetings(program) {
|
|
846
|
+
const m = program.command("meetings").description("Meeting transcripts (anonymised)");
|
|
847
|
+
|
|
848
|
+
withCommonOpts(m.command("list"))
|
|
849
|
+
.description("List transcripts")
|
|
850
|
+
.option("--limit <n>", "max rows", "50")
|
|
851
|
+
.action(async function () {
|
|
852
|
+
try {
|
|
853
|
+
const o = this.optsWithGlobals();
|
|
854
|
+
const r = await call("meetings", "list", { limit: Number(o.limit) }, ctxFrom(this));
|
|
855
|
+
emit(this, r, (d) => {
|
|
856
|
+
process.stdout.write(table(
|
|
857
|
+
["id", "name", "when"],
|
|
858
|
+
(d.meetings || []).map((x) => [
|
|
859
|
+
truncate(x.id, 8), truncate(x.name, 44), n(x.created_at).slice(0, 10),
|
|
860
|
+
]),
|
|
861
|
+
) + `\n${d.meetings?.length ?? 0} rows\n`);
|
|
862
|
+
});
|
|
863
|
+
} catch (e) { fail(e); }
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
withCommonOpts(m.command("show <id>"))
|
|
867
|
+
.description("Show a transcript (redacted unless anonymised)")
|
|
868
|
+
.action(async function (id) {
|
|
869
|
+
try {
|
|
870
|
+
const r = await call("meetings", "get", { id }, ctxFrom(this));
|
|
871
|
+
emit(this, r, (d) => process.stdout.write(JSON.stringify(d.meeting, null, 2) + "\n"));
|
|
872
|
+
} catch (e) { fail(e); }
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
withCommonOpts(m.command("transcripts"))
|
|
876
|
+
.description("List meeting transcripts from the transcript store")
|
|
877
|
+
.option("--provider <p>", "filter by provider")
|
|
878
|
+
.option("--since <iso>", "only meetings on/after this date")
|
|
879
|
+
.option("--limit <n>", "max rows", "50")
|
|
880
|
+
.action(async function () {
|
|
881
|
+
try {
|
|
882
|
+
const o = this.optsWithGlobals();
|
|
883
|
+
const r = await call("meetings", "transcripts", {
|
|
884
|
+
provider: o.provider, since: o.since, limit: Number(o.limit),
|
|
885
|
+
}, ctxFrom(this));
|
|
886
|
+
emit(this, r, (d) => {
|
|
887
|
+
process.stdout.write(table(
|
|
888
|
+
["id", "title", "provider", "date", "mins", "state"],
|
|
889
|
+
(d.transcripts || []).map((x) => [
|
|
890
|
+
truncate(x.id, 8), truncate(x.title, 34), x.provider,
|
|
891
|
+
n(x.meeting_date).slice(0, 10), String(x.duration_minutes ?? ""),
|
|
892
|
+
x.anonymised_status ?? "",
|
|
893
|
+
]),
|
|
894
|
+
) + `\n${d.transcripts?.length ?? 0} rows\n`);
|
|
895
|
+
});
|
|
896
|
+
} catch (e) { fail(e); }
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
withCommonOpts(m.command("transcript <id>"))
|
|
900
|
+
.description("Show one transcript (redacted unless anonymised)")
|
|
901
|
+
.action(async function (id) {
|
|
902
|
+
try {
|
|
903
|
+
const r = await call("meetings", "transcript", { id }, ctxFrom(this));
|
|
904
|
+
emit(this, r, (d) => process.stdout.write(JSON.stringify(d.transcript, null, 2) + "\n"));
|
|
905
|
+
} catch (e) { fail(e); }
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
withCommonOpts(m.command("sync"))
|
|
909
|
+
.description("Trigger a provider transcript sync")
|
|
910
|
+
.action(async function () {
|
|
911
|
+
try {
|
|
912
|
+
const r = await call("meetings", "sync", {}, ctxFrom(this));
|
|
913
|
+
emit(this, r, (d) => process.stdout.write(d.triggered ? "✔ Sync triggered\n" : "✖ Sync failed\n"));
|
|
914
|
+
} catch (e) { fail(e); }
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
withCommonOpts(m.command("integrations"))
|
|
918
|
+
.description("List connected meeting providers")
|
|
919
|
+
.action(async function () {
|
|
920
|
+
try {
|
|
921
|
+
const r = await call("meetings", "integrations", {}, ctxFrom(this));
|
|
922
|
+
emit(this, r, (d) => {
|
|
923
|
+
process.stdout.write(table(
|
|
924
|
+
["provider", "active", "last sync"],
|
|
925
|
+
(d.integrations || []).map((x) => [x.provider, x.is_active ? "yes" : "no", n(x.last_synced_at)]),
|
|
926
|
+
) + "\n");
|
|
927
|
+
});
|
|
928
|
+
} catch (e) { fail(e); }
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
export function registerActivity(program) {
|
|
933
|
+
withCommonOpts(program.command("activity"))
|
|
934
|
+
.description("Recent activity across agents, campaigns and content")
|
|
935
|
+
.option("--days <n>", "look-back window", "7")
|
|
936
|
+
.option("--limit <n>", "max rows", "50")
|
|
937
|
+
.action(async function () {
|
|
938
|
+
try {
|
|
939
|
+
const o = this.optsWithGlobals();
|
|
940
|
+
const r = await call("activity", "list", { days: Number(o.days), limit: Number(o.limit) }, ctxFrom(this));
|
|
941
|
+
emit(this, r, (d) => {
|
|
942
|
+
process.stdout.write(table(
|
|
943
|
+
["when", "kind", "type", "source"],
|
|
944
|
+
(d.activity || []).map((x) => [
|
|
945
|
+
n(x.at).replace("T", " ").slice(0, 19), x.kind, truncate(x.type, 28), truncate(x.source, 24),
|
|
946
|
+
]),
|
|
947
|
+
) + `\n${d.activity?.length ?? 0} rows\n`);
|
|
948
|
+
});
|
|
949
|
+
} catch (e) { fail(e); }
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
export function registerWorkspacesExtras(program) {
|
|
954
|
+
const w = program.command("workspaces").description("Workspaces (admin keys only)");
|
|
955
|
+
|
|
956
|
+
withCommonOpts(w.command("list"))
|
|
957
|
+
.description("List workspaces this key can reach")
|
|
958
|
+
.action(async function () {
|
|
959
|
+
try {
|
|
960
|
+
const r = await call("workspaces", "list", {}, ctxFrom(this));
|
|
961
|
+
emit(this, r, (d) => {
|
|
962
|
+
process.stdout.write(table(
|
|
963
|
+
["id", "name", "created"],
|
|
964
|
+
(d.workspaces || []).map((x) => [x.id, truncate(x.name, 34), n(x.created_at).slice(0, 10)]),
|
|
965
|
+
) + `\n${d.workspaces?.length ?? 0} rows\n`);
|
|
966
|
+
});
|
|
967
|
+
} catch (e) { fail(e); }
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
withCommonOpts(w.command("members <id>"))
|
|
971
|
+
.description("List members and roles of a workspace")
|
|
972
|
+
.action(async function (id) {
|
|
973
|
+
try {
|
|
974
|
+
const r = await call("workspaces", "members", { id }, ctxFrom(this));
|
|
975
|
+
emit(this, r, (d) => {
|
|
976
|
+
process.stdout.write(table(
|
|
977
|
+
["user_id", "role", "joined"],
|
|
978
|
+
(d.members || []).map((x) => [x.user_id, x.role, n(x.created_at).slice(0, 10)]),
|
|
979
|
+
) + `\n${d.members?.length ?? 0} members\n`);
|
|
980
|
+
});
|
|
981
|
+
} catch (e) { fail(e); }
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
export function registerExport(program) {
|
|
986
|
+
withCommonOpts(program.command("export <resource> <action>"))
|
|
987
|
+
.description("Run any list action and write the rows to CSV")
|
|
988
|
+
.option("--out <file>", "output file", "export.csv")
|
|
989
|
+
.option("--params <json>", "extra params as JSON", "{}")
|
|
990
|
+
.action(async function (resource, action) {
|
|
991
|
+
try {
|
|
992
|
+
const o = this.optsWithGlobals();
|
|
993
|
+
let params = {};
|
|
994
|
+
try { params = JSON.parse(o.params); } catch { throw new Error("--params must be valid JSON"); }
|
|
995
|
+
const r = await call(resource, action, params, ctxFrom(this));
|
|
996
|
+
const arrayKey = Object.keys(r).find((k) => Array.isArray(r[k]));
|
|
997
|
+
if (!arrayKey) throw new Error("That action did not return a list of rows");
|
|
998
|
+
const csv = toCsv(r[arrayKey]);
|
|
999
|
+
await writeFile(o.out, csv, "utf8");
|
|
1000
|
+
process.stdout.write(`✔ Wrote ${r[arrayKey].length} rows to ${o.out}\n`);
|
|
1001
|
+
} catch (e) { fail(e); }
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
|
|
1006
|
+
export function registerIntegrations(program) {
|
|
1007
|
+
const i = program.command("integrations").description("Integration status (read-only)");
|
|
1008
|
+
|
|
1009
|
+
withCommonOpts(i.command("status"))
|
|
1010
|
+
.description("List every configured integration and its state")
|
|
1011
|
+
.action(async function () {
|
|
1012
|
+
try {
|
|
1013
|
+
const r = await call("integrations", "list", {}, ctxFrom(this));
|
|
1014
|
+
emit(this, r, (d) => {
|
|
1015
|
+
process.stdout.write(table(
|
|
1016
|
+
["kind", "platform", "name", "state", "last activity"],
|
|
1017
|
+
(d.integrations || []).map((x) => [
|
|
1018
|
+
x.kind, truncate(x.platform, 20), truncate(x.display_name, 24), x.state,
|
|
1019
|
+
n(x.last_synced_at || x.last_tested_at).slice(0, 16),
|
|
1020
|
+
]),
|
|
1021
|
+
) + `\n${d.connected ?? 0}/${d.total ?? 0} connected\n`);
|
|
1022
|
+
});
|
|
1023
|
+
} catch (e) { fail(e); }
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
withCommonOpts(i.command("check <platform>"))
|
|
1027
|
+
.description("Health-check one integration")
|
|
1028
|
+
.action(async function (platform) {
|
|
1029
|
+
try {
|
|
1030
|
+
const r = await call("integrations", "check", { platform }, ctxFrom(this));
|
|
1031
|
+
emit(this, r, (d) => process.stdout.write(JSON.stringify(d, null, 2) + "\n"));
|
|
1032
|
+
} catch (e) { fail(e); }
|
|
1033
|
+
});
|
|
1034
|
+
}
|