@letterstory/cli 0.1.1 → 0.2.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.
@@ -0,0 +1,238 @@
1
+ // Discovery / generic passthrough: `tools` (browse the manifest — list + show) and
2
+ // `call`/`tool` (invoke any tool by name). This is the escape hatch that keeps every
3
+ // tool reachable even ones with no dedicated command group. `discover()` is the same
4
+ // unauthenticated GET both `tools list` and `tools show` read from — there's no
5
+ // separate bundled/static manifest to keep in sync (unlike phantomstory-cli's
6
+ // manifest.ts), so there's no `--live` flag here: it's always live, by construction.
7
+
8
+ import { readFileSync } from "node:fs";
9
+ import { CliError } from "../client.mjs";
10
+ import { flagStr, flagList, requirePositional } from "./shared.mjs";
11
+
12
+ // --- tools: list / show -----------------------------------------------------
13
+
14
+ export async function cmdTools(ctx) {
15
+ const sub = ctx.positionals[0];
16
+ if (sub === undefined || sub === "list" || sub === "ls") {
17
+ const rest = { ...ctx, positionals: ctx.positionals.slice(sub === undefined ? 0 : 1) };
18
+ return toolsList(rest);
19
+ }
20
+ if (sub === "show") {
21
+ return toolsShow({ ...ctx, positionals: ctx.positionals.slice(1) });
22
+ }
23
+ throw new CliError(`Unknown tools subcommand: ${sub}. Try: list, show <name>`);
24
+ }
25
+
26
+ async function toolsList(ctx) {
27
+ const { client, io, flags } = ctx;
28
+ const doc = await client.discover();
29
+ const tools = doc.tools ?? [];
30
+ if (flags.json) {
31
+ io.log(JSON.stringify(tools, null, 2));
32
+ return 0;
33
+ }
34
+ io.log(`${tools.length} tools available at ${client.url}:\n`);
35
+ for (const t of tools) io.log(` ${t.name} — ${t.description ?? ""}`);
36
+ io.log(``);
37
+ io.log(` Run \`${ctx.bin} tools show <name>\` for a tool's full argument schema.`);
38
+ return 0;
39
+ }
40
+
41
+ async function toolsShow(ctx) {
42
+ const { client, positionals, flags, io } = ctx;
43
+ const name = requirePositional(positionals, 0, "name");
44
+ const doc = await client.discover();
45
+ const tool = (doc.tools ?? []).find((t) => t.name === name);
46
+ if (!tool) {
47
+ throw new CliError(`No tool named "${name}". See \`${ctx.bin} tools list\`.`);
48
+ }
49
+ if (flags.json) {
50
+ io.log(JSON.stringify(tool, null, 2));
51
+ return 0;
52
+ }
53
+ renderTool(io, tool);
54
+ return 0;
55
+ }
56
+
57
+ function renderTool(io, tool) {
58
+ io.log(tool.name);
59
+ io.log(` ${tool.description ?? ""}`);
60
+ io.log(``);
61
+ io.log(` capability: ${tool.capability ?? "(none)"}`);
62
+ const props = tool.inputSchema?.properties ?? {};
63
+ const required = new Set(tool.inputSchema?.required ?? []);
64
+ const names = Object.keys(props);
65
+ if (names.length === 0) {
66
+ io.log(` (no arguments)`);
67
+ return;
68
+ }
69
+ io.log(``);
70
+ io.log(` arguments:`);
71
+ for (const name of names) {
72
+ const req = required.has(name) ? "required" : "optional";
73
+ const detail = detailOf(props[name]);
74
+ io.log(` ${name} (${typeLabel(props[name])}, ${req})${detail ? ` — ${detail}` : ""}`);
75
+ }
76
+ }
77
+
78
+ function typeLabel(prop) {
79
+ if (!prop) return "any";
80
+ if (prop.enum) return "enum";
81
+ if (typeof prop.type === "string") return prop.type;
82
+ if (Array.isArray(prop.type)) return prop.type.filter((t) => t !== "null").join("|") || "any";
83
+ if (prop.anyOf) {
84
+ const types = prop.anyOf.map((a) => (typeof a.type === "string" ? a.type : "any")).filter((t) => t !== "null");
85
+ return [...new Set(types)].join("|") || "any";
86
+ }
87
+ return "any";
88
+ }
89
+
90
+ function detailOf(prop) {
91
+ const parts = [];
92
+ if (prop.description) parts.push(prop.description);
93
+ if (prop.enum) parts.push(`one of: ${prop.enum.map(String).join(", ")}`);
94
+ if (prop.default !== undefined) parts.push(`default: ${JSON.stringify(prop.default)}`);
95
+ return parts.join(" · ");
96
+ }
97
+
98
+ // --- call / tool: raw dispatcher --------------------------------------------
99
+
100
+ // Escape hatch: call any tool by name. Args come from --args '<json>', or from
101
+ // individual string flags (everything after the tool name). Keeps the whole
102
+ // manifest reachable without a bespoke subcommand per tool. This is the original,
103
+ // stable spelling; `tool` (below) is Mathew's phantomstory-cli name for the same
104
+ // idea, with schema-aware argument coercion added on top.
105
+ export async function cmdCall(ctx) {
106
+ const { client, positionals, flags, io } = ctx;
107
+ const name = requirePositional(positionals, 0, "tool");
108
+ let args = {};
109
+ if (flags.args !== undefined) {
110
+ try {
111
+ args = JSON.parse(flagStr(flags.args) ?? "");
112
+ } catch {
113
+ throw new CliError(`--args must be a JSON object, e.g. --args '{"name":"My Blog"}'`);
114
+ }
115
+ } else {
116
+ for (const [k, v] of Object.entries(flags)) {
117
+ if (k === "json" || k === "url" || k === "key") continue;
118
+ args[k] = v;
119
+ }
120
+ }
121
+ const result = await client.callTool(name, args);
122
+ io.log(typeof result === "string" ? result : JSON.stringify(result, null, 2));
123
+ return 0;
124
+ }
125
+
126
+ // `tool <name> [--arg k=v ...] [--json-args '{…}'] [--stdin]` — phantomstory-cli's
127
+ // raw dispatcher. Unlike `call`, args are coerced toward the tool's real schema
128
+ // (`--arg limit=5` becomes a number, `--arg tags=a,b` an array), and can be piped in
129
+ // as JSON. Precedence low-to-high: --stdin, then --json-args, then --arg (most
130
+ // specific wins) — matches phantomstory-cli's tools.ts.
131
+ export async function cmdTool(ctx) {
132
+ const { client, positionals, flags, io } = ctx;
133
+ const name = requirePositional(positionals, 0, "tool");
134
+ const doc = await client.discover();
135
+ const tool = (doc.tools ?? []).find((t) => t.name === name);
136
+ if (!tool && !flags.quiet) {
137
+ io.error(`Warning: "${name}" is not in the discovered catalog; sending anyway.`);
138
+ }
139
+
140
+ let args = {};
141
+ if (flags.stdin) args = { ...args, ...asObject(readStdinJson()) };
142
+ const jsonArgs = flagStr(flags["json-args"]);
143
+ if (jsonArgs !== undefined) {
144
+ let parsed;
145
+ try {
146
+ parsed = JSON.parse(jsonArgs);
147
+ } catch (err) {
148
+ throw new CliError(`Invalid --json-args: ${err.message}`);
149
+ }
150
+ args = { ...args, ...asObject(parsed) };
151
+ }
152
+ const pairs = coerceToSchema(parseKeyVals(flagList(flags.arg)), tool?.inputSchema);
153
+ args = { ...args, ...pairs };
154
+
155
+ const result = await client.callTool(name, args);
156
+ io.log(typeof result === "string" ? result : JSON.stringify(result, null, 2));
157
+ return 0;
158
+ }
159
+
160
+ function asObject(v) {
161
+ if (v && typeof v === "object" && !Array.isArray(v)) return v;
162
+ throw new CliError("Arguments must be a JSON object.");
163
+ }
164
+
165
+ function readStdinJson() {
166
+ if (process.stdin.isTTY) {
167
+ throw new CliError("No data on stdin — pipe input in, or omit --stdin.");
168
+ }
169
+ let raw;
170
+ try {
171
+ raw = readFileSync(0, "utf8");
172
+ } catch (err) {
173
+ throw new CliError(`Could not read stdin: ${err.message}`);
174
+ }
175
+ if (!raw) return {};
176
+ try {
177
+ return JSON.parse(raw);
178
+ } catch (err) {
179
+ throw new CliError(`Invalid JSON on stdin: ${err.message}`);
180
+ }
181
+ }
182
+
183
+ function parseKeyVals(items) {
184
+ const out = {};
185
+ for (const item of items) {
186
+ const eq = item.indexOf("=");
187
+ if (eq === -1) throw new CliError(`Malformed --arg "${item}", expected key=value.`);
188
+ out[item.slice(0, eq)] = item.slice(eq + 1);
189
+ }
190
+ return out;
191
+ }
192
+
193
+ function coerceToSchema(raw, schema) {
194
+ const props = schema?.properties ?? {};
195
+ const out = {};
196
+ for (const [key, value] of Object.entries(raw)) out[key] = coerceValue(value, props[key]);
197
+ return out;
198
+ }
199
+
200
+ function coerceValue(value, prop) {
201
+ switch (resolveType(prop)) {
202
+ case "integer":
203
+ case "number": {
204
+ const n = Number(value);
205
+ return Number.isNaN(n) ? value : n;
206
+ }
207
+ case "boolean":
208
+ if (value === "true") return true;
209
+ if (value === "false") return false;
210
+ return value;
211
+ case "array":
212
+ return value
213
+ .split(",")
214
+ .map((s) => s.trim())
215
+ .filter((s) => s.length > 0);
216
+ case "object":
217
+ try {
218
+ return JSON.parse(value);
219
+ } catch (err) {
220
+ throw new CliError(`Invalid object value for --arg: ${err.message}`);
221
+ }
222
+ default:
223
+ return value;
224
+ }
225
+ }
226
+
227
+ function resolveType(prop) {
228
+ if (!prop) return undefined;
229
+ if (typeof prop.type === "string") return prop.type;
230
+ if (Array.isArray(prop.type)) return prop.type.find((t) => t !== "null");
231
+ if (prop.anyOf) {
232
+ for (const alt of prop.anyOf) {
233
+ const t = resolveType(alt);
234
+ if (t && t !== "null") return t;
235
+ }
236
+ }
237
+ return undefined;
238
+ }
@@ -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
+ }
@@ -0,0 +1,52 @@
1
+ // `mcp` — print ready-to-paste configuration for wiring the Letterstory MCP server
2
+ // into an agent (Claude Code, Claude Desktop, Cursor, …). Same endpoint/auth header
3
+ // the CLI itself uses (see client.mjs's `mcpEndpoint` / `x-integrations-key`), just
4
+ // packaged for copy-paste instead of for this process's own requests.
5
+
6
+ import { flagStr } from "./shared.mjs";
7
+
8
+ export function cmdMcp(ctx) {
9
+ const { config, flags, io } = ctx;
10
+ const name = flagStr(flags.name) ?? "letterstory";
11
+ const endpoint = `${config.url}/api/mcp`;
12
+ const header = "x-integrations-key";
13
+ const printKey = Boolean(flags["print-key"]);
14
+ const keyValue = printKey && config.key ? config.key : "${LETTERSTORY_API_KEY}";
15
+
16
+ const jsonConfig = {
17
+ mcpServers: {
18
+ [name]: {
19
+ type: "http",
20
+ url: endpoint,
21
+ headers: { [header]: keyValue },
22
+ },
23
+ },
24
+ };
25
+
26
+ if (flags.json) {
27
+ io.log(JSON.stringify(jsonConfig, null, 2));
28
+ return 0;
29
+ }
30
+
31
+ const masked = config.key ? `${config.key.slice(0, 6)}…${config.key.slice(-4)}` : undefined;
32
+
33
+ io.log(`Letterstory MCP`);
34
+ io.log(` endpoint: ${endpoint}`);
35
+ io.log(` transport: streamable-http`);
36
+ io.log(` auth: ${header}: ${masked ?? "(no key found — run `" + ctx.bin + " login` first)"}`);
37
+
38
+ io.log(``);
39
+ io.log(`Claude Code:`);
40
+ io.log(` claude mcp add --transport http ${name} ${endpoint} \\`);
41
+ io.log(` --header "${header}: ${printKey && config.key ? config.key : "YOUR_KEY"}"`);
42
+
43
+ io.log(``);
44
+ io.log(`Claude Desktop / Cursor (mcpServers block):`);
45
+ for (const line of JSON.stringify(jsonConfig, null, 2).split("\n")) io.log(` ${line}`);
46
+
47
+ if (!printKey) {
48
+ io.log(``);
49
+ io.log(`Tip: export LETTERSTORY_API_KEY=…, or re-run with --print-key to inline it.`);
50
+ }
51
+ return 0;
52
+ }
@@ -0,0 +1,122 @@
1
+ // `posts` (draft/read/publish articles) and `published` (the frozen public view).
2
+ // Ported from phantomstory-cli's posts.ts — same tool calls, same shape, adapted to
3
+ // this CLI's flag/positional/io conventions (no commander, no color).
4
+
5
+ import { CliError } from "../client.mjs";
6
+ import {
7
+ flagStr,
8
+ flagNum,
9
+ requireFlag,
10
+ requirePositional,
11
+ printResult,
12
+ compact,
13
+ ok,
14
+ readBodyInput,
15
+ } from "./shared.mjs";
16
+
17
+ export async function cmdPosts(ctx) {
18
+ const sub = ctx.positionals[0];
19
+ const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
20
+ switch (sub) {
21
+ case "list":
22
+ case "ls":
23
+ return postsList(rest);
24
+ case "show":
25
+ return postsShow(rest);
26
+ case "new":
27
+ return postsNew(rest);
28
+ case "publish":
29
+ return postsPublish(rest);
30
+ case "unpublish":
31
+ return postsUnpublish(rest);
32
+ default:
33
+ throw new CliError(
34
+ `Unknown posts subcommand: ${sub ?? "(none)"}. Try: list, show, new, publish, unpublish`
35
+ );
36
+ }
37
+ }
38
+
39
+ async function postsList(ctx) {
40
+ const { client, flags, io } = ctx;
41
+ const args = compact({ limit: flagNum(flags.limit), collection_id: flagStr(flags.collection) });
42
+ const result = await client.callTool("list_articles", args);
43
+ printResult(io, flags, result);
44
+ return 0;
45
+ }
46
+
47
+ async function postsShow(ctx) {
48
+ const { client, positionals, flags, io } = ctx;
49
+ const id = requirePositional(positionals, 0, "article-id");
50
+ const result = await client.callTool("get_article", { article_id: id });
51
+ printResult(io, flags, result);
52
+ return 0;
53
+ }
54
+
55
+ async function postsNew(ctx) {
56
+ const { client, flags, io } = ctx;
57
+ const title = requireFlag(flags, "title");
58
+ const collection = requireFlag(flags, "collection");
59
+ const body = readBodyInput(flags);
60
+ if (!body) throw new CliError("Provide --body or --file <path>.");
61
+ const result = await client.callTool("ingest_article", { title, body, collection_id: collection });
62
+ ok(ctx, `Created draft "${title}".`);
63
+ printResult(io, flags, result);
64
+ return 0;
65
+ }
66
+
67
+ async function postsPublish(ctx) {
68
+ const { client, positionals, flags, io } = ctx;
69
+ const id = requirePositional(positionals, 0, "article-id");
70
+ const result = await client.callTool("publish_article", { article_id: id });
71
+ ok(ctx, "Published.");
72
+ printResult(io, flags, result);
73
+ return 0;
74
+ }
75
+
76
+ async function postsUnpublish(ctx) {
77
+ const { client, positionals, flags, io } = ctx;
78
+ const id = requirePositional(positionals, 0, "article-id");
79
+ const result = await client.callTool("unpublish_article", { article_id: id });
80
+ ok(ctx, "Unpublished.");
81
+ printResult(io, flags, result);
82
+ return 0;
83
+ }
84
+
85
+ // --- published (frozen public view) -----------------------------------------
86
+
87
+ export async function cmdPublished(ctx) {
88
+ const sub = ctx.positionals[0];
89
+ const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
90
+ switch (sub) {
91
+ case "list":
92
+ case "ls":
93
+ return publishedList(rest);
94
+ case "show":
95
+ return publishedShow(rest);
96
+ default:
97
+ throw new CliError(`Unknown published subcommand: ${sub ?? "(none)"}. Try: list, show`);
98
+ }
99
+ }
100
+
101
+ async function publishedList(ctx) {
102
+ const { client, flags, io } = ctx;
103
+ const args = compact({
104
+ limit: flagNum(flags.limit),
105
+ collection_id: flagStr(flags.collection),
106
+ format: flagStr(flags.format),
107
+ });
108
+ const result = await client.callTool("list_published", args);
109
+ printResult(io, flags, result);
110
+ return 0;
111
+ }
112
+
113
+ async function publishedShow(ctx) {
114
+ const { client, flags, io } = ctx;
115
+ const id = flagStr(flags.id);
116
+ const slug = flagStr(flags.slug);
117
+ if (id === undefined && slug === undefined) throw new CliError("Pass --id or --slug.");
118
+ const args = compact({ article_id: id, slug, format: flagStr(flags.format) });
119
+ const result = await client.callTool("get_published", args);
120
+ printResult(io, flags, result);
121
+ return 0;
122
+ }
@@ -0,0 +1,95 @@
1
+ // Helpers shared by every command module: flag/positional readers, JSON-vs-formatted
2
+ // output, and the --quiet-aware success line. Kept dependency-free like the rest of
3
+ // the CLI — no formatting library, just small string builders.
4
+
5
+ import { readFileSync } from "node:fs";
6
+ import { CliError } from "../client.mjs";
7
+
8
+ // A flag with no value parses to `true`; coerce that back to undefined so a bare
9
+ // `--name` reads as "missing", not the string "true". A repeated flag (see cli.mjs's
10
+ // parseArgs) parses to an array — also "missing" for a single-value read.
11
+ export function flagStr(v) {
12
+ return typeof v === "string" ? v : undefined;
13
+ }
14
+
15
+ export function flagNum(v) {
16
+ const s = flagStr(v);
17
+ if (s === undefined) return undefined;
18
+ const n = Number(s);
19
+ if (!Number.isFinite(n)) throw new CliError(`Expected a number, got "${s}"`);
20
+ return n;
21
+ }
22
+
23
+ export function flagBool(v) {
24
+ return v === true;
25
+ }
26
+
27
+ // Repeatable flags (e.g. `--topic a --topic b`) accumulate into an array in
28
+ // parseArgs; a single occurrence stays a plain string. Normalize both to a list.
29
+ export function flagList(v) {
30
+ if (v === undefined) return [];
31
+ if (Array.isArray(v)) return v.filter((x) => typeof x === "string");
32
+ return typeof v === "string" ? [v] : [];
33
+ }
34
+
35
+ export function requireFlag(flags, name) {
36
+ const v = flagStr(flags[name]);
37
+ if (v === undefined) throw new CliError(`Missing required --${name}`);
38
+ return v;
39
+ }
40
+
41
+ export function requirePositional(positionals, index, label) {
42
+ const v = positionals[index];
43
+ if (v === undefined) throw new CliError(`Missing required <${label}>`);
44
+ return v;
45
+ }
46
+
47
+ // Drop undefined entries so callTool args only carry fields the user actually set.
48
+ export function compact(obj) {
49
+ const out = {};
50
+ for (const [k, v] of Object.entries(obj)) {
51
+ if (v !== undefined) out[k] = v;
52
+ }
53
+ return out;
54
+ }
55
+
56
+ export function printResult(io, flags, value, formatter) {
57
+ if (flags.json || !formatter) {
58
+ io.log(JSON.stringify(value, null, 2));
59
+ } else {
60
+ io.log(formatter(value));
61
+ }
62
+ }
63
+
64
+ // A success line that stays quiet under --quiet or --json (scripting doesn't want
65
+ // chatter mixed into a parseable stream). Existing commands (deploy/domain) print
66
+ // directly through io.log and are intentionally left alone; this gate is for the
67
+ // new command groups only.
68
+ export function ok(ctx, message) {
69
+ if (ctx.flags.quiet || ctx.flags.json) return;
70
+ ctx.io.log(message);
71
+ }
72
+
73
+ // Read a text body from --body, or --file (a path, or "-" for stdin). Returns
74
+ // undefined if neither flag was given, so callers can decide whether that's an error.
75
+ export function readBodyInput(flags) {
76
+ const body = flagStr(flags.body);
77
+ if (body !== undefined) return body;
78
+ const file = flagStr(flags.file);
79
+ if (file === undefined) return undefined;
80
+ if (file === "-") {
81
+ if (process.stdin.isTTY) {
82
+ throw new CliError("No data on stdin. Pipe input in, or use --body/--file <path> instead of --file -.");
83
+ }
84
+ try {
85
+ return readFileSync(0, "utf8");
86
+ } catch (err) {
87
+ throw new CliError(`Could not read stdin: ${err.message}`);
88
+ }
89
+ }
90
+ try {
91
+ return readFileSync(file, "utf8");
92
+ } catch (err) {
93
+ throw new CliError(`Could not read --file ${file}: ${err.message}`);
94
+ }
95
+ }