@letterstory/cli 0.1.1 → 0.2.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.
@@ -0,0 +1,125 @@
1
+ // Auth / config: login, logout, config, whoami. `letterstory login` (no --key)
2
+ // opens the browser for OAuth 2.1 + PKCE; `--key <ls_…>` keeps the static-key
3
+ // path for CI/automation, where there's no browser to open. Both save to the
4
+ // same config file, but under different keys (`oauth` vs `key`) so
5
+ // resolveConfig() can tell them apart. cmdWhoami mirrors phantomstory-cli's
6
+ // `phantom whoami` (alias `status`) — verify the resolved credential by
7
+ // calling get_company_info, same probe `login` uses to verify a fresh key.
8
+
9
+ import { LetterstoryClient, resolveConfig, writeConfigFile, clearConfigFile, configPath } from "../client.mjs";
10
+ import { flagStr } from "./shared.mjs";
11
+ import { browserLogin, revokeToken } from "../oauth.mjs";
12
+
13
+ export async function cmdLogin(ctx) {
14
+ const { flags, io } = ctx;
15
+ const url = flagStr(flags.url) || resolveConfig({}).url;
16
+ const key = flagStr(flags.key);
17
+
18
+ if (key) {
19
+ if (!key.startsWith("ls_") && !key.startsWith("lb_")) {
20
+ io.error(
21
+ "Warning: Letterstory API keys usually start with 'ls_' (legacy keys start with 'lb_'). Saving anyway."
22
+ );
23
+ }
24
+ const saved = { key, ...(flagStr(flags.url) ? { url } : {}) };
25
+ const path = writeConfigFile(saved);
26
+ io.log(`Saved credentials to ${path}`);
27
+
28
+ // Verify the key works (and report the tool count) without failing the save if
29
+ // the network is down — a saved-but-unverified key is still usable later.
30
+ try {
31
+ const resolved = resolveConfig({ url, key });
32
+ const client = new LetterstoryClient({ url: resolved.url, key: resolved.key });
33
+ const tools = await client.listTools();
34
+ io.log(`Authenticated to ${resolved.url} — ${tools.length} tools available.`);
35
+ return 0;
36
+ } catch (err) {
37
+ io.error(`Saved, but could not verify the key: ${err.message}`);
38
+ return 0;
39
+ }
40
+ }
41
+
42
+ const tokens = await browserLogin({ url, io });
43
+ const oauth = {
44
+ access_token: tokens.access_token,
45
+ refresh_token: tokens.refresh_token,
46
+ expires_at: Date.now() + (tokens.expires_in ?? 3600) * 1000,
47
+ scope: tokens.scope,
48
+ };
49
+ const saved = { oauth, ...(flagStr(flags.url) ? { url } : {}) };
50
+ const path = writeConfigFile(saved);
51
+ io.log(`Saved credentials to ${path}`);
52
+
53
+ try {
54
+ const client = new LetterstoryClient({ url, oauth });
55
+ const tools = await client.listTools();
56
+ io.log(`Authenticated to ${url} — ${tools.length} tools available.`);
57
+ } catch (err) {
58
+ io.error(`Saved, but could not verify the session: ${err.message}`);
59
+ }
60
+ return 0;
61
+ }
62
+
63
+ export async function cmdLogout(ctx) {
64
+ const { config, io } = ctx;
65
+ if (config.oauth?.access_token) {
66
+ await revokeToken({ url: config.url, token: config.oauth.access_token });
67
+ if (config.oauth.refresh_token) await revokeToken({ url: config.url, token: config.oauth.refresh_token });
68
+ }
69
+ const path = clearConfigFile();
70
+ io.log(`Cleared credentials at ${path}`);
71
+ return 0;
72
+ }
73
+
74
+ export function cmdConfig(ctx) {
75
+ const { config, io, flags } = ctx;
76
+ const masked = config.key
77
+ ? `${config.key.slice(0, 6)}…${config.key.slice(-4)}`
78
+ : config.oauth
79
+ ? "(browser session)"
80
+ : "(none)";
81
+ const value = {
82
+ url: config.url,
83
+ key: masked,
84
+ key_source: config.keySource,
85
+ config_file: configPath(),
86
+ };
87
+ if (flags.json) {
88
+ io.log(JSON.stringify(value, null, 2));
89
+ } else {
90
+ io.log(`url: ${value.url}`);
91
+ io.log(`key: ${value.key} (from ${value.key_source})`);
92
+ io.log(`config file: ${value.config_file}`);
93
+ }
94
+ return 0;
95
+ }
96
+
97
+ // `whoami` (alias `status`): confirm the resolved credential actually works and
98
+ // show who it's for. get_onboarding_status is best-effort (older orgs may not
99
+ // have onboarding state).
100
+ export async function cmdWhoami(ctx) {
101
+ const { client, config, io, flags } = ctx;
102
+ const company = await client.callTool("get_company_info", {});
103
+ let onboarding;
104
+ try {
105
+ onboarding = await client.callTool("get_onboarding_status", {});
106
+ } catch {
107
+ onboarding = undefined;
108
+ }
109
+ const masked = config.key
110
+ ? `${config.key.slice(0, 6)}…${config.key.slice(-4)}`
111
+ : config.oauth
112
+ ? "(browser session)"
113
+ : "(none)";
114
+ if (flags.json) {
115
+ io.log(
116
+ JSON.stringify({ url: config.url, key: masked, key_source: config.keySource, company, onboarding }, null, 2)
117
+ );
118
+ return 0;
119
+ }
120
+ io.log(`url: ${config.url}`);
121
+ io.log(`key: ${masked} (from ${config.keySource})`);
122
+ io.log(`company: ${JSON.stringify(company)}`);
123
+ if (onboarding !== undefined) io.log(`onboarding: ${JSON.stringify(onboarding)}`);
124
+ return 0;
125
+ }
@@ -0,0 +1,93 @@
1
+ // `collections` — group articles into collections and set a publishing cadence.
2
+ // Ported from phantomstory-cli's collections.ts.
3
+
4
+ import { CliError } from "../client.mjs";
5
+ import { flagStr, flagNum, requireFlag, requirePositional, printResult, compact, ok } from "./shared.mjs";
6
+
7
+ export async function cmdCollections(ctx) {
8
+ const sub = ctx.positionals[0];
9
+ const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
10
+ switch (sub) {
11
+ case "list":
12
+ case "ls":
13
+ return collectionsList(rest);
14
+ case "new":
15
+ case "create":
16
+ return collectionsNew(rest);
17
+ case "update":
18
+ return collectionsUpdate(rest);
19
+ case "delete":
20
+ case "rm":
21
+ return collectionsDelete(rest);
22
+ case "assign":
23
+ return collectionsAssign(rest);
24
+ default:
25
+ throw new CliError(
26
+ `Unknown collections subcommand: ${sub ?? "(none)"}. Try: list, new, update, delete, assign`
27
+ );
28
+ }
29
+ }
30
+
31
+ async function collectionsList(ctx) {
32
+ const { client, flags, io } = ctx;
33
+ const result = await client.callTool("list_collections", {});
34
+ printResult(io, flags, result);
35
+ return 0;
36
+ }
37
+
38
+ async function collectionsNew(ctx) {
39
+ const { client, flags, io } = ctx;
40
+ const name = requireFlag(flags, "name");
41
+ const args = compact({ name, description: flagStr(flags.description) });
42
+ const result = await client.callTool("create_collection", args);
43
+ ok(ctx, `Created collection "${name}".`);
44
+ printResult(io, flags, result);
45
+ return 0;
46
+ }
47
+
48
+ async function collectionsUpdate(ctx) {
49
+ const { client, positionals, flags, io } = ctx;
50
+ const id = requirePositional(positionals, 0, "collection-id");
51
+ const patch = compact({
52
+ collection_id: id,
53
+ name: flagStr(flags.name),
54
+ description: flagStr(flags.description),
55
+ cadence_target: flagNum(flags["cadence-target"]),
56
+ cadence_period: flagStr(flags["cadence-period"]),
57
+ });
58
+ if (Object.keys(patch).length <= 1) {
59
+ throw new CliError(
60
+ "Nothing to update — pass at least one of --name/--description/--cadence-target/--cadence-period."
61
+ );
62
+ }
63
+ const result = await client.callTool("update_collection", patch);
64
+ ok(ctx, "Updated collection.");
65
+ printResult(io, flags, result);
66
+ return 0;
67
+ }
68
+
69
+ async function collectionsDelete(ctx) {
70
+ const { client, positionals, flags, io } = ctx;
71
+ const id = requirePositional(positionals, 0, "collection-id");
72
+ if (!flags.yes) {
73
+ io.error(`Re-run with --yes to confirm: ${ctx.bin} collections delete ${id} --yes`);
74
+ return 1;
75
+ }
76
+ const result = await client.callTool("delete_collection", { collection_id: id });
77
+ ok(ctx, "Collection deleted.");
78
+ printResult(io, flags, result);
79
+ return 0;
80
+ }
81
+
82
+ async function collectionsAssign(ctx) {
83
+ const { client, positionals, flags, io } = ctx;
84
+ const articleId = requirePositional(positionals, 0, "article-id");
85
+ const collectionId = requirePositional(positionals, 1, "collection-id");
86
+ const result = await client.callTool("assign_article_to_collection", {
87
+ article_id: articleId,
88
+ collection_id: collectionId,
89
+ });
90
+ ok(ctx, "Article assigned.");
91
+ printResult(io, flags, result);
92
+ return 0;
93
+ }
@@ -0,0 +1,56 @@
1
+ // `connectors` — export finished articles to external CMSs/doc tools (Webflow,
2
+ // Framer, Google Docs, Contentful) and check publish status. Ported from
3
+ // phantomstory-cli's connectors.ts.
4
+
5
+ import { CliError } from "../client.mjs";
6
+ import { flagStr, requireFlag, requirePositional, printResult, compact, ok } from "./shared.mjs";
7
+
8
+ export async function cmdConnectors(ctx) {
9
+ const sub = ctx.positionals[0];
10
+ const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
11
+ switch (sub) {
12
+ case "list":
13
+ case "ls":
14
+ case "targets":
15
+ return connectorsList(rest);
16
+ case "publish":
17
+ return connectorsPublish(rest);
18
+ case "status":
19
+ return connectorsStatus(rest);
20
+ default:
21
+ throw new CliError(`Unknown connectors subcommand: ${sub ?? "(none)"}. Try: list, publish, status`);
22
+ }
23
+ }
24
+
25
+ async function connectorsList(ctx) {
26
+ const { client, flags, io } = ctx;
27
+ const result = await client.callTool("list_publish_targets", {});
28
+ printResult(io, flags, result);
29
+ return 0;
30
+ }
31
+
32
+ async function connectorsPublish(ctx) {
33
+ const { client, positionals, flags, io } = ctx;
34
+ const articleId = requirePositional(positionals, 0, "article-id");
35
+ const to = requireFlag(flags, "to");
36
+ const target = requireFlag(flags, "target");
37
+ const args = compact({
38
+ article_id: articleId,
39
+ connector: to,
40
+ target_id: target,
41
+ flow_run_id: flagStr(flags["flow-run"]),
42
+ });
43
+ const result = await client.callTool("publish_to_connector", args);
44
+ ok(ctx, `Publishing to ${to}.`);
45
+ printResult(io, flags, result);
46
+ return 0;
47
+ }
48
+
49
+ async function connectorsStatus(ctx) {
50
+ const { client, flags, io } = ctx;
51
+ const connector = requireFlag(flags, "connector");
52
+ const publishId = requireFlag(flags, "publish-id");
53
+ const result = await client.callTool("get_publish_status", { connector, publish_id: publishId });
54
+ printResult(io, flags, result);
55
+ return 0;
56
+ }
@@ -0,0 +1,241 @@
1
+ // Phantom blogs (deployments) and custom domains. `cmdDeploy`/`cmdDomain` are the
2
+ // original, stable command surface; `blogs`/`domains` are Mathew's phantomstory-cli
3
+ // names for the exact same operations. Rather than duplicate handler bodies, `blogs`
4
+ // and `domains` are literal references to `cmdDeploy`/`cmdDomain` (see the bottom of
5
+ // this file) and Mathew's verb spellings (ls/new/show/rm/diag) are additional cases
6
+ // in the same switch statements below — one implementation, two vocabularies.
7
+
8
+ import { CliError } from "../client.mjs";
9
+ import { flagStr, flagNum, requireFlag, requirePositional, printResult, compact } from "./shared.mjs";
10
+
11
+ function stripScheme(domain) {
12
+ return domain.replace(/^https?:\/\//i, "").replace(/\/+$/, "");
13
+ }
14
+
15
+ function formatDeployment(d) {
16
+ const lines = [
17
+ `${d.name} [${d.deployment_id}]`,
18
+ ` status: ${d.status}${d.phase ? ` · ${d.phase}` : ""}`,
19
+ ` theme: ${d.theme}`,
20
+ ` collection: ${d.collection_id ?? "(none)"}`,
21
+ ];
22
+ if (d.url) lines.push(` url: ${d.url}`);
23
+ if (d.domain) lines.push(` domain: ${d.domain}`);
24
+ if (d.last_error) lines.push(` error: ${d.last_error}`);
25
+ return lines.join("\n");
26
+ }
27
+
28
+ // Poll get_deployment until the blog reaches a terminal state. Emits a line only
29
+ // when status/phase changes so the output reads as progress, not a firehose.
30
+ async function pollUntilTerminal(client, id, io, bin) {
31
+ const maxPolls = io.maxPolls ?? 90;
32
+ const interval = io.pollIntervalMs ?? 4000;
33
+ let last = "";
34
+ for (let i = 0; i < maxPolls; i++) {
35
+ const d = await client.callTool("get_deployment", { deployment_id: id });
36
+ if (d.status === "live" || d.status === "error") return d;
37
+ const line = `${d.status}${d.phase ? ` · ${d.phase}` : ""}`;
38
+ if (line !== last) {
39
+ io.log(` … ${line}`);
40
+ last = line;
41
+ }
42
+ await io.sleep(interval);
43
+ }
44
+ return {
45
+ status: "error",
46
+ last_error: `Timed out waiting for it to go live. Check: ${bin} deploy get ${id}`,
47
+ url: null,
48
+ };
49
+ }
50
+
51
+ // --- deployments (phantom blogs) --------------------------------------------
52
+
53
+ export async function cmdDeploy(ctx) {
54
+ const sub = ctx.positionals[0];
55
+ const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
56
+ switch (sub) {
57
+ case "create":
58
+ case "new":
59
+ return deployCreate(rest);
60
+ case "list":
61
+ case "ls":
62
+ return deployList(rest);
63
+ case "get":
64
+ case "status":
65
+ case "show":
66
+ return deployGet(rest);
67
+ case "update":
68
+ return deployUpdate(rest);
69
+ case "rebuild":
70
+ return deployRebuild(rest);
71
+ case "diagnostics":
72
+ case "diag":
73
+ return deployDiagnostics(rest);
74
+ case "delete":
75
+ case "rm":
76
+ return deployDelete(rest);
77
+ default:
78
+ throw new CliError(
79
+ `Unknown deploy subcommand: ${sub ?? "(none)"}. Try: create, list, get, update, rebuild, diagnostics, delete`
80
+ );
81
+ }
82
+ }
83
+
84
+ async function deployCreate(ctx) {
85
+ const { client, flags, io } = ctx;
86
+ const name = requireFlag(flags, "name");
87
+ const args = { name };
88
+ const description = flagStr(flags.description);
89
+ const theme = flagStr(flags.theme);
90
+ const collection = flagStr(flags.collection);
91
+ if (description !== undefined) args.description = description;
92
+ if (theme !== undefined) args.theme = theme;
93
+ if (collection !== undefined) args.collection_id = collection;
94
+
95
+ if (flags["dry-run"]) {
96
+ io.log(`Dry run — would create a phantom blog with:`);
97
+ io.log(JSON.stringify(args, null, 2));
98
+ return 0;
99
+ }
100
+
101
+ const created = await client.callTool("create_deployment", args);
102
+ io.log(`Created deployment ${created.deployment_id} (${created.status})`);
103
+
104
+ if (flags["no-wait"]) {
105
+ io.log(`Provisioning in the background. Poll with: ${ctx.bin} deploy get ${created.deployment_id}`);
106
+ return 0;
107
+ }
108
+
109
+ const final = await pollUntilTerminal(client, created.deployment_id, io, ctx.bin);
110
+ if (final.status === "live") {
111
+ io.log(`\n✓ Live at ${final.url}`);
112
+ return 0;
113
+ }
114
+ io.error(`\n✗ Deployment ${final.status}: ${final.last_error ?? "unknown error"}`);
115
+ return 1;
116
+ }
117
+
118
+ async function deployList(ctx) {
119
+ const { client, flags, io } = ctx;
120
+ const limit = flagNum(flags.limit);
121
+ const result = await client.callTool("list_deployments", limit !== undefined ? { limit } : {});
122
+ const items = result.items ?? [];
123
+ if (flags.json) {
124
+ io.log(JSON.stringify(result, null, 2));
125
+ return 0;
126
+ }
127
+ if (items.length === 0) {
128
+ io.log(`No deployments yet. Create one with: ${ctx.bin} deploy create --name <name>`);
129
+ return 0;
130
+ }
131
+ for (const d of items) {
132
+ io.log(`${d.status.padEnd(13)} ${d.deployment_id} ${d.name}${d.url ? ` ${d.url}` : ""}`);
133
+ }
134
+ return 0;
135
+ }
136
+
137
+ async function deployGet(ctx) {
138
+ const { client, positionals, flags, io } = ctx;
139
+ const id = requirePositional(positionals, 0, "deployment-id");
140
+ const d = await client.callTool("get_deployment", { deployment_id: id });
141
+ printResult(io, flags, d, formatDeployment);
142
+ return 0;
143
+ }
144
+
145
+ async function deployUpdate(ctx) {
146
+ const { client, positionals, flags, io } = ctx;
147
+ const id = requirePositional(positionals, 0, "deployment-id");
148
+ const patch = compact({
149
+ deployment_id: id,
150
+ name: flagStr(flags.name),
151
+ description: flagStr(flags.description),
152
+ theme: flagStr(flags.theme),
153
+ collection_id: flagStr(flags.collection),
154
+ });
155
+ if (Object.keys(patch).length <= 1) {
156
+ throw new CliError("Nothing to update — pass at least one of --name/--description/--theme/--collection.");
157
+ }
158
+ const d = await client.callTool("update_deployment", patch);
159
+ io.log(`Updated deployment ${id}.`);
160
+ printResult(io, flags, d, formatDeployment);
161
+ return 0;
162
+ }
163
+
164
+ async function deployRebuild(ctx) {
165
+ const { client, positionals, io } = ctx;
166
+ const id = requirePositional(positionals, 0, "deployment-id");
167
+ await client.callTool("rebuild_deployment", { deployment_id: id });
168
+ io.log(`Rebuild started for ${id}. New published content will appear once the build finishes.`);
169
+ return 0;
170
+ }
171
+
172
+ async function deployDiagnostics(ctx) {
173
+ const { client, positionals, io } = ctx;
174
+ const id = requirePositional(positionals, 0, "deployment-id");
175
+ const diag = await client.callTool("get_deployment_diagnostics", { deployment_id: id });
176
+ io.log(JSON.stringify(diag, null, 2));
177
+ return 0;
178
+ }
179
+
180
+ async function deployDelete(ctx) {
181
+ const { client, positionals, flags, io } = ctx;
182
+ const id = requirePositional(positionals, 0, "deployment-id");
183
+ if (!flags.yes) {
184
+ io.error(`This tears down the blog's site and revokes its content key (the collection is kept).`);
185
+ io.error(`Re-run with --yes to confirm: ${ctx.bin} deploy delete ${id} --yes`);
186
+ return 1;
187
+ }
188
+ await client.callTool("delete_deployment", { deployment_id: id });
189
+ io.log(`Deleted deployment ${id}.`);
190
+ return 0;
191
+ }
192
+
193
+ // --- custom domains ---------------------------------------------------------
194
+
195
+ export async function cmdDomain(ctx) {
196
+ const sub = ctx.positionals[0];
197
+ const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
198
+ switch (sub) {
199
+ case "check":
200
+ return domainCheck(rest);
201
+ case "buy":
202
+ return domainBuy(rest);
203
+ default:
204
+ throw new CliError(`Unknown domain subcommand: ${sub ?? "(none)"}. Try: check, buy`);
205
+ }
206
+ }
207
+
208
+ async function domainCheck(ctx) {
209
+ const { client, positionals, flags, io } = ctx;
210
+ const domain = stripScheme(requirePositional(positionals, 0, "domain"));
211
+ const quote = await client.callTool("check_domain", { domain });
212
+ if (flags.json) {
213
+ io.log(JSON.stringify(quote, null, 2));
214
+ return 0;
215
+ }
216
+ io.log(`${domain}`);
217
+ io.log(` available: ${quote.available}`);
218
+ if (quote.price !== undefined) io.log(` price: $${quote.price}`);
219
+ io.log(` purchasable: ${quote.purchasable}`);
220
+ return 0;
221
+ }
222
+
223
+ // No confirm gate: the app's BuyDomainDialog charges on a single "Get this domain"
224
+ // click, and the CLI mirrors the app's user-facing flow. The spend is bounded on the
225
+ // server, not by a client prompt — `buy_domain` is server-priced and hard-capped
226
+ // (≤2 domains/org, <$30, blog must be live with no domain yet).
227
+ async function domainBuy(ctx) {
228
+ const { client, positionals, flags, io } = ctx;
229
+ const id = requirePositional(positionals, 0, "deployment-id");
230
+ const domain = stripScheme(requirePositional(positionals, 1, "domain"));
231
+ const result = await client.callTool("buy_domain", { deployment_id: id, domain });
232
+ io.log(`Purchase started for ${domain}. Attach + DNS run in the background.`);
233
+ io.log(`Poll with: ${ctx.bin} deploy get ${id}`);
234
+ if (flags.json) io.log(JSON.stringify(result, null, 2));
235
+ return 0;
236
+ }
237
+
238
+ // `blogs`/`domains` are phantomstory-cli's names for these exact same commands — a
239
+ // literal reference, not a wrapper, so they can never drift from deploy/domain.
240
+ export const cmdBlogs = cmdDeploy;
241
+ export const cmdDomains = cmdDomain;
@@ -0,0 +1,43 @@
1
+ // Discovery / generic passthrough: `tools` (list the manifest) and `call` (invoke any
2
+ // tool by name). Unchanged from the original commands.mjs — this is the escape hatch
3
+ // that keeps every tool reachable even ones with no dedicated command group.
4
+
5
+ import { CliError } from "../client.mjs";
6
+ import { flagStr, requirePositional } from "./shared.mjs";
7
+
8
+ export async function cmdTools(ctx) {
9
+ const { client, io, flags } = ctx;
10
+ const doc = await client.discover();
11
+ const tools = doc.tools ?? [];
12
+ if (flags.json) {
13
+ io.log(JSON.stringify(tools, null, 2));
14
+ return 0;
15
+ }
16
+ io.log(`${tools.length} tools available at ${client.url}:\n`);
17
+ for (const t of tools) io.log(` ${t.name} — ${t.description ?? ""}`);
18
+ return 0;
19
+ }
20
+
21
+ // Escape hatch: call any tool by name. Args come from --args '<json>', or from
22
+ // individual string flags (everything after the tool name). Keeps the whole
23
+ // manifest reachable without a bespoke subcommand per tool.
24
+ export async function cmdCall(ctx) {
25
+ const { client, positionals, flags, io } = ctx;
26
+ const name = requirePositional(positionals, 0, "tool");
27
+ let args = {};
28
+ if (flags.args !== undefined) {
29
+ try {
30
+ args = JSON.parse(flagStr(flags.args) ?? "");
31
+ } catch {
32
+ throw new CliError(`--args must be a JSON object, e.g. --args '{"name":"My Blog"}'`);
33
+ }
34
+ } else {
35
+ for (const [k, v] of Object.entries(flags)) {
36
+ if (k === "json" || k === "url" || k === "key") continue;
37
+ args[k] = v;
38
+ }
39
+ }
40
+ const result = await client.callTool(name, args);
41
+ io.log(typeof result === "string" ? result : JSON.stringify(result, null, 2));
42
+ return 0;
43
+ }
@@ -0,0 +1,83 @@
1
+ // `flows` — run editorial passes over articles, check run status, and manage the
2
+ // flow_run.completed completion webhook. Ported from phantomstory-cli's flows.ts.
3
+
4
+ import { CliError } from "../client.mjs";
5
+ import { flagStr, requirePositional, printResult, compact, ok } from "./shared.mjs";
6
+
7
+ export async function cmdFlows(ctx) {
8
+ const sub = ctx.positionals[0];
9
+ const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
10
+ switch (sub) {
11
+ case "list":
12
+ case "ls":
13
+ return flowsList(rest);
14
+ case "run":
15
+ return flowsRun(rest);
16
+ case "status":
17
+ return flowsStatus(rest);
18
+ case "webhook":
19
+ return flowsWebhook(rest);
20
+ default:
21
+ throw new CliError(`Unknown flows subcommand: ${sub ?? "(none)"}. Try: list, run, status, webhook`);
22
+ }
23
+ }
24
+
25
+ async function flowsList(ctx) {
26
+ const { client, flags, io } = ctx;
27
+ const result = await client.callTool("list_flows", {});
28
+ printResult(io, flags, result);
29
+ return 0;
30
+ }
31
+
32
+ async function flowsRun(ctx) {
33
+ const { client, positionals, flags, io } = ctx;
34
+ const flowId = requirePositional(positionals, 0, "flow-id");
35
+ const articleId = requirePositional(positionals, 1, "article-id");
36
+ const result = await client.callTool("run_flow", { flow_id: flowId, article_id: articleId });
37
+ ok(ctx, "Flow run started.");
38
+ printResult(io, flags, result);
39
+ return 0;
40
+ }
41
+
42
+ async function flowsStatus(ctx) {
43
+ const { client, positionals, flags, io } = ctx;
44
+ const runId = requirePositional(positionals, 0, "run-id");
45
+ const result = await client.callTool("get_run_status", { flow_run_id: runId });
46
+ printResult(io, flags, result);
47
+ return 0;
48
+ }
49
+
50
+ async function flowsWebhook(ctx) {
51
+ const sub = ctx.positionals[0];
52
+ const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
53
+ switch (sub) {
54
+ case "get":
55
+ return webhookGet(rest);
56
+ case "set":
57
+ return webhookSet(rest);
58
+ default:
59
+ throw new CliError(`Unknown flows webhook subcommand: ${sub ?? "(none)"}. Try: get, set`);
60
+ }
61
+ }
62
+
63
+ async function webhookGet(ctx) {
64
+ const { client, flags, io } = ctx;
65
+ const result = await client.callTool("get_webhook", {});
66
+ printResult(io, flags, result);
67
+ return 0;
68
+ }
69
+
70
+ async function webhookSet(ctx) {
71
+ const { client, flags, io } = ctx;
72
+ const url = flagStr(flags.url);
73
+ if (url === undefined) throw new CliError('Missing required --url (an https URL, or "none"/"off" to disable)');
74
+ const disable = ["none", "off", ""].includes(url.toLowerCase());
75
+ const args = compact({ webhook_secret: flagStr(flags.secret) });
76
+ // webhook_url is required and may legitimately be null; set it after compact()
77
+ // since compact() drops undefined (not null) — this assignment is deliberate.
78
+ args.webhook_url = disable ? null : url;
79
+ const result = await client.callTool("set_webhook", args);
80
+ ok(ctx, disable ? "Webhook disabled." : "Webhook set.");
81
+ printResult(io, flags, result);
82
+ return 0;
83
+ }
@@ -0,0 +1,51 @@
1
+ // `insights` — Search Console impressions/clicks across the phantom network, at the
2
+ // network, single-post, and top-posts levels. Ported from phantomstory-cli's
3
+ // insights.ts.
4
+
5
+ import { CliError } from "../client.mjs";
6
+ import { flagStr, flagNum, requirePositional, printResult, compact } from "./shared.mjs";
7
+
8
+ export async function cmdInsights(ctx) {
9
+ const sub = ctx.positionals[0];
10
+ const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
11
+ switch (sub) {
12
+ case "site":
13
+ return insightsSite(rest);
14
+ case "post":
15
+ return insightsPost(rest);
16
+ case "top":
17
+ return insightsTop(rest);
18
+ default:
19
+ throw new CliError(`Unknown insights subcommand: ${sub ?? "(none)"}. Try: site, post, top`);
20
+ }
21
+ }
22
+
23
+ async function insightsSite(ctx) {
24
+ const { client, flags, io } = ctx;
25
+ const args = compact({ period: flagStr(flags.period), collection_id: flagStr(flags.collection) });
26
+ const result = await client.callTool("get_site_performance", args);
27
+ printResult(io, flags, result);
28
+ return 0;
29
+ }
30
+
31
+ async function insightsPost(ctx) {
32
+ const { client, positionals, flags, io } = ctx;
33
+ const articleId = requirePositional(positionals, 0, "article-id");
34
+ const args = compact({ article_id: articleId, period: flagStr(flags.period) });
35
+ const result = await client.callTool("get_post_performance", args);
36
+ printResult(io, flags, result);
37
+ return 0;
38
+ }
39
+
40
+ async function insightsTop(ctx) {
41
+ const { client, flags, io } = ctx;
42
+ const args = compact({
43
+ period: flagStr(flags.period),
44
+ collection_id: flagStr(flags.collection),
45
+ limit: flagNum(flags.limit),
46
+ sort: flagStr(flags.sort),
47
+ });
48
+ const result = await client.callTool("list_top_posts", args);
49
+ printResult(io, flags, result);
50
+ return 0;
51
+ }