@letterstory/cli 0.19.0 → 0.23.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.
@@ -16,7 +16,42 @@
16
16
  // unless you tell it not to.
17
17
 
18
18
  import { CliError } from "../client.mjs";
19
- import { flagStr, requireFlag, requirePositional, printResult, compact, ok, readBodyInput } from "./shared.mjs";
19
+ import {
20
+ flagStr,
21
+ flagBool,
22
+ requireFlag,
23
+ requirePositional,
24
+ printResult,
25
+ compact,
26
+ ok,
27
+ readBodyInput,
28
+ toolField,
29
+ followJob,
30
+ argsFromFlags,
31
+ } from "./shared.mjs";
32
+
33
+ /**
34
+ * How the commands ADDED to this file source their arguments — see `argsFromFlags`
35
+ * in shared.mjs, and `scripts/parity-check.ts`, which compares these keys against
36
+ * each tool's published input schema.
37
+ *
38
+ * The rest of this module predates the field-level ratchet and is covered by the
39
+ * frozen UNMAPPED_TOOLS list. New commands declare; that list only shrinks.
40
+ */
41
+ export const ARGS = {
42
+ create_kernel_endpoint: {
43
+ name: { flag: "name", required: true },
44
+ // Both optional bindings the tool has always accepted at create: which kernel
45
+ // writes, and which shredder (if any) runs inside the loop. Exposed rather than
46
+ // left as a known gap — an endpoint you cannot bind at create is one you have to
47
+ // go and fix in the app.
48
+ kernel_id: "kernel",
49
+ shredder_endpoint_id: "shredder",
50
+ },
51
+ delete_kernel_endpoint: {
52
+ endpoint_id: { positional: 1, label: "endpoint-id", required: true },
53
+ },
54
+ };
20
55
 
21
56
  export async function cmdKernel(ctx) {
22
57
  const sub = ctx.positionals[0];
@@ -31,6 +66,8 @@ export async function cmdKernel(ctx) {
31
66
  return kernelStatus(rest);
32
67
  case "endpoints":
33
68
  return kernelEndpoints(rest);
69
+ case "endpoint":
70
+ return kernelEndpoint(rest);
34
71
  case "write":
35
72
  return kernelWrite(rest);
36
73
  case "polish":
@@ -43,7 +80,7 @@ export async function cmdKernel(ctx) {
43
80
  return kernelBalance(rest);
44
81
  default:
45
82
  throw new CliError(
46
- `Unknown kernel subcommand: ${sub ?? "(none)"}. Try: list, endpoints, write, polish, run-status, runs, balance, run, status`
83
+ `Unknown kernel subcommand: ${sub ?? "(none)"}. Try: list, endpoints, endpoint, write, polish, run-status, runs, balance, run, status`
47
84
  );
48
85
  }
49
86
  }
@@ -84,19 +121,6 @@ const POLL_INTERVAL_MS = 20_000;
84
121
  /** Stop following after this; the run keeps going and `run-status` picks it up. */
85
122
  const POLL_CEILING_MS = 45 * 60 * 1000;
86
123
 
87
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
88
-
89
- /** The tool result is a text payload holding JSON; pull one field out, leniently. */
90
- function readToolField(result, field) {
91
- const raw = result?.content?.[0]?.text;
92
- if (typeof raw !== "string") return undefined;
93
- try {
94
- return JSON.parse(raw)?.[field];
95
- } catch {
96
- return undefined;
97
- }
98
- }
99
-
100
124
  async function kernelEndpoints(ctx) {
101
125
  const { client, flags, io } = ctx;
102
126
  const result = await client.callTool("list_kernel_endpoints", {});
@@ -121,10 +145,17 @@ async function kernelWrite(ctx) {
121
145
  // it, so the verdict lands on the text that actually ships. Costs a shred as well as a
122
146
  // run. Reachable from every other surface and, until now, not from here.
123
147
  shredder_endpoint_id: flagStr(flags.shredder),
148
+ // The flag is --read-as, which is what shipped; an earlier draft of this branch
149
+ // called it --brief-mode and never reached anyone.
124
150
  brief_mode: flagStr(flags["read-as"]),
151
+ // The other three `run_kernel` has always taken and the CLI still did not expose,
152
+ // so the only way to reach them was hand-written JSON through `mcp call`.
153
+ person: flagStr(flags.person),
154
+ author_name: flagStr(flags.author),
155
+ allow_experience_claims: flagBool(flags["experience-claims"]),
125
156
  });
126
157
  const result = await client.callTool("run_kernel", args);
127
- const runId = readToolField(result, "run_id");
158
+ const runId = toolField(result, "run_id");
128
159
  if (!runId) {
129
160
  printResult(io, flags, result);
130
161
  return 0;
@@ -136,22 +167,62 @@ async function kernelWrite(ctx) {
136
167
  }
137
168
 
138
169
  ok(ctx, `Write started as ${runId}. Waiting — this keeps running on the server if you stop.`);
139
- const deadline = Date.now() + POLL_CEILING_MS;
140
- for (;;) {
141
- await sleep(POLL_INTERVAL_MS);
142
- const polled = await client.callTool("get_kernel_endpoint_run", { run_id: runId });
143
- const status = readToolField(polled, "status");
144
- if (status && status !== "running") {
145
- ok(ctx, `Write ${status}.`);
146
- printResult(io, flags, polled);
147
- return status === "done" ? 0 : 1;
148
- }
149
- if (Date.now() > deadline) {
150
- ok(ctx, `Still running after 45 minutes. Follow it with: letterstory kernel run-status ${runId}`);
151
- printResult(io, flags, polled);
152
- return 0;
170
+ let lastStatus;
171
+ const { settled, value: polled } = await followJob(
172
+ io,
173
+ async () => {
174
+ const r = await client.callTool("get_kernel_endpoint_run", { run_id: runId });
175
+ lastStatus = toolField(r, "status");
176
+ return { done: !!lastStatus && lastStatus !== "running" && lastStatus !== "queued", value: r };
177
+ },
178
+ { intervalMs: POLL_INTERVAL_MS, ceilingMs: POLL_CEILING_MS }
179
+ );
180
+
181
+ if (!settled) {
182
+ ok(ctx, `Still running after 45 minutes. Follow it with: letterstory kernel run-status ${runId}`);
183
+ printResult(io, flags, polled);
184
+ return 0;
185
+ }
186
+ ok(ctx, `Write ${lastStatus}.`);
187
+ // The operator waited for the ARTICLE; print it, not the envelope. --json still
188
+ // gets the whole payload.
189
+ printResult(io, flags, polled, (v) => (typeof v?.article === "string" ? v.article : JSON.stringify(v, null, 2)));
190
+ return lastStatus === "done" ? 0 : 1;
191
+ }
192
+
193
+ /**
194
+ * `kernel endpoint create|delete` — the standalone endpoint's own CRUD.
195
+ *
196
+ * `create_kernel_endpoint` and `delete_kernel_endpoint` existed as tools with no
197
+ * command at all, so provisioning the thing `kernel write` runs against could
198
+ * only be done through the app or hand-written JSON.
199
+ */
200
+ async function kernelEndpoint(ctx) {
201
+ const { client, positionals, flags, io } = ctx;
202
+ const action = positionals[0];
203
+ if (action === "create" || action === "new") {
204
+ const args = argsFromFlags(ARGS.create_kernel_endpoint, flags, positionals);
205
+ const name = args.name;
206
+ const result = await client.callTool("create_kernel_endpoint", args);
207
+ ok(ctx, `Created kernel endpoint "${name}".`);
208
+ printResult(io, flags, result);
209
+ return 0;
210
+ }
211
+ if (action === "delete" || action === "rm") {
212
+ const id = requirePositional(positionals, 1, "endpoint-id");
213
+ if (!flags.yes) {
214
+ io.error(`Re-run with --yes to confirm: ${ctx.bin} kernel endpoint delete ${id} --yes`);
215
+ return 1;
153
216
  }
217
+ const result = await client.callTool(
218
+ "delete_kernel_endpoint",
219
+ argsFromFlags(ARGS.delete_kernel_endpoint, flags, positionals)
220
+ );
221
+ ok(ctx, "Kernel endpoint deleted.");
222
+ printResult(io, flags, result);
223
+ return 0;
154
224
  }
225
+ throw new CliError(`Unknown kernel endpoint action: ${action ?? "(none)"}. Try: create, delete`);
155
226
  }
156
227
 
157
228
  /** The OTHER door: reword the document you already have, keeping its own markup. */