@letterstory/cli 0.23.0 → 0.25.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.
- package/lib/cli.mjs +11 -5
- package/lib/commands/autopen.mjs +11 -4
- package/lib/commands/connectors.mjs +3 -3
- package/lib/commands/flows.mjs +10 -4
- package/lib/commands/kernel.mjs +169 -41
- package/lib/commands/posts.mjs +9 -2
- package/lib/commands/research.mjs +15 -17
- package/lib/commands/seers.mjs +14 -3
- package/lib/commands/shredder.mjs +28 -20
- package/package.json +1 -1
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.
|
|
47
|
+
export const VERSION = "0.25.1";
|
|
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.
|
|
@@ -316,14 +316,20 @@ Writing kernels (kernels write the draft; they do not research — run research
|
|
|
316
316
|
to a kernel and an in-loop shredder
|
|
317
317
|
kernel endpoint delete <endpoint-id> --yes Remove one
|
|
318
318
|
kernel write <endpoint-id> (--body <text>|--file <path>|--file -)
|
|
319
|
-
export const VERSION = "0.23.0";
|
|
320
319
|
WRITE a new article from a headline,
|
|
321
320
|
outline, notes or a finished post.
|
|
322
321
|
Follows the run unless --no-wait, and
|
|
323
322
|
prints the article when it lands.
|
|
324
|
-
kernel polish <endpoint-id> (--body <html>|--file <path
|
|
325
|
-
|
|
326
|
-
|
|
323
|
+
kernel polish <endpoint-id> (--body <html>|--file <path>|--file -) [--no-wait]
|
|
324
|
+
Run the SAME pipeline against a
|
|
325
|
+
document you already have instead of
|
|
326
|
+
writing a new one: prose reworded,
|
|
327
|
+
your own markup kept, the same rules
|
|
328
|
+
and jury as any other run. Follows the
|
|
329
|
+
run unless --no-wait, and costs a run
|
|
330
|
+
exactly like 'kernel write' does —
|
|
331
|
+
there is no separate free, instant
|
|
332
|
+
polish any more.
|
|
327
333
|
kernel run-status <run-id> Poll a write started with 'kernel write'
|
|
328
334
|
kernel runs [--endpoint <id>] [--limit <n>] Recent endpoint writes (telemetry)
|
|
329
335
|
kernel balance Kernel runs left on this org
|
package/lib/commands/autopen.mjs
CHANGED
|
@@ -11,7 +11,14 @@
|
|
|
11
11
|
// finishes whether or not anyone polls.
|
|
12
12
|
|
|
13
13
|
import { CliError } from "../client.mjs";
|
|
14
|
-
import { flagBool, requireFlag, printResult, ok } from "./shared.mjs";
|
|
14
|
+
import { flagBool, requireFlag, printResult, ok, argsFromFlags } from "./shared.mjs";
|
|
15
|
+
|
|
16
|
+
/** See `argsFromFlags` in shared.mjs; `scripts/parity-check.ts` diffs these keys against the tool. */
|
|
17
|
+
export const ARGS = {
|
|
18
|
+
run_auto_pen: {
|
|
19
|
+
article_id: { flag: "article", required: true },
|
|
20
|
+
},
|
|
21
|
+
};
|
|
15
22
|
|
|
16
23
|
export async function cmdAutoPen(ctx) {
|
|
17
24
|
const sub = ctx.positionals[0];
|
|
@@ -31,9 +38,9 @@ export async function cmdAutoPen(ctx) {
|
|
|
31
38
|
|
|
32
39
|
async function autoPenStart(ctx) {
|
|
33
40
|
const { client, flags, io } = ctx;
|
|
34
|
-
const
|
|
35
|
-
const result = await client.callTool("run_auto_pen",
|
|
36
|
-
ok(ctx, `Auto-Pen started. Check progress with: autopen status --article ${
|
|
41
|
+
const args = argsFromFlags(ARGS.run_auto_pen, flags, ctx.positionals);
|
|
42
|
+
const result = await client.callTool("run_auto_pen", args);
|
|
43
|
+
ok(ctx, `Auto-Pen started. Check progress with: autopen status --article ${args.article_id}`);
|
|
37
44
|
printResult(io, flags, result);
|
|
38
45
|
return 0;
|
|
39
46
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
// `connectors` — export finished articles to external CMSs/doc tools (Webflow,
|
|
2
|
-
// Framer, Google Docs, Contentful) and check publish status. Ported
|
|
3
|
-
// phantomstory-cli's connectors.ts.
|
|
2
|
+
// Framer, Google Docs, Contentful, GitHub) and check publish status. Ported
|
|
3
|
+
// from phantomstory-cli's connectors.ts.
|
|
4
4
|
|
|
5
5
|
import { CliError } from "../client.mjs";
|
|
6
6
|
import { printResult, ok, argsFromFlags } from "./shared.mjs";
|
|
7
7
|
|
|
8
8
|
const CMS_PROVIDERS = ["webflow", "framer", "sanity", "contentful"];
|
|
9
|
-
const PUBLISH_CONNECTORS = ["webflow", "framer", "google_docs", "contentful"];
|
|
9
|
+
const PUBLISH_CONNECTORS = ["webflow", "framer", "google_docs", "contentful", "github"];
|
|
10
10
|
|
|
11
11
|
// How every tool this module calls gets its arguments — see `argsFromFlags` in shared.mjs.
|
|
12
12
|
// `scripts/parity-check.ts` compares these keys against each tool's published input schema.
|
package/lib/commands/flows.mjs
CHANGED
|
@@ -2,7 +2,15 @@
|
|
|
2
2
|
// flow_run.completed completion webhook. Ported from phantomstory-cli's flows.ts.
|
|
3
3
|
|
|
4
4
|
import { CliError } from "../client.mjs";
|
|
5
|
-
import { flagStr, requirePositional, printResult, compact, ok } from "./shared.mjs";
|
|
5
|
+
import { flagStr, requirePositional, printResult, compact, ok, argsFromFlags } from "./shared.mjs";
|
|
6
|
+
|
|
7
|
+
/** See `argsFromFlags` in shared.mjs; `scripts/parity-check.ts` diffs these keys against the tool. */
|
|
8
|
+
export const ARGS = {
|
|
9
|
+
run_flow: {
|
|
10
|
+
flow_id: { positional: 0, label: "flow-id", required: true },
|
|
11
|
+
article_id: { positional: 1, label: "article-id", required: true },
|
|
12
|
+
},
|
|
13
|
+
};
|
|
6
14
|
|
|
7
15
|
export async function cmdFlows(ctx) {
|
|
8
16
|
const sub = ctx.positionals[0];
|
|
@@ -31,9 +39,7 @@ async function flowsList(ctx) {
|
|
|
31
39
|
|
|
32
40
|
async function flowsRun(ctx) {
|
|
33
41
|
const { client, positionals, flags, io } = ctx;
|
|
34
|
-
const
|
|
35
|
-
const articleId = requirePositional(positionals, 1, "article-id");
|
|
36
|
-
const result = await client.callTool("run_flow", { flow_id: flowId, article_id: articleId });
|
|
42
|
+
const result = await client.callTool("run_flow", argsFromFlags(ARGS.run_flow, flags, positionals));
|
|
37
43
|
ok(ctx, "Flow run started.");
|
|
38
44
|
printResult(io, flags, result);
|
|
39
45
|
return 0;
|
package/lib/commands/kernel.mjs
CHANGED
|
@@ -15,11 +15,10 @@
|
|
|
15
15
|
// Neither door blocks: a write runs for minutes, so `write` submits and then follows the run
|
|
16
16
|
// unless you tell it not to.
|
|
17
17
|
|
|
18
|
+
import { readFileSync } from "node:fs";
|
|
18
19
|
import { CliError } from "../client.mjs";
|
|
19
20
|
import {
|
|
20
21
|
flagStr,
|
|
21
|
-
flagBool,
|
|
22
|
-
requireFlag,
|
|
23
22
|
requirePositional,
|
|
24
23
|
printResult,
|
|
25
24
|
compact,
|
|
@@ -30,6 +29,38 @@ import {
|
|
|
30
29
|
argsFromFlags,
|
|
31
30
|
} from "./shared.mjs";
|
|
32
31
|
|
|
32
|
+
/** `--<flag> <path>` → the file's parsed JSON, or undefined when the flag is not set. */
|
|
33
|
+
function readJsonFileFlag(flags, flag) {
|
|
34
|
+
const path = flagStr(flags[flag]);
|
|
35
|
+
if (path === undefined) return undefined;
|
|
36
|
+
let text;
|
|
37
|
+
try {
|
|
38
|
+
text = readFileSync(path, "utf8");
|
|
39
|
+
} catch (error) {
|
|
40
|
+
throw new CliError(
|
|
41
|
+
`Could not read --${flag} ${path}: ${error instanceof Error ? error.message : String(error)}`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
return JSON.parse(text);
|
|
46
|
+
} catch {
|
|
47
|
+
throw new CliError(`--${flag} ${path} is not valid JSON.`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** `--<flag> <path>` → the file's text, or undefined when the flag is not set. */
|
|
52
|
+
function readTextFileFlag(flags, flag) {
|
|
53
|
+
const path = flagStr(flags[flag]);
|
|
54
|
+
if (path === undefined) return undefined;
|
|
55
|
+
try {
|
|
56
|
+
return readFileSync(path, "utf8");
|
|
57
|
+
} catch (error) {
|
|
58
|
+
throw new CliError(
|
|
59
|
+
`Could not read --${flag} ${path}: ${error instanceof Error ? error.message : String(error)}`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
33
64
|
/**
|
|
34
65
|
* How the commands ADDED to this file source their arguments — see `argsFromFlags`
|
|
35
66
|
* in shared.mjs, and `scripts/parity-check.ts`, which compares these keys against
|
|
@@ -39,6 +70,67 @@ import {
|
|
|
39
70
|
* frozen UNMAPPED_TOOLS list. New commands declare; that list only shrinks.
|
|
40
71
|
*/
|
|
41
72
|
export const ARGS = {
|
|
73
|
+
// The article-bound door: submit a kernel run INTO an article you already have.
|
|
74
|
+
submit_kernel_run: {
|
|
75
|
+
article_id: { flag: "article", required: true },
|
|
76
|
+
kernel_id: { flag: "kernel", required: true },
|
|
77
|
+
// A brief overrides the article's own content as the instruction; --brief-file reads
|
|
78
|
+
// it from a path. Omit both to write from the article as it stands.
|
|
79
|
+
brief: { value: (flags) => flagStr(flags.brief) ?? readBodyInput({ file: flagStr(flags["brief-file"]) }) },
|
|
80
|
+
},
|
|
81
|
+
// The standalone door. Two subcommands share this one tool: `write` sends a `brief`,
|
|
82
|
+
// `polish` sends a `document`; the map is one, so a field the tool gains is a field
|
|
83
|
+
// BOTH subcommands must learn (or knowingly gap) — that is the ratchet.
|
|
84
|
+
run_kernel: {
|
|
85
|
+
endpoint_id: { positional: 0, label: "endpoint-id", required: true },
|
|
86
|
+
brief: { value: (flags, _p, extra) => (extra.door === "brief" ? readBodyInput(flags) : undefined) },
|
|
87
|
+
document: { value: (flags, _p, extra) => (extra.door === "document" ? readBodyInput(flags) : undefined) },
|
|
88
|
+
// Documented dead field on the tool; accepted so an old --format still validates.
|
|
89
|
+
format: { flag: "format", enum: ["auto", "html", "markdown"] },
|
|
90
|
+
kernel_id: "kernel",
|
|
91
|
+
target_word_count: { flag: "words", type: "num" },
|
|
92
|
+
content_type: "content-type",
|
|
93
|
+
// The flag is --read-as, which is what shipped; an earlier draft called it --brief-mode.
|
|
94
|
+
brief_mode: { flag: "read-as", enum: ["auto", "brief", "content", "blog", "outline", "research", "notes"] },
|
|
95
|
+
// The two products, in one run: every round is shredded before the kernel's jury sees
|
|
96
|
+
// it, so the verdict lands on the text that actually ships. Costs a shred as well as a
|
|
97
|
+
// run.
|
|
98
|
+
shredder_endpoint_id: "shredder",
|
|
99
|
+
shred_aggression: { flag: "shred-aggression", enum: ["standard", "high", "max"] },
|
|
100
|
+
person: { flag: "person", enum: ["third", "first_plural", "first_singular"] },
|
|
101
|
+
author_name: "author",
|
|
102
|
+
allow_experience_claims: { flag: "experience-claims", type: "bool" },
|
|
103
|
+
// The mechanism's full contract, declared on the door (2026-09-17, Kernels wave step 1).
|
|
104
|
+
// A shape and a typed brief are files, not flags — `--skeleton-file shape.json` is the
|
|
105
|
+
// field a template resolves to, sent by a caller who has their own.
|
|
106
|
+
skeleton: { value: (flags) => readJsonFileFlag(flags, "skeleton-file") },
|
|
107
|
+
brief_json: { value: (flags) => readJsonFileFlag(flags, "brief-json-file") },
|
|
108
|
+
max_iterations: { flag: "max-iterations", type: "num" },
|
|
109
|
+
kernel_version: { flag: "kernel-version", type: "num" },
|
|
110
|
+
// In-loop shred tuning. Only sent when a flag is set; ignored when no shred runs.
|
|
111
|
+
shred: {
|
|
112
|
+
object: {
|
|
113
|
+
tier: { flag: "shred-tier", enum: ["cheap", "mid", "strong", "frontier"] },
|
|
114
|
+
families: { flag: "shred-family", type: "list" },
|
|
115
|
+
revise_shredded: { flag: "shred-revise", type: "bool" },
|
|
116
|
+
revision_model: "shred-revision-model",
|
|
117
|
+
skip_voice_bearing: { flag: "shred-skip-voice-bearing", type: "bool" },
|
|
118
|
+
judge: { flag: "shred-judge", type: "bool" },
|
|
119
|
+
attempts_per_unit: { flag: "shred-attempts", type: "num" },
|
|
120
|
+
max_changeable_retention: { flag: "shred-max-retention", type: "num" },
|
|
121
|
+
turnover_retries: { flag: "shred-turnover-retries", type: "num" },
|
|
122
|
+
turnover_avoid_list: { flag: "shred-turnover-avoid-list", type: "bool" },
|
|
123
|
+
evidence: { value: (flags) => readTextFileFlag(flags, "shred-evidence-file") },
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
// Named-entity treatment (2026-09-17): a list, not a scalar flag, so it's a file like
|
|
127
|
+
// skeleton/brief_json — `--entities-file entities.json` of `[{"name":"...","treatment":
|
|
128
|
+
// "never"|"name_only"|"subtle"|"woven"|"lead"}, ...]`.
|
|
129
|
+
entities: { value: (flags) => readJsonFileFlag(flags, "entities-file") },
|
|
130
|
+
// document-only: hold the document's own shape through the edit. Default true; pass
|
|
131
|
+
// --no-preserve-shape to let the kernel restructure freely.
|
|
132
|
+
preserve_shape: { flag: "preserve-shape", type: "bool" },
|
|
133
|
+
},
|
|
42
134
|
create_kernel_endpoint: {
|
|
43
135
|
name: { flag: "name", required: true },
|
|
44
136
|
// Both optional bindings the tool has always accepted at create: which kernel
|
|
@@ -51,6 +143,15 @@ export const ARGS = {
|
|
|
51
143
|
delete_kernel_endpoint: {
|
|
52
144
|
endpoint_id: { positional: 1, label: "endpoint-id", required: true },
|
|
53
145
|
},
|
|
146
|
+
// The endpoint's configuration as ONE file: `kernel endpoint config <id>` prints it,
|
|
147
|
+
// `kernel endpoint config <id> --file cfg.json` replaces it with the file's document.
|
|
148
|
+
get_kernel_endpoint_config: {
|
|
149
|
+
endpoint_id: { positional: 1, label: "endpoint-id", required: true },
|
|
150
|
+
},
|
|
151
|
+
set_kernel_endpoint_config: {
|
|
152
|
+
endpoint_id: { positional: 1, label: "endpoint-id", required: true },
|
|
153
|
+
config: { value: (flags) => readJsonFileFlag(flags, "file") },
|
|
154
|
+
},
|
|
54
155
|
};
|
|
55
156
|
|
|
56
157
|
export async function cmdKernel(ctx) {
|
|
@@ -94,10 +195,7 @@ async function kernelList(ctx) {
|
|
|
94
195
|
|
|
95
196
|
async function kernelRun(ctx) {
|
|
96
197
|
const { client, flags, io } = ctx;
|
|
97
|
-
const
|
|
98
|
-
const kernelId = requireFlag(flags, "kernel");
|
|
99
|
-
const brief = flagStr(flags.brief) ?? readBodyInput({ file: flagStr(flags["brief-file"]) });
|
|
100
|
-
const args = compact({ article_id: article, kernel_id: kernelId, brief });
|
|
198
|
+
const args = argsFromFlags(ARGS.submit_kernel_run, flags, ctx.positionals);
|
|
101
199
|
const result = await client.callTool("submit_kernel_run", args);
|
|
102
200
|
ok(ctx, `Kernel run started. Check progress with: kernel status ${result?.kernel_job_id ?? "<kernel-job-id>"}`);
|
|
103
201
|
printResult(io, flags, result);
|
|
@@ -130,30 +228,10 @@ async function kernelEndpoints(ctx) {
|
|
|
130
228
|
|
|
131
229
|
async function kernelWrite(ctx) {
|
|
132
230
|
const { client, positionals, flags, io } = ctx;
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
if (brief === undefined) {
|
|
231
|
+
const args = argsFromFlags(ARGS.run_kernel, flags, positionals, { door: "brief" });
|
|
232
|
+
if (args.brief === undefined) {
|
|
136
233
|
throw new CliError("Provide the brief with --body <text>, --file <path>, or --file - (stdin).");
|
|
137
234
|
}
|
|
138
|
-
const args = compact({
|
|
139
|
-
endpoint_id: endpointId,
|
|
140
|
-
brief,
|
|
141
|
-
kernel_id: flagStr(flags.kernel),
|
|
142
|
-
target_word_count: flags.words === undefined ? undefined : Number(flags.words),
|
|
143
|
-
content_type: flagStr(flags["content-type"]),
|
|
144
|
-
// The two products, in one run: every round is shredded before the kernel's jury sees
|
|
145
|
-
// it, so the verdict lands on the text that actually ships. Costs a shred as well as a
|
|
146
|
-
// run. Reachable from every other surface and, until now, not from here.
|
|
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.
|
|
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"]),
|
|
156
|
-
});
|
|
157
235
|
const result = await client.callTool("run_kernel", args);
|
|
158
236
|
const runId = toolField(result, "run_id");
|
|
159
237
|
if (!runId) {
|
|
@@ -222,26 +300,76 @@ async function kernelEndpoint(ctx) {
|
|
|
222
300
|
printResult(io, flags, result);
|
|
223
301
|
return 0;
|
|
224
302
|
}
|
|
225
|
-
|
|
303
|
+
if (action === "config") {
|
|
304
|
+
const id = requirePositional(positionals, 1, "endpoint-id");
|
|
305
|
+
if (flagStr(flags.file) === undefined) {
|
|
306
|
+
const result = await client.callTool(
|
|
307
|
+
"get_kernel_endpoint_config",
|
|
308
|
+
argsFromFlags(ARGS.get_kernel_endpoint_config, flags, positionals)
|
|
309
|
+
);
|
|
310
|
+
// The document itself, so `> cfg.json` round-trips into `--file cfg.json`.
|
|
311
|
+
printResult(io, flags, result, (r) => JSON.stringify(r?.config ?? r, null, 2));
|
|
312
|
+
return 0;
|
|
313
|
+
}
|
|
314
|
+
const args = argsFromFlags(ARGS.set_kernel_endpoint_config, flags, positionals);
|
|
315
|
+
const result = await client.callTool("set_kernel_endpoint_config", args);
|
|
316
|
+
ok(ctx, `Replaced the configuration of kernel endpoint ${id}.`);
|
|
317
|
+
printResult(io, flags, result);
|
|
318
|
+
return 0;
|
|
319
|
+
}
|
|
320
|
+
throw new CliError(`Unknown kernel endpoint action: ${action ?? "(none)"}. Try: create, delete, config`);
|
|
226
321
|
}
|
|
227
322
|
|
|
228
|
-
/**
|
|
323
|
+
/**
|
|
324
|
+
* The OTHER door: run the same pipeline against a document you already have, instead of
|
|
325
|
+
* writing a new one. Skips generation only — the rules, the in-loop shred and the jury all
|
|
326
|
+
* still run, which is what keeps your own markup (images, embeds, links, tables, anchors,
|
|
327
|
+
* shortcodes) intact through a real pass rather than an unguarded shortcut.
|
|
328
|
+
*
|
|
329
|
+
* ASYNCHRONOUS, same as `write` — this used to be a separate, synchronous, unmetered "polish"
|
|
330
|
+
* that answered in one request. That path was deleted (2026-09-15, letterstory#1783): it
|
|
331
|
+
* worked by skipping the rules and the jury, and that was the actual cost of being instant
|
|
332
|
+
* and free. `document` now costs a run and follows the identical submit-and-poll shape
|
|
333
|
+
* `write` does, so this command does too — `--no-wait` behaves the same way here.
|
|
334
|
+
*/
|
|
229
335
|
async function kernelPolish(ctx) {
|
|
230
336
|
const { client, positionals, flags, io } = ctx;
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
if (document === undefined) {
|
|
337
|
+
const args = argsFromFlags(ARGS.run_kernel, flags, positionals, { door: "document" });
|
|
338
|
+
if (args.document === undefined) {
|
|
234
339
|
throw new CliError("Provide the document with --body <text>, --file <path>, or --file - (stdin).");
|
|
235
340
|
}
|
|
236
|
-
const args = compact({
|
|
237
|
-
endpoint_id: endpointId,
|
|
238
|
-
document,
|
|
239
|
-
format: flagStr(flags.format),
|
|
240
|
-
});
|
|
241
|
-
// Synchronous by nature: a polish is one pass over the sentences, not a multi-round write.
|
|
242
341
|
const result = await client.callTool("run_kernel", args);
|
|
243
|
-
|
|
244
|
-
|
|
342
|
+
const runId = toolField(result, "run_id");
|
|
343
|
+
if (!runId) {
|
|
344
|
+
printResult(io, flags, result);
|
|
345
|
+
return 0;
|
|
346
|
+
}
|
|
347
|
+
if (flags["no-wait"] === true) {
|
|
348
|
+
ok(ctx, `Polish started as ${runId}. Follow it with: letterstory kernel run-status ${runId}`);
|
|
349
|
+
printResult(io, flags, result);
|
|
350
|
+
return 0;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
ok(ctx, `Polish started as ${runId}. Waiting — this keeps running on the server if you stop.`);
|
|
354
|
+
let lastStatus;
|
|
355
|
+
const { settled, value: polled } = await followJob(
|
|
356
|
+
io,
|
|
357
|
+
async () => {
|
|
358
|
+
const r = await client.callTool("get_kernel_endpoint_run", { run_id: runId });
|
|
359
|
+
lastStatus = toolField(r, "status");
|
|
360
|
+
return { done: !!lastStatus && lastStatus !== "running" && lastStatus !== "queued", value: r };
|
|
361
|
+
},
|
|
362
|
+
{ intervalMs: POLL_INTERVAL_MS, ceilingMs: POLL_CEILING_MS }
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
if (!settled) {
|
|
366
|
+
ok(ctx, `Still running after 45 minutes. Follow it with: letterstory kernel run-status ${runId}`);
|
|
367
|
+
printResult(io, flags, polled);
|
|
368
|
+
return 0;
|
|
369
|
+
}
|
|
370
|
+
ok(ctx, `Polish ${lastStatus}.`);
|
|
371
|
+
printResult(io, flags, polled, (v) => (typeof v?.article === "string" ? v.article : JSON.stringify(v, null, 2)));
|
|
372
|
+
return lastStatus === "done" ? 0 : 1;
|
|
245
373
|
}
|
|
246
374
|
|
|
247
375
|
async function kernelRunStatus(ctx) {
|
package/lib/commands/posts.mjs
CHANGED
|
@@ -12,8 +12,16 @@ import {
|
|
|
12
12
|
compact,
|
|
13
13
|
ok,
|
|
14
14
|
readBodyInput,
|
|
15
|
+
argsFromFlags,
|
|
15
16
|
} from "./shared.mjs";
|
|
16
17
|
|
|
18
|
+
/** See `argsFromFlags` in shared.mjs; `scripts/parity-check.ts` diffs these keys against the tool. */
|
|
19
|
+
export const ARGS = {
|
|
20
|
+
publish_article: {
|
|
21
|
+
article_id: { positional: 0, label: "article-id", required: true },
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
17
25
|
export async function cmdPosts(ctx) {
|
|
18
26
|
const sub = ctx.positionals[0];
|
|
19
27
|
const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
|
|
@@ -66,8 +74,7 @@ async function postsNew(ctx) {
|
|
|
66
74
|
|
|
67
75
|
async function postsPublish(ctx) {
|
|
68
76
|
const { client, positionals, flags, io } = ctx;
|
|
69
|
-
const
|
|
70
|
-
const result = await client.callTool("publish_article", { article_id: id });
|
|
77
|
+
const result = await client.callTool("publish_article", argsFromFlags(ARGS.publish_article, flags, positionals));
|
|
71
78
|
ok(ctx, "Published.");
|
|
72
79
|
printResult(io, flags, result);
|
|
73
80
|
return 0;
|
|
@@ -4,7 +4,19 @@
|
|
|
4
4
|
// kernels do NOT research — `research start` then `kernel run` is the full pipeline.
|
|
5
5
|
|
|
6
6
|
import { CliError } from "../client.mjs";
|
|
7
|
-
import { flagStr, flagNum, flagBool,
|
|
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
|
+
};
|
|
8
20
|
|
|
9
21
|
export async function cmdResearch(ctx) {
|
|
10
22
|
const sub = ctx.positionals[0];
|
|
@@ -28,23 +40,9 @@ export async function cmdResearch(ctx) {
|
|
|
28
40
|
|
|
29
41
|
async function researchStart(ctx) {
|
|
30
42
|
const { client, flags, io } = ctx;
|
|
31
|
-
const
|
|
32
|
-
const depth = flagStr(flags.depth);
|
|
33
|
-
if (depth !== undefined && depth !== "barebones" && depth !== "fleshed_out") {
|
|
34
|
-
throw new CliError(`--depth must be barebones or fleshed_out, got "${depth}"`);
|
|
35
|
-
}
|
|
36
|
-
const sourceUrls = flagList(flags.url);
|
|
37
|
-
const mustInclude = flagList(flags["must-include"]);
|
|
38
|
-
const args = compact({
|
|
39
|
-
article_id: article,
|
|
40
|
-
topic: flagStr(flags.topic),
|
|
41
|
-
source_urls: sourceUrls.length ? sourceUrls : undefined,
|
|
42
|
-
must_include: mustInclude.length ? mustInclude : undefined,
|
|
43
|
-
depth,
|
|
44
|
-
direction_gate: flagBool(flags.gate) ? true : undefined,
|
|
45
|
-
});
|
|
43
|
+
const args = argsFromFlags(ARGS.start_research, flags, ctx.positionals);
|
|
46
44
|
const result = await client.callTool("start_research", args);
|
|
47
|
-
ok(ctx, `Research started. Check progress with: research status --article ${
|
|
45
|
+
ok(ctx, `Research started. Check progress with: research status --article ${args.article_id}`);
|
|
48
46
|
printResult(io, flags, result);
|
|
49
47
|
return 0;
|
|
50
48
|
}
|
package/lib/commands/seers.mjs
CHANGED
|
@@ -12,7 +12,18 @@
|
|
|
12
12
|
|
|
13
13
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
14
14
|
import { CliError } from "../client.mjs";
|
|
15
|
-
import { flagStr, flagNum, flagBool, requirePositional, printResult, compact, ok } from "./shared.mjs";
|
|
15
|
+
import { flagStr, flagNum, flagBool, requirePositional, printResult, compact, ok, argsFromFlags } from "./shared.mjs";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* How the ratcheted commands in this file source their tool inputs — see `argsFromFlags` in
|
|
19
|
+
* shared.mjs and `scripts/parity-check.ts`. The rest of the module predates the field-level
|
|
20
|
+
* ratchet and stays in UNMAPPED_TOOLS; that list only shrinks.
|
|
21
|
+
*/
|
|
22
|
+
export const ARGS = {
|
|
23
|
+
run_seer: {
|
|
24
|
+
seer_id: { positional: 0, label: "seer-id", required: true },
|
|
25
|
+
},
|
|
26
|
+
};
|
|
16
27
|
|
|
17
28
|
export async function cmdSeers(ctx) {
|
|
18
29
|
const sub = ctx.positionals[0];
|
|
@@ -177,9 +188,9 @@ async function seersDelete(ctx) {
|
|
|
177
188
|
|
|
178
189
|
async function seersRun(ctx) {
|
|
179
190
|
const { client, positionals, flags, io } = ctx;
|
|
180
|
-
const
|
|
191
|
+
const args = argsFromFlags(ARGS.run_seer, flags, positionals);
|
|
181
192
|
ok(ctx, "Polling now — web-scan providers can take a few minutes…");
|
|
182
|
-
const result = await client.callTool("run_seer",
|
|
193
|
+
const result = await client.callTool("run_seer", args);
|
|
183
194
|
printResult(
|
|
184
195
|
io,
|
|
185
196
|
flags,
|
|
@@ -15,8 +15,32 @@ import {
|
|
|
15
15
|
ok,
|
|
16
16
|
toolField,
|
|
17
17
|
followJob,
|
|
18
|
+
argsFromFlags,
|
|
18
19
|
} from "./shared.mjs";
|
|
19
20
|
|
|
21
|
+
/**
|
|
22
|
+
* How `shredder run` sources `run_shred`'s inputs — see `argsFromFlags` in shared.mjs and
|
|
23
|
+
* `scripts/parity-check.ts`, which diffs these keys against the tool's published schema.
|
|
24
|
+
* The rest of this module predates the ratchet and stays in UNMAPPED_TOOLS.
|
|
25
|
+
*/
|
|
26
|
+
export const ARGS = {
|
|
27
|
+
run_shred: {
|
|
28
|
+
endpoint_id: { positional: 0, label: "endpoint-id", required: true },
|
|
29
|
+
text: { value: (flags) => readBodyInput(flags) },
|
|
30
|
+
coverage: { flag: "coverage", type: "num" },
|
|
31
|
+
attempts_per_unit: { flag: "attempts", type: "num" },
|
|
32
|
+
// What the text IS. Without this an HTML file could only go through the `auto`
|
|
33
|
+
// heuristic, which misreads a markdown post carrying an embed.
|
|
34
|
+
format: { flag: "format", enum: ["auto", "html", "markdown"] },
|
|
35
|
+
// How hard to rewrite. Validated service-side; sent only when the caller asked.
|
|
36
|
+
aggression: { flag: "aggression", enum: ["standard", "high", "max"] },
|
|
37
|
+
// Hold every rewrite to the live writing rules. A trade, not a free win.
|
|
38
|
+
apply_rules: { flag: "apply-rules", type: "bool" },
|
|
39
|
+
// ASYNCHRONOUS BY DEFAULT — `--sync` is the opt-out (see shredderRun for why).
|
|
40
|
+
async: { value: (flags) => (flags.sync === true ? undefined : true) },
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
20
44
|
export async function cmdShredder(ctx) {
|
|
21
45
|
const sub = ctx.positionals[0];
|
|
22
46
|
const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
|
|
@@ -87,11 +111,6 @@ const POLL_CEILING_MS = 30 * 60 * 1000;
|
|
|
87
111
|
|
|
88
112
|
async function shredderRun(ctx) {
|
|
89
113
|
const { client, positionals, flags, io } = ctx;
|
|
90
|
-
const id = requirePositional(positionals, 0, "endpoint-id");
|
|
91
|
-
const text = readBodyInput(flags);
|
|
92
|
-
if (text === undefined) {
|
|
93
|
-
throw new CliError("Provide the prose to shred with --body <text>, --file <path>, or --file - (stdin).");
|
|
94
|
-
}
|
|
95
114
|
// ASYNCHRONOUS BY DEFAULT, and `--sync` is the opt-out rather than the other way round.
|
|
96
115
|
//
|
|
97
116
|
// A shred has no workable synchronous ceiling — the identical article took 203s, 541s and
|
|
@@ -100,21 +119,10 @@ async function shredderRun(ctx) {
|
|
|
100
119
|
// answer, so the caller gets no text, no error, and no record. Defaulting the other way
|
|
101
120
|
// made the common case the broken one.
|
|
102
121
|
const sync = flags.sync === true;
|
|
103
|
-
const args =
|
|
104
|
-
|
|
105
|
-
text
|
|
106
|
-
|
|
107
|
-
attempts_per_unit: flagNum(flags.attempts),
|
|
108
|
-
// What the text IS. Without this an HTML file could only go through the
|
|
109
|
-
// `auto` heuristic, which misreads a markdown post carrying an embed.
|
|
110
|
-
...(flags.format ? { format: String(flags.format) } : {}),
|
|
111
|
-
// How hard to rewrite. Validated service-side; sent only when the caller asked.
|
|
112
|
-
...(flags.aggression ? { aggression: String(flags.aggression) } : {}),
|
|
113
|
-
// Hold every rewrite to the live writing rules. A trade, not a free win: refused rewrites
|
|
114
|
-
// cost provider attempts and a sentence with no clean rewrite keeps its original.
|
|
115
|
-
...(flags["apply-rules"] === true ? { apply_rules: true } : {}),
|
|
116
|
-
...(sync ? {} : { async: true }),
|
|
117
|
-
});
|
|
122
|
+
const args = argsFromFlags(ARGS.run_shred, flags, positionals);
|
|
123
|
+
if (args.text === undefined) {
|
|
124
|
+
throw new CliError("Provide the prose to shred with --body <text>, --file <path>, or --file - (stdin).");
|
|
125
|
+
}
|
|
118
126
|
const result = await client.callTool("run_shred", args);
|
|
119
127
|
if (sync) {
|
|
120
128
|
ok(ctx, "Shred complete.");
|