@letterstory/cli 0.25.5 → 0.26.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.
package/README.md CHANGED
@@ -312,25 +312,34 @@ letterstory series create --name "Secrets, tool by tool" \
312
312
  letterstory series backfill <series-id> --collection <uuid>
313
313
  ```
314
314
 
315
- ## Research and writing kernels
315
+ ## Arenas and writing kernels
316
316
 
317
- Two different steps, in this order. The **research agent** reads the live web and writes a
318
- sourced outline into the post; a **writing kernel** writes the draft from the brief it's
319
- given and does no retrieval of its own. Both take minutes, so both are start-then-poll:
317
+ Two different steps, in this order. An **arena** — an adversarial cluster of research agents —
318
+ reads the live web and writes a sourced, fact-checked outline (into the post when you pass
319
+ `--article`, otherwise back on the run); a **writing kernel** writes the draft from the brief
320
+ it's given and does no retrieval of its own. Both take minutes, so both are start-then-poll:
320
321
 
321
322
  ```bash
322
- # 1. Research the topic and write an outline into the post
323
- letterstory research start --article <uuid> --topic "payroll solutions" \
324
- --url https://example.com/a --must-include "Pricing" --depth fleshed_out
325
- letterstory research status --article <uuid> # phases, sources, outline
326
- letterstory research cancel --article <uuid>
323
+ # 1. Research the topic — hosted on a post (outline written into it) or standalone
324
+ letterstory arena run --article <uuid> --topic "payroll solutions" \
325
+ --source https://example.com/a --must-include "Pricing" --depth fleshed_out
326
+ letterstory arena run --topic "payroll solutions" --no-wait # standalone; prints the run id
327
+ letterstory arena run-status <run-id> # phases, sources, outline, verification
328
+ letterstory arena cancel <run-id>
329
+ letterstory arena balance # arena runs left (direct runs only)
327
330
 
328
331
  # …with --gate, the run pauses for you to pick the angle:
329
- letterstory research start --article <uuid> --gate
330
- letterstory research status --article <uuid> # status: awaiting_direction + direction_options
331
- letterstory research direction --article <uuid> --choice 1
332
- letterstory research direction --article <uuid> --custom "Lead with the hidden costs"
333
- letterstory research direction --article <uuid> --skip # let the agent decide
332
+ letterstory arena run --article <uuid> --gate
333
+ letterstory arena run-status <run-id> # status: awaiting_direction + direction_options
334
+ letterstory arena direction <run-id> --choice 1
335
+ letterstory arena direction <run-id> --custom "Lead with the hidden costs"
336
+ letterstory arena direction <run-id> --skip # let the agents decide
337
+
338
+ # Arenas are callable endpoints with one-file defaults (the org's default arena is "Letterstory"):
339
+ letterstory arena list
340
+ letterstory arena endpoint create --name "Docs research" --defaults '{"depth":"barebones"}'
341
+ letterstory arena endpoint config <arena-id> [--file cfg.json]
342
+ letterstory arena endpoint delete <arena-id> --yes
334
343
 
335
344
  # 2. Have a kernel write the draft
336
345
  letterstory kernel list
package/lib/cli.mjs CHANGED
@@ -30,7 +30,7 @@ import {
30
30
  cmdOnboarding,
31
31
  cmdSeries,
32
32
  cmdInsights,
33
- cmdResearch,
33
+ cmdArena,
34
34
  cmdAutoPen,
35
35
  cmdCovers,
36
36
  cmdKernel,
@@ -44,7 +44,7 @@ import {
44
44
  } from "./commands.mjs";
45
45
 
46
46
  // Keep in sync with cli/package.json.
47
- export const VERSION = "0.25.5";
47
+ export const VERSION = "0.26.0";
48
48
 
49
49
  // Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
50
50
  // can't accidentally swallow the id as --json's value.
@@ -64,7 +64,7 @@ const BOOLEAN_FLAGS = new Set([
64
64
  "rebuild",
65
65
  "print-key",
66
66
  "stdin",
67
- // `covers generate <id> --regenerate` and `research start --gate --article <id>`
67
+ // `covers generate <id> --regenerate` and `arena run --gate --article <id>`
68
68
  // both put a value-less flag next to a value: without these entries the parser
69
69
  // hands the following token to the flag and the real argument disappears.
70
70
  "regenerate",
@@ -74,6 +74,7 @@ const BOOLEAN_FLAGS = new Set([
74
74
  "no-auto-deploy",
75
75
  "stock-only",
76
76
  "gate",
77
+ "no-verify",
77
78
  "skip",
78
79
  // `phantom-onboard domain` / `admin create-org --onboard` switches.
79
80
  "watch",
@@ -277,14 +278,25 @@ Strategy & onboarding:
277
278
  series create --name <n> --description <d> [--coverage a,b,c] [--collection <uuid>] [--template <key>]
278
279
  series backfill <series-id> [--collection <uuid>] Reconstruct a series from existing posts
279
280
 
280
- Research agent (deep research -> outline written into the post):
281
- research start --article <uuid> [--topic <text>] [--url <url> …] [--must-include <text> …]
282
- [--depth barebones|fleshed_out] [--gate]
283
- Start a run; takes minutes
284
- research status --article <uuid> Phases, sources, outline
285
- research direction --article <uuid> (--choice <n> | --custom <text> | --skip)
281
+ Arenas (adversarial clusters of research agents -> a sourced, fact-checked outline):
282
+ arena run --topic <text> [--source <url> …] [--must-include <text> …] [--depth barebones|fleshed_out]
283
+ [--gate] [--no-verify] [--provider native|exa|youcom|firecrawl] [--mention off|subtle|woven]
284
+ [--context-file ctx.json] [--block <name> …] [--block-domain <domain> …]
285
+ [--arena <arena-id>] [--article <uuid>] [--no-wait]
286
+ Start a run and follow it. --article makes
287
+ it HOSTED (outline written into that post,
288
+ its title is the default topic); otherwise
289
+ everything comes back on the run.
290
+ arena run-status <run-id> Phases, sources, outline, verification
291
+ arena runs [--arena <id>] [--limit <n>] Recent runs (origin, caller, verification)
292
+ arena direction <run-id> (--choice <n> | --custom <text> | --skip)
286
293
  Answer the --gate pause
287
- research cancel --article <uuid> Cancel a run in flight
294
+ arena cancel <run-id> Cancel a run in flight
295
+ arena balance Arena runs left on this org
296
+ arena list Your arenas (callable endpoints)
297
+ arena endpoint create --name <n> [--defaults '<json>'|--defaults-file <path>]
298
+ arena endpoint config <arena-id> [--file cfg.json] Print the one-file config, or replace it
299
+ arena endpoint delete <arena-id> --yes
288
300
 
289
301
  Auto-Pen (research -> kernel -> Enhance, in one run — and the ONLY path that
290
302
  applies a collection's content template; kernel run does not):
@@ -455,10 +467,13 @@ Seers (event-driven signals -> drafts):
455
467
  seers list List seers with recent activity
456
468
  seers get <seer-id> One seer's full config + poll state
457
469
  seers providers Provider catalog: config fields + behavior keys
458
- seers create --name <n> --collection <uuid> --provider <p>
470
+ seers create --name <n> --provider <p> [--collection <uuid>]
459
471
  [--mode suggest|auto] [--interval <min>] [--config '<json>'|--config-file <path>]
460
- seers update <seer-id> [--name] [--collection] [--mode] [--enabled true|false]
461
- [--interval <min>] [--config '<json>'|--config-file <path>]
472
+ --collection is optional: omit it for a
473
+ detect-only seer (scans + judges, never
474
+ produces). --mode auto requires one.
475
+ seers update <seer-id> [--name] [--collection <uuid> | --clear-collection] [--mode]
476
+ [--enabled true|false] [--interval <min>] [--config '<json>'|--config-file <path>]
462
477
  seers delete <seer-id> --yes
463
478
  seers run <seer-id> [--dry-run] [--no-produce] [--async]
464
479
  Run now — THE DOOR (one seer run from the balance).
@@ -591,7 +606,7 @@ const CLIENT_COMMANDS = {
591
606
  onboarding: cmdOnboarding,
592
607
  series: cmdSeries,
593
608
  insights: cmdInsights,
594
- research: cmdResearch,
609
+ arena: cmdArena,
595
610
  autopen: cmdAutoPen,
596
611
  covers: cmdCovers,
597
612
  kernel: cmdKernel,
@@ -0,0 +1,320 @@
1
+ // `arena` — Arenas, adversarial clusters of research agents, on both of their doors.
2
+ //
3
+ // An arena turns a TOPIC into a sourced, fact-checked outline: gather agents for the topic
4
+ // and for every named subject it finds, a reader, a direction agent proposing competing
5
+ // theses, a synthesis gated on a ledger of sourced figures, and a verifier that argues
6
+ // every claim against the sources and the live web. Minutes long, so `arena run` submits
7
+ // and then follows the run unless you tell it not to (`--no-wait`); `arena run-status`
8
+ // picks it up later. Pass `--article <id>` to make it a HOSTED run (the outline is written
9
+ // into that post — what the post editor's Research panel does); omit it and everything
10
+ // comes back on the run. Every direct run spends one arena run (`arena balance`).
11
+
12
+ import { readFileSync } from "node:fs";
13
+ import { CliError } from "../client.mjs";
14
+ import {
15
+ flagStr,
16
+ flagNum,
17
+ flagBool,
18
+ requirePositional,
19
+ printResult,
20
+ compact,
21
+ ok,
22
+ toolField,
23
+ followJob,
24
+ argsFromFlags,
25
+ } from "./shared.mjs";
26
+
27
+ /** `--<flag> <path>` → the file's parsed JSON, or undefined when the flag is not set. */
28
+ function readJsonFileFlag(flags, flag) {
29
+ const path = flagStr(flags[flag]);
30
+ if (path === undefined) return undefined;
31
+ let text;
32
+ try {
33
+ text = readFileSync(path, "utf8");
34
+ } catch (error) {
35
+ throw new CliError(
36
+ `Could not read --${flag} ${path}: ${error instanceof Error ? error.message : String(error)}`
37
+ );
38
+ }
39
+ try {
40
+ return JSON.parse(text);
41
+ } catch {
42
+ throw new CliError(`--${flag} ${path} is not valid JSON.`);
43
+ }
44
+ }
45
+
46
+ /** `--<flag> '<json>'` inline, or `--<flag>-file <path>` → a JSON object, or undefined. */
47
+ function readJsonObjectFlag(flags, name) {
48
+ const inline = flagStr(flags[name]);
49
+ if (inline !== undefined) {
50
+ let parsed;
51
+ try {
52
+ parsed = JSON.parse(inline);
53
+ } catch (err) {
54
+ throw new CliError(`--${name} is not valid JSON: ${err.message}`);
55
+ }
56
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
57
+ throw new CliError(`--${name} must be a JSON object`);
58
+ return parsed;
59
+ }
60
+ return readJsonFileFlag(flags, `${name}-file`);
61
+ }
62
+
63
+ /** The per-run knobs, shared by run_arena and the arena's own defaults. */
64
+ const RUN_OPTIONS = {
65
+ depth: { flag: "depth", enum: ["barebones", "fleshed_out"] },
66
+ direction_gate: { flag: "gate", type: "bool" },
67
+ provider: { flag: "provider", enum: ["native", "exa", "youcom", "firecrawl"] },
68
+ // --no-verify turns the fact-check off; nothing sent otherwise (the default is on).
69
+ verify: { value: (flags) => (flags["no-verify"] === true ? false : undefined) },
70
+ mention_degree: { flag: "mention", enum: ["off", "subtle", "woven"] },
71
+ // Positioning as data — a file, not flags: `--context-file ctx.json` of
72
+ // {manifesto, stances[], priority_topics[], avoid_topics[], identity{is[],is_not[],one_liner}}.
73
+ context: { value: (flags) => readJsonObjectFlag(flags, "context") },
74
+ blocked_mentions: { flag: "block", type: "list" },
75
+ blocked_domains: { flag: "block-domain", type: "list" },
76
+ };
77
+
78
+ /**
79
+ * How every command in this file sources its tool inputs — see `argsFromFlags` in
80
+ * shared.mjs and `scripts/parity-check.ts`, which compares these keys against each tool's
81
+ * published input schema.
82
+ */
83
+ export const ARGS = {
84
+ run_arena: {
85
+ arena_id: { flag: "arena" },
86
+ topic: "topic",
87
+ source_urls: { flag: "source", type: "list" },
88
+ must_include: { flag: "must-include", type: "list" },
89
+ shape: { value: (flags) => readJsonObjectFlag(flags, "shape") },
90
+ existing_content: { value: (flags) => flagStr(flags["existing-content"]) },
91
+ article_id: { flag: "article" },
92
+ ...RUN_OPTIONS,
93
+ },
94
+ get_arena_run: {
95
+ run_id: { positional: 0, label: "run-id", required: true },
96
+ },
97
+ list_arena_runs: {
98
+ arena_id: { flag: "arena" },
99
+ limit: { flag: "limit", type: "num" },
100
+ },
101
+ submit_arena_direction: {
102
+ run_id: { positional: 0, label: "run-id", required: true },
103
+ choice: { flag: "choice", type: "num" },
104
+ custom: "custom",
105
+ skip: { flag: "skip", type: "bool" },
106
+ },
107
+ cancel_arena_run: {
108
+ run_id: { positional: 0, label: "run-id", required: true },
109
+ },
110
+ get_arena_run_balance: {},
111
+ list_arenas: {},
112
+ create_arena: {
113
+ name: { flag: "name", required: true },
114
+ defaults: { value: (flags) => readJsonObjectFlag(flags, "defaults") },
115
+ },
116
+ delete_arena: {
117
+ arena_id: { positional: 1, label: "arena-id", required: true },
118
+ },
119
+ // The arena's configuration as ONE file: `arena endpoint config <id>` prints it,
120
+ // `arena endpoint config <id> --file cfg.json` replaces it with the file's document.
121
+ get_arena_config: {
122
+ arena_id: { positional: 1, label: "arena-id", required: true },
123
+ },
124
+ set_arena_config: {
125
+ arena_id: { positional: 1, label: "arena-id", required: true },
126
+ config: { value: (flags) => readJsonFileFlag(flags, "file") },
127
+ },
128
+ };
129
+
130
+ export async function cmdArena(ctx) {
131
+ const sub = ctx.positionals[0];
132
+ const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
133
+ switch (sub) {
134
+ case "run":
135
+ case "start":
136
+ return arenaRun(rest);
137
+ case "run-status":
138
+ case "status":
139
+ return arenaRunStatus(rest);
140
+ case "runs":
141
+ return arenaRuns(rest);
142
+ case "direction":
143
+ return arenaDirection(rest);
144
+ case "cancel":
145
+ return arenaCancel(rest);
146
+ case "balance":
147
+ return arenaBalance(rest);
148
+ case "list":
149
+ case "ls":
150
+ case "endpoints":
151
+ return arenaList(rest);
152
+ case "endpoint":
153
+ return arenaEndpoint(rest);
154
+ default:
155
+ throw new CliError(
156
+ `Unknown arena subcommand: ${sub ?? "(none)"}. Try: run, run-status, runs, direction, cancel, balance, list, endpoint`
157
+ );
158
+ }
159
+ }
160
+
161
+ /** A run takes minutes; this is about feeling alive. */
162
+ const POLL_INTERVAL_MS = 15_000;
163
+ /** Stop following after this; the run keeps going and `run-status` picks it up. */
164
+ const POLL_CEILING_MS = 30 * 60 * 1000;
165
+
166
+ function isActive(status) {
167
+ return status === "running";
168
+ }
169
+
170
+ async function arenaRun(ctx) {
171
+ const { client, flags, io } = ctx;
172
+ const args = argsFromFlags(ARGS.run_arena, flags, ctx.positionals);
173
+ if (args.topic === undefined && args.article_id === undefined) {
174
+ throw new CliError("Provide --topic <text> (or --article <id> to research a post by its title).");
175
+ }
176
+ const result = await client.callTool("run_arena", args);
177
+ const runId = toolField(result, "run_id");
178
+ if (!runId) {
179
+ printResult(io, flags, result);
180
+ return 0;
181
+ }
182
+ if (flags["no-wait"] === true) {
183
+ ok(ctx, `Run started as ${runId}. Follow it with: ${ctx.bin} arena run-status ${runId}`);
184
+ printResult(io, flags, result);
185
+ return 0;
186
+ }
187
+
188
+ ok(ctx, `Run started as ${runId}. Waiting — this keeps running on the server if you stop.`);
189
+ let lastStatus;
190
+ const { settled, value: polled } = await followJob(
191
+ io,
192
+ async () => {
193
+ const r = await client.callTool("get_arena_run", { run_id: runId });
194
+ lastStatus = toolField(r?.run ?? r, "status");
195
+ return { done: !!lastStatus && !isActive(lastStatus), value: r };
196
+ },
197
+ { intervalMs: POLL_INTERVAL_MS, ceilingMs: POLL_CEILING_MS }
198
+ );
199
+
200
+ if (!settled) {
201
+ ok(ctx, `Still running after 30 minutes. Follow it with: ${ctx.bin} arena run-status ${runId}`);
202
+ printResult(io, flags, polled);
203
+ return 0;
204
+ }
205
+ if (lastStatus === "awaiting_direction") {
206
+ ok(
207
+ ctx,
208
+ `Paused for a direction. Answer with: ${ctx.bin} arena direction ${runId} (--choice <n> | --custom <text> | --skip)`
209
+ );
210
+ printResult(io, flags, polled);
211
+ return 0;
212
+ }
213
+ ok(ctx, `Run ${lastStatus}.`);
214
+ // The operator waited for the OUTLINE; print it, not the envelope. --json gets it all.
215
+ printResult(io, flags, polled, (v) =>
216
+ typeof v?.run?.outline === "string" ? v.run.outline : JSON.stringify(v, null, 2)
217
+ );
218
+ return lastStatus === "done" ? 0 : 1;
219
+ }
220
+
221
+ async function arenaRunStatus(ctx) {
222
+ const { client, flags, io } = ctx;
223
+ const args = argsFromFlags(ARGS.get_arena_run, flags, ctx.positionals);
224
+ const result = await client.callTool("get_arena_run", args);
225
+ printResult(io, flags, result);
226
+ return 0;
227
+ }
228
+
229
+ async function arenaRuns(ctx) {
230
+ const { client, flags, io } = ctx;
231
+ const args = argsFromFlags(ARGS.list_arena_runs, flags, ctx.positionals);
232
+ const result = await client.callTool("list_arena_runs", args);
233
+ printResult(io, flags, result);
234
+ return 0;
235
+ }
236
+
237
+ async function arenaDirection(ctx) {
238
+ const { client, flags, io } = ctx;
239
+ const runId = requirePositional(ctx.positionals, 0, "run-id");
240
+ const choice = flagNum(flags.choice);
241
+ const custom = flagStr(flags.custom);
242
+ const skip = flagBool(flags.skip);
243
+ const given = [choice !== undefined, custom !== undefined, skip].filter(Boolean).length;
244
+ if (given !== 1) {
245
+ throw new CliError("Provide exactly one of --choice <n>, --custom <text>, --skip");
246
+ }
247
+ const args = compact({ run_id: runId, choice, custom, skip: skip ? true : undefined });
248
+ const result = await client.callTool("submit_arena_direction", args);
249
+ ok(ctx, "Direction recorded; the run resumed into synthesis.");
250
+ printResult(io, flags, result);
251
+ return 0;
252
+ }
253
+
254
+ async function arenaCancel(ctx) {
255
+ const { client, flags, io } = ctx;
256
+ const args = argsFromFlags(ARGS.cancel_arena_run, flags, ctx.positionals);
257
+ const result = await client.callTool("cancel_arena_run", args);
258
+ ok(ctx, result?.cancelled ? "Arena run cancelled." : "No run was in flight.");
259
+ printResult(io, flags, result);
260
+ return 0;
261
+ }
262
+
263
+ async function arenaBalance(ctx) {
264
+ const { client, flags, io } = ctx;
265
+ const result = await client.callTool("get_arena_run_balance", {});
266
+ printResult(io, flags, result);
267
+ return 0;
268
+ }
269
+
270
+ async function arenaList(ctx) {
271
+ const { client, flags, io } = ctx;
272
+ const result = await client.callTool("list_arenas", {});
273
+ printResult(io, flags, result);
274
+ return 0;
275
+ }
276
+
277
+ /**
278
+ * `arena endpoint create|delete|config` — the arena's own CRUD, and its configuration as
279
+ * one file (the same document `get_arena_config` returns and `set_arena_config` accepts).
280
+ */
281
+ async function arenaEndpoint(ctx) {
282
+ const { client, positionals, flags, io } = ctx;
283
+ const action = positionals[0];
284
+ if (action === "create" || action === "new") {
285
+ const args = argsFromFlags(ARGS.create_arena, flags, positionals);
286
+ const result = await client.callTool("create_arena", args);
287
+ ok(ctx, `Created arena "${args.name}".`);
288
+ printResult(io, flags, result);
289
+ return 0;
290
+ }
291
+ if (action === "delete" || action === "rm") {
292
+ const id = requirePositional(positionals, 1, "arena-id");
293
+ if (!flags.yes) {
294
+ io.error(`Re-run with --yes to confirm: ${ctx.bin} arena endpoint delete ${id} --yes`);
295
+ return 1;
296
+ }
297
+ const result = await client.callTool("delete_arena", argsFromFlags(ARGS.delete_arena, flags, positionals));
298
+ ok(ctx, "Arena deleted.");
299
+ printResult(io, flags, result);
300
+ return 0;
301
+ }
302
+ if (action === "config") {
303
+ const id = requirePositional(positionals, 1, "arena-id");
304
+ if (flagStr(flags.file) === undefined) {
305
+ const result = await client.callTool(
306
+ "get_arena_config",
307
+ argsFromFlags(ARGS.get_arena_config, flags, positionals)
308
+ );
309
+ // The document itself, so `> cfg.json` round-trips into `--file cfg.json`.
310
+ printResult(io, flags, result, (r) => JSON.stringify(r?.config ?? r, null, 2));
311
+ return 0;
312
+ }
313
+ const args = argsFromFlags(ARGS.set_arena_config, flags, positionals);
314
+ const result = await client.callTool("set_arena_config", args);
315
+ ok(ctx, `Replaced the configuration of arena ${id}.`);
316
+ printResult(io, flags, result);
317
+ return 0;
318
+ }
319
+ throw new CliError(`Unknown arena endpoint action: ${action ?? "(none)"}. Try: create, delete, config`);
320
+ }
@@ -16,6 +16,16 @@ import { readFileSync, writeFileSync } from "node:fs";
16
16
  import { CliError } from "../client.mjs";
17
17
  import { flagStr, flagBool, requirePositional, printResult, ok, argsFromFlags } from "./shared.mjs";
18
18
 
19
+ // A single-value flag the API clears with an explicit `null` (same idiom as
20
+ // strategy.mjs's clearableList): `--clear-collection` sends null, `--collection <uuid>`
21
+ // sends it, neither sends nothing (no change).
22
+ const clearableValue = (flag, clearFlag) => ({
23
+ value: (flags) => {
24
+ if (flags[clearFlag]) return null;
25
+ return flagStr(flags[flag]);
26
+ },
27
+ });
28
+
19
29
  /**
20
30
  * How the ratcheted commands in this file source their tool inputs — see `argsFromFlags` in
21
31
  * shared.mjs and `scripts/parity-check.ts`. The rest of the module predates the field-level
@@ -46,7 +56,9 @@ export const ARGS = {
46
56
  },
47
57
  create_seer: {
48
58
  name: { flag: "name", required: true },
49
- collection_id: { flag: "collection", required: true },
59
+ // Optional (2026-09-18): omit it for a detect-only seer — it can scan and judge
60
+ // forever but never produce (auto mode is rejected without one).
61
+ collection_id: { flag: "collection" },
50
62
  provider: { flag: "provider", required: true },
51
63
  mode: { flag: "mode", enum: ["suggest", "auto"] },
52
64
  poll_interval_minutes: { flag: "interval", type: "num" },
@@ -57,7 +69,9 @@ export const ARGS = {
57
69
  update_seer: {
58
70
  seer_id: { positional: 0, label: "seer-id", required: true },
59
71
  name: { flag: "name" },
60
- collection_id: { flag: "collection" },
72
+ // --collection <uuid> sets it, --clear-collection sends null (the seer becomes
73
+ // detect-only — rejected if it's mode "auto"), neither leaves it unchanged.
74
+ collection_id: clearableValue("collection", "clear-collection"),
61
75
  mode: { flag: "mode", enum: ["suggest", "auto"] },
62
76
  enabled: { value: (flags) => readEnabledFlag(flags) },
63
77
  poll_interval_minutes: { flag: "interval", type: "num" },
package/lib/commands.mjs CHANGED
@@ -19,7 +19,7 @@ export * from "./commands/probes.mjs";
19
19
  export * from "./commands/series.mjs";
20
20
  export * from "./commands/insights.mjs";
21
21
  export * from "./commands/covers.mjs";
22
- export * from "./commands/research.mjs";
22
+ export * from "./commands/arena.mjs";
23
23
  export * from "./commands/autopen.mjs";
24
24
  export * from "./commands/kernel.mjs";
25
25
  export * from "./commands/phantom-job.mjs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.25.5",
3
+ "version": "0.26.0",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -1,87 +0,0 @@
1
- // `research` — the deep-research agent: read supplied URLs, search the live web, and
2
- // write a sourced outline into a post. Same shape as `kernel`: a run takes minutes, so
3
- // `start` returns as soon as the run is queued and `status` checks on demand. Writing
4
- // kernels do NOT research — `research start` then `kernel run` is the full pipeline.
5
-
6
- import { CliError } from "../client.mjs";
7
- import { flagStr, flagNum, flagBool, requireFlag, printResult, compact, ok, argsFromFlags } from "./shared.mjs";
8
-
9
- /** See `argsFromFlags` in shared.mjs; `scripts/parity-check.ts` diffs these keys against the tool. */
10
- export const ARGS = {
11
- start_research: {
12
- article_id: { flag: "article", required: true },
13
- topic: "topic",
14
- source_urls: { flag: "url", type: "list" },
15
- must_include: { flag: "must-include", type: "list" },
16
- depth: { flag: "depth", enum: ["barebones", "fleshed_out"] },
17
- direction_gate: { flag: "gate", type: "bool" },
18
- },
19
- };
20
-
21
- export async function cmdResearch(ctx) {
22
- const sub = ctx.positionals[0];
23
- const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
24
- switch (sub) {
25
- case "start":
26
- case "run":
27
- return researchStart(rest);
28
- case "status":
29
- return researchStatus(rest);
30
- case "direction":
31
- return researchDirection(rest);
32
- case "cancel":
33
- return researchCancel(rest);
34
- default:
35
- throw new CliError(
36
- `Unknown research subcommand: ${sub ?? "(none)"}. Try: start, status, direction, cancel`
37
- );
38
- }
39
- }
40
-
41
- async function researchStart(ctx) {
42
- const { client, flags, io } = ctx;
43
- const args = argsFromFlags(ARGS.start_research, flags, ctx.positionals);
44
- const result = await client.callTool("start_research", args);
45
- ok(ctx, `Research started. Check progress with: research status --article ${args.article_id}`);
46
- printResult(io, flags, result);
47
- return 0;
48
- }
49
-
50
- async function researchStatus(ctx) {
51
- const { client, flags, io } = ctx;
52
- const article = requireFlag(flags, "article");
53
- const result = await client.callTool("get_research_status", { article_id: article });
54
- printResult(io, flags, result);
55
- return 0;
56
- }
57
-
58
- async function researchDirection(ctx) {
59
- const { client, flags, io } = ctx;
60
- const article = requireFlag(flags, "article");
61
- const choice = flagNum(flags.choice);
62
- const custom = flagStr(flags.custom);
63
- const skip = flagBool(flags.skip);
64
- const given = [choice !== undefined, custom !== undefined, skip].filter(Boolean).length;
65
- if (given !== 1) {
66
- throw new CliError("Provide exactly one of --choice <n>, --custom <text>, --skip");
67
- }
68
- const args = compact({
69
- article_id: article,
70
- choice,
71
- custom,
72
- skip: skip ? true : undefined,
73
- });
74
- const result = await client.callTool("submit_research_direction", args);
75
- ok(ctx, "Direction recorded; the run resumed into synthesis.");
76
- printResult(io, flags, result);
77
- return 0;
78
- }
79
-
80
- async function researchCancel(ctx) {
81
- const { client, flags, io } = ctx;
82
- const article = requireFlag(flags, "article");
83
- const result = await client.callTool("cancel_research", { article_id: article });
84
- ok(ctx, result?.cancelled ? "Research run cancelled." : "No research run was in flight.");
85
- printResult(io, flags, result);
86
- return 0;
87
- }