@letterstory/cli 0.53.0 → 0.54.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 +8 -0
- package/lib/cli.mjs +1 -1
- package/lib/commands/admin.mjs +117 -1
- package/lib/commands/arena.mjs +81 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -520,6 +520,14 @@ letterstory arena endpoint config <arena-id> [--file cfg.json]
|
|
|
520
520
|
letterstory arena endpoint update <arena-id> --name "Docs research v2" # creator or org admin only
|
|
521
521
|
letterstory arena endpoint delete <arena-id> --yes
|
|
522
522
|
|
|
523
|
+
# The research agent as ONE versioned file (like a kernel file; schema at /api/schema/arena):
|
|
524
|
+
letterstory arena file <arena-id> > arena.json # the file the arena runs now
|
|
525
|
+
letterstory arena file <arena-id> --patch '{"rules":{"context-figures":"measure"}}' --note "try it"
|
|
526
|
+
letterstory arena file <arena-id> --file arena.json # save a whole file as the next version
|
|
527
|
+
letterstory arena versions <arena-id> # your copy's versions and our releases
|
|
528
|
+
letterstory arena pin <arena-id> <version-id|none> # pin one of our releases, or follow live
|
|
529
|
+
letterstory arena offer <arena-id> [--accept] # our newer version, over your own changes
|
|
530
|
+
|
|
523
531
|
# Ledes: a prospect's context in, ready-to-send opening lines out (chosen from a
|
|
524
532
|
# hand-approved library, never generated; the top choice is flagged). Synchronous.
|
|
525
533
|
letterstory lede create --name "Clay outbound table"
|
package/lib/cli.mjs
CHANGED
|
@@ -54,7 +54,7 @@ import {
|
|
|
54
54
|
} from "./commands.mjs";
|
|
55
55
|
|
|
56
56
|
// Keep in sync with cli/package.json.
|
|
57
|
-
export const VERSION = "0.
|
|
57
|
+
export const VERSION = "0.54.0";
|
|
58
58
|
|
|
59
59
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
60
60
|
// can't accidentally swallow the id as --json's value.
|
package/lib/commands/admin.mjs
CHANGED
|
@@ -429,7 +429,29 @@ async function cmdAdminTuningContext(ctx) {
|
|
|
429
429
|
* admin tuning pick <comparison-id> --cell <key> --pick left|right|tie record a blind pick
|
|
430
430
|
* admin tuning reading-list <comparison-id> [--n 10] your next blind pairs as links, to read off the screen
|
|
431
431
|
* admin tuning promote <experiment-id> --versions <service-kernel-id>:<version>,… promote from a "ship" decision
|
|
432
|
+
*
|
|
433
|
+
* Research (Phase 3): our arena's versions as files, research comparisons, the deploy split.
|
|
434
|
+
* admin tuning arenas | arena <version-id> our arena's versions / one version's file and diff
|
|
435
|
+
* admin tuning arena-candidate --patch '{…}' | --file f.json [--of <version-id>] [--note "…"] save a candidate
|
|
436
|
+
* admin tuning research-compare --title "…" (--candidate <version-id> | --patch '{…}' | --null-check) [--keys …]
|
|
437
|
+
* admin tuning research-inputs [--set v1] research's fixed inputs
|
|
438
|
+
* admin tuning research-split [--since-hours 72] [--at <iso>,…] what each deploy did to research in production
|
|
439
|
+
* admin tuning research-standing live's level on the fixed inputs, week by week
|
|
440
|
+
* admin tuning promote-arena <experiment-id> --version <arena-version-id> promote our arena from a "ship" decision
|
|
432
441
|
*/
|
|
442
|
+
/** `--flag '<json>'` → the parsed object, or undefined when the flag is absent. */
|
|
443
|
+
function jsonFlag(raw, name) {
|
|
444
|
+
const text = flagStr(raw);
|
|
445
|
+
if (text === undefined) return undefined;
|
|
446
|
+
try {
|
|
447
|
+
const v = JSON.parse(text);
|
|
448
|
+
if (!v || typeof v !== "object" || Array.isArray(v)) throw new Error("not an object");
|
|
449
|
+
return v;
|
|
450
|
+
} catch (err) {
|
|
451
|
+
throw new CliError(`${name} is not a JSON object: ${err.message}`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
433
455
|
async function cmdAdminTuning(ctx) {
|
|
434
456
|
const [sub, id] = ctx.positionals;
|
|
435
457
|
const { flags, io } = ctx;
|
|
@@ -534,6 +556,100 @@ async function cmdAdminTuning(ctx) {
|
|
|
534
556
|
);
|
|
535
557
|
return json ? printResult(io, flags, r) : io.log(r.text);
|
|
536
558
|
}
|
|
559
|
+
// --- Research: our arena's versions, research comparisons, the deploy split ---
|
|
560
|
+
case "arenas": {
|
|
561
|
+
const r = await staffRequest(ctx, "GET", `${T}/arena-files`);
|
|
562
|
+
if (json) return printResult(io, flags, r);
|
|
563
|
+
return io.log(
|
|
564
|
+
r.versions
|
|
565
|
+
.map((v) => `${v.label.padEnd(18)} ${v.status.padEnd(9)} ${v.id}${v.note ? ` ${v.note}` : ""}`)
|
|
566
|
+
.join("\n") || "(no versions)"
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
case "arena": {
|
|
570
|
+
if (!id) throw new CliError("Usage: admin tuning arena <version-id>");
|
|
571
|
+
return printResult(io, { ...flags, json: true }, await staffRequest(ctx, "GET", `${T}/arena-files/${id}`));
|
|
572
|
+
}
|
|
573
|
+
case "arena-candidate": {
|
|
574
|
+
const patch = jsonFlag(flags.patch, "--patch");
|
|
575
|
+
const file = jsonFile({ "arena-file": flags.file }, "arena");
|
|
576
|
+
if ((patch === undefined) === (file === undefined))
|
|
577
|
+
throw new CliError(
|
|
578
|
+
'Usage: admin tuning arena-candidate --patch \'{"rules":{"context-figures":"off"}}\' | --file arena.json [--of <version-id>] [--note "…"]'
|
|
579
|
+
);
|
|
580
|
+
const r = await staffRequest(
|
|
581
|
+
ctx,
|
|
582
|
+
"POST",
|
|
583
|
+
`${T}/arena-files`,
|
|
584
|
+
compact({ patch, file, patch_of: flagStr(flags.of), note: flagStr(flags.note) })
|
|
585
|
+
);
|
|
586
|
+
return json ? printResult(io, flags, r) : io.log(`saved candidate ${r.version.label} (${r.version.id})`);
|
|
587
|
+
}
|
|
588
|
+
case "research-compare": {
|
|
589
|
+
const title = flagStr(flags.title);
|
|
590
|
+
const versionId = flagStr(flags.candidate);
|
|
591
|
+
const patch = jsonFlag(flags.patch, "--patch");
|
|
592
|
+
const nullCheck = flagBool(flags["null-check"]);
|
|
593
|
+
const named = [versionId !== undefined, patch !== undefined, nullCheck === true].filter(Boolean).length;
|
|
594
|
+
if (!title || named !== 1)
|
|
595
|
+
throw new CliError(
|
|
596
|
+
"Usage: admin tuning research-compare --title \"…\" (--candidate <version-id> | --patch '{…}' | --null-check) [--keys k1,k2] [--concurrency 6] [--experiment <id>]"
|
|
597
|
+
);
|
|
598
|
+
const candidate = versionId ? { version_id: versionId } : patch ? { patch } : { null_check: true };
|
|
599
|
+
const r = await staffRequest(
|
|
600
|
+
ctx,
|
|
601
|
+
"POST",
|
|
602
|
+
`${T}/comparisons`,
|
|
603
|
+
compact({
|
|
604
|
+
primitive: "research",
|
|
605
|
+
title,
|
|
606
|
+
candidate,
|
|
607
|
+
keys: flagList(flags.keys),
|
|
608
|
+
concurrency: flagNum(flags.concurrency),
|
|
609
|
+
experiment_id: flagStr(flags.experiment),
|
|
610
|
+
})
|
|
611
|
+
);
|
|
612
|
+
return json ? printResult(io, flags, r) : io.log(`research comparison ${r.comparison?.id ?? ""} started`);
|
|
613
|
+
}
|
|
614
|
+
case "research-inputs": {
|
|
615
|
+
const set = flagStr(flags.set);
|
|
616
|
+
return printResult(
|
|
617
|
+
io,
|
|
618
|
+
{ ...flags, json: true },
|
|
619
|
+
await staffRequest(ctx, "GET", `${T}/research/inputs${set ? `?${new URLSearchParams({ set })}` : ""}`)
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
case "research-split": {
|
|
623
|
+
const q = new URLSearchParams(
|
|
624
|
+
compact({
|
|
625
|
+
since_hours: flagStr(flags["since-hours"]),
|
|
626
|
+
window_hours: flagStr(flags["window-hours"]),
|
|
627
|
+
at: flagStr(flags.at),
|
|
628
|
+
})
|
|
629
|
+
);
|
|
630
|
+
return printResult(
|
|
631
|
+
io,
|
|
632
|
+
{ ...flags, json: true },
|
|
633
|
+
await staffRequest(ctx, "GET", `${T}/research/deploy-split${q.size ? `?${q}` : ""}`)
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
case "research-standing":
|
|
637
|
+
return printResult(io, { ...flags, json: true }, await staffRequest(ctx, "GET", `${T}/research/standing`));
|
|
638
|
+
case "promote-arena": {
|
|
639
|
+
const version = flagStr(flags.version);
|
|
640
|
+
if (!id || !version)
|
|
641
|
+
throw new CliError("Usage: admin tuning promote-arena <experiment-id> --version <arena-version-id>");
|
|
642
|
+
return printResult(
|
|
643
|
+
io,
|
|
644
|
+
{ ...flags, json: true },
|
|
645
|
+
await staffRequest(
|
|
646
|
+
ctx,
|
|
647
|
+
"POST",
|
|
648
|
+
`${EXPERIMENTS}/${id}/promote`,
|
|
649
|
+
compact({ arena_version_id: version, actor: actorFrom(flags) })
|
|
650
|
+
)
|
|
651
|
+
);
|
|
652
|
+
}
|
|
537
653
|
case "promote": {
|
|
538
654
|
const list = flagList(flags.versions);
|
|
539
655
|
if (!id || !list?.length)
|
|
@@ -557,7 +673,7 @@ async function cmdAdminTuning(ctx) {
|
|
|
557
673
|
}
|
|
558
674
|
default:
|
|
559
675
|
throw new CliError(
|
|
560
|
-
"Usage: admin tuning kernels|kernel|candidate|compare|comparisons|comparison|picks|pick|reading-list|promote (see --help)"
|
|
676
|
+
"Usage: admin tuning kernels|kernel|candidate|compare|comparisons|comparison|picks|pick|reading-list|promote|arenas|arena|arena-candidate|research-compare|research-inputs|research-split|research-standing|promote-arena (see --help)"
|
|
561
677
|
);
|
|
562
678
|
}
|
|
563
679
|
}
|
package/lib/commands/arena.mjs
CHANGED
|
@@ -186,6 +186,33 @@ export const ARGS = {
|
|
|
186
186
|
arena_id: { positional: 1, label: "arena-id", required: true },
|
|
187
187
|
config: { value: (flags) => readJsonFileFlag(flags, "file") },
|
|
188
188
|
},
|
|
189
|
+
// The arena's research agent as a versioned file (the kernel file's twin): `arena file <id>`
|
|
190
|
+
// prints it, `--file f.json` saves a whole file, `--patch '{…}'` only the fields to change.
|
|
191
|
+
get_arena_file: {
|
|
192
|
+
arena_id: { positional: 0, label: "arena-id", required: true },
|
|
193
|
+
},
|
|
194
|
+
save_arena_file: {
|
|
195
|
+
arena_id: { positional: 0, label: "arena-id", required: true },
|
|
196
|
+
file: { value: (flags) => readJsonFileFlag(flags, "file") },
|
|
197
|
+
patch: { value: (flags) => readJsonObjectFlag(flags, "patch") },
|
|
198
|
+
note: "note",
|
|
199
|
+
},
|
|
200
|
+
list_arena_versions: {
|
|
201
|
+
arena_id: { positional: 0, label: "arena-id", required: true },
|
|
202
|
+
},
|
|
203
|
+
pin_arena_version: {
|
|
204
|
+
arena_id: { positional: 0, label: "arena-id", required: true },
|
|
205
|
+
version_id: {
|
|
206
|
+
value: (_flags, positionals) => {
|
|
207
|
+
const v = positionals[1];
|
|
208
|
+
if (v === undefined) throw new CliError("Missing required <version-id|none>");
|
|
209
|
+
return v === "none" ? null : v;
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
accept_arena_offer: {
|
|
214
|
+
arena_id: { positional: 0, label: "arena-id", required: true },
|
|
215
|
+
},
|
|
189
216
|
};
|
|
190
217
|
|
|
191
218
|
export async function cmdArena(ctx) {
|
|
@@ -218,9 +245,17 @@ export async function cmdArena(ctx) {
|
|
|
218
245
|
return arenaList(rest);
|
|
219
246
|
case "endpoint":
|
|
220
247
|
return arenaEndpoint(rest);
|
|
248
|
+
case "file":
|
|
249
|
+
return arenaFile(rest);
|
|
250
|
+
case "versions":
|
|
251
|
+
return arenaTool(rest, "list_arena_versions");
|
|
252
|
+
case "pin":
|
|
253
|
+
return arenaTool(rest, "pin_arena_version");
|
|
254
|
+
case "offer":
|
|
255
|
+
return rest.flags.accept === true ? arenaTool(rest, "accept_arena_offer") : arenaOffer(rest);
|
|
221
256
|
default:
|
|
222
257
|
throw new CliError(
|
|
223
|
-
`Unknown arena subcommand: ${sub ?? "(none)"}. Try: run, run-status, runs, latest, direction, cancel, balance, list, endpoint, roles, role`
|
|
258
|
+
`Unknown arena subcommand: ${sub ?? "(none)"}. Try: run, run-status, runs, latest, direction, cancel, balance, list, endpoint, file, versions, pin, offer, roles, role`
|
|
224
259
|
);
|
|
225
260
|
}
|
|
226
261
|
}
|
|
@@ -386,6 +421,51 @@ async function arenaList(ctx) {
|
|
|
386
421
|
* `arena endpoint create|delete|config` — the arena's own CRUD, and its configuration as
|
|
387
422
|
* one file (the same document `get_arena_config` returns and `set_arena_config` accepts).
|
|
388
423
|
*/
|
|
424
|
+
/**
|
|
425
|
+
* `arena file <arena-id>` prints the file the arena runs (the file itself, so `> f.json` round-trips);
|
|
426
|
+
* `arena file <arena-id> --file f.json | --patch '{…}' [--note …]` saves a new live version of the
|
|
427
|
+
* arena's own copy.
|
|
428
|
+
*/
|
|
429
|
+
async function arenaFile(ctx) {
|
|
430
|
+
const { client, flags, io } = ctx;
|
|
431
|
+
const saving =
|
|
432
|
+
flagStr(flags.file) !== undefined ||
|
|
433
|
+
flagStr(flags.patch) !== undefined ||
|
|
434
|
+
flagStr(flags["patch-file"]) !== undefined;
|
|
435
|
+
if (!saving) {
|
|
436
|
+
const result = await client.callTool(
|
|
437
|
+
"get_arena_file",
|
|
438
|
+
argsFromFlags(ARGS.get_arena_file, flags, ctx.positionals)
|
|
439
|
+
);
|
|
440
|
+
printResult(io, flags, result, (r) => JSON.stringify(r?.file ?? r, null, 2));
|
|
441
|
+
return 0;
|
|
442
|
+
}
|
|
443
|
+
const result = await client.callTool(
|
|
444
|
+
"save_arena_file",
|
|
445
|
+
argsFromFlags(ARGS.save_arena_file, flags, ctx.positionals)
|
|
446
|
+
);
|
|
447
|
+
ok(ctx, `Saved ${toolField(result, "version")?.label ?? "a new version"}; the arena runs it from now on.`);
|
|
448
|
+
printResult(io, flags, result);
|
|
449
|
+
return 0;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** `arena offer <arena-id>`: what our newer version would change; `--accept` takes it. */
|
|
453
|
+
async function arenaOffer(ctx) {
|
|
454
|
+
const { client, flags, io } = ctx;
|
|
455
|
+
const result = await client.callTool("get_arena_file", argsFromFlags(ARGS.get_arena_file, flags, ctx.positionals));
|
|
456
|
+
const offer = toolField(result, "offer");
|
|
457
|
+
if (!offer) ok(ctx, "No offer: your arena runs our latest version, or a copy based on it.");
|
|
458
|
+
printResult(io, flags, result, (r) => JSON.stringify(r?.offer ?? null, null, 2));
|
|
459
|
+
return 0;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** One arena-file tool, its arguments from ARGS. */
|
|
463
|
+
async function arenaTool(ctx, tool) {
|
|
464
|
+
const result = await ctx.client.callTool(tool, argsFromFlags(ARGS[tool], ctx.flags, ctx.positionals));
|
|
465
|
+
printResult(ctx.io, ctx.flags, result);
|
|
466
|
+
return 0;
|
|
467
|
+
}
|
|
468
|
+
|
|
389
469
|
async function arenaEndpoint(ctx) {
|
|
390
470
|
const { client, positionals, flags, io } = ctx;
|
|
391
471
|
const action = positionals[0];
|