@series-a/cli 0.15.2 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/commands/brand.mjs +439 -0
- package/src/commands/insights.mjs +171 -2
- package/src/commands/resources.mjs +44 -0
- package/src/index.mjs +4 -1
package/package.json
CHANGED
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import { call } from "../api.mjs";
|
|
2
|
+
import { emit, fail, ctxFrom, withCommonOpts, confirmDestructive } from "../ui.mjs";
|
|
3
|
+
|
|
4
|
+
const list = (v) => (v ? String(v).split(",").map((s) => s.trim()).filter(Boolean) : undefined);
|
|
5
|
+
|
|
6
|
+
function collectFields(o) {
|
|
7
|
+
const body = {};
|
|
8
|
+
const text = {
|
|
9
|
+
primary: "primary_color", secondary: "secondary_color", accent: "accent_color",
|
|
10
|
+
ctaColor: "cta_color", ctaTextColor: "cta_text_color",
|
|
11
|
+
fontHeading: "font_heading", fontBody: "font_body", tone: "tone",
|
|
12
|
+
tagline: "tagline", notes: "notes",
|
|
13
|
+
logo: "logo_url", logoFull: "logo_full_url", logoFullDark: "logo_full_dark_url",
|
|
14
|
+
logoIconLight: "logo_icon_light_url", logoIconDark: "logo_icon_dark_url",
|
|
15
|
+
videoCtaType: "video_default_cta_type", videoCtaLabel: "video_default_cta_label",
|
|
16
|
+
videoCtaUrl: "video_default_cta_url", videoIntro: "video_intro_text",
|
|
17
|
+
videoShareDomain: "video_share_domain",
|
|
18
|
+
};
|
|
19
|
+
for (const [opt, field] of Object.entries(text)) if (o[opt] !== undefined) body[field] = o[opt];
|
|
20
|
+
if (o.extraColors !== undefined) body.extra_colors = list(o.extraColors) ?? [];
|
|
21
|
+
return body;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function printBrand(b) {
|
|
25
|
+
if (!b) return process.stdout.write("No brand record for this workspace yet.\n");
|
|
26
|
+
process.stdout.write(
|
|
27
|
+
`Colours: primary ${b.primary_color || "-"} secondary ${b.secondary_color || "-"} accent ${b.accent_color || "-"}\n` +
|
|
28
|
+
`CTA: ${b.cta_color || "-"} on ${b.cta_text_color || "-"}\n` +
|
|
29
|
+
`Fonts: heading ${b.font_heading || "-"} body ${b.font_body || "-"}\n` +
|
|
30
|
+
`Tone: ${b.tone || "-"}\n` +
|
|
31
|
+
`Tagline: ${b.tagline || "-"}\n` +
|
|
32
|
+
`Logos: ${[b.logo_url, b.logo_full_url, b.logo_full_dark_url, b.logo_icon_light_url, b.logo_icon_dark_url].filter(Boolean).length} set\n` +
|
|
33
|
+
`Imagery: style ${b.imagery_style || "-"} | mood ${b.imagery_mood || "-"}\n` +
|
|
34
|
+
` motifs: ${(b.imagery_motifs || []).join(", ") || "-"}\n` +
|
|
35
|
+
` avoid: ${b.imagery_avoid || "-"}\n` +
|
|
36
|
+
` refs: ${(b.imagery_reference_urls || []).join(", ") || "-"}\n` +
|
|
37
|
+
`Voice: ${b.tone_of_voice_directive ? `${String(b.tone_of_voice_directive).slice(0, 120)}${String(b.tone_of_voice_directive).length > 120 ? "…" : ""}` : "-"}\n` +
|
|
38
|
+
` source: ${b.tone_of_voice_source || "-"}${b.tone_of_voice_synced_at ? ` (synced ${b.tone_of_voice_synced_at})` : ""}\n` +
|
|
39
|
+
`Video: cta ${b.video_default_cta_type || "-"}${b.video_default_cta_label ? ` "${b.video_default_cta_label}"` : ""} | domain ${b.video_share_domain || "-"}\n` +
|
|
40
|
+
`Social: ${Object.entries(b.reference_library || {}).filter(([k, v]) => k.startsWith("social_") && v).map(([k]) => k.replace(/^social_|_url$/g, "")).join(", ") || "-"}\n` +
|
|
41
|
+
(b.notes ? `Notes: ${b.notes}\n` : ""),
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function printImagery(i) {
|
|
46
|
+
process.stdout.write(
|
|
47
|
+
`Style: ${i.imagery_style || "-"}\nMood: ${i.imagery_mood || "-"}\n` +
|
|
48
|
+
`Motifs: ${(i.imagery_motifs || []).join(", ") || "-"}\n` +
|
|
49
|
+
`Avoid: ${i.imagery_avoid || "-"}\nRefs: ${(i.imagery_reference_urls || []).join(", ") || "-"}\n`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function registerBrand(program) {
|
|
54
|
+
const b = program
|
|
55
|
+
.command("brand")
|
|
56
|
+
.description("Read and edit the workspace brand: colours, fonts, logos, tagline and imagery direction");
|
|
57
|
+
|
|
58
|
+
withCommonOpts(b.command("show"))
|
|
59
|
+
.alias("get")
|
|
60
|
+
.description("Show the workspace brand record")
|
|
61
|
+
.action(async function () {
|
|
62
|
+
try {
|
|
63
|
+
const r = await call("brand", "get", {}, ctxFrom(this));
|
|
64
|
+
emit(this, r, (d) => printBrand(d.brand));
|
|
65
|
+
} catch (e) { fail(e); }
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
withCommonOpts(b.command("set"))
|
|
69
|
+
.alias("update")
|
|
70
|
+
.description("Update any brand field (only the flags you pass are changed)")
|
|
71
|
+
.option("--primary <hex>", "primary colour")
|
|
72
|
+
.option("--secondary <hex>", "secondary colour")
|
|
73
|
+
.option("--accent <hex>", "accent colour")
|
|
74
|
+
.option("--extra-colors <list>", "comma-separated extra colours")
|
|
75
|
+
.option("--cta-color <hex>", "CTA background colour")
|
|
76
|
+
.option("--cta-text-color <hex>", "CTA text colour")
|
|
77
|
+
.option("--font-heading <name>", "heading font")
|
|
78
|
+
.option("--font-body <name>", "body font")
|
|
79
|
+
.option("--tone <tone>", "brand tone keyword")
|
|
80
|
+
.option("--tagline <text>", "tagline")
|
|
81
|
+
.option("--notes <text>", "brand notes")
|
|
82
|
+
.option("--logo <url>", "primary logo URL")
|
|
83
|
+
.option("--logo-full <url>", "full logo URL")
|
|
84
|
+
.option("--logo-full-dark <url>", "full logo (dark) URL")
|
|
85
|
+
.option("--logo-icon-light <url>", "icon logo (light) URL")
|
|
86
|
+
.option("--logo-icon-dark <url>", "icon logo (dark) URL")
|
|
87
|
+
.option("--video-cta-type <type>", "default video CTA type")
|
|
88
|
+
.option("--video-cta-label <text>", "default video CTA label")
|
|
89
|
+
.option("--video-cta-url <url>", "default video CTA URL")
|
|
90
|
+
.option("--video-intro <text>", "default video intro text")
|
|
91
|
+
.option("--video-share-domain <domain>", "video share domain")
|
|
92
|
+
.action(async function (o) {
|
|
93
|
+
try {
|
|
94
|
+
const body = collectFields(o);
|
|
95
|
+
if (!Object.keys(body).length) fail(new Error("Pass at least one field to update."));
|
|
96
|
+
const r = await call("brand", "update", body, ctxFrom(this));
|
|
97
|
+
emit(this, r, (d) => { process.stdout.write(`Updated: ${(d.updated || []).join(", ")}\n\n`); printBrand(d.brand); });
|
|
98
|
+
} catch (e) { fail(e); }
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ── imagery direction ──────────────────────────────────────────────────
|
|
102
|
+
const img = b.command("imagery").description("Manage the imagery direction used for every generated image");
|
|
103
|
+
|
|
104
|
+
withCommonOpts(img.command("show"))
|
|
105
|
+
.alias("get")
|
|
106
|
+
.description("Show the imagery direction")
|
|
107
|
+
.action(async function () {
|
|
108
|
+
try {
|
|
109
|
+
const r = await call("brand", "imagery", {}, ctxFrom(this));
|
|
110
|
+
emit(this, r, (d) => printImagery(d.imagery || {}));
|
|
111
|
+
} catch (e) { fail(e); }
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
withCommonOpts(img.command("set"))
|
|
115
|
+
.alias("update")
|
|
116
|
+
.description("Set imagery style, motifs, mood, avoid list or reference URLs")
|
|
117
|
+
.option("--style <text>", "imagery style")
|
|
118
|
+
.option("--mood <text>", "imagery mood")
|
|
119
|
+
.option("--avoid <text>", "what to avoid")
|
|
120
|
+
.option("--motifs <list>", "comma-separated motifs (replaces the list)")
|
|
121
|
+
.option("--refs <list>", "comma-separated reference URLs (replaces the list)")
|
|
122
|
+
.action(async function (o) {
|
|
123
|
+
try {
|
|
124
|
+
const body = {};
|
|
125
|
+
if (o.style !== undefined) body.imagery_style = o.style;
|
|
126
|
+
if (o.mood !== undefined) body.imagery_mood = o.mood;
|
|
127
|
+
if (o.avoid !== undefined) body.imagery_avoid = o.avoid;
|
|
128
|
+
if (o.motifs !== undefined) body.imagery_motifs = list(o.motifs) ?? [];
|
|
129
|
+
if (o.refs !== undefined) body.imagery_reference_urls = list(o.refs) ?? [];
|
|
130
|
+
if (!Object.keys(body).length) fail(new Error("Pass at least one imagery field."));
|
|
131
|
+
const r = await call("brand", "set_imagery", body, ctxFrom(this));
|
|
132
|
+
emit(this, r, (d) => printImagery(d.imagery || {}));
|
|
133
|
+
} catch (e) { fail(e); }
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
withCommonOpts(img.command("add-motifs <motifs>"))
|
|
137
|
+
.description("Add motifs (comma-separated) without replacing existing ones")
|
|
138
|
+
.action(async function (m) {
|
|
139
|
+
try {
|
|
140
|
+
const r = await call("brand", "add_motifs", { motifs: list(m) }, ctxFrom(this));
|
|
141
|
+
emit(this, r, (d) => process.stdout.write(`Motifs: ${(d.imagery_motifs || []).join(", ") || "-"}\n`));
|
|
142
|
+
} catch (e) { fail(e); }
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
withCommonOpts(img.command("remove-motifs <motifs>"))
|
|
146
|
+
.description("Remove motifs (comma-separated)")
|
|
147
|
+
.action(async function (m) {
|
|
148
|
+
try {
|
|
149
|
+
const r = await call("brand", "remove_motifs", { motifs: list(m) }, ctxFrom(this));
|
|
150
|
+
emit(this, r, (d) => process.stdout.write(`Motifs: ${(d.imagery_motifs || []).join(", ") || "-"}\n`));
|
|
151
|
+
} catch (e) { fail(e); }
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
withCommonOpts(img.command("add-refs <urls>"))
|
|
155
|
+
.description("Add reference URLs (comma-separated)")
|
|
156
|
+
.action(async function (u) {
|
|
157
|
+
try {
|
|
158
|
+
const r = await call("brand", "add_reference_urls", { urls: list(u) }, ctxFrom(this));
|
|
159
|
+
emit(this, r, (d) => process.stdout.write(`Refs: ${(d.imagery_reference_urls || []).join(", ") || "-"}\n`));
|
|
160
|
+
} catch (e) { fail(e); }
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
withCommonOpts(img.command("remove-refs <urls>"))
|
|
164
|
+
.description("Remove reference URLs (comma-separated)")
|
|
165
|
+
.action(async function (u) {
|
|
166
|
+
try {
|
|
167
|
+
const r = await call("brand", "remove_reference_urls", { urls: list(u) }, ctxFrom(this));
|
|
168
|
+
emit(this, r, (d) => process.stdout.write(`Refs: ${(d.imagery_reference_urls || []).join(", ") || "-"}\n`));
|
|
169
|
+
} catch (e) { fail(e); }
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
withCommonOpts(img.command("clear"))
|
|
173
|
+
.description("Clear the imagery direction (or only --fields style,motifs...)")
|
|
174
|
+
.option("--fields <list>", "comma-separated imagery fields to clear")
|
|
175
|
+
.action(async function (o) {
|
|
176
|
+
try {
|
|
177
|
+
await confirmDestructive(this, "Clear the workspace imagery direction.");
|
|
178
|
+
const fields = list(o.fields)?.map((f) => (f.startsWith("imagery_") ? f : `imagery_${f}`));
|
|
179
|
+
const r = await call("brand", "clear_imagery", fields ? { fields } : {}, ctxFrom(this));
|
|
180
|
+
emit(this, r, (d) => process.stdout.write(`Cleared: ${(d.cleared || []).join(", ")}\n`));
|
|
181
|
+
} catch (e) { fail(e); }
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// ── logos ──────────────────────────────────────────────────────────────
|
|
185
|
+
const printLogos = (l = {}) =>
|
|
186
|
+
process.stdout.write(
|
|
187
|
+
`Primary: ${l.logo_url || "-"}\nFull: ${l.logo_full_url || "-"}\n` +
|
|
188
|
+
`Full dark: ${l.logo_full_dark_url || "-"}\nIcon light: ${l.logo_icon_light_url || "-"}\n` +
|
|
189
|
+
`Icon dark: ${l.logo_icon_dark_url || "-"}\n`,
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const lg = b.command("logos").alias("logo").description("Manage the workspace logo files and URLs");
|
|
193
|
+
|
|
194
|
+
withCommonOpts(lg.command("show"))
|
|
195
|
+
.alias("get")
|
|
196
|
+
.description("Show every logo variant")
|
|
197
|
+
.action(async function () {
|
|
198
|
+
try {
|
|
199
|
+
const r = await call("brand", "logos", {}, ctxFrom(this));
|
|
200
|
+
emit(this, r, (d) => printLogos(d.logos || {}));
|
|
201
|
+
} catch (e) { fail(e); }
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
withCommonOpts(lg.command("set <variant> <url>"))
|
|
205
|
+
.description("Point a logo variant (full, full_dark, icon_light, icon_dark, primary) at a URL")
|
|
206
|
+
.action(async function (variant, url) {
|
|
207
|
+
try {
|
|
208
|
+
const r = await call("brand", "set_logos", { variant, url }, ctxFrom(this));
|
|
209
|
+
emit(this, r, (d) => printLogos(d.logos || {}));
|
|
210
|
+
} catch (e) { fail(e); }
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
withCommonOpts(lg.command("upload <file>"))
|
|
214
|
+
.description("Upload a logo image file and store it against a variant")
|
|
215
|
+
.option("--variant <variant>", "full, full_dark, icon_light, icon_dark or primary", "full")
|
|
216
|
+
.action(async function (file, o) {
|
|
217
|
+
try {
|
|
218
|
+
const { readFile } = await import("node:fs/promises");
|
|
219
|
+
const path = await import("node:path");
|
|
220
|
+
const buf = await readFile(file);
|
|
221
|
+
if (buf.byteLength > 5 * 1024 * 1024) fail(new Error("Logo must be under 5 MB."));
|
|
222
|
+
const body = {
|
|
223
|
+
variant: o.variant,
|
|
224
|
+
filename: path.basename(file),
|
|
225
|
+
data: buf.toString("base64"),
|
|
226
|
+
};
|
|
227
|
+
const r = await call("brand", "upload_logo", body, ctxFrom(this));
|
|
228
|
+
emit(this, r, (d) => {
|
|
229
|
+
process.stdout.write(`Uploaded ${d.uploaded?.variant}: ${d.uploaded?.url}\n\n`);
|
|
230
|
+
printLogos(d.logos || {});
|
|
231
|
+
});
|
|
232
|
+
} catch (e) { fail(e); }
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
withCommonOpts(lg.command("clear"))
|
|
236
|
+
.description("Remove all logos, or only --variants full,icon_dark")
|
|
237
|
+
.option("--variants <list>", "comma-separated variants to clear")
|
|
238
|
+
.action(async function (o) {
|
|
239
|
+
try {
|
|
240
|
+
await confirmDestructive(this, "Clear the workspace logos.");
|
|
241
|
+
const variants = list(o.variants);
|
|
242
|
+
const r = await call("brand", "clear_logos", variants ? { variants } : {}, ctxFrom(this));
|
|
243
|
+
emit(this, r, (d) => process.stdout.write(`Cleared: ${(d.cleared || []).join(", ")}\n`));
|
|
244
|
+
} catch (e) { fail(e); }
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// ── social links ───────────────────────────────────────────────────────
|
|
248
|
+
const NETWORKS = ["linkedin", "twitter", "facebook", "instagram", "youtube", "tiktok", "threads", "github"];
|
|
249
|
+
const printSocial = (s = {}) =>
|
|
250
|
+
process.stdout.write(NETWORKS.map((n) => `${(n + ":").padEnd(11)}${s[n] || "-"}`).join("\n") + "\n");
|
|
251
|
+
|
|
252
|
+
const so = b.command("social").description("Manage the brand's social profile links");
|
|
253
|
+
|
|
254
|
+
withCommonOpts(so.command("show"))
|
|
255
|
+
.alias("get")
|
|
256
|
+
.description("Show the social profile links")
|
|
257
|
+
.action(async function () {
|
|
258
|
+
try {
|
|
259
|
+
const r = await call("brand", "social", {}, ctxFrom(this));
|
|
260
|
+
emit(this, r, (d) => printSocial(d.social || {}));
|
|
261
|
+
} catch (e) { fail(e); }
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const soSet = withCommonOpts(so.command("set"))
|
|
265
|
+
.alias("update")
|
|
266
|
+
.description("Set one or more social profile links");
|
|
267
|
+
for (const n of NETWORKS) soSet.option(`--${n} <url>`, `${n} profile URL`);
|
|
268
|
+
soSet.action(async function (o) {
|
|
269
|
+
try {
|
|
270
|
+
const body = {};
|
|
271
|
+
for (const n of NETWORKS) if (o[n] !== undefined) body[n] = o[n];
|
|
272
|
+
if (!Object.keys(body).length) fail(new Error(`Pass at least one of: ${NETWORKS.map((n) => `--${n}`).join(", ")}.`));
|
|
273
|
+
const r = await call("brand", "set_social", body, ctxFrom(this));
|
|
274
|
+
emit(this, r, (d) => printSocial(d.social || {}));
|
|
275
|
+
} catch (e) { fail(e); }
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
withCommonOpts(so.command("clear"))
|
|
279
|
+
.description("Remove all social links, or only --networks linkedin,github")
|
|
280
|
+
.option("--networks <list>", "comma-separated networks to clear")
|
|
281
|
+
.action(async function (o) {
|
|
282
|
+
try {
|
|
283
|
+
await confirmDestructive(this, "Clear the workspace social links.");
|
|
284
|
+
const networks = list(o.networks);
|
|
285
|
+
const r = await call("brand", "clear_social", networks ? { networks } : {}, ctxFrom(this));
|
|
286
|
+
emit(this, r, (d) => process.stdout.write(`Cleared: ${(d.cleared || []).join(", ")}\n`));
|
|
287
|
+
} catch (e) { fail(e); }
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// ── video share branding ───────────────────────────────────────────────
|
|
291
|
+
const printVideo = (v = {}) =>
|
|
292
|
+
process.stdout.write(
|
|
293
|
+
`CTA type: ${v.video_default_cta_type || "-"}\nCTA label: ${v.video_default_cta_label || "-"}\n` +
|
|
294
|
+
`CTA URL: ${v.video_default_cta_url || "-"}\nIntro text: ${v.video_intro_text || "-"}\n` +
|
|
295
|
+
`Share domain: ${v.video_share_domain || "-"}\n`,
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
const vd = b.command("video").description("Manage the branded video share page defaults");
|
|
299
|
+
|
|
300
|
+
withCommonOpts(vd.command("show"))
|
|
301
|
+
.alias("get")
|
|
302
|
+
.description("Show the video share branding defaults")
|
|
303
|
+
.action(async function () {
|
|
304
|
+
try {
|
|
305
|
+
const r = await call("brand", "video", {}, ctxFrom(this));
|
|
306
|
+
emit(this, r, (d) => printVideo(d.video || {}));
|
|
307
|
+
} catch (e) { fail(e); }
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
withCommonOpts(vd.command("set"))
|
|
311
|
+
.alias("update")
|
|
312
|
+
.description("Set the default video CTA, intro text or share domain")
|
|
313
|
+
.option("--cta-type <type>", "default CTA type (e.g. booking, website, none)")
|
|
314
|
+
.option("--cta-label <text>", "button label")
|
|
315
|
+
.option("--cta-url <url>", "CTA destination URL")
|
|
316
|
+
.option("--intro <text>", "intro text shown on the share page")
|
|
317
|
+
.option("--share-domain <domain>", "custom share domain")
|
|
318
|
+
.action(async function (o) {
|
|
319
|
+
try {
|
|
320
|
+
const body = {};
|
|
321
|
+
if (o.ctaType !== undefined) body.video_default_cta_type = o.ctaType;
|
|
322
|
+
if (o.ctaLabel !== undefined) body.video_default_cta_label = o.ctaLabel;
|
|
323
|
+
if (o.ctaUrl !== undefined) body.video_default_cta_url = o.ctaUrl;
|
|
324
|
+
if (o.intro !== undefined) body.video_intro_text = o.intro;
|
|
325
|
+
if (o.shareDomain !== undefined) body.video_share_domain = o.shareDomain;
|
|
326
|
+
if (!Object.keys(body).length) fail(new Error("Pass at least one video field."));
|
|
327
|
+
const r = await call("brand", "set_video", body, ctxFrom(this));
|
|
328
|
+
emit(this, r, (d) => printVideo(d.video || {}));
|
|
329
|
+
} catch (e) { fail(e); }
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
withCommonOpts(vd.command("clear"))
|
|
333
|
+
.description("Clear the video share defaults (or only --fields cta_url,intro_text)")
|
|
334
|
+
.option("--fields <list>", "comma-separated video fields to clear")
|
|
335
|
+
.action(async function (o) {
|
|
336
|
+
try {
|
|
337
|
+
await confirmDestructive(this, "Clear the video share branding defaults.");
|
|
338
|
+
const fields = list(o.fields)?.map((f) => (f.startsWith("video_") ? f : `video_${f}`));
|
|
339
|
+
const r = await call("brand", "clear_video", fields ? { fields } : {}, ctxFrom(this));
|
|
340
|
+
emit(this, r, (d) => process.stdout.write(`Cleared: ${(d.cleared || []).join(", ")}\n`));
|
|
341
|
+
} catch (e) { fail(e); }
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// ── tone of voice ──────────────────────────────────────────────────────
|
|
345
|
+
|
|
346
|
+
const tv = b.command("tone").description("Manage the tone of voice directive used when writing copy");
|
|
347
|
+
|
|
348
|
+
const printTone = (t = {}) =>
|
|
349
|
+
process.stdout.write(
|
|
350
|
+
`Keyword: ${t.tone || "-"}\nDirective: ${t.directive || "-"}\n` +
|
|
351
|
+
`Source: ${t.source || "-"}\nSynced: ${t.synced_at || "-"}\n` +
|
|
352
|
+
(t.apply_by_default === undefined ? "" : `Applied by default: ${t.apply_by_default ? "yes" : "no"}\n`),
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
withCommonOpts(tv.command("show"))
|
|
356
|
+
.alias("get")
|
|
357
|
+
.description("Show the tone of voice directive")
|
|
358
|
+
.action(async function () {
|
|
359
|
+
try {
|
|
360
|
+
const r = await call("brand", "tone", {}, ctxFrom(this));
|
|
361
|
+
emit(this, r, (d) => printTone(d.tone || {}));
|
|
362
|
+
} catch (e) { fail(e); }
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
withCommonOpts(tv.command("set"))
|
|
366
|
+
.alias("update")
|
|
367
|
+
.description("Set the tone of voice directive (and optional keyword/source)")
|
|
368
|
+
.option("--directive <text>", "the tone of voice directive")
|
|
369
|
+
.option("--file <path>", "read the directive from a file")
|
|
370
|
+
.option("--source <name>", "where it came from (default: manual)")
|
|
371
|
+
.option("--keyword <tone>", "short brand tone keyword")
|
|
372
|
+
.action(async function (o) {
|
|
373
|
+
try {
|
|
374
|
+
const body = {};
|
|
375
|
+
if (o.file) body.directive = await (await import("node:fs/promises")).readFile(o.file, "utf8");
|
|
376
|
+
if (o.directive !== undefined) body.directive = o.directive;
|
|
377
|
+
if (o.source !== undefined) body.source = o.source;
|
|
378
|
+
if (o.keyword !== undefined) body.tone = o.keyword;
|
|
379
|
+
if (!Object.keys(body).length) fail(new Error("Pass --directive, --file or --keyword."));
|
|
380
|
+
const r = await call("brand", "set_tone", body, ctxFrom(this));
|
|
381
|
+
emit(this, r, (d) => printTone(d.tone || {}));
|
|
382
|
+
} catch (e) { fail(e); }
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
withCommonOpts(tv.command("clear"))
|
|
386
|
+
.description("Remove the tone of voice directive")
|
|
387
|
+
.action(async function () {
|
|
388
|
+
try {
|
|
389
|
+
await confirmDestructive(this, "Clear this workspace's tone of voice directive.");
|
|
390
|
+
const r = await call("brand", "clear_tone", {}, ctxFrom(this));
|
|
391
|
+
emit(this, r, () => process.stdout.write("Tone of voice cleared.\n"));
|
|
392
|
+
} catch (e) { fail(e); }
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
withCommonOpts(tv.command("apply <on|off>"))
|
|
396
|
+
.description("Apply the tone of voice by default when generating content")
|
|
397
|
+
.action(async function (v) {
|
|
398
|
+
try {
|
|
399
|
+
const enabled = ["on", "true", "yes", "1"].includes(String(v).toLowerCase());
|
|
400
|
+
const r = await call("brand", "set_tone_default", { apply_by_default: enabled }, ctxFrom(this));
|
|
401
|
+
emit(this, r, (d) => process.stdout.write(`Applied by default: ${d.apply_by_default ? "yes" : "no"}\n`));
|
|
402
|
+
} catch (e) { fail(e); }
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
withCommonOpts(tv.command("sync"))
|
|
406
|
+
.description("Pull the latest tone of voice from Business Build")
|
|
407
|
+
.option("--email <email>", "client identity email to resolve against")
|
|
408
|
+
.action(async function (o) {
|
|
409
|
+
try {
|
|
410
|
+
const r = await call("brand", "sync_tone", o.email ? { email: o.email } : {}, ctxFrom(this));
|
|
411
|
+
emit(this, r, (d) => printTone(d.tone || {}));
|
|
412
|
+
} catch (e) { fail(e); }
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
withCommonOpts(b.command("delete"))
|
|
416
|
+
.alias("reset")
|
|
417
|
+
.description("Delete the entire brand record for this workspace")
|
|
418
|
+
.action(async function () {
|
|
419
|
+
try {
|
|
420
|
+
await confirmDestructive(this, "Delete this workspace's whole brand record.");
|
|
421
|
+
const r = await call("brand", "delete", {}, ctxFrom(this));
|
|
422
|
+
emit(this, r, () => process.stdout.write("Brand record deleted.\n"));
|
|
423
|
+
} catch (e) { fail(e); }
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
withCommonOpts(b.command("fields"))
|
|
427
|
+
.description("List every editable brand field")
|
|
428
|
+
.action(async function () {
|
|
429
|
+
try {
|
|
430
|
+
const r = await call("brand", "fields", {}, ctxFrom(this));
|
|
431
|
+
emit(this, r, (d) => process.stdout.write(
|
|
432
|
+
`Text: ${(d.text_fields || []).join(", ")}\nLists: ${(d.list_fields || []).join(", ")}\n` +
|
|
433
|
+
`Imagery: ${(d.imagery_fields || []).join(", ")}\nTone: ${(d.tone_fields || []).join(", ")}\n` +
|
|
434
|
+
`Logos: ${(d.logo_fields || []).join(", ")}\nLogo variants: ${(d.logo_variants || []).join(", ")}\n` +
|
|
435
|
+
`Video: ${(d.video_fields || []).join(", ")}\nSocial: ${(d.social_networks || []).join(", ")}\n`,
|
|
436
|
+
));
|
|
437
|
+
} catch (e) { fail(e); }
|
|
438
|
+
});
|
|
439
|
+
}
|
|
@@ -1231,16 +1231,185 @@ export function registerWatchlist(program) {
|
|
|
1231
1231
|
} catch (e) { fail(e); }
|
|
1232
1232
|
});
|
|
1233
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(`✔ ${d.item?.name} → ${d.item?.is_active ? "active" : "inactive"}\n`));
|
|
1302
|
+
} catch (e) { fail(e); }
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
withCommonOpts(w.command("suggest"))
|
|
1306
|
+
.description("Generate watchlist suggestions from the knowledge base")
|
|
1307
|
+
.action(async function () {
|
|
1308
|
+
try {
|
|
1309
|
+
const r = await call("watchlist", "suggest", {}, ctxFrom(this));
|
|
1310
|
+
emit(this, r, (d) => process.stdout.write(`✔ Added ${d.added ?? 0} suggested items\n`));
|
|
1311
|
+
} catch (e) { fail(e); }
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
withCommonOpts(w.command("sync-prompts"))
|
|
1315
|
+
.description("Resync the agent intelligence prompts derived from the watchlist")
|
|
1316
|
+
.action(async function () {
|
|
1317
|
+
try {
|
|
1318
|
+
const r = await call("watchlist", "sync_prompts", {}, ctxFrom(this));
|
|
1319
|
+
emit(this, r, () => process.stdout.write("✔ Prompts resynced\n"));
|
|
1320
|
+
} catch (e) { fail(e); }
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1234
1323
|
withCommonOpts(w.command("remove <id>"))
|
|
1235
|
-
.description("Remove a watchlist item")
|
|
1324
|
+
.description("Remove a watchlist item (suppressed by default so it is not re-suggested)")
|
|
1325
|
+
.option("--hard", "permanently delete the row")
|
|
1236
1326
|
.action(async function (id) {
|
|
1237
1327
|
try {
|
|
1238
|
-
const
|
|
1328
|
+
const o = this.optsWithGlobals();
|
|
1329
|
+
const r = await call("watchlist", "remove", { id, hard: !!o.hard }, ctxFrom(this));
|
|
1239
1330
|
emit(this, r, () => process.stdout.write(`✔ Removed ${id}\n`));
|
|
1240
1331
|
} catch (e) { fail(e); }
|
|
1241
1332
|
});
|
|
1242
1333
|
}
|
|
1243
1334
|
|
|
1335
|
+
export function registerIntelligencePrompts(program) {
|
|
1336
|
+
const p = program.command("intel-prompts")
|
|
1337
|
+
.description("Agent intelligence prompts (news, competitive, market, leads, content)");
|
|
1338
|
+
|
|
1339
|
+
withCommonOpts(p.command("list"))
|
|
1340
|
+
.description("List the workspace intelligence prompts")
|
|
1341
|
+
.action(async function () {
|
|
1342
|
+
try {
|
|
1343
|
+
const r = await call("intelligence_prompts", "list", {}, ctxFrom(this));
|
|
1344
|
+
emit(this, r, (d) => {
|
|
1345
|
+
process.stdout.write(table(
|
|
1346
|
+
["type", "source", "active", "prompt"],
|
|
1347
|
+
(d.prompts || []).map((x) => [
|
|
1348
|
+
n(x.prompt_type), x.is_auto === false ? "manual" : "auto",
|
|
1349
|
+
String(!!x.is_active), truncate(n(x.prompt_text).replace(/\s+/g, " "), 70),
|
|
1350
|
+
]),
|
|
1351
|
+
) + "\n");
|
|
1352
|
+
});
|
|
1353
|
+
} catch (e) { fail(e); }
|
|
1354
|
+
});
|
|
1355
|
+
|
|
1356
|
+
withCommonOpts(p.command("show <type>"))
|
|
1357
|
+
.description("Show one prompt in full")
|
|
1358
|
+
.action(async function (type) {
|
|
1359
|
+
try {
|
|
1360
|
+
const r = await call("intelligence_prompts", "get", { prompt_type: type }, ctxFrom(this));
|
|
1361
|
+
emit(this, r, (d) => process.stdout.write(`${n(d.prompt?.prompt_text)}\n`));
|
|
1362
|
+
} catch (e) { fail(e); }
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1365
|
+
withCommonOpts(p.command("set <type>"))
|
|
1366
|
+
.description("Override a prompt with custom text (marks it manual)")
|
|
1367
|
+
.option("--text <text>", "prompt text")
|
|
1368
|
+
.option("--file <path>", "read prompt text from a file")
|
|
1369
|
+
.action(async function (type) {
|
|
1370
|
+
try {
|
|
1371
|
+
const o = this.optsWithGlobals();
|
|
1372
|
+
const text = o.file ? await readFile(o.file, "utf8") : o.text;
|
|
1373
|
+
if (!text) throw new Error("--text or --file required");
|
|
1374
|
+
const r = await call("intelligence_prompts", "set", { prompt_type: type, prompt_text: text }, ctxFrom(this));
|
|
1375
|
+
emit(this, r, (d) => process.stdout.write(`✔ ${d.prompt?.prompt_type} updated (manual override)\n`));
|
|
1376
|
+
} catch (e) { fail(e); }
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
withCommonOpts(p.command("toggle <type>"))
|
|
1380
|
+
.description("Activate or deactivate a prompt")
|
|
1381
|
+
.option("--on", "force active")
|
|
1382
|
+
.option("--off", "force inactive")
|
|
1383
|
+
.action(async function (type) {
|
|
1384
|
+
try {
|
|
1385
|
+
const o = this.optsWithGlobals();
|
|
1386
|
+
const body = { prompt_type: type };
|
|
1387
|
+
if (o.on) body.is_active = true;
|
|
1388
|
+
if (o.off) body.is_active = false;
|
|
1389
|
+
const r = await call("intelligence_prompts", "toggle", body, ctxFrom(this));
|
|
1390
|
+
emit(this, r, (d) => process.stdout.write(`✔ ${d.prompt?.prompt_type} → ${d.prompt?.is_active ? "active" : "inactive"}\n`));
|
|
1391
|
+
} catch (e) { fail(e); }
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1394
|
+
withCommonOpts(p.command("reset <type>"))
|
|
1395
|
+
.description("Reset a prompt back to auto (regenerated from the watchlist)")
|
|
1396
|
+
.action(async function (type) {
|
|
1397
|
+
try {
|
|
1398
|
+
const r = await call("intelligence_prompts", "reset", { prompt_type: type }, ctxFrom(this));
|
|
1399
|
+
emit(this, r, () => process.stdout.write(`✔ ${type} reset to auto\n`));
|
|
1400
|
+
} catch (e) { fail(e); }
|
|
1401
|
+
});
|
|
1402
|
+
|
|
1403
|
+
withCommonOpts(p.command("resync"))
|
|
1404
|
+
.description("Regenerate all auto prompts from the current watchlist")
|
|
1405
|
+
.action(async function () {
|
|
1406
|
+
try {
|
|
1407
|
+
const r = await call("intelligence_prompts", "resync", {}, ctxFrom(this));
|
|
1408
|
+
emit(this, r, () => process.stdout.write("✔ Prompts resynced\n"));
|
|
1409
|
+
} catch (e) { fail(e); }
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1244
1413
|
export function registerMeetings(program) {
|
|
1245
1414
|
const m = program.command("meetings").description("Meeting transcripts (anonymised)");
|
|
1246
1415
|
|
|
@@ -938,6 +938,50 @@ export function registerKnowledge(program) {
|
|
|
938
938
|
} catch (e) { fail(e); }
|
|
939
939
|
});
|
|
940
940
|
|
|
941
|
+
withCommonOpts(k.command("approve <id...>"))
|
|
942
|
+
.description("Confirm knowledge item(s) as the client's own material (unlocks voice training + evidence use)")
|
|
943
|
+
.option("--author-name <name>", "Attribute to a named expert")
|
|
944
|
+
.option("--authorship <kind>", "human|expert|client|team (default human)")
|
|
945
|
+
.action(async function (ids, opts) {
|
|
946
|
+
try {
|
|
947
|
+
const payload = ids.length > 1
|
|
948
|
+
? { ids, author_name: opts.authorName, authorship: opts.authorship }
|
|
949
|
+
: { id: ids[0], author_name: opts.authorName, authorship: opts.authorship };
|
|
950
|
+
const r = await call("knowledge", "approve", payload, ctxFrom(this));
|
|
951
|
+
emit(this, r, (d) => process.stdout.write(`✓ Confirmed ${(d.items || [d.item]).filter(Boolean).length} item(s)\n`));
|
|
952
|
+
} catch (e) { fail(e); }
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
withCommonOpts(k.command("reject <id...>"))
|
|
956
|
+
.description("Mark knowledge item(s) as NOT the client's own material")
|
|
957
|
+
.action(async function (ids) {
|
|
958
|
+
try {
|
|
959
|
+
const payload = ids.length > 1 ? { ids } : { id: ids[0] };
|
|
960
|
+
const r = await call("knowledge", "reject", payload, ctxFrom(this));
|
|
961
|
+
emit(this, r, (d) => process.stdout.write(`✓ Rejected ${(d.items || [d.item]).filter(Boolean).length} item(s)\n`));
|
|
962
|
+
} catch (e) { fail(e); }
|
|
963
|
+
});
|
|
964
|
+
|
|
965
|
+
withCommonOpts(k.command("set-authorship <id>"))
|
|
966
|
+
.description("Set who wrote a knowledge item (human|expert|client|team|ai|unknown)")
|
|
967
|
+
.requiredOption("--authorship <kind>", "human|expert|client|team|ai|unknown")
|
|
968
|
+
.option("--author-name <name>", "Named author")
|
|
969
|
+
.option("--approval-status <status>", "approved|pending|rejected")
|
|
970
|
+
.action(async function (id, opts) {
|
|
971
|
+
try {
|
|
972
|
+
const r = await call("knowledge", "set_authorship", {
|
|
973
|
+
id,
|
|
974
|
+
authorship: opts.authorship,
|
|
975
|
+
author_name: opts.authorName,
|
|
976
|
+
approval_status: opts.approvalStatus,
|
|
977
|
+
}, ctxFrom(this));
|
|
978
|
+
emit(this, r, (d) => {
|
|
979
|
+
const it = d.item || {};
|
|
980
|
+
process.stdout.write(`✓ ${it.id}: ${it.authorship}${it.author_name ? ` (${it.author_name})` : ""} · ${it.approval_status}\n`);
|
|
981
|
+
});
|
|
982
|
+
} catch (e) { fail(e); }
|
|
983
|
+
});
|
|
984
|
+
|
|
941
985
|
withCommonOpts(k.command("reclassify <id>"))
|
|
942
986
|
.description("Re-run AI auto-tag/classify (same as the UI 'sparkles' button)")
|
|
943
987
|
.action(async function (id) {
|
package/src/index.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { registerStrategies } from "./commands/strategies.mjs";
|
|
2
2
|
import { registerVoice } from "./commands/voice.mjs";
|
|
3
|
+
import { registerBrand } from "./commands/brand.mjs";
|
|
3
4
|
import { Command } from "commander";
|
|
4
5
|
import { readFileSync } from "node:fs";
|
|
5
6
|
import { registerAuth } from "./commands/auth.mjs";
|
|
@@ -12,7 +13,7 @@ import {
|
|
|
12
13
|
import {
|
|
13
14
|
registerCampaignAnalytics, registerAnalytics, registerSafety, registerCtas,
|
|
14
15
|
registerNotifications, registerAudiences, registerNewsletter, registerVideo,
|
|
15
|
-
registerWatchlist, registerMeetings, registerActivity, registerWorkspacesExtras,
|
|
16
|
+
registerWatchlist, registerIntelligencePrompts, registerMeetings, registerActivity, registerWorkspacesExtras,
|
|
16
17
|
registerExport, registerIntegrations, registerAudit, registerReports,
|
|
17
18
|
} from "./commands/insights.mjs";
|
|
18
19
|
import { registerFlow } from "./commands/flow.mjs";
|
|
@@ -40,6 +41,7 @@ registerSignals(program);
|
|
|
40
41
|
registerPipelines(program);
|
|
41
42
|
registerStrategies(program);
|
|
42
43
|
registerVoice(program);
|
|
44
|
+
registerBrand(program);
|
|
43
45
|
registerAuthority(program);
|
|
44
46
|
registerBriefs(program);
|
|
45
47
|
registerAgents(program);
|
|
@@ -55,6 +57,7 @@ registerAudiences(program);
|
|
|
55
57
|
registerNewsletter(program);
|
|
56
58
|
registerVideo(program);
|
|
57
59
|
registerWatchlist(program);
|
|
60
|
+
registerIntelligencePrompts(program);
|
|
58
61
|
registerMeetings(program);
|
|
59
62
|
registerActivity(program);
|
|
60
63
|
registerAudit(program);
|