@zenera/cli 1.1.24 → 1.1.25
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 +53 -0
- package/dist/ask.js +28 -5
- package/dist/banner.js +2 -2
- package/dist/batch.d.ts +6 -0
- package/dist/batch.js +76 -1
- package/dist/catalog.d.ts +11 -1
- package/dist/catalog.js +80 -7
- package/dist/commands/inspect.d.ts +47 -1
- package/dist/commands/inspect.js +167 -18
- package/dist/commands/memory.js +128 -29
- package/dist/commands/meta.js +311 -21
- package/dist/commands/models.js +96 -12
- package/dist/commands/open.js +4 -4
- package/dist/commands/run.js +38 -4
- package/dist/commands/sandbox.js +76 -14
- package/dist/engine.d.ts +5 -0
- package/dist/engine.js +1 -0
- package/dist/eventlog.d.ts +59 -0
- package/dist/eventlog.js +146 -0
- package/dist/liveness.d.ts +7 -0
- package/dist/liveness.js +15 -1
- package/dist/meta/editor.d.ts +9 -0
- package/dist/meta/editor.js +60 -0
- package/dist/meta/finetune/children.d.ts +8 -0
- package/dist/meta/finetune/children.js +76 -0
- package/dist/meta/finetune/command.d.ts +30 -0
- package/dist/meta/finetune/command.js +338 -0
- package/dist/meta/finetune/dataset/anchors.d.ts +41 -0
- package/dist/meta/finetune/dataset/anchors.js +239 -0
- package/dist/meta/finetune/dataset/changes.d.ts +36 -0
- package/dist/meta/finetune/dataset/changes.js +319 -0
- package/dist/meta/finetune/dataset/command.d.ts +17 -0
- package/dist/meta/finetune/dataset/command.js +1011 -0
- package/dist/meta/finetune/dataset/drift.d.ts +33 -0
- package/dist/meta/finetune/dataset/drift.js +71 -0
- package/dist/meta/finetune/dataset/export.d.ts +26 -0
- package/dist/meta/finetune/dataset/export.js +51 -0
- package/dist/meta/finetune/dataset/index.d.ts +2 -0
- package/dist/meta/finetune/dataset/index.js +2 -0
- package/dist/meta/finetune/dataset/sample.d.ts +50 -0
- package/dist/meta/finetune/dataset/sample.js +197 -0
- package/dist/meta/finetune/dataset/schema.d.ts +33 -0
- package/dist/meta/finetune/dataset/schema.js +216 -0
- package/dist/meta/finetune/dataset/store.d.ts +35 -0
- package/dist/meta/finetune/dataset/store.js +177 -0
- package/dist/meta/finetune/dataset/types.d.ts +125 -0
- package/dist/meta/finetune/dataset/types.js +39 -0
- package/dist/meta/finetune/failure.d.ts +15 -0
- package/dist/meta/finetune/failure.js +26 -0
- package/dist/meta/finetune/feedback.d.ts +45 -0
- package/dist/meta/finetune/feedback.js +85 -0
- package/dist/meta/finetune/improvements.d.ts +51 -0
- package/dist/meta/finetune/improvements.js +244 -0
- package/dist/meta/finetune/index.d.ts +2 -0
- package/dist/meta/finetune/index.js +2 -0
- package/dist/meta/finetune/journal.d.ts +35 -0
- package/dist/meta/finetune/journal.js +135 -0
- package/dist/meta/finetune/live.d.ts +74 -0
- package/dist/meta/finetune/live.js +226 -0
- package/dist/meta/finetune/loop.d.ts +3 -0
- package/dist/meta/finetune/loop.js +114 -0
- package/dist/meta/finetune/memories.d.ts +47 -0
- package/dist/meta/finetune/memories.js +154 -0
- package/dist/meta/finetune/report.d.ts +33 -0
- package/dist/meta/finetune/report.js +1284 -0
- package/dist/meta/finetune/retry.d.ts +24 -0
- package/dist/meta/finetune/retry.js +96 -0
- package/dist/meta/finetune/sampler.d.ts +18 -0
- package/dist/meta/finetune/sampler.js +41 -0
- package/dist/meta/finetune/system.d.ts +27 -0
- package/dist/meta/finetune/system.js +132 -0
- package/dist/meta/finetune/train.d.ts +39 -0
- package/dist/meta/finetune/train.js +391 -0
- package/dist/meta/finetune/tui.d.ts +16 -0
- package/dist/meta/finetune/tui.js +291 -0
- package/dist/meta/finetune/tuning.d.ts +167 -0
- package/dist/meta/finetune/tuning.js +289 -0
- package/dist/meta/finetune/usage.d.ts +62 -0
- package/dist/meta/finetune/usage.js +225 -0
- package/dist/meta/host.d.ts +14 -0
- package/dist/meta/host.js +20 -0
- package/dist/meta/index.d.ts +11 -0
- package/dist/meta/index.js +11 -0
- package/dist/meta/launch.d.ts +81 -0
- package/dist/meta/launch.js +286 -0
- package/dist/meta/model.d.ts +44 -0
- package/dist/meta/model.js +119 -0
- package/dist/meta/prompts.d.ts +18 -0
- package/dist/meta/prompts.js +61 -0
- package/dist/meta/relay.d.ts +20 -0
- package/dist/meta/relay.js +123 -0
- package/dist/meta/tokens.d.ts +52 -0
- package/dist/meta/tokens.js +226 -0
- package/dist/meta/window.d.ts +61 -0
- package/dist/meta/window.js +198 -0
- package/dist/meta/wire.d.ts +42 -0
- package/dist/meta/wire.js +129 -0
- package/dist/modelref.d.ts +16 -0
- package/dist/modelref.js +41 -0
- package/dist/narrate.js +5 -2
- package/dist/podman.d.ts +26 -0
- package/dist/podman.js +83 -3
- package/dist/projects.d.ts +0 -5
- package/dist/projects.js +2 -15
- package/dist/sandbox.d.ts +2 -0
- package/dist/sandbox.js +10 -3
- package/dist/scaffold.d.ts +15 -6
- package/dist/scaffold.js +5 -60
- package/dist/session.js +4 -29
- package/dist/term.d.ts +2 -0
- package/dist/term.js +11 -2
- package/dist/trace.js +53 -2
- package/dist/tui/models.d.ts +33 -0
- package/dist/tui/models.js +389 -0
- package/dist/tui/wrap.d.ts +5 -0
- package/dist/tui/wrap.js +12 -0
- package/dist/usage.d.ts +78 -0
- package/dist/usage.js +51 -0
- package/package.json +3 -2
- package/templates/editor/.github/copilot-instructions.md +31 -102
- package/templates/editor/.github/prompts/analyze.prompt.md +31 -0
- package/templates/editor/.github/prompts/dataset.prompt.md +108 -0
- package/templates/editor/.github/prompts/finetune-apply.prompt.md +91 -0
- package/templates/editor/.github/prompts/finetune.prompt.md +691 -198
- package/templates/editor/.github/prompts/new-agent.prompt.md +3 -1
- package/templates/editor/.github/prompts/new-skill.prompt.md +2 -0
- package/templates/editor/.github/prompts/project-review.prompt.md +4 -3
- package/templates/editor/.github/skills/zen-analyze-run/SKILL.md +483 -0
- package/templates/editor/.github/skills/zen-cli/SKILL.md +9 -1
- package/templates/editor/.github/skills/zen-cli/references/dataset.md +34 -0
- package/templates/editor/.github/skills/zen-cli/references/finetune.md +77 -0
- package/templates/editor/.github/skills/zen-cli/references/inspect.md +47 -9
- package/templates/editor/.github/skills/zen-cli/references/memory.md +33 -10
- package/templates/editor/.github/skills/zen-cli/references/meta.md +61 -0
- package/templates/editor/.github/skills/zen-cli/references/models.md +5 -1
- package/templates/editor/.github/skills/zen-cli/references/rag.md +24 -6
- package/templates/editor/.github/skills/zen-cli/references/sandbox.md +10 -1
- package/templates/editor/.github/skills/zen-code-python/SKILL.md +123 -0
- package/templates/editor/.github/skills/zen-code-python/references/code_python.md +48 -0
- package/templates/editor/.github/skills/zen-dataset/SKILL.md +253 -0
- package/templates/editor/.github/skills/zen-finetune/SKILL.md +1449 -658
- package/templates/editor/.github/skills/zen-finetune/references/readme-template.md +268 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/batch.mjs +390 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/difficult.mjs +243 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/memory.mjs +280 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/next.mjs +398 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/report.mjs +797 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/select.mjs +354 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/usage.mjs +722 -0
- package/templates/editor/.github/skills/zen-inspect/SKILL.md +60 -25
- package/templates/editor/.github/skills/zen-inspect-ask/SKILL.md +270 -0
- package/templates/editor/.github/skills/zen-memory/SKILL.md +18 -14
- package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +20 -8
- package/templates/editor/.github/skills/zen-review/SKILL.md +5 -3
- package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +5 -4
- package/templates/editor/.github/skills/zen-sandbox/SKILL.md +7 -3
- package/templates/editor/.github/skills/zen-sandbox-capacity/SKILL.md +2 -2
- package/templates/editor/.github/skills/zen-topology/SKILL.md +283 -0
- package/templates/editor/.vscode/settings.json +1 -0
- package/templates/project/agents/files-instructions.md +29 -20
- package/templates/project/agents/memory-instructions.md +31 -32
- package/templates/project/agents/tools-instructions.md +7 -4
- package/templates/project/gitignore +2 -0
- package/dist/meta.d.ts +0 -221
- package/dist/meta.js +0 -684
- package/templates/editor/.github/skills/zen-finetune/scripts/collect.sh +0 -432
- package/templates/editor/.github/skills/zen-finetune/scripts/sample.sh +0 -199
package/dist/commands/inspect.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { assertState, buildRunReport, FilePayloadStore, memoryDir, MemoryStore, PayloadResolver, projectRegistry, readProjectConfig, renderReportHtml, } from '@zenera/neo';
|
|
1
|
+
import { assertState, buildRunReport, FilePayloadStore, memoryDir, MemoryStore, PayloadResolver, projectRegistry, readProjectConfig, renderReportHtml, text, } from '@zenera/neo';
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
4
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
@@ -11,7 +11,10 @@ import { sessionIds } from "../projects.js";
|
|
|
11
11
|
import { project as resolveProject } from "../resolve.js";
|
|
12
12
|
import { display, listRuns, listSessions, newestRun, readRunMeta, requireSession, runPaths, runPathsAt, sessionPaths, } from "../session.js";
|
|
13
13
|
import { nodeDetail, parseNodeIds, traceIndex, traceMermaid, traceOf, } from "../trace.js";
|
|
14
|
-
import {
|
|
14
|
+
import { formatMarkdown } from "../tui/markdown.js";
|
|
15
|
+
import { boxWidth } from "../tui/wrap.js";
|
|
16
|
+
import { ago, bold, bytes, choose, cyan, dim, invalidError, isInteractive, json, note, pad, ask as prompt, readStdin, usageError, write, writeAll, yellow, } from "../term.js";
|
|
17
|
+
import { appendUsage } from "../usage.js";
|
|
15
18
|
const USAGE = 'zen inspect [report|graph|node|ask] [run] [--dir <run dir>] [--open]';
|
|
16
19
|
const SUBCOMMANDS = ['report', 'graph', 'node', 'ask'];
|
|
17
20
|
// ---------------------------------------------------------------------------
|
|
@@ -44,7 +47,8 @@ export const inspect = {
|
|
|
44
47
|
' report Build and print the path to report.html. Default.',
|
|
45
48
|
' graph The whole run as one Mermaid flowchart, on stdout.',
|
|
46
49
|
' node <id...> Those nodes of the graph in full. Ranges: n5..n9.',
|
|
47
|
-
' ask <id> <question>
|
|
50
|
+
' ask <id> <question> One answer on stdout, then exit. Never prompts.',
|
|
51
|
+
' ask [<id>] A conversation at a terminal. Prompts for the rest.',
|
|
48
52
|
'',
|
|
49
53
|
' --project <name|dir> Which project. Defaults to the one you are in.',
|
|
50
54
|
' --session <id> Which session. Defaults to the newest that ran.',
|
|
@@ -52,6 +56,7 @@ export const inspect = {
|
|
|
52
56
|
' --dir <dir> A run directory, as `zen run --json` reports it.',
|
|
53
57
|
' --memory <dir> Read this memory instead of the project’s.',
|
|
54
58
|
' --model <ref> Answer `ask` with this model instead of the run’s.',
|
|
59
|
+
' --question-file <path> Read the `ask` question from a file; `-` is stdin.',
|
|
55
60
|
' --part <name> Print this part of a node in full. Repeatable.',
|
|
56
61
|
' --full Print every part, the recorded `request` included.',
|
|
57
62
|
' --rebuild Build report.html again from the run state.',
|
|
@@ -59,8 +64,8 @@ export const inspect = {
|
|
|
59
64
|
' --no-timing Leave the clock off the graph.',
|
|
60
65
|
' --no-style Leave the colours off the graph. Shorter to read.',
|
|
61
66
|
'',
|
|
62
|
-
'With no
|
|
63
|
-
'each when there is nothing to ask on.',
|
|
67
|
+
'With no run named, report, graph and node ask which session and run at',
|
|
68
|
+
'a terminal, and take the newest of each when there is nothing to ask on.',
|
|
64
69
|
'',
|
|
65
70
|
'`graph` is written to be read by a model. Nodes are declared in the',
|
|
66
71
|
'order they happened, with ids `n1`, `n2`, …; every edge is collected in',
|
|
@@ -82,9 +87,27 @@ export const inspect = {
|
|
|
82
87
|
'`ask` replays one `llm_call` — its system prompt, its messages, its tool',
|
|
83
88
|
'schemas — to a model, with your question on the end and tool calling off.',
|
|
84
89
|
'It is told the run is over and that it may quote its own instructions, so',
|
|
85
|
-
'the answer names the prompt or skill behind the behaviour
|
|
90
|
+
'the answer names the prompt or skill behind the behaviour. It has two',
|
|
91
|
+
'modes, and which one is decided by whether the question is given:',
|
|
92
|
+
'',
|
|
93
|
+
'One-shot — a question on the line, in --question-file, or --json. For a',
|
|
94
|
+
'script or a model: no banner, no prompt, no picker; stdout is the answer',
|
|
95
|
+
'and nothing else. Name the run with --dir, --run or --session, or the',
|
|
96
|
+
'newest is taken.',
|
|
86
97
|
'',
|
|
87
98
|
' zen inspect ask n11 "why run python -c when the skill says npm test?"',
|
|
99
|
+
' zen inspect ask n11 --question-file q.md --dir <run dir>',
|
|
100
|
+
'',
|
|
101
|
+
'A question with quotes, backticks or several lines goes in a file, so no',
|
|
102
|
+
'shell ever parses it.',
|
|
103
|
+
'',
|
|
104
|
+
'Interactive — no question. For a person at a terminal: asks for whatever',
|
|
105
|
+
'is not named (session, run, LLM call), then the question, and keeps the',
|
|
106
|
+
'conversation going until an empty question. Without a terminal it is an',
|
|
107
|
+
'error, never a wait.',
|
|
108
|
+
'',
|
|
109
|
+
' zen inspect ask',
|
|
110
|
+
' zen inspect ask n11 --dir <run dir>',
|
|
88
111
|
'',
|
|
89
112
|
'All of it, at length: .github/skills/zen-cli/references/inspect.md',
|
|
90
113
|
],
|
|
@@ -97,6 +120,7 @@ export const inspect = {
|
|
|
97
120
|
memory: { type: 'string' },
|
|
98
121
|
model: { type: 'string' },
|
|
99
122
|
part: { type: 'string', multiple: true },
|
|
123
|
+
'question-file': { type: 'string' },
|
|
100
124
|
full: { type: 'boolean' },
|
|
101
125
|
open: { type: 'boolean' },
|
|
102
126
|
rebuild: { type: 'boolean' },
|
|
@@ -117,8 +141,13 @@ export const inspect = {
|
|
|
117
141
|
return await nodes(at, values, rest, ctx.json);
|
|
118
142
|
}
|
|
119
143
|
if (what === 'ask') {
|
|
120
|
-
|
|
121
|
-
|
|
144
|
+
// Given a question it is one-shot; only a bare `ask` converses.
|
|
145
|
+
const oneShot = ctx.json || values['question-file'] !== undefined || rest.length > 1;
|
|
146
|
+
if (!oneShot && !(asking && process.stdout.isTTY)) {
|
|
147
|
+
throw usageError('interactive `ask` needs a terminal; give the node id and a question', 'zen inspect ask n11 --question-file q.md --dir <run dir>');
|
|
148
|
+
}
|
|
149
|
+
const at = await locate(ctx.cwd, values, undefined, !oneShot);
|
|
150
|
+
return await ask(at, values, rest, ctx.cwd, ctx.json, !oneShot);
|
|
122
151
|
}
|
|
123
152
|
const at = await locate(ctx.cwd, values, rest[0], asking);
|
|
124
153
|
if (what === 'graph') {
|
|
@@ -245,14 +274,82 @@ export function wholeParts(found, values) {
|
|
|
245
274
|
* it. Everything it sees is what it saw at the time, so an answer naming the
|
|
246
275
|
* skill that steered it is checkable against the same node.
|
|
247
276
|
*/
|
|
248
|
-
|
|
277
|
+
export function replayableCalls(trace) {
|
|
278
|
+
return trace.entries.flatMap((entry) => entry.node.type === 'llm_call' && entry.node.request ? [{ entry, node: entry.node }] : []);
|
|
279
|
+
}
|
|
280
|
+
const ASK_BOX = { tl: '╭', tr: '╮', bl: '╰', br: '╯', h: '─', v: '│' };
|
|
281
|
+
function askPanel(title, text, outer, border) {
|
|
282
|
+
const inner = outer - 4;
|
|
283
|
+
const rule = ASK_BOX.h.repeat(Math.max(1, outer - title.length - 5));
|
|
284
|
+
return [
|
|
285
|
+
border(`${ASK_BOX.tl}${ASK_BOX.h} ${title} ${rule}${ASK_BOX.tr}`),
|
|
286
|
+
...formatMarkdown(text, inner).map((line) => `${border(ASK_BOX.v)} ${pad(line, inner)} ${border(ASK_BOX.v)}`),
|
|
287
|
+
border(`${ASK_BOX.bl}${ASK_BOX.h.repeat(outer - 2)}${ASK_BOX.br}`),
|
|
288
|
+
];
|
|
289
|
+
}
|
|
290
|
+
export function renderAskExchange(question, answer, columns = 80) {
|
|
291
|
+
const outer = boxWidth(`${question}\n\n${answer}`, columns);
|
|
292
|
+
return [
|
|
293
|
+
'',
|
|
294
|
+
...askPanel('Question', question, outer, cyan),
|
|
295
|
+
'',
|
|
296
|
+
...askPanel('Answer', answer, outer, dim),
|
|
297
|
+
'',
|
|
298
|
+
];
|
|
299
|
+
}
|
|
300
|
+
export async function repeatQuestions(initial, next, answer) {
|
|
301
|
+
let question = initial;
|
|
302
|
+
while (question) {
|
|
303
|
+
await answer(question);
|
|
304
|
+
question = (await next()).trim();
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
/** The question: the words on the line, or the whole of `--question-file` (`-` is stdin). */
|
|
308
|
+
export async function readQuestion(words, file, cwd) {
|
|
309
|
+
const typed = words.join(' ').trim();
|
|
310
|
+
if (file === undefined) {
|
|
311
|
+
return typed;
|
|
312
|
+
}
|
|
313
|
+
if (typed) {
|
|
314
|
+
throw usageError('give the question either as words or as --question-file, not both', 'zen inspect ask n11 --question-file q.md');
|
|
315
|
+
}
|
|
316
|
+
if (file === '-') {
|
|
317
|
+
return (await readStdin()) ?? '';
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
return (await readFile(resolve(cwd, file), 'utf8')).trim();
|
|
321
|
+
}
|
|
322
|
+
catch (err) {
|
|
323
|
+
throw invalidError(`cannot read question file ${file}: ${err.code ?? err}`, 'write the question to that path first');
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
async function ask(at, values, rest, cwd, asJson, interactive) {
|
|
249
327
|
const { project, session, run } = at;
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
if (!id
|
|
328
|
+
const trace = traceOf(await readState(run));
|
|
329
|
+
let [id, ...words] = rest;
|
|
330
|
+
if (!id && interactive) {
|
|
331
|
+
const calls = replayableCalls(trace);
|
|
332
|
+
if (calls.length === 0) {
|
|
333
|
+
throw invalidError(`run ${run.id} has no recorded LLM call to replay`, 'only runs that record requests can be asked about');
|
|
334
|
+
}
|
|
335
|
+
const entry = await choose('Which LLM call?', calls.map((candidate) => ({
|
|
336
|
+
label: `${candidate.entry.key} ${candidate.node.model}`,
|
|
337
|
+
detail: [
|
|
338
|
+
candidate.node.agent,
|
|
339
|
+
candidate.node.toolCalls.length
|
|
340
|
+
? `calls ${candidate.node.toolCalls.map((call) => call.name).join(', ')}`
|
|
341
|
+
: '',
|
|
342
|
+
candidate.entry.branch ? `branch ${candidate.entry.branch}` : '',
|
|
343
|
+
]
|
|
344
|
+
.filter(Boolean)
|
|
345
|
+
.join(' '),
|
|
346
|
+
value: candidate.entry,
|
|
347
|
+
})));
|
|
348
|
+
id = entry.key;
|
|
349
|
+
}
|
|
350
|
+
if (!id) {
|
|
253
351
|
throw usageError('ask takes one node id and a question', 'zen inspect ask n11 "why did you run python -c instead of the tests?"');
|
|
254
352
|
}
|
|
255
|
-
const trace = traceOf(await readState(run));
|
|
256
353
|
let wanted;
|
|
257
354
|
try {
|
|
258
355
|
wanted = parseNodeIds([id], trace.byKey);
|
|
@@ -272,6 +369,15 @@ async function ask(at, values, rest, asJson) {
|
|
|
272
369
|
throw invalidError(`${entry.key} did not record the request that produced it`, 'only a run made by this CLI records requests; an SDK run needs ' +
|
|
273
370
|
'`runner({ recordRequests: true })`');
|
|
274
371
|
}
|
|
372
|
+
const query = interactive
|
|
373
|
+
? await prompt('Question (empty to finish)?')
|
|
374
|
+
: await readQuestion(words, values['question-file'], cwd);
|
|
375
|
+
if (!query && interactive) {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
if (!query) {
|
|
379
|
+
throw usageError('ask takes one node id and a question', 'zen inspect ask n11 "why did you run python -c instead of the tests?"');
|
|
380
|
+
}
|
|
275
381
|
const payloads = new PayloadResolver(new FilePayloadStore({ dir: session.blobs, id: 'file' }));
|
|
276
382
|
// Deliberately not the preview fallback `node` uses: a truncated request is
|
|
277
383
|
// not the call that happened, and an answer about it would be about nothing.
|
|
@@ -292,8 +398,32 @@ async function ask(at, values, rest, asJson) {
|
|
|
292
398
|
'a second opinion, not the model looking at itself'));
|
|
293
399
|
}
|
|
294
400
|
const model = projectRegistry(config).model(picked.ref);
|
|
295
|
-
const
|
|
401
|
+
const record = async (call) => {
|
|
402
|
+
const startedAt = new Date();
|
|
403
|
+
const res = await call();
|
|
404
|
+
appendUsage(project, {
|
|
405
|
+
kind: 'ask',
|
|
406
|
+
ts: new Date().toISOString(),
|
|
407
|
+
runDir: run.dir,
|
|
408
|
+
node: entry.key,
|
|
409
|
+
nodeAgent: node.agent,
|
|
410
|
+
nodeModel: node.model,
|
|
411
|
+
model: picked.label,
|
|
412
|
+
usage: res.usage,
|
|
413
|
+
startedAt: startedAt.toISOString(),
|
|
414
|
+
durationMs: Date.now() - startedAt.getTime(),
|
|
415
|
+
});
|
|
416
|
+
return res;
|
|
417
|
+
};
|
|
418
|
+
const generate = (question) => record(() => model.generate(buildDiagnostic({ request: recorded, answer, toolCalls, query: question })));
|
|
419
|
+
const usage = (res) => {
|
|
420
|
+
note(dim(`${bold(entry.key)} ${picked.label}` +
|
|
421
|
+
(res.usage
|
|
422
|
+
? ` · ${res.usage.inputTokens} in, ${res.usage.outputTokens} out`
|
|
423
|
+
: '')));
|
|
424
|
+
};
|
|
296
425
|
if (asJson) {
|
|
426
|
+
const res = await generate(query);
|
|
297
427
|
json({
|
|
298
428
|
session: session.id,
|
|
299
429
|
run: run.id,
|
|
@@ -306,9 +436,28 @@ async function ask(at, values, rest, asJson) {
|
|
|
306
436
|
});
|
|
307
437
|
return;
|
|
308
438
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
439
|
+
if (!interactive) {
|
|
440
|
+
write((await generate(query)).text);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
let conversation;
|
|
444
|
+
await repeatQuestions(query, () => prompt('Question (empty to finish)?'), async (question) => {
|
|
445
|
+
if (!conversation) {
|
|
446
|
+
conversation = buildDiagnostic({
|
|
447
|
+
request: recorded,
|
|
448
|
+
answer,
|
|
449
|
+
toolCalls,
|
|
450
|
+
query: question,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
conversation.messages.push({ role: 'user', content: [text(question)] });
|
|
455
|
+
}
|
|
456
|
+
const res = await record(() => model.generate(conversation));
|
|
457
|
+
conversation.messages.push({ role: 'assistant', content: res.text });
|
|
458
|
+
writeAll(renderAskExchange(question, res.text, process.stdout.columns ?? 80));
|
|
459
|
+
usage(res);
|
|
460
|
+
});
|
|
312
461
|
}
|
|
313
462
|
/**
|
|
314
463
|
* A part name as it can be typed back. The tool-call parts are named
|
|
@@ -371,7 +520,7 @@ export function renderNode(node, whole = () => true) {
|
|
|
371
520
|
* it can be used again would be work for nothing. A person holds an id, or
|
|
372
521
|
* nothing at all and gets asked.
|
|
373
522
|
*/
|
|
374
|
-
async function locate(cwd, values, positional, asking) {
|
|
523
|
+
export async function locate(cwd, values, positional, asking) {
|
|
375
524
|
// A run id is a stamp and never has a separator in it, so a positional
|
|
376
525
|
// with one is unambiguously a path.
|
|
377
526
|
const asPath = positional?.includes('/') ? positional : undefined;
|
package/dist/commands/memory.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { ALL_AGENTS, buildMemoryReport, createEmbedder, GREP_FIELDS, grepMemory, hostPath, memoryDir, MemoryError, MemoryIndex, MemoryStore, MergeConflicts, mergeMemories, readProjectConfig, renderMemoryHtml, renderRecollection, } from '@zenera/neo';
|
|
1
|
+
import { ALL_AGENTS, buildMemoryReport, createEmbedder, diffMemories, GREP_FIELDS, grepMemory, hostPath, memoryDir, MemoryError, MemoryIndex, MemoryStore, MergeConflicts, mergeMemories, readProjectConfig, renderMemoryHtml, renderRecollection, } from '@zenera/neo';
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
4
|
import { writeFile } from 'node:fs/promises';
|
|
5
|
-
import { basename, isAbsolute, join,
|
|
5
|
+
import { basename, isAbsolute, join, resolve } from 'node:path';
|
|
6
6
|
import { parse } from "../args.js";
|
|
7
7
|
import { loadProjectEnv } from "../env.js";
|
|
8
8
|
import { KeyStore } from "../keys.js";
|
|
9
9
|
import { project as resolveProject } from "../resolve.js";
|
|
10
|
-
import { ago, bold, bytes, CliError, confirm, count, cyan, dim, EXIT, green, isInteractive, json, note, table, usageError, write, writeAll, yellow, } from "../term.js";
|
|
11
|
-
const USAGE = 'zen memory [stats|ls|search|grep|show|export|merge|forget] [args] [options]';
|
|
12
|
-
const SUBCOMMANDS = ['stats', 'ls', 'search', 'grep', 'show', 'export', 'merge', 'forget'];
|
|
10
|
+
import { ago, bold, bytes, CliError, confirm, count, cyan, dim, EXIT, green, isInteractive, json, note, plain, table, usageError, write, writeAll, yellow, } from "../term.js";
|
|
11
|
+
const USAGE = 'zen memory [stats|ls|search|grep|show|diff|export|merge|forget] [args] [options]';
|
|
12
|
+
const SUBCOMMANDS = ['stats', 'ls', 'search', 'grep', 'show', 'diff', 'export', 'merge', 'forget'];
|
|
13
13
|
/** Enough to see the shape of it; the rest is a number. */
|
|
14
14
|
const LISTED = 30;
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
@@ -42,6 +42,7 @@ export const memory = {
|
|
|
42
42
|
' search <text> Recall it, the way an agent does. Ranked.',
|
|
43
43
|
' grep <pattern> Every node containing it, with the matching lines.',
|
|
44
44
|
' show <id> One node in full, with what it links to.',
|
|
45
|
+
' diff <base> What this memory holds that <base> did not.',
|
|
45
46
|
' export [file] The whole graph as one HTML page.',
|
|
46
47
|
' merge <dir...> Fold other memories into this one.',
|
|
47
48
|
' forget <id...> Remove nodes, their vectors and their files.',
|
|
@@ -104,6 +105,12 @@ export const memory = {
|
|
|
104
105
|
' zen memory merge <batch-dir>/*/memory',
|
|
105
106
|
'',
|
|
106
107
|
'which is where `zen run batch` leaves them.',
|
|
108
|
+
'',
|
|
109
|
+
'`diff` is the question after a run that started from a copy: what it',
|
|
110
|
+
'added, which of those `merge` would fold onto a node <base> already held',
|
|
111
|
+
'(a re-commit), what it revised, and which nodes `memory_load` read:',
|
|
112
|
+
'',
|
|
113
|
+
' zen memory diff <candidate> --dir <batch-dir>/<id>/memory',
|
|
107
114
|
],
|
|
108
115
|
run: async (ctx) => {
|
|
109
116
|
const { values, positionals } = parse(ctx.args, {
|
|
@@ -156,6 +163,8 @@ export const memory = {
|
|
|
156
163
|
return await grep(opened, rest, values, ctx.json);
|
|
157
164
|
case 'show':
|
|
158
165
|
return show(opened, rest, ctx.json);
|
|
166
|
+
case 'diff':
|
|
167
|
+
return await diff(opened, rest, ctx.cwd, ctx.json);
|
|
159
168
|
case 'export':
|
|
160
169
|
return await write_(opened, rest, values, ctx.cwd, ctx.json);
|
|
161
170
|
case 'merge':
|
|
@@ -309,23 +318,22 @@ function list(o, flags, asJson) {
|
|
|
309
318
|
.filter((n) => !flags.stale || stale.has(n.id))
|
|
310
319
|
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
311
320
|
if (asJson) {
|
|
312
|
-
json({ total: all.length, nodes: all.slice(0, limit) });
|
|
321
|
+
json({ total: all.length, nodes: all.slice(0, limit).map((n) => onHost(o, n)) });
|
|
313
322
|
return;
|
|
314
323
|
}
|
|
315
324
|
if (!all.length) {
|
|
316
325
|
note('nothing matches');
|
|
317
326
|
return;
|
|
318
327
|
}
|
|
319
|
-
writeAll(
|
|
328
|
+
writeAll(textLast(all
|
|
320
329
|
.slice(0, limit)
|
|
321
330
|
.map((n) => [
|
|
322
331
|
cyan(n.id),
|
|
323
|
-
n.kind,
|
|
324
|
-
n.
|
|
325
|
-
|
|
326
|
-
dim(ago(n.createdAt)),
|
|
327
|
-
|
|
328
|
-
])));
|
|
332
|
+
n.kind + (n.file ? dim(' ⎘ ' + n.file.format) : ''),
|
|
333
|
+
n.audience.join(','),
|
|
334
|
+
dim(`r${n.revision}`),
|
|
335
|
+
dim(ago(n.createdAt)) + (stale.has(n.id) ? ' ' + yellow('superseded') : ''),
|
|
336
|
+
]), all.slice(0, limit).map((n) => n.text)));
|
|
329
337
|
if (all.length > limit) {
|
|
330
338
|
write();
|
|
331
339
|
note(dim(`${all.length - limit} more — raise --limit, or use \`zen memory export\``));
|
|
@@ -552,7 +560,7 @@ async function grep(o, rest, flags, asJson) {
|
|
|
552
560
|
hits: m.hits,
|
|
553
561
|
more: m.more,
|
|
554
562
|
})),
|
|
555
|
-
unsearched: res.skipped,
|
|
563
|
+
unsearched: res.skipped.map((s) => ({ ...s, path: hostPath(o.dir, s) })),
|
|
556
564
|
});
|
|
557
565
|
return;
|
|
558
566
|
}
|
|
@@ -562,7 +570,7 @@ async function grep(o, rest, flags, asJson) {
|
|
|
562
570
|
}
|
|
563
571
|
if (!res.found) {
|
|
564
572
|
note(`nothing matches ${pattern}`);
|
|
565
|
-
unsearched(res.skipped);
|
|
573
|
+
unsearched(o, res.skipped);
|
|
566
574
|
return;
|
|
567
575
|
}
|
|
568
576
|
for (const m of res.matches) {
|
|
@@ -570,7 +578,7 @@ async function grep(o, rest, flags, asJson) {
|
|
|
570
578
|
' ' +
|
|
571
579
|
m.node.kind +
|
|
572
580
|
(m.stale ? ' ' + yellow('superseded') : '') +
|
|
573
|
-
(m.node.file ? ' ' + dim(m.node.file
|
|
581
|
+
(m.node.file ? ' ' + dim(hostPath(o.dir, m.node.file)) : ''));
|
|
574
582
|
writeAll(table(m.hits.map((h) => [` ${dim(h.where + ':' + h.line)}`, clip(h.text, 120)])));
|
|
575
583
|
if (m.more) {
|
|
576
584
|
write(dim(` … ${count(m.more, 'more line')} in this node`));
|
|
@@ -580,19 +588,19 @@ async function grep(o, rest, flags, asJson) {
|
|
|
580
588
|
if (res.truncated) {
|
|
581
589
|
note(dim(`${res.found - res.matches.length} more nodes — raise --limit`));
|
|
582
590
|
}
|
|
583
|
-
unsearched(res.skipped);
|
|
591
|
+
unsearched(o, res.skipped);
|
|
584
592
|
}
|
|
585
593
|
/**
|
|
586
594
|
* Named rather than counted. A file that was not read is a hole in an answer
|
|
587
595
|
* whose whole value is that it has none, so the ids have to be printable.
|
|
588
596
|
*/
|
|
589
|
-
function unsearched(skipped) {
|
|
597
|
+
function unsearched(o, skipped) {
|
|
590
598
|
if (!skipped.length) {
|
|
591
599
|
return;
|
|
592
600
|
}
|
|
593
601
|
write();
|
|
594
602
|
note(yellow(`${count(skipped.length, 'file')} not searched`));
|
|
595
|
-
writeAll(table(skipped.map((s) => [` ${cyan(s.id)}`, dim(s.reason), s
|
|
603
|
+
writeAll(table(skipped.map((s) => [` ${cyan(s.id)}`, dim(s.reason), hostPath(o.dir, s)])));
|
|
596
604
|
}
|
|
597
605
|
// ---------------------------------------------------------------------------
|
|
598
606
|
// show
|
|
@@ -608,7 +616,11 @@ function show(o, ids, asJson) {
|
|
|
608
616
|
}
|
|
609
617
|
const links = o.store.graph.neighbors(id);
|
|
610
618
|
if (asJson) {
|
|
611
|
-
json({
|
|
619
|
+
json({
|
|
620
|
+
node: onHost(o, node),
|
|
621
|
+
links,
|
|
622
|
+
file: node.file ? hostPath(o.dir, node.file) : undefined,
|
|
623
|
+
});
|
|
612
624
|
return;
|
|
613
625
|
}
|
|
614
626
|
write(bold(node.id) + ' ' + cyan(node.kind));
|
|
@@ -653,6 +665,82 @@ function show(o, ids, asJson) {
|
|
|
653
665
|
})));
|
|
654
666
|
}
|
|
655
667
|
// ---------------------------------------------------------------------------
|
|
668
|
+
// diff
|
|
669
|
+
// ---------------------------------------------------------------------------
|
|
670
|
+
async function diff(o, rest, cwd, asJson) {
|
|
671
|
+
if (rest.length !== 1) {
|
|
672
|
+
throw usageError('diff takes exactly one base memory', 'zen memory diff <base> [--dir <dir>]');
|
|
673
|
+
}
|
|
674
|
+
const at = resolve(cwd, rest[0]);
|
|
675
|
+
if (!existsSync(join(at, 'manifest.json'))) {
|
|
676
|
+
throw usageError(`${at} is not a memory`, 'no manifest.json in it');
|
|
677
|
+
}
|
|
678
|
+
const base = await MemoryStore.open(at, { lock: false });
|
|
679
|
+
try {
|
|
680
|
+
const d = diffMemories(base, o.store);
|
|
681
|
+
const mine = (n) => onHost(o, n);
|
|
682
|
+
const theirs = (n) => n.file ? { ...n, file: { ...n.file, path: hostPath(at, n.file) } } : n;
|
|
683
|
+
if (asJson) {
|
|
684
|
+
json({
|
|
685
|
+
...d,
|
|
686
|
+
added: d.added.map((a) => ({ ...a, node: mine(a.node) })),
|
|
687
|
+
revised: d.revised.map((r) => ({ before: theirs(r.before), after: mine(r.after) })),
|
|
688
|
+
used: d.used.map((u) => ({ ...u, node: mine(u.node) })),
|
|
689
|
+
removed: d.removed.map(theirs),
|
|
690
|
+
});
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
const twins = d.added.filter((a) => a.twin).length;
|
|
694
|
+
write(bold(o.dir) + dim(` against ${at} · nearest by ${d.by}`));
|
|
695
|
+
write();
|
|
696
|
+
write(`${bold('added')} ${d.added.length}` +
|
|
697
|
+
(twins ? ` · ${yellow(`${twins} already held`)}` : '') +
|
|
698
|
+
` · ${bold('revised')} ${d.revised.length}` +
|
|
699
|
+
` · ${bold('used')} ${d.used.length}` +
|
|
700
|
+
` · ${bold('removed')} ${d.removed.length}` +
|
|
701
|
+
` · ${bold('edges')} +${d.edges}`);
|
|
702
|
+
if (d.added.length) {
|
|
703
|
+
write();
|
|
704
|
+
write(dim('added'));
|
|
705
|
+
writeAll(textLast(d.added.map((a) => [
|
|
706
|
+
` ${cyan(a.node.id)}`,
|
|
707
|
+
a.node.kind,
|
|
708
|
+
a.node.audience.join(','),
|
|
709
|
+
a.nearest
|
|
710
|
+
? `${a.twin ? yellow('twin of') : dim('near')} ${a.nearest.id} ${dim(a.nearest.score.toFixed(2))}`
|
|
711
|
+
: dim('nothing near'),
|
|
712
|
+
]), d.added.map((a) => a.node.text)));
|
|
713
|
+
}
|
|
714
|
+
if (d.revised.length) {
|
|
715
|
+
write();
|
|
716
|
+
write(dim('revised'));
|
|
717
|
+
writeAll(textLast(d.revised.map((r) => [
|
|
718
|
+
` ${cyan(r.after.id)}`,
|
|
719
|
+
r.after.kind,
|
|
720
|
+
dim(`r${r.before.revision} → r${r.after.revision}`),
|
|
721
|
+
]), d.revised.map((r) => r.after.text)));
|
|
722
|
+
}
|
|
723
|
+
if (d.used.length) {
|
|
724
|
+
write();
|
|
725
|
+
write(dim('used — returned by memory_load'));
|
|
726
|
+
writeAll(textLast(d.used.map((u) => [` ${cyan(u.node.id)}`, u.node.kind, `${u.loads}×`]), d.used.map((u) => u.node.text)));
|
|
727
|
+
}
|
|
728
|
+
if (d.removed.length) {
|
|
729
|
+
write();
|
|
730
|
+
write(dim('removed'));
|
|
731
|
+
writeAll(textLast(d.removed.map((n) => [` ${cyan(n.id)}`, n.kind]), d.removed.map((n) => n.text)));
|
|
732
|
+
}
|
|
733
|
+
if (d.supersedes.length) {
|
|
734
|
+
write();
|
|
735
|
+
write(dim('supersedes'));
|
|
736
|
+
writeAll(table(d.supersedes.map((s) => [` ${cyan(s.source)}`, '→', cyan(s.target)])));
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
finally {
|
|
740
|
+
base.release();
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
// ---------------------------------------------------------------------------
|
|
656
744
|
// export
|
|
657
745
|
// ---------------------------------------------------------------------------
|
|
658
746
|
async function write_(o, rest, flags, cwd, asJson) {
|
|
@@ -661,7 +749,8 @@ async function write_(o, rest, flags, cwd, asJson) {
|
|
|
661
749
|
}
|
|
662
750
|
const named = rest[0] ?? flags.out ?? 'memory.html';
|
|
663
751
|
const target = isAbsolute(named) ? named : resolve(cwd, named);
|
|
664
|
-
const
|
|
752
|
+
const built = await buildMemoryReport(o.store, { title: `${o.project} · memory` });
|
|
753
|
+
const report = { ...built, nodes: built.nodes.map((n) => onHost(o, n)) };
|
|
665
754
|
await writeFile(target, renderMemoryHtml(report), 'utf8');
|
|
666
755
|
if (asJson) {
|
|
667
756
|
json({ path: target, nodes: report.nodes.length, edges: report.edges.length });
|
|
@@ -720,7 +809,7 @@ async function merge(o, dirs, flags, cwd, asJson) {
|
|
|
720
809
|
}
|
|
721
810
|
catch (err) {
|
|
722
811
|
if (err instanceof MergeConflicts) {
|
|
723
|
-
throw diverged(err,
|
|
812
|
+
throw diverged(err, asJson);
|
|
724
813
|
}
|
|
725
814
|
throw err;
|
|
726
815
|
}
|
|
@@ -731,7 +820,7 @@ async function merge(o, dirs, flags, cwd, asJson) {
|
|
|
731
820
|
writeAll(table([
|
|
732
821
|
[dim('memory'), dim('nodes'), dim('new'), dim('shared'), dim('folded'), dim('files')],
|
|
733
822
|
...report.sources.map((s) => [
|
|
734
|
-
|
|
823
|
+
s.dir,
|
|
735
824
|
String(s.nodes),
|
|
736
825
|
s.added ? green(String(s.added)) : '0',
|
|
737
826
|
String(s.shared),
|
|
@@ -752,7 +841,7 @@ async function merge(o, dirs, flags, cwd, asJson) {
|
|
|
752
841
|
* point of stopping is that a person decides, and they cannot decide from a
|
|
753
842
|
* count.
|
|
754
843
|
*/
|
|
755
|
-
function diverged(err,
|
|
844
|
+
function diverged(err, asJson) {
|
|
756
845
|
if (asJson) {
|
|
757
846
|
json({ error: err.message, conflicts: err.conflicts });
|
|
758
847
|
}
|
|
@@ -761,7 +850,7 @@ function diverged(err, cwd, asJson) {
|
|
|
761
850
|
cyan(c.id),
|
|
762
851
|
dim(`r${c.mine} ↔ r${c.theirs}`),
|
|
763
852
|
clip(c.text, 48),
|
|
764
|
-
dim(
|
|
853
|
+
dim(c.dir),
|
|
765
854
|
])));
|
|
766
855
|
write();
|
|
767
856
|
}
|
|
@@ -818,9 +907,19 @@ function clip(text, n) {
|
|
|
818
907
|
const one = text.replace(/\s+/g, ' ').trim();
|
|
819
908
|
return one.length > n ? one.slice(0, n - 1) + '…' : one;
|
|
820
909
|
}
|
|
821
|
-
/**
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
910
|
+
/**
|
|
911
|
+
* A table whose last column is the text: cut to what the terminal has left,
|
|
912
|
+
* and whole when nobody is looking at a terminal — a pipe or an agent wants all of it.
|
|
913
|
+
*/
|
|
914
|
+
function textLast(rows, texts) {
|
|
915
|
+
const widths = rows[0]?.map((_, c) => Math.max(...rows.map((r) => plain(r[c] ?? '').length)));
|
|
916
|
+
const room = process.stdout.isTTY
|
|
917
|
+
? Math.max(24, (process.stdout.columns || 120) - (widths ?? []).reduce((t, w) => t + w + 2, 0))
|
|
918
|
+
: Infinity;
|
|
919
|
+
return table(rows.map((r, i) => [...r, clip(texts[i] ?? '', room)]));
|
|
920
|
+
}
|
|
921
|
+
/** The stored `/memory/<id>.<ext>` is the agent's mount path; a terminal needs the host one. */
|
|
922
|
+
function onHost(o, n) {
|
|
923
|
+
return n.file ? { ...n, file: { ...n.file, path: hostPath(o.dir, n.file) } } : n;
|
|
825
924
|
}
|
|
826
925
|
//# sourceMappingURL=memory.js.map
|