@letterstory/cli 0.137.0 → 0.139.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 +22 -2
- package/lib/commands/insights.mjs +8 -2
- package/lib/commands/repo.mjs +34 -2
- package/lib/commands/sim.mjs +285 -0
- package/lib/commands.mjs +1 -0
- package/package.json +1 -1
package/lib/cli.mjs
CHANGED
|
@@ -60,10 +60,11 @@ import {
|
|
|
60
60
|
cmdCampaigns,
|
|
61
61
|
cmdTargets,
|
|
62
62
|
cmdVisits,
|
|
63
|
+
cmdSim,
|
|
63
64
|
} from "./commands.mjs";
|
|
64
65
|
|
|
65
66
|
// Keep in sync with cli/package.json.
|
|
66
|
-
export const VERSION = "0.
|
|
67
|
+
export const VERSION = "0.139.0";
|
|
67
68
|
|
|
68
69
|
// Flags that never take a value. Listing them explicitly means `deploy get --json <id>`
|
|
69
70
|
// can't accidentally swallow the id as --json's value. Every `type: "bool"` flag (and its
|
|
@@ -71,6 +72,9 @@ export const VERSION = "0.137.0";
|
|
|
71
72
|
export const BOOLEAN_FLAGS = new Set([
|
|
72
73
|
"remap",
|
|
73
74
|
"json",
|
|
75
|
+
// sim seed: make the new seed active; collections it doesn't cover start empty.
|
|
76
|
+
"activate",
|
|
77
|
+
"empty",
|
|
74
78
|
// admin models runs: only the starred ones.
|
|
75
79
|
"starred",
|
|
76
80
|
// shredder file / arena file: save the version as a candidate beside live.
|
|
@@ -1329,6 +1333,7 @@ Insights:
|
|
|
1329
1333
|
insights reads [--period …] [--site all|own|phantoms|<key>]... [--path <path>] [--engine <operator>]
|
|
1330
1334
|
Measure → Reads: AI engines and people reading your sites, counted one way
|
|
1331
1335
|
insights questions [--period 14d|30d|90d] Measure → Questions: every question we ask, named / share / change / cited
|
|
1336
|
+
insights pages [--period 14d|30d|90d] [--site own|phantoms|<key>] Measure → Pages: your pages AI answers cited, won questions first
|
|
1332
1337
|
insights competitors [--period 14d|30d|90d] GEO → Competitors: share of mentions per brand, is it moving, by engine/question
|
|
1333
1338
|
insights question <key> [--period 14d|30d|90d] [--filter all|named_you|rival_not_you|no_one] [--limit n]
|
|
1334
1339
|
One By-question row: the wordings asked and the AI answers
|
|
@@ -1401,6 +1406,9 @@ Repo changes (Claude Code edits your repo in a sandbox; the Letterstory GitHub A
|
|
|
1401
1406
|
repo install-letterprove --repo <owner/name> [--branch <base>] [--action build|build_pr|build_merge|slack|linear] [--wait]
|
|
1402
1407
|
One PR adding Letterprove's script and identify calls
|
|
1403
1408
|
(pick the repo where users sign in)
|
|
1409
|
+
repo install-visitors [--site <id>] [--repo <owner/name>] [--branch <base>] [--action build|build_pr|build_merge|slack|linear] [--wait]
|
|
1410
|
+
One PR adding the Visitors script tag to the repo that
|
|
1411
|
+
builds the site (--repo: another repo)
|
|
1404
1412
|
repo sites [--detect [--force]] Which website each repo builds, and the evidence
|
|
1405
1413
|
repo set-site --repo <owner/name> --site <host> [--not] [--branch <name>|none]
|
|
1406
1414
|
Say which site a repo builds (--not: correct a wrong label)
|
|
@@ -1414,7 +1422,7 @@ Repo changes (Claude Code edits your repo in a sandbox; the Letterstory GitHub A
|
|
|
1414
1422
|
repo status <run-id> Phase, spend, PR link and Claude's summary
|
|
1415
1423
|
repo activity <run-id> [--follow] What the sandbox is doing, line by line (--follow: until it stops)
|
|
1416
1424
|
repo list [--status running|succeeded|no_changes|failed|cancelled] [--limit n] [--audit <id>]
|
|
1417
|
-
[--source site_audit_issue|site_quick_check|letterprove_install]
|
|
1425
|
+
[--source site_audit_issue|site_quick_check|letterprove_install|visitors_install]
|
|
1418
1426
|
repo publish <run-id> Make PR on a preview in review
|
|
1419
1427
|
repo revise <run-id> --instructions <text>|--instructions-file <path|->
|
|
1420
1428
|
Ask for more on a preview; it takes over the preview's link
|
|
@@ -1436,6 +1444,17 @@ Visits (Visitors: AI agents reading your site, and AI answers sending people to
|
|
|
1436
1444
|
visits pages [--since 28d] [--path '/docs/*'] [--agent <id>] [--limit <n>] [--site <id>]
|
|
1437
1445
|
People and AI agents per page
|
|
1438
1446
|
|
|
1447
|
+
Sim (Simulations: a free, simulated copy of your API for agents, and what they did with it):
|
|
1448
|
+
sim [list] Your simulations and their URLs
|
|
1449
|
+
sim create --name <n> (--spec-url <url>|--spec-file <path>) [--slug <s>] [--upstream-url <url> --upstream-credential <c>]
|
|
1450
|
+
sim show <id> Resources, their serving rules, and who set them
|
|
1451
|
+
sim rule <id> <resource> synthesize|forward|sink|refuse|inferred [--note <why>]
|
|
1452
|
+
sim pause <id> | sim resume <id>
|
|
1453
|
+
sim key <id> [--agent <a>] [--model <m>] [--goal <g>] Mint a key for your own agents or CI
|
|
1454
|
+
sim sessions <id> [--limit N] Agent sessions, newest first
|
|
1455
|
+
sim session <session-id> One session, call by call
|
|
1456
|
+
sim report <id> [--since 30d] Funnel, missing operations, argument errors, loops, by agent
|
|
1457
|
+
|
|
1439
1458
|
Evals (how the pipeline is doing on your posts, and the regression cases captured from them):
|
|
1440
1459
|
evals scoreboard [--collection <id>] [--days N] Every measure by stage: posts, failing, rate
|
|
1441
1460
|
evals cases [--status open|guarding|proposed|retired|rejected] [--stage research|brief|links|mentions|accuracy|pipeline|shred]
|
|
@@ -1533,6 +1552,7 @@ const CLIENT_COMMANDS = {
|
|
|
1533
1552
|
site: cmdSite,
|
|
1534
1553
|
evals: cmdEvals,
|
|
1535
1554
|
visits: cmdVisits,
|
|
1555
|
+
sim: cmdSim,
|
|
1536
1556
|
// Platform-scoped (staff key) — the only commands that act BEFORE an org exists.
|
|
1537
1557
|
admin: cmdAdmin,
|
|
1538
1558
|
};
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// network, single-post, and top-posts levels (ported from phantomstory-cli's
|
|
3
3
|
// insights.ts); plus Measure's Overview (`summary`), GEO → Competitors
|
|
4
4
|
// (`competitors`), AI Visibility Bank (`bank`) and phantom Page Access (`access`);
|
|
5
|
-
// Measure → Reads (`reads`)
|
|
5
|
+
// Measure → Reads (`reads`), Questions (`questions`) and Pages (`pages`), and the live feed of what
|
|
6
6
|
// is reading the phantom sites (`activity`) — the same reads the web app renders.
|
|
7
7
|
|
|
8
8
|
import { CliError } from "../client.mjs";
|
|
@@ -38,6 +38,10 @@ export const ARGS = {
|
|
|
38
38
|
list_measure_questions: {
|
|
39
39
|
period: { flag: "period", enum: PERIODS },
|
|
40
40
|
},
|
|
41
|
+
list_cited_pages: {
|
|
42
|
+
period: { flag: "period", enum: PERIODS },
|
|
43
|
+
sites: { flag: "site", type: "list" },
|
|
44
|
+
},
|
|
41
45
|
get_recent_activity: {
|
|
42
46
|
phantom_id: { flag: "phantom" },
|
|
43
47
|
limit: { flag: "limit", type: "num" },
|
|
@@ -76,11 +80,13 @@ export async function cmdInsights(ctx) {
|
|
|
76
80
|
return call(rest, "get_reads");
|
|
77
81
|
case "questions":
|
|
78
82
|
return call(rest, "list_measure_questions");
|
|
83
|
+
case "pages":
|
|
84
|
+
return call(rest, "list_cited_pages");
|
|
79
85
|
case "activity":
|
|
80
86
|
return call(rest, "get_recent_activity");
|
|
81
87
|
default:
|
|
82
88
|
throw new CliError(
|
|
83
|
-
`Unknown insights subcommand: ${sub ?? "(none)"}. Try: summary, reads, questions, competitors, question, detection, activity, site, post, top, bank, access`
|
|
89
|
+
`Unknown insights subcommand: ${sub ?? "(none)"}. Try: summary, reads, questions, pages, competitors, question, detection, activity, site, post, top, bank, access`
|
|
84
90
|
);
|
|
85
91
|
}
|
|
86
92
|
}
|
package/lib/commands/repo.mjs
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// repo repos → list_github_app_repos repo branches → list_repo_branches
|
|
9
9
|
// repo prs → list_open_pull_requests (every open PR, whoever opened it)
|
|
10
10
|
// repo install-letterprove → letterprove_open_install_pr
|
|
11
|
+
// repo install-visitors → visitors_open_install_pr
|
|
11
12
|
// repo secrets → list_repo_preview_secrets repo set-secret → set_repo_preview_secret
|
|
12
13
|
// repo delete-secret → delete_repo_preview_secret
|
|
13
14
|
// repo suggestions → list_suggested_fixes
|
|
@@ -117,6 +118,13 @@ const ARGS = {
|
|
|
117
118
|
base_branch: "branch",
|
|
118
119
|
action: { flag: "action", enum: ["build", "build_pr", "build_merge", "slack", "linear"] },
|
|
119
120
|
},
|
|
121
|
+
visitors_open_install_pr: {
|
|
122
|
+
site_id: "site",
|
|
123
|
+
repo_owner: { value: (flags) => splitRepo(flags).owner },
|
|
124
|
+
repo_name: { value: (flags) => splitRepo(flags).name },
|
|
125
|
+
base_branch: "branch",
|
|
126
|
+
action: { flag: "action", enum: ["build", "build_pr", "build_merge", "slack", "linear"] },
|
|
127
|
+
},
|
|
120
128
|
get_repo_change_settings: {},
|
|
121
129
|
list_suggested_fixes: {},
|
|
122
130
|
list_slack_fix_targets: {},
|
|
@@ -187,7 +195,13 @@ const ARGS = {
|
|
|
187
195
|
audit_id: "audit",
|
|
188
196
|
source_kind: {
|
|
189
197
|
flag: "source",
|
|
190
|
-
enum: [
|
|
198
|
+
enum: [
|
|
199
|
+
"site_audit_issue",
|
|
200
|
+
"site_quick_check",
|
|
201
|
+
"letterprove_install",
|
|
202
|
+
"visitors_install",
|
|
203
|
+
"collection_publish",
|
|
204
|
+
],
|
|
191
205
|
},
|
|
192
206
|
site_host: "site",
|
|
193
207
|
},
|
|
@@ -247,6 +261,8 @@ export async function cmdRepo(ctx) {
|
|
|
247
261
|
return repoFixAudit(rest);
|
|
248
262
|
case "install-letterprove":
|
|
249
263
|
return repoInstallLetterprove(rest);
|
|
264
|
+
case "install-visitors":
|
|
265
|
+
return repoInstallVisitors(rest);
|
|
250
266
|
case "sites":
|
|
251
267
|
return repoSites(rest);
|
|
252
268
|
case "set-site":
|
|
@@ -283,7 +299,7 @@ export async function cmdRepo(ctx) {
|
|
|
283
299
|
return repoSlackTest(rest);
|
|
284
300
|
default:
|
|
285
301
|
throw new CliError(
|
|
286
|
-
`Unknown repo subcommand: ${sub ?? "(none)"}. Try: start, fix-audit, install-letterprove, sites, set-site, reset-site, secrets, set-secret, delete-secret, status, list, publish, cancel, repos, branches, prs, settings, suggestions, slack-targets, slack-test`
|
|
302
|
+
`Unknown repo subcommand: ${sub ?? "(none)"}. Try: start, fix-audit, install-letterprove, install-visitors, sites, set-site, reset-site, secrets, set-secret, delete-secret, status, list, publish, cancel, repos, branches, prs, settings, suggestions, slack-targets, slack-test`
|
|
287
303
|
);
|
|
288
304
|
}
|
|
289
305
|
}
|
|
@@ -434,6 +450,22 @@ async function repoInstallLetterprove(ctx) {
|
|
|
434
450
|
return followRun(ctx, result.run);
|
|
435
451
|
}
|
|
436
452
|
|
|
453
|
+
/** `repo install-visitors [--site id] [--repo owner/name] [--branch b] [--wait]`: the Visitors script tag → one PR. */
|
|
454
|
+
async function repoInstallVisitors(ctx) {
|
|
455
|
+
const { client, flags, io } = ctx;
|
|
456
|
+
refuseUnknownFlags(ctx, {}, ["site", "repo", "owner", "name", "branch", "action", "wait"]);
|
|
457
|
+
const result = await client.callTool(
|
|
458
|
+
"visitors_open_install_pr",
|
|
459
|
+
argsFromFlags(ARGS.visitors_open_install_pr, flags, ctx.positionals)
|
|
460
|
+
);
|
|
461
|
+
ok(ctx, result.message);
|
|
462
|
+
if (!flags.wait) {
|
|
463
|
+
printResult(io, flags, result.run, describe);
|
|
464
|
+
return 0;
|
|
465
|
+
}
|
|
466
|
+
return followRun(ctx, result.run);
|
|
467
|
+
}
|
|
468
|
+
|
|
437
469
|
/** `repo sites [--detect] [--force]`: which website each repo builds, and why. */
|
|
438
470
|
async function repoSites(ctx) {
|
|
439
471
|
const { client, flags, io } = ctx;
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
// `sim` — Simulations from a terminal: a free, simulated copy of your API that agents can
|
|
2
|
+
// use, and what they did with it. Thin wrappers over the simulation tools (create_simulation,
|
|
3
|
+
// get_simulation, update_simulation, mint_simulation_key, list_simulation_sessions,
|
|
4
|
+
// get_simulation_session, get_simulation_report), the same lib functions the app calls.
|
|
5
|
+
//
|
|
6
|
+
// letterstory sim create --name "Attio API" --spec-url https://api.attio.com/openapi/api
|
|
7
|
+
// letterstory sim show <id>
|
|
8
|
+
// letterstory sim rule <id> /v2/webhooks sink --note "never call real endpoints"
|
|
9
|
+
// letterstory sim create --name "Letterstory" --tools-file tools.json --tool-route "/api/integrations/tools/{name}"
|
|
10
|
+
// letterstory sim key <id> --agent our-ci --goal "nightly integration test"
|
|
11
|
+
// letterstory sim seed <id> --prompt "a 3-person content team with two blogs" --activate
|
|
12
|
+
// letterstory sim seed <id> --use <seed-id|none>
|
|
13
|
+
// letterstory sim report <id> --since 7d
|
|
14
|
+
// letterstory sim sessions <id>
|
|
15
|
+
// letterstory sim session <session-id>
|
|
16
|
+
|
|
17
|
+
import { readFileSync } from "node:fs";
|
|
18
|
+
import { CliError } from "../client.mjs";
|
|
19
|
+
import { flagStr, printResult, argsFromFlags, refuseUnknownFlags } from "./shared.mjs";
|
|
20
|
+
import { sinceDays } from "./visits.mjs";
|
|
21
|
+
|
|
22
|
+
const RULES = ["synthesize", "forward", "sink", "refuse", "inferred"];
|
|
23
|
+
const simulationId = { positional: 0, label: "simulation-id", required: true };
|
|
24
|
+
|
|
25
|
+
export const ARGS = {
|
|
26
|
+
create_simulation: {
|
|
27
|
+
name: { flag: "name", required: true },
|
|
28
|
+
slug: "slug",
|
|
29
|
+
spec_url: "spec-url",
|
|
30
|
+
spec: {
|
|
31
|
+
value: (flags) => {
|
|
32
|
+
const path = flagStr(flags["spec-file"]);
|
|
33
|
+
if (!path) return undefined;
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
36
|
+
} catch {
|
|
37
|
+
throw new CliError(`--spec-file must be a JSON OpenAPI document (could not parse ${path})`);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
offering_id: "offering",
|
|
42
|
+
upstream_base_url: "upstream-url",
|
|
43
|
+
upstream_credential: "upstream-credential",
|
|
44
|
+
mcp_tools: {
|
|
45
|
+
value: (flags) => {
|
|
46
|
+
const path = flagStr(flags["tools-file"]);
|
|
47
|
+
if (!path) return undefined;
|
|
48
|
+
let parsed;
|
|
49
|
+
try {
|
|
50
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
51
|
+
} catch {
|
|
52
|
+
throw new CliError(
|
|
53
|
+
`--tools-file must be JSON: a tools/list result or its tools array (could not parse ${path})`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
return Array.isArray(parsed) ? parsed : (parsed?.result?.tools ?? parsed?.tools);
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
tool_route: "tool-route",
|
|
60
|
+
auth_header: "auth-header",
|
|
61
|
+
},
|
|
62
|
+
list_simulations: {},
|
|
63
|
+
get_simulation: { simulation_id: simulationId },
|
|
64
|
+
update_simulation: {
|
|
65
|
+
simulation_id: simulationId,
|
|
66
|
+
status: { flag: "status", enum: ["live", "paused"] },
|
|
67
|
+
active_seed_id: {
|
|
68
|
+
value: (flags) => {
|
|
69
|
+
const v = flagStr(flags.use);
|
|
70
|
+
return v === undefined ? undefined : v === "none" ? null : v;
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
resource_rules: {
|
|
74
|
+
value: (flags, positionals) => {
|
|
75
|
+
if (positionals.length < 3) return undefined;
|
|
76
|
+
const rule = positionals[2];
|
|
77
|
+
if (!RULES.includes(rule)) throw new CliError(`rule must be one of: ${RULES.join(", ")}`);
|
|
78
|
+
return [
|
|
79
|
+
{ resource: positionals[1], rule: rule === "inferred" ? null : rule, note: flagStr(flags.note) },
|
|
80
|
+
];
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
generate_simulation_seed: {
|
|
85
|
+
simulation_id: simulationId,
|
|
86
|
+
prompt: { flag: "prompt", required: true },
|
|
87
|
+
resources: { flag: "resources", type: "list" },
|
|
88
|
+
items_per_collection: { flag: "items", type: "num" },
|
|
89
|
+
empty_workspace: { flag: "empty", type: "bool" },
|
|
90
|
+
activate: { flag: "activate", type: "bool" },
|
|
91
|
+
},
|
|
92
|
+
mint_simulation_key: { simulation_id: simulationId, agent: "agent", model: "model", goal: "goal" },
|
|
93
|
+
list_simulation_sessions: { simulation_id: simulationId, limit: { flag: "limit", type: "num" } },
|
|
94
|
+
get_simulation_session: { session_id: { positional: 0, label: "session-id", required: true } },
|
|
95
|
+
get_simulation_report: { simulation_id: simulationId, days: { value: (flags) => sinceDays(flags.since) } },
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
function call(ctx, tool) {
|
|
99
|
+
return ctx.client.callTool(tool, argsFromFlags(ARGS[tool], ctx.flags, ctx.positionals));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function cmdSim(ctx) {
|
|
103
|
+
const sub = ctx.positionals[0];
|
|
104
|
+
const rest = { ...ctx, positionals: ctx.positionals.slice(1) };
|
|
105
|
+
switch (sub) {
|
|
106
|
+
case undefined:
|
|
107
|
+
case "list":
|
|
108
|
+
case "ls":
|
|
109
|
+
return simList(rest);
|
|
110
|
+
case "create":
|
|
111
|
+
refuseUnknownFlags(rest, ARGS.create_simulation, ["spec-file", "tools-file"]);
|
|
112
|
+
return simShow(rest, await call(rest, "create_simulation"));
|
|
113
|
+
case "show":
|
|
114
|
+
return simShow(rest, await call(rest, "get_simulation"));
|
|
115
|
+
case "rule":
|
|
116
|
+
case "pause":
|
|
117
|
+
case "resume": {
|
|
118
|
+
refuseUnknownFlags(rest, ARGS.update_simulation, ["note"]);
|
|
119
|
+
if (sub !== "rule") rest.flags = { ...rest.flags, status: sub === "pause" ? "paused" : "live" };
|
|
120
|
+
return simShow(rest, await call(rest, "update_simulation"));
|
|
121
|
+
}
|
|
122
|
+
case "seed":
|
|
123
|
+
return simSeed(rest);
|
|
124
|
+
case "key":
|
|
125
|
+
return simKey(rest);
|
|
126
|
+
case "sessions":
|
|
127
|
+
return simSessions(rest);
|
|
128
|
+
case "session":
|
|
129
|
+
return simSession(rest);
|
|
130
|
+
case "report":
|
|
131
|
+
return simReport(rest);
|
|
132
|
+
default:
|
|
133
|
+
throw new CliError("Usage: sim [list|create|show|rule|pause|resume|seed|key|sessions|session|report]");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function simList(ctx) {
|
|
138
|
+
const result = await call(ctx, "list_simulations");
|
|
139
|
+
printResult(ctx.io, ctx.flags, result, (r) => {
|
|
140
|
+
if (!r.simulations?.length)
|
|
141
|
+
return "No simulations yet. Create one: sim create --name <name> --spec-url <openapi url>";
|
|
142
|
+
return r.simulations
|
|
143
|
+
.map(
|
|
144
|
+
(s) =>
|
|
145
|
+
`${s.id} ${s.name.padEnd(24)} ${s.status.padEnd(7)} ${String(s.sessions_7d).padStart(4)} sessions/7d ${s.urls.base}`
|
|
146
|
+
)
|
|
147
|
+
.join("\n");
|
|
148
|
+
});
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function simShow(ctx, result) {
|
|
153
|
+
printResult(ctx.io, ctx.flags, result, (r) => {
|
|
154
|
+
const s = r.simulation;
|
|
155
|
+
const lines = [
|
|
156
|
+
`${s.name} (${s.title} v${s.version}) — ${s.status}, ${s.operations} operations`,
|
|
157
|
+
` REST ${s.urls.base}`,
|
|
158
|
+
` MCP ${s.urls.mcp}`,
|
|
159
|
+
` Docs ${s.urls.llms}`,
|
|
160
|
+
"",
|
|
161
|
+
"Resources (rule · side effect · who set it):",
|
|
162
|
+
];
|
|
163
|
+
for (const res of r.resources ?? []) {
|
|
164
|
+
lines.push(` ${res.rule.padEnd(10)} ${res.side_effect.padEnd(9)} ${res.set_by.padEnd(8)} ${res.resource}`);
|
|
165
|
+
}
|
|
166
|
+
return lines.join("\n");
|
|
167
|
+
});
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** `sim seed <id> --prompt …` writes a seed; `sim seed <id> --use <seed-id|none>` switches the active one. */
|
|
172
|
+
async function simSeed(ctx) {
|
|
173
|
+
if (ctx.flags.use !== undefined) {
|
|
174
|
+
refuseUnknownFlags(ctx, ARGS.update_simulation, ["use"]);
|
|
175
|
+
return simShow(ctx, await call(ctx, "update_simulation"));
|
|
176
|
+
}
|
|
177
|
+
const result = await call(ctx, "generate_simulation_seed");
|
|
178
|
+
printResult(ctx.io, ctx.flags, result, (r) => {
|
|
179
|
+
const s = r.seed;
|
|
180
|
+
const rep = s.report;
|
|
181
|
+
return [
|
|
182
|
+
`Seed v${s.version} (${s.id})${s.active ? " — active" : ""}, by ${s.model ?? "upload"}`,
|
|
183
|
+
...Object.entries(s.items).map(([k, n]) => ` ${k}: ${n} items`),
|
|
184
|
+
`Kept ${rep.kept}, completed ${rep.filled}, dropped ${rep.dropped}, dangling ids ${rep.dangling.length}`,
|
|
185
|
+
s.active ? "" : `Make it active: sim seed <simulation-id> --use ${s.id}`,
|
|
186
|
+
]
|
|
187
|
+
.filter(Boolean)
|
|
188
|
+
.join("\n");
|
|
189
|
+
});
|
|
190
|
+
return 0;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function simKey(ctx) {
|
|
194
|
+
const result = await call(ctx, "mint_simulation_key");
|
|
195
|
+
printResult(ctx.io, ctx.flags, result, (r) =>
|
|
196
|
+
[
|
|
197
|
+
`Key (shown once): ${r.key}`,
|
|
198
|
+
`Expires: ${r.expires_at}`,
|
|
199
|
+
`Base URL: ${r.urls.base}`,
|
|
200
|
+
`Session: ${r.session_id}`,
|
|
201
|
+
].join("\n")
|
|
202
|
+
);
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function simSessions(ctx) {
|
|
207
|
+
const result = await call(ctx, "list_simulation_sessions");
|
|
208
|
+
printResult(ctx.io, ctx.flags, result, (r) => {
|
|
209
|
+
if (!r.sessions?.length) return "No sessions yet.";
|
|
210
|
+
return r.sessions
|
|
211
|
+
.map(
|
|
212
|
+
(s) =>
|
|
213
|
+
`${s.id} ${String(s.calls).padStart(4)} calls ${(s.agent ?? s.client ?? s.requester_agent ?? "unknown").padEnd(22)} ${s.goal ?? ""}`
|
|
214
|
+
)
|
|
215
|
+
.join("\n");
|
|
216
|
+
});
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function simSession(ctx) {
|
|
221
|
+
const result = await call(ctx, "get_simulation_session");
|
|
222
|
+
printResult(ctx.io, ctx.flags, result, (r) => {
|
|
223
|
+
const lines = [
|
|
224
|
+
`Session ${r.session.id} — ${r.session.agent ?? r.session.client ?? "unknown agent"}, ${r.calls.length} events`,
|
|
225
|
+
];
|
|
226
|
+
for (const c of r.calls) {
|
|
227
|
+
if (c.kind === "feedback") {
|
|
228
|
+
lines.push(` ${String(c.seq).padStart(3)} feedback: ${c.note}`);
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
const errs =
|
|
232
|
+
Array.isArray(c.field_errors) && c.field_errors.length
|
|
233
|
+
? ` ${c.field_errors.map((e) => `${e.path || "body"}:${e.code}`).join(", ")}`
|
|
234
|
+
: "";
|
|
235
|
+
lines.push(
|
|
236
|
+
` ${String(c.seq).padStart(3)} ${String(c.status).padEnd(4)}${(c.served ?? "").padEnd(12)} ${c.method} ${c.path}${errs}`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
return lines.join("\n");
|
|
240
|
+
});
|
|
241
|
+
return 0;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function simReport(ctx) {
|
|
245
|
+
const result = await call(ctx, "get_simulation_report");
|
|
246
|
+
printResult(ctx.io, ctx.flags, result, ({ report: r }) => {
|
|
247
|
+
const f = r.funnel;
|
|
248
|
+
const lines = [
|
|
249
|
+
`${r.sessions} sessions, ${r.calls} calls in ${r.window_days} days`,
|
|
250
|
+
`Funnel: ${f.minted} keys → ${f.called} called → ${f.succeeded} succeeded → ${f.wrote} wrote`,
|
|
251
|
+
`First success: median ${r.first_success.median_calls ?? "–"} calls, ${r.first_success.median_seconds ?? "–"}s`,
|
|
252
|
+
];
|
|
253
|
+
if (r.hallucinated.length) {
|
|
254
|
+
lines.push("", "Expected but missing:");
|
|
255
|
+
for (const h of r.hallucinated.slice(0, 10))
|
|
256
|
+
lines.push(
|
|
257
|
+
` ${String(h.sessions).padStart(3)} sessions ${h.method} ${h.path}${h.nearest_operation ? ` (nearest: ${h.nearest_operation})` : ""}`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
if (r.field_errors.length) {
|
|
261
|
+
lines.push("", "Argument errors:");
|
|
262
|
+
for (const e of r.field_errors.slice(0, 10))
|
|
263
|
+
lines.push(` ${String(e.count).padStart(3)}× ${e.operation_id} ${e.field} (${e.code})`);
|
|
264
|
+
}
|
|
265
|
+
if (r.error_loops.length) {
|
|
266
|
+
lines.push("", "Error loops:");
|
|
267
|
+
for (const l of r.error_loops)
|
|
268
|
+
lines.push(` ${l.operation_id}: ${l.sessions} sessions, up to ${l.longest} in a row`);
|
|
269
|
+
}
|
|
270
|
+
if (r.by_agent.length) {
|
|
271
|
+
lines.push("", "By agent:");
|
|
272
|
+
for (const a of r.by_agent)
|
|
273
|
+
lines.push(
|
|
274
|
+
` ${a.agent.padEnd(22)} ${a.sessions} sessions, ${a.calls} calls, ${Math.round(a.success_rate * 100)}% ok`
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
if (r.feedback.length) {
|
|
278
|
+
lines.push("", "Feedback:");
|
|
279
|
+
for (const fb of r.feedback.slice(-5)) lines.push(` “${fb.note.slice(0, 200)}”`);
|
|
280
|
+
}
|
|
281
|
+
lines.push("", `Unused: ${r.unused.count} of ${r.unused.total} operations`);
|
|
282
|
+
return lines.join("\n");
|
|
283
|
+
});
|
|
284
|
+
return 0;
|
|
285
|
+
}
|
package/lib/commands.mjs
CHANGED