@letterstory/cli 0.25.4 → 0.25.5

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/lib/cli.mjs CHANGED
@@ -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.4";
47
+ export const VERSION = "0.25.5";
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.
@@ -460,7 +460,14 @@ Seers (event-driven signals -> drafts):
460
460
  seers update <seer-id> [--name] [--collection] [--mode] [--enabled true|false]
461
461
  [--interval <min>] [--config '<json>'|--config-file <path>]
462
462
  seers delete <seer-id> --yes
463
- seers run <seer-id> Poll + produce now (can take minutes; spends AI budget)
463
+ seers run <seer-id> [--dry-run] [--no-produce] [--async]
464
+ Run now — THE DOOR (one seer run from the balance).
465
+ --dry-run: scan + judge, persist nothing, print signals
466
+ --no-produce: detect + record events only
467
+ --async: queue for the heartbeat, print a run id
468
+ seers run-status <run-id> One run: status, counts, constraints, verification
469
+ seers runs [--seer <seer-id>] [--limit <n>] Recent runs (origin, caller, verification)
470
+ seers balance Seer runs granted / used / available
464
471
  seers events <seer-id> [--limit <n>] [--offset <n>] Detected signals w/ dedup + relevance
465
472
  seers dismiss <seer-id> <event-id> Dismiss a signal (feeds the relevance judge)
466
473
  seers headlines [--collection <uuid>] Seer-sourced suggestion feed
@@ -7,12 +7,14 @@
7
7
  // Connections are both headless: `seers connect github --token` (a PAT) and
8
8
  // `seers connect notion --token` (a Notion internal-integration secret; run
9
9
  // `seers connect notion` with no token for how to mint one and which pages to share).
10
- // One thing to know: `seers run` executes synchronously server-side (news/regulation
11
- // web scans can take minutes) and spends model budget — it is usage-gated per org.
10
+ // `seers run` is THE DOOR (lib/seers/run.ts): a direct, metered run (`seers balance`).
11
+ // Synchronous by default — news/regulation web scans can take minutes; pass --async to
12
+ // queue it and poll with `seers run-status`, --dry-run to see what it would record
13
+ // without recording anything, --no-produce to detect and record events only.
12
14
 
13
15
  import { readFileSync, writeFileSync } from "node:fs";
14
16
  import { CliError } from "../client.mjs";
15
- import { flagStr, flagNum, flagBool, requirePositional, printResult, compact, ok, argsFromFlags } from "./shared.mjs";
17
+ import { flagStr, flagBool, requirePositional, printResult, ok, argsFromFlags } from "./shared.mjs";
16
18
 
17
19
  /**
18
20
  * How the ratcheted commands in this file source their tool inputs — see `argsFromFlags` in
@@ -20,11 +22,89 @@ import { flagStr, flagNum, flagBool, requirePositional, printResult, compact, ok
20
22
  * ratchet and stays in UNMAPPED_TOOLS; that list only shrinks.
21
23
  */
22
24
  export const ARGS = {
25
+ // THE DOOR. `seers run <id>` — a direct, metered run. `--dry-run` scans + judges and
26
+ // persists nothing (returns the signals it would have recorded, with verdicts);
27
+ // `--no-produce` detects and records events only; `--async` queues it and prints a
28
+ // run id to poll with `seers run-status`.
23
29
  run_seer: {
24
30
  seer_id: { positional: 0, label: "seer-id", required: true },
31
+ dry_run: { flag: "dry-run", type: "bool" },
32
+ produce: { value: (flags) => (flags["no-produce"] === true ? false : undefined) },
33
+ async: { flag: "async", type: "bool" },
34
+ },
35
+ get_seer_run: {
36
+ run_id: { positional: 0, label: "run-id", required: true },
37
+ },
38
+ list_seer_runs: {
39
+ seer_id: { flag: "seer" },
40
+ limit: { flag: "limit", type: "num" },
41
+ },
42
+ get_seer_run_balance: {},
43
+ list_seers: {},
44
+ get_seer: {
45
+ seer_id: { positional: 0, label: "seer-id", required: true },
46
+ },
47
+ create_seer: {
48
+ name: { flag: "name", required: true },
49
+ collection_id: { flag: "collection", required: true },
50
+ provider: { flag: "provider", required: true },
51
+ mode: { flag: "mode", enum: ["suggest", "auto"] },
52
+ poll_interval_minutes: { flag: "interval", type: "num" },
53
+ // The whole config document — provider source params plus the behavior keys
54
+ // (auto_draft_scope, template_key, mention_degree, kernel_family, relevance{…}).
55
+ config: { value: (flags) => readJsonObjectFlag(flags, "config") ?? {} },
56
+ },
57
+ update_seer: {
58
+ seer_id: { positional: 0, label: "seer-id", required: true },
59
+ name: { flag: "name" },
60
+ collection_id: { flag: "collection" },
61
+ mode: { flag: "mode", enum: ["suggest", "auto"] },
62
+ enabled: { value: (flags) => readEnabledFlag(flags) },
63
+ poll_interval_minutes: { flag: "interval", type: "num" },
64
+ config: { value: (flags) => readJsonObjectFlag(flags, "config") },
65
+ },
66
+ delete_seer: {
67
+ seer_id: { positional: 0, label: "seer-id", required: true },
68
+ },
69
+ list_seer_events: {
70
+ seer_id: { positional: 0, label: "seer-id", required: true },
71
+ limit: { flag: "limit", type: "num" },
72
+ offset: { flag: "offset", type: "num" },
73
+ },
74
+ dismiss_seer_event: {
75
+ seer_id: { positional: 0, label: "seer-id", required: true },
76
+ event_id: { positional: 1, label: "event-id", required: true },
77
+ },
78
+ list_seer_headlines: {
79
+ collection_id: { flag: "collection" },
80
+ limit: { flag: "limit", type: "num" },
81
+ offset: { flag: "offset", type: "num" },
82
+ },
83
+ list_held_drafts: {
84
+ collection_id: { flag: "collection" },
85
+ },
86
+ resolve_held_draft: {
87
+ article_id: { positional: 0, label: "article-id", required: true },
88
+ // `seers release <id>` / `seers discard <id>` — the subcommand IS the action.
89
+ action: { value: (_flags, _positionals, extra) => extra.action },
90
+ },
91
+ list_seer_providers: {},
92
+ get_seer_connections: {},
93
+ set_github_connection: {
94
+ access_token: { flag: "token", required: true },
95
+ },
96
+ set_notion_connection: {
97
+ access_token: { flag: "token", required: true },
25
98
  },
26
99
  };
27
100
 
101
+ function readEnabledFlag(flags) {
102
+ const enabledRaw = flagStr(flags.enabled);
103
+ if (enabledRaw === undefined) return undefined;
104
+ if (enabledRaw !== "true" && enabledRaw !== "false") throw new CliError('--enabled must be "true" or "false"');
105
+ return enabledRaw === "true";
106
+ }
107
+
28
108
  export async function cmdSeers(ctx) {
29
109
  const sub = ctx.positionals[0];
30
110
  const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
@@ -45,6 +125,13 @@ export async function cmdSeers(ctx) {
45
125
  return seersDelete(rest);
46
126
  case "run":
47
127
  return seersRun(rest);
128
+ case "run-status":
129
+ case "run-get":
130
+ return seersRunStatus(rest);
131
+ case "runs":
132
+ return seersRuns(rest);
133
+ case "balance":
134
+ return seersBalance(rest);
48
135
  case "events":
49
136
  return seersEvents(rest);
50
137
  case "dismiss":
@@ -69,7 +156,7 @@ export async function cmdSeers(ctx) {
69
156
  return seersApply(rest);
70
157
  default:
71
158
  throw new CliError(
72
- `Unknown seers subcommand: ${sub ?? "(none)"}. Try: list, get, create, update, delete, run, events, dismiss, headlines, held, release, discard, providers, connections, connect, export, apply`
159
+ `Unknown seers subcommand: ${sub ?? "(none)"}. Try: list, get, create, update, delete, run, run-status, runs, balance, events, dismiss, headlines, held, release, discard, providers, connections, connect, export, apply`
73
160
  );
74
161
  }
75
162
  }
@@ -113,7 +200,7 @@ function formatSeerLine(s) {
113
200
 
114
201
  async function seersList(ctx) {
115
202
  const { client, flags, io } = ctx;
116
- const result = await client.callTool("list_seers", {});
203
+ const result = await client.callTool("list_seers", argsFromFlags(ARGS.list_seers, flags));
117
204
  printResult(io, flags, result, (r) => {
118
205
  const lines = (r.seers ?? []).map(formatSeerLine);
119
206
  if (!lines.length) lines.push("No seers yet. Create one with `seers create` (see `seers providers`).");
@@ -128,36 +215,15 @@ async function seersList(ctx) {
128
215
 
129
216
  async function seersGet(ctx) {
130
217
  const { client, positionals, flags, io } = ctx;
131
- const seerId = requirePositional(positionals, 0, "seer-id");
132
- const result = await client.callTool("get_seer", { seer_id: seerId });
218
+ const result = await client.callTool("get_seer", argsFromFlags(ARGS.get_seer, flags, positionals));
133
219
  printResult(io, flags, result);
134
220
  return 0;
135
221
  }
136
222
 
137
- function seerWriteArgsFromFlags(flags) {
138
- const enabledRaw = flagStr(flags.enabled);
139
- if (enabledRaw !== undefined && enabledRaw !== "true" && enabledRaw !== "false") {
140
- throw new CliError('--enabled must be "true" or "false"');
141
- }
142
- return compact({
143
- name: flagStr(flags.name),
144
- collection_id: flagStr(flags.collection),
145
- mode: flagStr(flags.mode),
146
- poll_interval_minutes: flagNum(flags.interval),
147
- enabled: enabledRaw === undefined ? undefined : enabledRaw === "true",
148
- config: readJsonObjectFlag(flags, "config"),
149
- });
150
- }
151
-
152
223
  async function seersCreate(ctx) {
153
224
  const { client, flags, io } = ctx;
154
- const args = seerWriteArgsFromFlags(flags);
155
- if (!args.name) throw new CliError("Missing required --name");
156
- if (!args.collection_id) throw new CliError("Missing required --collection <uuid>");
157
- const provider = flagStr(flags.provider);
158
- if (!provider) throw new CliError("Missing required --provider (run `seers providers` for the catalog)");
159
- delete args.enabled; // create has no enabled field; new seers start enabled
160
- const result = await client.callTool("create_seer", { ...args, provider, config: args.config ?? {} });
225
+ const args = argsFromFlags(ARGS.create_seer, flags);
226
+ const result = await client.callTool("create_seer", args);
161
227
  ok(ctx, "Seer created.");
162
228
  printResult(io, flags, result, (r) => formatSeerLine(r.seer));
163
229
  return 0;
@@ -165,12 +231,11 @@ async function seersCreate(ctx) {
165
231
 
166
232
  async function seersUpdate(ctx) {
167
233
  const { client, positionals, flags, io } = ctx;
168
- const seerId = requirePositional(positionals, 0, "seer-id");
169
- const args = seerWriteArgsFromFlags(flags);
170
- if (Object.keys(args).length === 0) {
234
+ const args = argsFromFlags(ARGS.update_seer, flags, positionals);
235
+ if (Object.keys(args).length <= 1) {
171
236
  throw new CliError("Nothing to update — pass --name/--collection/--mode/--enabled/--interval/--config");
172
237
  }
173
- const result = await client.callTool("update_seer", { seer_id: seerId, ...args });
238
+ const result = await client.callTool("update_seer", args);
174
239
  ok(ctx, "Seer updated.");
175
240
  printResult(io, flags, result, (r) => formatSeerLine(r.seer));
176
241
  return 0;
@@ -178,26 +243,84 @@ async function seersUpdate(ctx) {
178
243
 
179
244
  async function seersDelete(ctx) {
180
245
  const { client, positionals, flags, io } = ctx;
181
- const seerId = requirePositional(positionals, 0, "seer-id");
182
246
  if (!flagBool(flags.yes)) throw new CliError("Deleting a seer removes its detected events. Re-run with --yes.");
183
- const result = await client.callTool("delete_seer", { seer_id: seerId });
247
+ const result = await client.callTool("delete_seer", argsFromFlags(ARGS.delete_seer, flags, positionals));
184
248
  ok(ctx, "Seer deleted.");
185
249
  printResult(io, flags, result);
186
250
  return 0;
187
251
  }
188
252
 
253
+ function describeRun(r) {
254
+ const run = r.run ?? r;
255
+ const counts = `Detected ${r.detected ?? run.detected ?? 0}, produced ${r.produced ?? run.produced ?? 0} (auto-drafted ${
256
+ r.auto_drafted ?? run.auto_drafted ?? 0
257
+ }, suggested-only ${r.suggested_only ?? run.suggested_only ?? 0}, dismissed ${r.dismissed ?? run.dismissed ?? 0}).`;
258
+ const lines = [counts];
259
+ if (run.run_id)
260
+ lines.push(
261
+ `Run ${run.run_id} [${run.status}] origin=${run.origin} dry_run=${run.dry_run} produce=${run.produce}`
262
+ );
263
+ if (run.verification?.checks?.length) {
264
+ lines.push(
265
+ "Verification: " +
266
+ run.verification.checks.map((c) => `${c.id}=${c.verdict}${c.detail ? ` (${c.detail})` : ""}`).join("; ")
267
+ );
268
+ }
269
+ if (Array.isArray(run.signals) && run.signals.length) {
270
+ lines.push("", "Signals (dry run — nothing recorded):");
271
+ for (const sig of run.signals) {
272
+ lines.push(
273
+ ` [${sig.relevance?.decision ?? "?"} ${
274
+ typeof sig.relevance?.score === "number" ? sig.relevance.score.toFixed(2) : "-"
275
+ }] ${sig.title}${sig.url ? ` ${sig.url}` : ""}`
276
+ );
277
+ }
278
+ }
279
+ return lines.join("\n");
280
+ }
281
+
189
282
  async function seersRun(ctx) {
190
283
  const { client, positionals, flags, io } = ctx;
191
284
  const args = argsFromFlags(ARGS.run_seer, flags, positionals);
192
- ok(ctx, "Polling now — web-scan providers can take a few minutes…");
285
+ if (!args.async)
286
+ ok(ctx, `${args.dry_run ? "Dry run" : "Polling now"} — web-scan providers can take a few minutes…`);
193
287
  const result = await client.callTool("run_seer", args);
194
- printResult(
195
- io,
196
- flags,
197
- result,
198
- (r) =>
199
- `Detected ${r.detected}, produced ${r.produced} (auto-drafted ${r.auto_drafted}, suggested-only ${r.suggested_only}, dismissed ${r.dismissed}).`
200
- );
288
+ if (result?.status === "queued" && result?.run_id) {
289
+ ok(ctx, `Run queued as ${result.run_id}. Poll it with: ${ctx.bin} seers run-status ${result.run_id}`);
290
+ printResult(io, flags, result);
291
+ return 0;
292
+ }
293
+ printResult(io, flags, result, describeRun);
294
+ return 0;
295
+ }
296
+
297
+ async function seersRunStatus(ctx) {
298
+ const { client, positionals, flags, io } = ctx;
299
+ const result = await client.callTool("get_seer_run", argsFromFlags(ARGS.get_seer_run, flags, positionals));
300
+ printResult(io, flags, result, describeRun);
301
+ return 0;
302
+ }
303
+
304
+ async function seersRuns(ctx) {
305
+ const { client, flags, io } = ctx;
306
+ const result = await client.callTool("list_seer_runs", argsFromFlags(ARGS.list_seer_runs, flags));
307
+ printResult(io, flags, result, (r) => {
308
+ const lines = (r.runs ?? []).map(
309
+ (run) =>
310
+ `${run.run_id} [${run.status}] ${run.created_at} ${run.seer_name ?? run.seer_id ?? "?"} origin=${run.origin}${
311
+ run.dry_run ? " dry" : ""
312
+ } detected=${run.detected ?? "-"} produced=${run.produced ?? "-"}`
313
+ );
314
+ if (!lines.length) lines.push("No seer runs yet.");
315
+ return lines.join("\n");
316
+ });
317
+ return 0;
318
+ }
319
+
320
+ async function seersBalance(ctx) {
321
+ const { client, flags, io } = ctx;
322
+ const result = await client.callTool("get_seer_run_balance", argsFromFlags(ARGS.get_seer_run_balance, flags));
323
+ printResult(io, flags, result, (r) => `Seer runs: ${r.available} available (${r.used} of ${r.granted} used).`);
201
324
  return 0;
202
325
  }
203
326
 
@@ -205,8 +328,7 @@ async function seersRun(ctx) {
205
328
 
206
329
  async function seersEvents(ctx) {
207
330
  const { client, positionals, flags, io } = ctx;
208
- const seerId = requirePositional(positionals, 0, "seer-id");
209
- const args = compact({ seer_id: seerId, limit: flagNum(flags.limit), offset: flagNum(flags.offset) });
331
+ const args = argsFromFlags(ARGS.list_seer_events, flags, positionals);
210
332
  const result = await client.callTool("list_seer_events", args);
211
333
  printResult(io, flags, result, (r) => {
212
334
  const lines = (r.events ?? []).map((e) => {
@@ -223,9 +345,10 @@ async function seersEvents(ctx) {
223
345
 
224
346
  async function seersDismiss(ctx) {
225
347
  const { client, positionals, flags, io } = ctx;
226
- const seerId = requirePositional(positionals, 0, "seer-id");
227
- const eventId = requirePositional(positionals, 1, "event-id");
228
- const result = await client.callTool("dismiss_seer_event", { seer_id: seerId, event_id: eventId });
348
+ const result = await client.callTool(
349
+ "dismiss_seer_event",
350
+ argsFromFlags(ARGS.dismiss_seer_event, flags, positionals)
351
+ );
229
352
  ok(ctx, "Event dismissed.");
230
353
  printResult(io, flags, result);
231
354
  return 0;
@@ -233,12 +356,7 @@ async function seersDismiss(ctx) {
233
356
 
234
357
  async function seersHeadlines(ctx) {
235
358
  const { client, flags, io } = ctx;
236
- const args = compact({
237
- collection_id: flagStr(flags.collection),
238
- limit: flagNum(flags.limit),
239
- offset: flagNum(flags.offset),
240
- });
241
- const result = await client.callTool("list_seer_headlines", args);
359
+ const result = await client.callTool("list_seer_headlines", argsFromFlags(ARGS.list_seer_headlines, flags));
242
360
  printResult(io, flags, result, (r) => {
243
361
  const lines = (r.headlines ?? []).map(
244
362
  (h) => `${h.id} [${h.status}] ${h.title ?? h.headline ?? "(untitled)"}`
@@ -251,8 +369,7 @@ async function seersHeadlines(ctx) {
251
369
 
252
370
  async function seersHeld(ctx) {
253
371
  const { client, flags, io } = ctx;
254
- const args = compact({ collection_id: flagStr(flags.collection) });
255
- const result = await client.callTool("list_held_drafts", args);
372
+ const result = await client.callTool("list_held_drafts", argsFromFlags(ARGS.list_held_drafts, flags));
256
373
  printResult(io, flags, result, (r) => {
257
374
  const lines = (r.drafts ?? []).map(
258
375
  (d) => `${d.articleId} [${d.kind}] ${d.title} — ${d.reason} (dropped ${d.dropped}, unsure ${d.unsure})`
@@ -266,8 +383,10 @@ async function seersHeld(ctx) {
266
383
 
267
384
  async function seersResolveHeld(ctx, action) {
268
385
  const { client, positionals, flags, io } = ctx;
269
- const articleId = requirePositional(positionals, 0, "article-id");
270
- const result = await client.callTool("resolve_held_draft", { article_id: articleId, action });
386
+ const result = await client.callTool(
387
+ "resolve_held_draft",
388
+ argsFromFlags(ARGS.resolve_held_draft, flags, positionals, { action })
389
+ );
271
390
  ok(ctx, action === "release" ? "Draft released — it can publish through the normal path." : "Draft discarded.");
272
391
  printResult(io, flags, result);
273
392
  return 0;
@@ -277,7 +396,7 @@ async function seersResolveHeld(ctx, action) {
277
396
 
278
397
  async function seersProviders(ctx) {
279
398
  const { client, flags, io } = ctx;
280
- const result = await client.callTool("list_seer_providers", {});
399
+ const result = await client.callTool("list_seer_providers", argsFromFlags(ARGS.list_seer_providers, flags));
281
400
  printResult(io, flags, result, (r) => {
282
401
  const blocks = (r.providers ?? []).map((p) => {
283
402
  const fields = p.fields
@@ -296,7 +415,7 @@ async function seersProviders(ctx) {
296
415
 
297
416
  async function seersConnections(ctx) {
298
417
  const { client, flags, io } = ctx;
299
- const result = await client.callTool("get_seer_connections", {});
418
+ const result = await client.callTool("get_seer_connections", argsFromFlags(ARGS.get_seer_connections, flags));
300
419
  printResult(io, flags, result, (r) => {
301
420
  const gh = r.github.connected
302
421
  ? `GitHub: connected as ${r.github.account_login} (${r.github.connected_at})`
@@ -313,9 +432,11 @@ async function seersConnect(ctx) {
313
432
  const which = ctx.positionals[0];
314
433
  const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
315
434
  if (which === "github") {
316
- const token = flagStr(rest.flags.token);
317
- if (!token) throw new CliError("Missing required --token <github-pat> (repo read access)");
318
- const result = await rest.client.callTool("set_github_connection", { access_token: token });
435
+ if (!flagStr(rest.flags.token)) throw new CliError("Missing required --token <github-pat> (repo read access)");
436
+ const result = await rest.client.callTool(
437
+ "set_github_connection",
438
+ argsFromFlags(ARGS.set_github_connection, rest.flags)
439
+ );
319
440
  ok(rest, "GitHub connected.");
320
441
  printResult(rest.io, rest.flags, result);
321
442
  return 0;
@@ -340,7 +461,10 @@ async function seersConnect(ctx) {
340
461
  );
341
462
  return 0;
342
463
  }
343
- const result = await rest.client.callTool("set_notion_connection", { access_token: token });
464
+ const result = await rest.client.callTool(
465
+ "set_notion_connection",
466
+ argsFromFlags(ARGS.set_notion_connection, rest.flags)
467
+ );
344
468
  ok(rest, `Notion connected${result.workspace_name ? ` to ${result.workspace_name}` : ""}.`);
345
469
  ok(rest, "Reminder: the integration only watches pages you've shared with it in Notion.");
346
470
  printResult(rest.io, rest.flags, result);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@letterstory/cli",
3
- "version": "0.25.4",
3
+ "version": "0.25.5",
4
4
  "description": "Spin up and manage Letterstory phantom blogs from your terminal.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",