@letterstory/cli 0.63.1 → 0.65.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/lib/cli.mjs +1 -1
- package/lib/commands/admin.mjs +59 -4
- package/lib/commands/kernel.mjs +368 -2
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -55,7 +55,7 @@ import {
|
|
|
55
55
|
} from "./commands.mjs";
|
|
56
56
|
|
|
57
57
|
// Keep in sync with cli/package.json.
|
|
58
|
-
export const VERSION = "0.
|
|
58
|
+
export const VERSION = "0.65.0";
|
|
59
59
|
|
|
60
60
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
61
61
|
// can't accidentally swallow the id as --json's value.
|
package/lib/commands/admin.mjs
CHANGED
|
@@ -452,6 +452,10 @@ async function cmdAdminTuningContext(ctx) {
|
|
|
452
452
|
* admin tuning arenas | arena <version-id> our arena's versions / one version's file and diff
|
|
453
453
|
* admin tuning arena-candidate --patch '{…}' | --file f.json [--of <version-id>] [--note "…"] save a candidate
|
|
454
454
|
* admin tuning research-compare --title "…" (--candidate <version-id> | --patch '{…}' | --null-check) [--keys …]
|
|
455
|
+
* admin tuning research-compare --title "…" --candidates <version-id>,… | --patches '[{…},{…}]' several
|
|
456
|
+
* candidates against ONE control arm (k + 1 runs per input, not 2k)
|
|
457
|
+
* admin tuning arena-dials every arena setting: help, default, evidence, live's value
|
|
458
|
+
* admin tuning comparison-extend <comparison-id> --keys k1,k2 | --all add inputs to a comparison on fixed inputs
|
|
455
459
|
* admin tuning research-inputs [--set v1] research's fixed inputs
|
|
456
460
|
* admin tuning research-split [--since-hours 72] [--at <iso>,…] what each deploy did to research in production
|
|
457
461
|
* admin tuning research-standing live's level on the fixed inputs, week by week
|
|
@@ -899,12 +903,34 @@ async function cmdAdminTuning(ctx) {
|
|
|
899
903
|
const versionId = flagStr(flags.candidate);
|
|
900
904
|
const patch = jsonFlag(flags.patch, "--patch");
|
|
901
905
|
const nullCheck = flagBool(flags["null-check"]);
|
|
902
|
-
const
|
|
906
|
+
const versionIds = flagList(flags.candidates);
|
|
907
|
+
const patches = jsonFlag(flags.patches, "--patches");
|
|
908
|
+
if (patches !== undefined && !Array.isArray(patches))
|
|
909
|
+
throw new CliError("--patches is a JSON array of patches: '[{…},{…}]'");
|
|
910
|
+
const named = [
|
|
911
|
+
versionId !== undefined,
|
|
912
|
+
patch !== undefined,
|
|
913
|
+
nullCheck === true,
|
|
914
|
+
(versionIds?.length ?? 0) + (patches?.length ?? 0) > 0,
|
|
915
|
+
].filter(Boolean).length;
|
|
903
916
|
if (!title || named !== 1)
|
|
904
917
|
throw new CliError(
|
|
905
|
-
"Usage: admin tuning research-compare --title \"…\" (--candidate <version-id> | --patch '{…}' | --null-check) [--keys k1,k2] [--concurrency 6] [--experiment <id>]"
|
|
918
|
+
"Usage: admin tuning research-compare --title \"…\" (--candidate <version-id> | --patch '{…}' | --null-check | --candidates <id>,… [--patches '[{…},…]']) [--keys k1,k2] [--concurrency 6] [--experiment <id>]"
|
|
906
919
|
);
|
|
907
|
-
const
|
|
920
|
+
const several =
|
|
921
|
+
versionIds?.length || patches?.length
|
|
922
|
+
? [
|
|
923
|
+
...(versionIds ?? []).map((v) => ({ version_id: v })),
|
|
924
|
+
...(patches ?? []).map((p) => ({ patch: p })),
|
|
925
|
+
]
|
|
926
|
+
: undefined;
|
|
927
|
+
const candidate = several
|
|
928
|
+
? undefined
|
|
929
|
+
: versionId
|
|
930
|
+
? { version_id: versionId }
|
|
931
|
+
: patch
|
|
932
|
+
? { patch }
|
|
933
|
+
: { null_check: true };
|
|
908
934
|
const r = await staffRequest(
|
|
909
935
|
ctx,
|
|
910
936
|
"POST",
|
|
@@ -913,6 +939,7 @@ async function cmdAdminTuning(ctx) {
|
|
|
913
939
|
primitive: "research",
|
|
914
940
|
title,
|
|
915
941
|
candidate,
|
|
942
|
+
candidates: several,
|
|
916
943
|
keys: flagList(flags.keys),
|
|
917
944
|
concurrency: flagNum(flags.concurrency),
|
|
918
945
|
experiment_id: flagStr(flags.experiment),
|
|
@@ -920,6 +947,34 @@ async function cmdAdminTuning(ctx) {
|
|
|
920
947
|
);
|
|
921
948
|
return json ? printResult(io, flags, r) : io.log(`research comparison ${r.comparison?.id ?? ""} started`);
|
|
922
949
|
}
|
|
950
|
+
case "arena-dials": {
|
|
951
|
+
const r = await staffRequest(ctx, "GET", `${T}/arena-files/dials`);
|
|
952
|
+
if (json) return printResult(io, flags, r);
|
|
953
|
+
const valueOf = (key) => key.split(".").reduce((v, k) => (v == null ? v : v[k]), r.live.file);
|
|
954
|
+
const lines = [];
|
|
955
|
+
for (const panel of r.panels) {
|
|
956
|
+
lines.push(`${panel.title} — ${panel.question}`);
|
|
957
|
+
for (const d of r.dials.filter((x) => x.panel === panel.id))
|
|
958
|
+
lines.push(
|
|
959
|
+
` ${d.key.padEnd(36)} live ${JSON.stringify(valueOf(d.key))} (default ${JSON.stringify(d.fleetDefault)}) ${d.label}`
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
lines.push(
|
|
963
|
+
`providers: ${r.providers.map((p) => `${p.id}${p.configured ? "" : " (no key here)"}`).join(", ")}`
|
|
964
|
+
);
|
|
965
|
+
return io.log(lines.join("\n"));
|
|
966
|
+
}
|
|
967
|
+
case "comparison-extend": {
|
|
968
|
+
const keys = flagBool(flags.all) ? "all" : flagList(flags.keys);
|
|
969
|
+
if (!id || !keys?.length)
|
|
970
|
+
throw new CliError("Usage: admin tuning comparison-extend <comparison-id> --keys k1,k2 | --all");
|
|
971
|
+
const r = await staffRequest(ctx, "POST", `${T}/comparisons/${id}/extend`, { keys });
|
|
972
|
+
return json
|
|
973
|
+
? printResult(io, flags, r)
|
|
974
|
+
: io.log(
|
|
975
|
+
`comparison ${r.comparison.id}: ${r.comparison.added.length} input(s) added, ${r.comparison.pairs} in all`
|
|
976
|
+
);
|
|
977
|
+
}
|
|
923
978
|
case "research-inputs": {
|
|
924
979
|
const set = flagStr(flags.set);
|
|
925
980
|
return printResult(
|
|
@@ -1182,7 +1237,7 @@ async function cmdAdminTuning(ctx) {
|
|
|
1182
1237
|
}
|
|
1183
1238
|
default:
|
|
1184
1239
|
throw new CliError(
|
|
1185
|
-
"Usage: admin tuning kernels|kernel|candidate|compare|comparisons|comparison|picks|pick|reading-list|pair|side-by-side|candidates|base-kernels|base-versions|sessions|session|session-new|propose|trial-add|trial-run|trial-save|promote|arenas|arena|arena-candidate|research-compare|research-inputs|research-split|research-standing|promote-arena|shred-compare|shred-inputs|shred-split|shred-standing|shredders|shredder|shredder-candidate|promote-shredder|enhance-compare|enhance-inputs|enhance-split|enhance-standing|enhancers|enhancer|enhancer-candidate|promote-enhancer (see --help)"
|
|
1240
|
+
"Usage: admin tuning kernels|kernel|candidate|compare|comparisons|comparison|picks|pick|reading-list|pair|side-by-side|candidates|base-kernels|base-versions|sessions|session|session-new|propose|trial-add|trial-run|trial-save|promote|arenas|arena|arena-candidate|arena-dials|comparison-extend|research-compare|research-inputs|research-split|research-standing|promote-arena|shred-compare|shred-inputs|shred-split|shred-standing|shredders|shredder|shredder-candidate|promote-shredder|enhance-compare|enhance-inputs|enhance-split|enhance-standing|enhancers|enhancer|enhancer-candidate|promote-enhancer (see --help)"
|
|
1186
1241
|
);
|
|
1187
1242
|
}
|
|
1188
1243
|
}
|
package/lib/commands/kernel.mjs
CHANGED
|
@@ -228,8 +228,109 @@ export const ARGS = {
|
|
|
228
228
|
get_kernel_endpoint_run_steps: {
|
|
229
229
|
run_id: { positional: 0, label: "run-id", required: true },
|
|
230
230
|
},
|
|
231
|
+
// `kernel endpoint bind <id> --kernel <id|none> --shredder <id|none>`: "none" unbinds (null),
|
|
232
|
+
// which is not the same as leaving the flag out (unchanged).
|
|
233
|
+
update_kernel_endpoint: {
|
|
234
|
+
endpoint_id: { positional: 1, label: "endpoint-id", required: true },
|
|
235
|
+
kernel_id: { value: (flags) => noneIsNull(flags.kernel) },
|
|
236
|
+
shredder_endpoint_id: { value: (flags) => noneIsNull(flags.shredder) },
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
// ── Making a kernel your own, and choosing the org's ───────────────────────────────
|
|
240
|
+
configure_kernel: {
|
|
241
|
+
source_kernel_id: { positional: 0, label: "source-kernel-id", required: true },
|
|
242
|
+
name: "name",
|
|
243
|
+
},
|
|
244
|
+
create_kernel: {
|
|
245
|
+
file: { value: (flags) => readJsonFileFlag(flags, "file") },
|
|
246
|
+
},
|
|
247
|
+
get_org_writing_kernel: {},
|
|
248
|
+
set_org_writing_kernel: {
|
|
249
|
+
writing_kernel_id: {
|
|
250
|
+
value: (_flags, positionals) => {
|
|
251
|
+
const id = positionals[0];
|
|
252
|
+
if (id === undefined) throw new CliError("Missing required <kernel-id> (or none to clear it)");
|
|
253
|
+
return id === "none" ? null : id;
|
|
254
|
+
},
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
|
|
258
|
+
// ── The kernel file and its versions ──────────────────────────────────────────────
|
|
259
|
+
get_kernel_file: {
|
|
260
|
+
kernel_id: { positional: 0, label: "kernel-id", required: true },
|
|
261
|
+
version: { flag: "version", type: "num" },
|
|
262
|
+
},
|
|
263
|
+
get_kernel_file_schema: {},
|
|
264
|
+
list_kernel_versions: {
|
|
265
|
+
kernel_id: { positional: 0, label: "kernel-id", required: true },
|
|
266
|
+
},
|
|
267
|
+
// A candidate unless --live: the safe default is the one that changes no run.
|
|
268
|
+
save_kernel_file: {
|
|
269
|
+
kernel_id: { positional: 0, label: "kernel-id", required: true },
|
|
270
|
+
file: { value: (flags) => readJsonFileFlag(flags, "file") },
|
|
271
|
+
status: { value: (flags) => (flags.live === true ? "live" : undefined) },
|
|
272
|
+
note: "note",
|
|
273
|
+
},
|
|
274
|
+
promote_kernel_version: {
|
|
275
|
+
kernel_id: { positional: 0, label: "kernel-id", required: true },
|
|
276
|
+
version: { value: (_flags, positionals) => positiveInt(positionals[1], "version") },
|
|
277
|
+
},
|
|
278
|
+
get_kernel_offer: {
|
|
279
|
+
kernel_id: { positional: 0, label: "kernel-id", required: true },
|
|
280
|
+
},
|
|
281
|
+
accept_kernel_offer: {
|
|
282
|
+
kernel_id: { positional: 0, label: "kernel-id", required: true },
|
|
283
|
+
status: { value: (flags) => (flags.live === true ? "live" : undefined) },
|
|
284
|
+
},
|
|
285
|
+
suggest_kernel_voice_settings: {
|
|
286
|
+
kernel_id: { positional: 0, label: "kernel-id", required: true },
|
|
287
|
+
notes: { value: (flags) => flagStr(flags.notes) ?? readTextFileFlag(flags, "notes-file") },
|
|
288
|
+
current: { value: (flags) => readJsonFileFlag(flags, "voice-config-file") },
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
// ── The Studio: try a kernel, keep the history ────────────────────────────────────
|
|
292
|
+
list_kernel_studio_runs: {
|
|
293
|
+
kernel_id: "kernel",
|
|
294
|
+
limit: { flag: "limit", type: "num" },
|
|
295
|
+
},
|
|
296
|
+
get_kernel_studio_run: {
|
|
297
|
+
run_id: { positional: 0, label: "run-id", required: true },
|
|
298
|
+
},
|
|
231
299
|
};
|
|
232
300
|
|
|
301
|
+
/** `--kernel none` → null (unbind); a value → itself; absent → undefined (leave it). */
|
|
302
|
+
function noneIsNull(raw) {
|
|
303
|
+
const v = flagStr(raw);
|
|
304
|
+
if (v === undefined) return undefined;
|
|
305
|
+
return v === "none" ? null : v;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function positiveInt(raw, label) {
|
|
309
|
+
if (raw === undefined) throw new CliError(`Missing required <${label}>`);
|
|
310
|
+
const n = Number(raw);
|
|
311
|
+
if (!Number.isInteger(n) || n < 1) throw new CliError(`<${label}> must be a positive whole number (got "${raw}")`);
|
|
312
|
+
return n;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// `kernel try` is the Studio's run: the same per-run options as `kernel write` (one map, so a
|
|
316
|
+
// field run_kernel gains is a field this learns too), on a kernel rather than an endpoint.
|
|
317
|
+
{
|
|
318
|
+
const {
|
|
319
|
+
endpoint_id: _endpoint,
|
|
320
|
+
input: _input,
|
|
321
|
+
article_id: _article,
|
|
322
|
+
kernel_id: _kernel,
|
|
323
|
+
...runOptions
|
|
324
|
+
} = ARGS.run_kernel;
|
|
325
|
+
ARGS.start_kernel_studio_run = {
|
|
326
|
+
kernel_id: { positional: 0, label: "kernel-id", required: true },
|
|
327
|
+
brief: { value: (flags) => readBodyInput(flags) },
|
|
328
|
+
// --name, not --title: --title is the template's title in the run options.
|
|
329
|
+
title: "name",
|
|
330
|
+
...runOptions,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
|
|
233
334
|
export async function cmdKernel(ctx) {
|
|
234
335
|
const sub = ctx.positionals[0];
|
|
235
336
|
const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
|
|
@@ -257,11 +358,37 @@ export async function cmdKernel(ctx) {
|
|
|
257
358
|
return kernelRunStatus(rest);
|
|
258
359
|
case "runs":
|
|
259
360
|
return kernelRuns(rest);
|
|
361
|
+
case "fork":
|
|
362
|
+
return kernelFork(rest);
|
|
363
|
+
case "create":
|
|
364
|
+
return kernelCreate(rest);
|
|
365
|
+
case "default":
|
|
366
|
+
return kernelDefault(rest);
|
|
367
|
+
case "use":
|
|
368
|
+
return kernelUse(rest);
|
|
369
|
+
case "file":
|
|
370
|
+
return kernelFile(rest);
|
|
371
|
+
case "schema":
|
|
372
|
+
return kernelSchema(rest);
|
|
373
|
+
case "versions":
|
|
374
|
+
return kernelVersions(rest);
|
|
375
|
+
case "save":
|
|
376
|
+
return kernelSave(rest);
|
|
377
|
+
case "promote":
|
|
378
|
+
return kernelPromote(rest);
|
|
379
|
+
case "offer":
|
|
380
|
+
return kernelOffer(rest);
|
|
381
|
+
case "suggest-voice":
|
|
382
|
+
return kernelSuggestVoice(rest);
|
|
383
|
+
case "try":
|
|
384
|
+
return kernelTry(rest);
|
|
385
|
+
case "history":
|
|
386
|
+
return kernelHistory(rest);
|
|
260
387
|
case "balance":
|
|
261
388
|
return kernelBalance(rest);
|
|
262
389
|
default:
|
|
263
390
|
throw new CliError(
|
|
264
|
-
`Unknown kernel subcommand: ${sub ?? "(none)"}. Try: list,
|
|
391
|
+
`Unknown kernel subcommand: ${sub ?? "(none)"}. Try: list, fork, create, default, use, file, schema, versions, save, promote, offer, update, suggest-voice, archive, eval, try, history, endpoints, endpoint, write, polish, run-status, runs, balance, status`
|
|
265
392
|
);
|
|
266
393
|
}
|
|
267
394
|
}
|
|
@@ -435,7 +562,17 @@ async function kernelEndpoint(ctx) {
|
|
|
435
562
|
printResult(io, flags, result);
|
|
436
563
|
return 0;
|
|
437
564
|
}
|
|
438
|
-
|
|
565
|
+
if (action === "bind") {
|
|
566
|
+
const args = argsFromFlags(ARGS.update_kernel_endpoint, flags, positionals);
|
|
567
|
+
if (args.kernel_id === undefined && args.shredder_endpoint_id === undefined) {
|
|
568
|
+
throw new CliError("Provide --kernel <id|none> and/or --shredder <id|none>.");
|
|
569
|
+
}
|
|
570
|
+
const result = await client.callTool("update_kernel_endpoint", args);
|
|
571
|
+
ok(ctx, `Rebound kernel endpoint ${args.endpoint_id}.`);
|
|
572
|
+
printResult(io, flags, result);
|
|
573
|
+
return 0;
|
|
574
|
+
}
|
|
575
|
+
throw new CliError(`Unknown kernel endpoint action: ${action ?? "(none)"}. Try: create, delete, config, bind`);
|
|
439
576
|
}
|
|
440
577
|
|
|
441
578
|
/**
|
|
@@ -615,3 +752,232 @@ async function kernelBalance(ctx) {
|
|
|
615
752
|
printResult(io, flags, result);
|
|
616
753
|
return 0;
|
|
617
754
|
}
|
|
755
|
+
|
|
756
|
+
/* ------------------------------------------------------------------ */
|
|
757
|
+
/* Making a kernel your own, and its file */
|
|
758
|
+
/* ------------------------------------------------------------------ */
|
|
759
|
+
|
|
760
|
+
/** `kernel fork <source-kernel-id> [--name <name>]` — copy a catalog kernel (or one of yours) into one your org can tune. */
|
|
761
|
+
async function kernelFork(ctx) {
|
|
762
|
+
const { client, positionals, flags, io } = ctx;
|
|
763
|
+
const result = await client.callTool("configure_kernel", argsFromFlags(ARGS.configure_kernel, flags, positionals));
|
|
764
|
+
const kernel = toolField(result, "kernel");
|
|
765
|
+
if (kernel?.id) ok(ctx, `Forked as "${kernel.name}" (${kernel.id}). Its file: ${ctx.bin} kernel file ${kernel.id}`);
|
|
766
|
+
printResult(io, flags, result);
|
|
767
|
+
return 0;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/** `kernel create --file kernel.json` — bring your own: a kernel made from a file (a name and a voice is enough). */
|
|
771
|
+
async function kernelCreate(ctx) {
|
|
772
|
+
const { client, positionals, flags, io } = ctx;
|
|
773
|
+
if (flagStr(flags.file) === undefined) {
|
|
774
|
+
throw new CliError('Provide the kernel file with --file <path> (as short as {"name": "…", "voice": "…"}).');
|
|
775
|
+
}
|
|
776
|
+
const result = await client.callTool("create_kernel", argsFromFlags(ARGS.create_kernel, flags, positionals));
|
|
777
|
+
const kernel = toolField(result, "kernel");
|
|
778
|
+
if (kernel?.id) ok(ctx, `Created "${kernel.name}" (${kernel.id}), version 1, live.`);
|
|
779
|
+
printResult(io, flags, result);
|
|
780
|
+
return 0;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/** `kernel default` — the org's selected writing kernel. */
|
|
784
|
+
async function kernelDefault(ctx) {
|
|
785
|
+
const { client, positionals, flags, io } = ctx;
|
|
786
|
+
const result = await client.callTool(
|
|
787
|
+
"get_org_writing_kernel",
|
|
788
|
+
argsFromFlags(ARGS.get_org_writing_kernel, flags, positionals)
|
|
789
|
+
);
|
|
790
|
+
printResult(io, flags, result);
|
|
791
|
+
return 0;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/** `kernel use <kernel-id|none>` — make a kernel the org's default (a shared one needs an org admin's key). */
|
|
795
|
+
async function kernelUse(ctx) {
|
|
796
|
+
const { client, positionals, flags, io } = ctx;
|
|
797
|
+
const args = argsFromFlags(ARGS.set_org_writing_kernel, flags, positionals);
|
|
798
|
+
const result = await client.callTool("set_org_writing_kernel", args);
|
|
799
|
+
ok(
|
|
800
|
+
ctx,
|
|
801
|
+
args.writing_kernel_id ? `The org now writes with ${args.writing_kernel_id}.` : "Cleared the org's kernel."
|
|
802
|
+
);
|
|
803
|
+
printResult(io, flags, result);
|
|
804
|
+
return 0;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* `kernel file <kernel-id> [--version N]` — the kernel file itself, printed as the document so
|
|
809
|
+
* `> kernel.json` round-trips into `kernel save <kernel-id> --file kernel.json`.
|
|
810
|
+
*/
|
|
811
|
+
async function kernelFile(ctx) {
|
|
812
|
+
const { client, positionals, flags, io } = ctx;
|
|
813
|
+
const result = await client.callTool("get_kernel_file", argsFromFlags(ARGS.get_kernel_file, flags, positionals));
|
|
814
|
+
printResult(io, flags, result, (r) => JSON.stringify(r?.file ?? r, null, 2));
|
|
815
|
+
return 0;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/** `kernel schema` — the kernel file's JSON Schema, as the kernels service validates it. */
|
|
819
|
+
async function kernelSchema(ctx) {
|
|
820
|
+
const { client, positionals, flags, io } = ctx;
|
|
821
|
+
const result = await client.callTool(
|
|
822
|
+
"get_kernel_file_schema",
|
|
823
|
+
argsFromFlags(ARGS.get_kernel_file_schema, flags, positionals)
|
|
824
|
+
);
|
|
825
|
+
printResult(io, flags, result, (r) => JSON.stringify(r?.schema ?? r, null, 2));
|
|
826
|
+
return 0;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/** `kernel versions <kernel-id>` — every version, the live one, the base it came from and any offer. */
|
|
830
|
+
async function kernelVersions(ctx) {
|
|
831
|
+
const { client, positionals, flags, io } = ctx;
|
|
832
|
+
const result = await client.callTool(
|
|
833
|
+
"list_kernel_versions",
|
|
834
|
+
argsFromFlags(ARGS.list_kernel_versions, flags, positionals)
|
|
835
|
+
);
|
|
836
|
+
printResult(io, flags, result, (r) => {
|
|
837
|
+
const lines = (r?.versions ?? []).map(
|
|
838
|
+
(v) =>
|
|
839
|
+
`v${v.version} ${v.status.padEnd(9)} ${v.writer.padEnd(18)} ${v.note ?? ""}${v.changes?.length ? ` [${v.changes.join(", ")}]` : ""}`
|
|
840
|
+
);
|
|
841
|
+
if (r?.base)
|
|
842
|
+
lines.push(
|
|
843
|
+
`\nCopied from ${r.base.name} (v${r.base.version}); differs in: ${r.base.changes.join(", ") || "nothing"}`
|
|
844
|
+
);
|
|
845
|
+
if (r?.offer)
|
|
846
|
+
lines.push(
|
|
847
|
+
`Offer: ${r.base?.name ?? "the base"} v${r.offer.base_version_from} → v${r.offer.base_version_to} changes ${r.offer.changes.join(", ")}`
|
|
848
|
+
);
|
|
849
|
+
return lines.join("\n");
|
|
850
|
+
});
|
|
851
|
+
return 0;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* `kernel save <kernel-id> --file kernel.json [--live] [--note "…"]` — a new version. A candidate
|
|
856
|
+
* unless --live: try it with `kernel try <kernel-id> --kernel-version N`, then `kernel promote`.
|
|
857
|
+
*/
|
|
858
|
+
async function kernelSave(ctx) {
|
|
859
|
+
const { client, positionals, flags, io } = ctx;
|
|
860
|
+
if (flagStr(flags.file) === undefined) throw new CliError("Provide the kernel file with --file <path>.");
|
|
861
|
+
const args = argsFromFlags(ARGS.save_kernel_file, flags, positionals);
|
|
862
|
+
const result = await client.callTool("save_kernel_file", args);
|
|
863
|
+
const version = toolField(result, "version");
|
|
864
|
+
ok(
|
|
865
|
+
ctx,
|
|
866
|
+
args.status === "live"
|
|
867
|
+
? `Version ${version} is live.`
|
|
868
|
+
: `Saved version ${version} as a candidate. Try it: ${ctx.bin} kernel try ${args.kernel_id} --kernel-version ${version} --body "…"; ship it: ${ctx.bin} kernel promote ${args.kernel_id} ${version} --yes`
|
|
869
|
+
);
|
|
870
|
+
if (toolField(result, "voice_settings_detached"))
|
|
871
|
+
ok(ctx, "The voice is now edited as text; voice_config no longer applies.");
|
|
872
|
+
printResult(io, flags, result);
|
|
873
|
+
return 0;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/** `kernel promote <kernel-id> <version> --yes` — make a version live: ship a candidate, or roll back. */
|
|
877
|
+
async function kernelPromote(ctx) {
|
|
878
|
+
const { client, positionals, flags, io } = ctx;
|
|
879
|
+
const args = argsFromFlags(ARGS.promote_kernel_version, flags, positionals);
|
|
880
|
+
if (!flags.yes) {
|
|
881
|
+
io.error(
|
|
882
|
+
`Every run of this kernel will use version ${args.version} from its next call. Re-run with --yes to confirm: ${ctx.bin} kernel promote ${args.kernel_id} ${args.version} --yes`
|
|
883
|
+
);
|
|
884
|
+
return 1;
|
|
885
|
+
}
|
|
886
|
+
const result = await client.callTool("promote_kernel_version", args);
|
|
887
|
+
const previous = toolField(result, "previous_live");
|
|
888
|
+
ok(
|
|
889
|
+
ctx,
|
|
890
|
+
`Version ${args.version} is live.${previous ? ` To undo: ${ctx.bin} kernel promote ${args.kernel_id} ${previous} --yes` : ""}`
|
|
891
|
+
);
|
|
892
|
+
printResult(io, flags, result);
|
|
893
|
+
return 0;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/** `kernel offer <kernel-id> [--accept [--live]]` — the base's newer version, and taking it. */
|
|
897
|
+
async function kernelOffer(ctx) {
|
|
898
|
+
const { client, positionals, flags, io } = ctx;
|
|
899
|
+
if (flags.accept !== true) {
|
|
900
|
+
const result = await client.callTool(
|
|
901
|
+
"get_kernel_offer",
|
|
902
|
+
argsFromFlags(ARGS.get_kernel_offer, flags, positionals)
|
|
903
|
+
);
|
|
904
|
+
printResult(io, flags, result);
|
|
905
|
+
return 0;
|
|
906
|
+
}
|
|
907
|
+
const args = argsFromFlags(ARGS.accept_kernel_offer, flags, positionals);
|
|
908
|
+
const result = await client.callTool("accept_kernel_offer", args);
|
|
909
|
+
ok(ctx, `Accepted as version ${toolField(result, "version")} (${toolField(result, "status")}).`);
|
|
910
|
+
printResult(io, flags, result);
|
|
911
|
+
return 0;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* `kernel suggest-voice <kernel-id> --notes "make it funnier" --voice-config-file current.json` —
|
|
916
|
+
* which voice settings a note asks for. Changes nothing; `kernel update --voice-config-file` saves.
|
|
917
|
+
*/
|
|
918
|
+
async function kernelSuggestVoice(ctx) {
|
|
919
|
+
const { client, positionals, flags, io } = ctx;
|
|
920
|
+
const args = argsFromFlags(ARGS.suggest_kernel_voice_settings, flags, positionals);
|
|
921
|
+
if (args.notes === undefined) throw new CliError("Provide the note with --notes <text> or --notes-file <path>.");
|
|
922
|
+
if (args.current === undefined) {
|
|
923
|
+
throw new CliError(
|
|
924
|
+
"Provide the kernel's current voice settings with --voice-config-file <path> (register, humor, inquisitive)."
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
const result = await client.callTool("suggest_kernel_voice_settings", args);
|
|
928
|
+
printResult(io, flags, result);
|
|
929
|
+
return 0;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* `kernel try <kernel-id> --body "…" [--kernel-version N] [--name "…"]` — the Studio's run: a
|
|
934
|
+
* throwaway article from any kernel the org can use, kept in the Studio's History. Follows the
|
|
935
|
+
* run like `write` unless --no-wait.
|
|
936
|
+
*/
|
|
937
|
+
async function kernelTry(ctx) {
|
|
938
|
+
const { client, positionals, flags, io } = ctx;
|
|
939
|
+
const args = argsFromFlags(ARGS.start_kernel_studio_run, flags, positionals);
|
|
940
|
+
if (args.brief === undefined) {
|
|
941
|
+
throw new CliError("Provide the brief with --body <text>, --file <path>, or --file - (stdin).");
|
|
942
|
+
}
|
|
943
|
+
const result = await client.callTool("start_kernel_studio_run", args);
|
|
944
|
+
const runId = toolField(result, "run")?.external_id;
|
|
945
|
+
if (!runId || flags["no-wait"] === true) {
|
|
946
|
+
if (runId) ok(ctx, `Started ${runId}. Follow it with: ${ctx.bin} kernel history --kernel ${args.kernel_id}`);
|
|
947
|
+
printResult(io, flags, result);
|
|
948
|
+
return 0;
|
|
949
|
+
}
|
|
950
|
+
ok(ctx, `Started ${runId}. Waiting — this keeps running on the server if you stop.`);
|
|
951
|
+
let lastStatus;
|
|
952
|
+
const { settled, value: polled } = await followJob(
|
|
953
|
+
io,
|
|
954
|
+
async () => {
|
|
955
|
+
const r = await client.callTool(
|
|
956
|
+
"get_kernel_studio_run",
|
|
957
|
+
argsFromFlags(ARGS.get_kernel_studio_run, {}, [runId])
|
|
958
|
+
);
|
|
959
|
+
lastStatus = toolField(r, "run")?.status;
|
|
960
|
+
return { done: !!lastStatus && lastStatus !== "running", value: r };
|
|
961
|
+
},
|
|
962
|
+
{ intervalMs: POLL_INTERVAL_MS, ceilingMs: POLL_CEILING_MS }
|
|
963
|
+
);
|
|
964
|
+
if (!settled) {
|
|
965
|
+
ok(ctx, `Still running after 45 minutes. It stays in the Studio's History: ${ctx.bin} kernel history`);
|
|
966
|
+
printResult(io, flags, polled);
|
|
967
|
+
return 0;
|
|
968
|
+
}
|
|
969
|
+
ok(ctx, `Run ${lastStatus}.`);
|
|
970
|
+
printResult(io, flags, polled, (v) => (typeof v?.article === "string" ? v.article : JSON.stringify(v, null, 2)));
|
|
971
|
+
return lastStatus === "done" ? 0 : 1;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
/** `kernel history [--kernel <id>] [--limit N]` — the Studio's History. */
|
|
975
|
+
async function kernelHistory(ctx) {
|
|
976
|
+
const { client, positionals, flags, io } = ctx;
|
|
977
|
+
const result = await client.callTool(
|
|
978
|
+
"list_kernel_studio_runs",
|
|
979
|
+
argsFromFlags(ARGS.list_kernel_studio_runs, flags, positionals)
|
|
980
|
+
);
|
|
981
|
+
printResult(io, flags, result);
|
|
982
|
+
return 0;
|
|
983
|
+
}
|