@zenera/cli 1.1.23 → 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.
Files changed (184) hide show
  1. package/README.md +88 -2
  2. package/dist/ask.d.ts +51 -0
  3. package/dist/ask.js +166 -0
  4. package/dist/banner.js +2 -2
  5. package/dist/batch.d.ts +83 -0
  6. package/dist/batch.js +413 -0
  7. package/dist/catalog.d.ts +11 -1
  8. package/dist/catalog.js +80 -7
  9. package/dist/command.d.ts +6 -0
  10. package/dist/commands/inspect.d.ts +57 -0
  11. package/dist/commands/inspect.js +539 -91
  12. package/dist/commands/list.js +128 -19
  13. package/dist/commands/memory.js +461 -32
  14. package/dist/commands/meta.js +311 -21
  15. package/dist/commands/models.js +110 -14
  16. package/dist/commands/open.js +4 -4
  17. package/dist/commands/run.js +175 -36
  18. package/dist/commands/sandbox.js +148 -15
  19. package/dist/engine.d.ts +24 -0
  20. package/dist/engine.js +61 -8
  21. package/dist/eventlog.d.ts +59 -0
  22. package/dist/eventlog.js +146 -0
  23. package/dist/liveness.d.ts +7 -0
  24. package/dist/liveness.js +20 -3
  25. package/dist/main.js +5 -2
  26. package/dist/meta/editor.d.ts +9 -0
  27. package/dist/meta/editor.js +60 -0
  28. package/dist/meta/finetune/children.d.ts +8 -0
  29. package/dist/meta/finetune/children.js +76 -0
  30. package/dist/meta/finetune/command.d.ts +30 -0
  31. package/dist/meta/finetune/command.js +338 -0
  32. package/dist/meta/finetune/dataset/anchors.d.ts +41 -0
  33. package/dist/meta/finetune/dataset/anchors.js +239 -0
  34. package/dist/meta/finetune/dataset/changes.d.ts +36 -0
  35. package/dist/meta/finetune/dataset/changes.js +319 -0
  36. package/dist/meta/finetune/dataset/command.d.ts +17 -0
  37. package/dist/meta/finetune/dataset/command.js +1011 -0
  38. package/dist/meta/finetune/dataset/drift.d.ts +33 -0
  39. package/dist/meta/finetune/dataset/drift.js +71 -0
  40. package/dist/meta/finetune/dataset/export.d.ts +26 -0
  41. package/dist/meta/finetune/dataset/export.js +51 -0
  42. package/dist/meta/finetune/dataset/index.d.ts +2 -0
  43. package/dist/meta/finetune/dataset/index.js +2 -0
  44. package/dist/meta/finetune/dataset/sample.d.ts +50 -0
  45. package/dist/meta/finetune/dataset/sample.js +197 -0
  46. package/dist/meta/finetune/dataset/schema.d.ts +33 -0
  47. package/dist/meta/finetune/dataset/schema.js +216 -0
  48. package/dist/meta/finetune/dataset/store.d.ts +35 -0
  49. package/dist/meta/finetune/dataset/store.js +177 -0
  50. package/dist/meta/finetune/dataset/types.d.ts +125 -0
  51. package/dist/meta/finetune/dataset/types.js +39 -0
  52. package/dist/meta/finetune/failure.d.ts +15 -0
  53. package/dist/meta/finetune/failure.js +26 -0
  54. package/dist/meta/finetune/feedback.d.ts +45 -0
  55. package/dist/meta/finetune/feedback.js +85 -0
  56. package/dist/meta/finetune/improvements.d.ts +51 -0
  57. package/dist/meta/finetune/improvements.js +244 -0
  58. package/dist/meta/finetune/index.d.ts +2 -0
  59. package/dist/meta/finetune/index.js +2 -0
  60. package/dist/meta/finetune/journal.d.ts +35 -0
  61. package/dist/meta/finetune/journal.js +135 -0
  62. package/dist/meta/finetune/live.d.ts +74 -0
  63. package/dist/meta/finetune/live.js +226 -0
  64. package/dist/meta/finetune/loop.d.ts +3 -0
  65. package/dist/meta/finetune/loop.js +114 -0
  66. package/dist/meta/finetune/memories.d.ts +47 -0
  67. package/dist/meta/finetune/memories.js +154 -0
  68. package/dist/meta/finetune/report.d.ts +33 -0
  69. package/dist/meta/finetune/report.js +1284 -0
  70. package/dist/meta/finetune/retry.d.ts +24 -0
  71. package/dist/meta/finetune/retry.js +96 -0
  72. package/dist/meta/finetune/sampler.d.ts +18 -0
  73. package/dist/meta/finetune/sampler.js +41 -0
  74. package/dist/meta/finetune/system.d.ts +27 -0
  75. package/dist/meta/finetune/system.js +132 -0
  76. package/dist/meta/finetune/train.d.ts +39 -0
  77. package/dist/meta/finetune/train.js +391 -0
  78. package/dist/meta/finetune/tui.d.ts +16 -0
  79. package/dist/meta/finetune/tui.js +291 -0
  80. package/dist/meta/finetune/tuning.d.ts +167 -0
  81. package/dist/meta/finetune/tuning.js +289 -0
  82. package/dist/meta/finetune/usage.d.ts +62 -0
  83. package/dist/meta/finetune/usage.js +225 -0
  84. package/dist/meta/host.d.ts +14 -0
  85. package/dist/meta/host.js +20 -0
  86. package/dist/meta/index.d.ts +11 -0
  87. package/dist/meta/index.js +11 -0
  88. package/dist/meta/launch.d.ts +81 -0
  89. package/dist/meta/launch.js +286 -0
  90. package/dist/meta/model.d.ts +44 -0
  91. package/dist/meta/model.js +119 -0
  92. package/dist/meta/prompts.d.ts +18 -0
  93. package/dist/meta/prompts.js +61 -0
  94. package/dist/meta/relay.d.ts +20 -0
  95. package/dist/meta/relay.js +123 -0
  96. package/dist/meta/tokens.d.ts +52 -0
  97. package/dist/meta/tokens.js +226 -0
  98. package/dist/meta/window.d.ts +61 -0
  99. package/dist/meta/window.js +198 -0
  100. package/dist/meta/wire.d.ts +42 -0
  101. package/dist/meta/wire.js +129 -0
  102. package/dist/modelref.d.ts +16 -0
  103. package/dist/modelref.js +41 -0
  104. package/dist/narrate.js +5 -2
  105. package/dist/podman.d.ts +42 -0
  106. package/dist/podman.js +198 -5
  107. package/dist/progress.d.ts +33 -0
  108. package/dist/progress.js +402 -0
  109. package/dist/projects.d.ts +0 -5
  110. package/dist/projects.js +2 -15
  111. package/dist/request.d.ts +45 -0
  112. package/dist/request.js +300 -0
  113. package/dist/sandbox.d.ts +2 -0
  114. package/dist/sandbox.js +20 -3
  115. package/dist/scaffold.d.ts +15 -6
  116. package/dist/scaffold.js +5 -60
  117. package/dist/session.d.ts +26 -1
  118. package/dist/session.js +46 -32
  119. package/dist/term.d.ts +5 -0
  120. package/dist/term.js +28 -3
  121. package/dist/trace.d.ts +84 -0
  122. package/dist/trace.js +737 -0
  123. package/dist/tui/models.d.ts +33 -0
  124. package/dist/tui/models.js +389 -0
  125. package/dist/tui/wrap.d.ts +5 -0
  126. package/dist/tui/wrap.js +12 -0
  127. package/dist/usage.d.ts +78 -0
  128. package/dist/usage.js +51 -0
  129. package/dist/validate.js +25 -3
  130. package/package.json +3 -2
  131. package/templates/editor/.github/copilot-instructions.md +41 -107
  132. package/templates/editor/.github/prompts/analyze.prompt.md +31 -0
  133. package/templates/editor/.github/prompts/dataset.prompt.md +108 -0
  134. package/templates/editor/.github/prompts/finetune-apply.prompt.md +91 -0
  135. package/templates/editor/.github/prompts/finetune.prompt.md +744 -0
  136. package/templates/editor/.github/prompts/new-agent.prompt.md +3 -1
  137. package/templates/editor/.github/prompts/new-skill.prompt.md +2 -0
  138. package/templates/editor/.github/prompts/project-review.prompt.md +4 -3
  139. package/templates/editor/.github/skills/zen-analyze-run/SKILL.md +483 -0
  140. package/templates/editor/.github/skills/zen-cli/SKILL.md +18 -4
  141. package/templates/editor/.github/skills/zen-cli/references/check.md +7 -5
  142. package/templates/editor/.github/skills/zen-cli/references/dataset.md +34 -0
  143. package/templates/editor/.github/skills/zen-cli/references/finetune.md +77 -0
  144. package/templates/editor/.github/skills/zen-cli/references/inspect.md +300 -21
  145. package/templates/editor/.github/skills/zen-cli/references/memory.md +121 -28
  146. package/templates/editor/.github/skills/zen-cli/references/meta.md +61 -0
  147. package/templates/editor/.github/skills/zen-cli/references/models.md +6 -2
  148. package/templates/editor/.github/skills/zen-cli/references/projects.md +12 -5
  149. package/templates/editor/.github/skills/zen-cli/references/rag.md +24 -6
  150. package/templates/editor/.github/skills/zen-cli/references/run.md +201 -4
  151. package/templates/editor/.github/skills/zen-cli/references/sandbox.md +10 -1
  152. package/templates/editor/.github/skills/zen-code-python/SKILL.md +123 -0
  153. package/templates/editor/.github/skills/zen-code-python/references/code_python.md +48 -0
  154. package/templates/editor/.github/skills/zen-dataset/SKILL.md +253 -0
  155. package/templates/editor/.github/skills/zen-finetune/SKILL.md +1791 -0
  156. package/templates/editor/.github/skills/zen-finetune/references/readme-template.md +268 -0
  157. package/templates/editor/.github/skills/zen-finetune/scripts/batch.mjs +390 -0
  158. package/templates/editor/.github/skills/zen-finetune/scripts/difficult.mjs +243 -0
  159. package/templates/editor/.github/skills/zen-finetune/scripts/memory.mjs +280 -0
  160. package/templates/editor/.github/skills/zen-finetune/scripts/next.mjs +398 -0
  161. package/templates/editor/.github/skills/zen-finetune/scripts/report.mjs +797 -0
  162. package/templates/editor/.github/skills/zen-finetune/scripts/select.mjs +354 -0
  163. package/templates/editor/.github/skills/zen-finetune/scripts/usage.mjs +722 -0
  164. package/templates/editor/.github/skills/zen-inspect/SKILL.md +431 -0
  165. package/templates/editor/.github/skills/zen-inspect-ask/SKILL.md +270 -0
  166. package/templates/editor/.github/skills/zen-memory/SKILL.md +78 -29
  167. package/templates/editor/.github/skills/zen-memory-warmup/SKILL.md +363 -531
  168. package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +20 -8
  169. package/templates/editor/.github/skills/zen-review/SKILL.md +5 -3
  170. package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +5 -4
  171. package/templates/editor/.github/skills/zen-sandbox/SKILL.md +25 -2
  172. package/templates/editor/.github/skills/zen-sandbox-capacity/SKILL.md +334 -0
  173. package/templates/editor/.github/skills/zen-sandbox-capacity/references/loadtest.md +181 -0
  174. package/templates/editor/.github/skills/zen-sandbox-capacity/scripts/preflight_sandbox.sh +457 -0
  175. package/templates/editor/.github/skills/zen-topology/SKILL.md +283 -0
  176. package/templates/editor/.vscode/agents.schema.json +1 -1
  177. package/templates/editor/.vscode/settings.json +2 -0
  178. package/templates/project/agents/files-instructions.md +35 -13
  179. package/templates/project/agents/fork-instructions.md +27 -0
  180. package/templates/project/agents/memory-instructions.md +106 -34
  181. package/templates/project/agents/tools-instructions.md +7 -4
  182. package/templates/project/gitignore +5 -0
  183. package/dist/meta.d.ts +0 -221
  184. package/dist/meta.js +0 -684
@@ -1,13 +1,15 @@
1
- import { ALL_AGENTS, buildMemoryReport, hostPath, memoryDir, MemoryIndex, MemoryStore, MergeConflicts, mergeMemories, readProjectConfig, renderMemoryHtml, } 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, relative, resolve } from 'node:path';
5
+ import { basename, isAbsolute, join, resolve } from 'node:path';
6
6
  import { parse } from "../args.js";
7
+ import { loadProjectEnv } from "../env.js";
8
+ import { KeyStore } from "../keys.js";
7
9
  import { project as resolveProject } from "../resolve.js";
8
- import { ago, bold, bytes, CliError, confirm, count, cyan, dim, EXIT, green, isInteractive, json, note, table, usageError, write, writeAll, yellow, } from "../term.js";
9
- const USAGE = 'zen memory [stats|ls|show|export|merge|forget] [args] [options]';
10
- const SUBCOMMANDS = ['stats', 'ls', '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'];
11
13
  /** Enough to see the shape of it; the rest is a number. */
12
14
  const LISTED = 30;
13
15
  // ---------------------------------------------------------------------------
@@ -24,9 +26,11 @@ const LISTED = 30;
24
26
  // an agent inside a run, and the mask exists to keep agents apart rather than
25
27
  // to keep secrets from the person who owns the files.
26
28
  //
27
- // Nothing here contacts a model. The store is opened without an embedder, so
28
- // inspection is free and works offline; the price is that `ls` filters by text
29
- // rather than by meaning, which is the right trade for a debugging tool.
29
+ // `search` is the one subcommand that contacts a model, and it has to: the
30
+ // question it answers is what an agent's recall returns, and a recall ranked
31
+ // any other way is a different answer wearing the same shape. Everything else
32
+ // is offline, so inspecting a graph whose credentials have gone missing —
33
+ // which is exactly when someone is debugging — still works.
30
34
  // ---------------------------------------------------------------------------
31
35
  export const memory = {
32
36
  summary: 'What the agents remember, and getting rid of it.',
@@ -35,7 +39,10 @@ export const memory = {
35
39
  details: [
36
40
  ' stats Size, vocabulary, and whether it is embedded.',
37
41
  ' ls Nodes, newest first. Changes nothing.',
42
+ ' search <text> Recall it, the way an agent does. Ranked.',
43
+ ' grep <pattern> Every node containing it, with the matching lines.',
38
44
  ' show <id> One node in full, with what it links to.',
45
+ ' diff <base> What this memory holds that <base> did not.',
39
46
  ' export [file] The whole graph as one HTML page.',
40
47
  ' merge <dir...> Fold other memories into this one.',
41
48
  ' forget <id...> Remove nodes, their vectors and their files.',
@@ -46,7 +53,16 @@ export const memory = {
46
53
  ' --audience <name> Only nodes committed under this label.',
47
54
  ' --files Only nodes that remember a file.',
48
55
  ' --stale Only nodes something has superseded.',
49
- ' --limit <n> Rows to list. Default 30.',
56
+ ' --all For `search` and `grep`: superseded nodes too, marked.',
57
+ ' --regex Read the pattern as a regular expression, per line.',
58
+ ' --case-sensitive Match case exactly. Off by default.',
59
+ ' --in <text|metadata|file> Where to look. Repeatable. All three by default.',
60
+ ' --ids-only Print bare ids, for piping into `zen memory show`.',
61
+ ' --limit <n> Rows to list, or seeds to rank. Default 30, search 10.',
62
+ ' --hops <n> How far `search` follows links from a seed. Default 2.',
63
+ ' --nodes <n> Cap on the subgraph `search` returns. Default 25.',
64
+ ' --min-score <n> Drop seeds scoring below this. Default 0.15.',
65
+ ' --embedding <ref> Rank `search` with this model, not the project’s.',
50
66
  ' --out <file> Where `export` writes. Default memory.html.',
51
67
  ' --open Open the exported page.',
52
68
  ' --dry-run Say what `merge` would do, and stop.',
@@ -62,10 +78,39 @@ export const memory = {
62
78
  'page: the node list on the left, the graph in the middle, and whatever',
63
79
  'you click on the right, file contents and all.',
64
80
  '',
81
+ '`search` is recall itself, run from here. It embeds the query with the',
82
+ 'project’s own model and prints the block a model would have been given,',
83
+ 'scores and all — so *why did it recall that?* has an answer, and so does',
84
+ '*why did it not?*:',
85
+ '',
86
+ ' zen memory search ‘how do we deploy’ --audience reviewer',
87
+ '',
88
+ 'Passing `--audience` recalls as an agent that sees only that label,',
89
+ 'which is how a memory that is present but invisible gives itself away.',
90
+ '',
91
+ '`grep` is the counterpart. Recall ranks, so it answers “what is closest”',
92
+ 'and can never answer “is this in here at all”; `grep` reads every node',
93
+ 'exactly — text, metadata and the bytes of remembered files — and the',
94
+ 'count it reports is the true one even when the list was cut:',
95
+ '',
96
+ ' zen memory grep ‘staging.example.com’ --in metadata --in file',
97
+ '',
98
+ 'Nothing that only reads takes the directory lock, so every subcommand',
99
+ 'here but `merge` and `forget` works on a memory a run is writing, and on',
100
+ 'the read-only /memory mount inside a sandbox.',
101
+ '',
65
102
  '`merge` is for warming a memory in parallel. The lock is per directory,',
66
103
  'so N runs write N memories; this folds them back into one:',
67
104
  '',
68
- ' zen memory merge .tmp/warmup-*/memory',
105
+ ' zen memory merge <batch-dir>/*/memory',
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',
69
114
  ],
70
115
  run: async (ctx) => {
71
116
  const { values, positionals } = parse(ctx.args, {
@@ -75,7 +120,16 @@ export const memory = {
75
120
  audience: { type: 'string' },
76
121
  files: { type: 'boolean' },
77
122
  stale: { type: 'boolean' },
123
+ all: { type: 'boolean' },
124
+ regex: { type: 'boolean' },
125
+ 'case-sensitive': { type: 'boolean' },
126
+ in: { type: 'string', multiple: true },
127
+ 'ids-only': { type: 'boolean' },
78
128
  limit: { type: 'string' },
129
+ hops: { type: 'string' },
130
+ nodes: { type: 'string' },
131
+ 'min-score': { type: 'string' },
132
+ embedding: { type: 'string' },
79
133
  out: { type: 'string' },
80
134
  open: { type: 'boolean' },
81
135
  yes: { type: 'boolean' },
@@ -90,15 +144,27 @@ export const memory = {
90
144
  const rest = positionals.slice(1);
91
145
  // `merge` is the one subcommand that may write a memory into existence;
92
146
  // every other one is an inspector and a missing manifest is a mistake.
93
- const opened = await open(ctx.cwd, values.project, values.dir, what === 'merge');
147
+ // And only the two that write take the lock: the moments a reader is
148
+ // most wanted are while a run is writing and against the read-only
149
+ // /memory mount, in both of which claiming it fails.
150
+ const opened = await open(ctx.cwd, values.project, values.dir, {
151
+ create: what === 'merge',
152
+ lock: what === 'merge' || what === 'forget',
153
+ });
94
154
  try {
95
155
  switch (what) {
96
156
  case 'stats':
97
157
  return stats(opened, ctx.json);
98
158
  case 'ls':
99
159
  return list(opened, values, ctx.json);
160
+ case 'search':
161
+ return await search(opened, rest, values, ctx.json);
162
+ case 'grep':
163
+ return await grep(opened, rest, values, ctx.json);
100
164
  case 'show':
101
165
  return show(opened, rest, ctx.json);
166
+ case 'diff':
167
+ return await diff(opened, rest, ctx.cwd, ctx.json);
102
168
  case 'export':
103
169
  return await write_(opened, rest, values, ctx.cwd, ctx.json);
104
170
  case 'merge':
@@ -125,14 +191,19 @@ export const memory = {
125
191
  * `create` drops the requirement that anything be there yet, for `merge`,
126
192
  * which has somewhere to put what it reads even when the destination is a name
127
193
  * nothing has written to.
194
+ *
195
+ * `lock` is off for a read that must not fail on a busy or read-only memory.
196
+ * The lock exists to stop two writers losing each other's edges; a reader that
197
+ * took it would only be refusing itself.
128
198
  */
129
- async function open(cwd, want, at, create = false) {
199
+ async function open(cwd, want, at, opts = {}) {
200
+ const { create = false, lock = true } = opts;
130
201
  if (at) {
131
202
  const dir = resolve(cwd, at);
132
203
  if (!create && !existsSync(join(dir, 'manifest.json'))) {
133
204
  throw usageError(`${dir} is not a memory`, 'no manifest.json in it');
134
205
  }
135
- return { store: await MemoryStore.open(dir), dir, project: basename(dir) };
206
+ return { store: await MemoryStore.open(dir, { lock }), dir, project: basename(dir) };
136
207
  }
137
208
  const project = await resolveProject({ cwd, project: want });
138
209
  const { config } = readProjectConfig(project.dir);
@@ -143,7 +214,12 @@ async function open(cwd, want, at, create = false) {
143
214
  if (!create && !existsSync(join(dir, 'manifest.json'))) {
144
215
  throw usageError(`${project.name} has a memory configured but nothing in it yet`, 'it is written the first time an agent commits something');
145
216
  }
146
- return { store: await MemoryStore.open(dir), dir, project: project.name };
217
+ return {
218
+ store: await MemoryStore.open(dir, { lock }),
219
+ dir,
220
+ project: project.name,
221
+ at: { root: project.dir, config },
222
+ };
147
223
  }
148
224
  // ---------------------------------------------------------------------------
149
225
  // stats
@@ -242,29 +318,291 @@ function list(o, flags, asJson) {
242
318
  .filter((n) => !flags.stale || stale.has(n.id))
243
319
  .sort((a, b) => b.createdAt.localeCompare(a.createdAt));
244
320
  if (asJson) {
245
- json({ total: all.length, nodes: all.slice(0, limit) });
321
+ json({ total: all.length, nodes: all.slice(0, limit).map((n) => onHost(o, n)) });
246
322
  return;
247
323
  }
248
324
  if (!all.length) {
249
325
  note('nothing matches');
250
326
  return;
251
327
  }
252
- writeAll(table(all
328
+ writeAll(textLast(all
253
329
  .slice(0, limit)
254
330
  .map((n) => [
255
331
  cyan(n.id),
256
- n.kind,
257
- n.file ? dim('⎘ ' + n.file.format) : '',
258
- clip(n.text, 56),
259
- dim(ago(n.createdAt)),
260
- stale.has(n.id) ? yellow('superseded') : '',
261
- ])));
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)));
262
337
  if (all.length > limit) {
263
338
  write();
264
339
  note(dim(`${all.length - limit} more — raise --limit, or use \`zen memory export\``));
265
340
  }
266
341
  }
267
342
  // ---------------------------------------------------------------------------
343
+ // search
344
+ // ---------------------------------------------------------------------------
345
+ /**
346
+ * Recall, run from outside a run. `ls` and `grep` read the graph; this asks it
347
+ * the question an agent asks, through the same ranker, the same traversal and
348
+ * the same renderer — so what it prints is the block a model would have been
349
+ * handed, scores and all.
350
+ *
351
+ * That is the only way to answer *why did it recall that*, and the harder *why
352
+ * did it not*: a memory that is present but ranked sixth looks exactly like a
353
+ * memory that was never written, until the score is on the screen.
354
+ */
355
+ async function search(o, rest, flags, asJson) {
356
+ // Joined rather than taken singly. A query is prose, and this is the one
357
+ // subcommand whose whole argument is prose, so quoting it is the step
358
+ // everybody forgets.
359
+ const text = rest.join(' ').trim();
360
+ if (!text) {
361
+ throw usageError('search takes something to recall', 'zen memory search <text>');
362
+ }
363
+ // Recall either includes what has been superseded or does not; there is no
364
+ // "only", because a correction alone is not the subgraph anyone wanted.
365
+ if (flags.stale) {
366
+ throw usageError('search has no --stale', 'use --all to recall superseded nodes too');
367
+ }
368
+ const query = {
369
+ text,
370
+ kinds: flags.kind ? [flags.kind] : undefined,
371
+ limit: whole(flags.limit, '--limit'),
372
+ // Zero hops is a real request — the seeds and nothing they dragged in.
373
+ maxHops: whole(flags.hops, '--hops', 0),
374
+ maxNodes: whole(flags.nodes, '--nodes'),
375
+ minScore: threshold(flags['min-score']),
376
+ };
377
+ const ranker = await vectoriser(o, flags.embedding);
378
+ const sees = flags.audience ? [flags.audience] : audiences(o);
379
+ let rec;
380
+ try {
381
+ const index = new MemoryIndex({ store: o.store, embedder: ranker.embedder });
382
+ rec = await index.search(query, sees, { stale: flags.all });
383
+ }
384
+ catch (err) {
385
+ if (err instanceof MemoryError) {
386
+ throw new CliError(err.message, EXIT.usage, err.hint);
387
+ }
388
+ throw err;
389
+ }
390
+ if (asJson) {
391
+ json({
392
+ query,
393
+ sees,
394
+ // Named field by field: the ranker carries the embedder itself,
395
+ // which is a client object and no part of an answer.
396
+ ranking: { by: ranker.by, model: ranker.model, reason: ranker.reason },
397
+ seeds: rec.seeds,
398
+ truncated: rec.truncated,
399
+ nodes: rec.nodes.map((n) => ({
400
+ id: n.node.id,
401
+ kind: n.node.kind,
402
+ score: n.score,
403
+ seed: n.seed,
404
+ via: n.via,
405
+ text: n.node.text,
406
+ file: n.node.file ? hostPath(o.dir, n.node.file) : undefined,
407
+ })),
408
+ edges: rec.edges,
409
+ // Verbatim, because it is the thing that went into a prompt.
410
+ block: renderRecollection(rec, { tagged: false }),
411
+ });
412
+ return;
413
+ }
414
+ if (flags['ids-only']) {
415
+ writeAll(rec.nodes.map((n) => n.node.id));
416
+ return;
417
+ }
418
+ if (!rec.nodes.length) {
419
+ note(`nothing recalled for ${text}`);
420
+ ranking(ranker);
421
+ return;
422
+ }
423
+ write(renderRecollection(rec, { tagged: false }));
424
+ write();
425
+ ranking(ranker);
426
+ }
427
+ /**
428
+ * The embedder the project ranks with. Reproducing a recall means reproducing
429
+ * its vectors, so the ref is resolved through the same alias table a run uses
430
+ * and the keyring is materialised the same way.
431
+ *
432
+ * What cannot be built falls back to term overlap rather than failing, because
433
+ * a graph whose credentials have gone missing is one worth looking at. The
434
+ * fallback is a different ranking, though, so it is reported rather than passed
435
+ * off as recall.
436
+ */
437
+ async function vectoriser(o, override) {
438
+ // A memory with no vectors cannot be ranked by meaning whatever we build:
439
+ // embedding a query against an empty block finds nothing, which would read
440
+ // as "nothing is remembered" rather than "there is nothing to rank with".
441
+ if (!o.store.embedding) {
442
+ return { by: 'term overlap', reason: 'this memory holds no vectors' };
443
+ }
444
+ const config = o.at?.config;
445
+ const declared = override ?? config?.memory?.embedding ?? config?.embedding ?? o.store.embedding.model;
446
+ // The project's own file first, then the keyring — the order `zen run`
447
+ // uses, so a query embeds here exactly when it would embed there.
448
+ if (o.at) {
449
+ loadProjectEnv(o.at.root);
450
+ }
451
+ (await KeyStore.open()).materialize();
452
+ try {
453
+ const ref = config?.embeddings?.[declared] ?? declared;
454
+ const embedder = createEmbedder(ref);
455
+ return { embedder, by: 'meaning', model: embedder.id };
456
+ }
457
+ catch (err) {
458
+ return { by: 'term overlap', reason: err instanceof Error ? err.message : String(err) };
459
+ }
460
+ }
461
+ /**
462
+ * Which ranking that was. A lexical fallback is not what an agent sees, and a
463
+ * block that looked like recall without being it would send someone chasing a
464
+ * difference that exists only in this command.
465
+ */
466
+ function ranking(r) {
467
+ if (r.by === 'meaning') {
468
+ note(dim(`ranked by meaning · ${r.model}`));
469
+ return;
470
+ }
471
+ note(yellow('ranked by term overlap — this is not the order an agent sees'));
472
+ if (r.reason) {
473
+ note(dim(` ${r.reason}`));
474
+ }
475
+ }
476
+ /**
477
+ * Every label the graph uses. `recall` masks against a list rather than taking
478
+ * `undefined` for "no mask", so reading unmasked has to be spelled out — and
479
+ * spelling it out from the graph is exact, since a label nothing carries can
480
+ * hide nothing.
481
+ */
482
+ function audiences(o) {
483
+ return [...new Set(o.store.graph.nodes().flatMap((n) => n.audience))];
484
+ }
485
+ /** Absent stays absent, so the engine's own default is the one on display. */
486
+ function whole(value, flag, min = 1) {
487
+ if (value === undefined) {
488
+ return undefined;
489
+ }
490
+ const n = Number(value);
491
+ if (!Number.isInteger(n) || n < min) {
492
+ throw usageError(`${flag} wants a whole number ${min} or more, got ${value}`, USAGE);
493
+ }
494
+ return n;
495
+ }
496
+ function threshold(value) {
497
+ if (value === undefined) {
498
+ return undefined;
499
+ }
500
+ const n = Number(value);
501
+ if (!Number.isFinite(n) || n < 0 || n > 1) {
502
+ throw usageError(`--min-score wants a number from 0 to 1, got ${value}`, USAGE);
503
+ }
504
+ return n;
505
+ }
506
+ // ---------------------------------------------------------------------------
507
+ // grep
508
+ // ---------------------------------------------------------------------------
509
+ /**
510
+ * The exhaustive read, and the counterpart to recall rather than a variant of
511
+ * it. Recall ranks, and a ranking returns the top of a list, so it cannot tell
512
+ * "there is nothing" from "nothing was close enough" — which is the question
513
+ * anyone actually has when they come to a memory looking for a name, a path or
514
+ * a command they think an agent wrote down.
515
+ */
516
+ async function grep(o, rest, flags, asJson) {
517
+ const pattern = rest[0];
518
+ if (!pattern || rest.length > 1) {
519
+ throw usageError('grep takes exactly one pattern', 'zen memory grep <pattern>');
520
+ }
521
+ const limit = Number(flags.limit ?? LISTED);
522
+ if (!Number.isInteger(limit) || limit < 1) {
523
+ throw usageError(`--limit wants a positive whole number, got ${flags.limit}`, USAGE);
524
+ }
525
+ for (const field of flags.in ?? []) {
526
+ if (!GREP_FIELDS.includes(field)) {
527
+ throw usageError(`--in wants one of ${GREP_FIELDS.join(', ')}, got ${field}`, USAGE);
528
+ }
529
+ }
530
+ let res;
531
+ try {
532
+ res = await grepMemory(o.store, pattern, {
533
+ // Unmasked, like everything else here: this is the person who owns
534
+ // the files, not an agent inside a run.
535
+ kinds: flags.kind ? [flags.kind] : undefined,
536
+ audience: flags.audience,
537
+ files: flags.files,
538
+ in: flags.in,
539
+ stale: flags.stale ? 'only' : flags.all ? 'include' : 'exclude',
540
+ regex: flags.regex,
541
+ caseSensitive: flags['case-sensitive'],
542
+ limit,
543
+ });
544
+ }
545
+ catch (err) {
546
+ if (err instanceof MemoryError) {
547
+ throw new CliError(err.message, EXIT.usage, err.hint);
548
+ }
549
+ throw err;
550
+ }
551
+ if (asJson) {
552
+ json({
553
+ found: res.found,
554
+ truncated: res.truncated,
555
+ matches: res.matches.map((m) => ({
556
+ id: m.node.id,
557
+ kind: m.node.kind,
558
+ stale: m.stale,
559
+ file: m.node.file ? hostPath(o.dir, m.node.file) : undefined,
560
+ hits: m.hits,
561
+ more: m.more,
562
+ })),
563
+ unsearched: res.skipped.map((s) => ({ ...s, path: hostPath(o.dir, s) })),
564
+ });
565
+ return;
566
+ }
567
+ if (flags['ids-only']) {
568
+ writeAll(res.matches.map((m) => m.node.id));
569
+ return;
570
+ }
571
+ if (!res.found) {
572
+ note(`nothing matches ${pattern}`);
573
+ unsearched(o, res.skipped);
574
+ return;
575
+ }
576
+ for (const m of res.matches) {
577
+ write(cyan(m.node.id) +
578
+ ' ' +
579
+ m.node.kind +
580
+ (m.stale ? ' ' + yellow('superseded') : '') +
581
+ (m.node.file ? ' ' + dim(hostPath(o.dir, m.node.file)) : ''));
582
+ writeAll(table(m.hits.map((h) => [` ${dim(h.where + ':' + h.line)}`, clip(h.text, 120)])));
583
+ if (m.more) {
584
+ write(dim(` … ${count(m.more, 'more line')} in this node`));
585
+ }
586
+ write();
587
+ }
588
+ if (res.truncated) {
589
+ note(dim(`${res.found - res.matches.length} more nodes — raise --limit`));
590
+ }
591
+ unsearched(o, res.skipped);
592
+ }
593
+ /**
594
+ * Named rather than counted. A file that was not read is a hole in an answer
595
+ * whose whole value is that it has none, so the ids have to be printable.
596
+ */
597
+ function unsearched(o, skipped) {
598
+ if (!skipped.length) {
599
+ return;
600
+ }
601
+ write();
602
+ note(yellow(`${count(skipped.length, 'file')} not searched`));
603
+ writeAll(table(skipped.map((s) => [` ${cyan(s.id)}`, dim(s.reason), hostPath(o.dir, s)])));
604
+ }
605
+ // ---------------------------------------------------------------------------
268
606
  // show
269
607
  // ---------------------------------------------------------------------------
270
608
  function show(o, ids, asJson) {
@@ -278,7 +616,11 @@ function show(o, ids, asJson) {
278
616
  }
279
617
  const links = o.store.graph.neighbors(id);
280
618
  if (asJson) {
281
- json({ node, links, file: node.file ? hostPath(o.dir, node.file) : undefined });
619
+ json({
620
+ node: onHost(o, node),
621
+ links,
622
+ file: node.file ? hostPath(o.dir, node.file) : undefined,
623
+ });
282
624
  return;
283
625
  }
284
626
  write(bold(node.id) + ' ' + cyan(node.kind));
@@ -323,6 +665,82 @@ function show(o, ids, asJson) {
323
665
  })));
324
666
  }
325
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
+ // ---------------------------------------------------------------------------
326
744
  // export
327
745
  // ---------------------------------------------------------------------------
328
746
  async function write_(o, rest, flags, cwd, asJson) {
@@ -331,7 +749,8 @@ async function write_(o, rest, flags, cwd, asJson) {
331
749
  }
332
750
  const named = rest[0] ?? flags.out ?? 'memory.html';
333
751
  const target = isAbsolute(named) ? named : resolve(cwd, named);
334
- const report = await buildMemoryReport(o.store, { title: `${o.project} · memory` });
752
+ const built = await buildMemoryReport(o.store, { title: `${o.project} · memory` });
753
+ const report = { ...built, nodes: built.nodes.map((n) => onHost(o, n)) };
335
754
  await writeFile(target, renderMemoryHtml(report), 'utf8');
336
755
  if (asJson) {
337
756
  json({ path: target, nodes: report.nodes.length, edges: report.edges.length });
@@ -390,7 +809,7 @@ async function merge(o, dirs, flags, cwd, asJson) {
390
809
  }
391
810
  catch (err) {
392
811
  if (err instanceof MergeConflicts) {
393
- throw diverged(err, cwd, asJson);
812
+ throw diverged(err, asJson);
394
813
  }
395
814
  throw err;
396
815
  }
@@ -401,7 +820,7 @@ async function merge(o, dirs, flags, cwd, asJson) {
401
820
  writeAll(table([
402
821
  [dim('memory'), dim('nodes'), dim('new'), dim('shared'), dim('folded'), dim('files')],
403
822
  ...report.sources.map((s) => [
404
- near(s.dir, cwd),
823
+ s.dir,
405
824
  String(s.nodes),
406
825
  s.added ? green(String(s.added)) : '0',
407
826
  String(s.shared),
@@ -422,7 +841,7 @@ async function merge(o, dirs, flags, cwd, asJson) {
422
841
  * point of stopping is that a person decides, and they cannot decide from a
423
842
  * count.
424
843
  */
425
- function diverged(err, cwd, asJson) {
844
+ function diverged(err, asJson) {
426
845
  if (asJson) {
427
846
  json({ error: err.message, conflicts: err.conflicts });
428
847
  }
@@ -431,7 +850,7 @@ function diverged(err, cwd, asJson) {
431
850
  cyan(c.id),
432
851
  dim(`r${c.mine} ↔ r${c.theirs}`),
433
852
  clip(c.text, 48),
434
- dim(near(c.dir, cwd)),
853
+ dim(c.dir),
435
854
  ])));
436
855
  write();
437
856
  }
@@ -488,9 +907,19 @@ function clip(text, n) {
488
907
  const one = text.replace(/\s+/g, ' ').trim();
489
908
  return one.length > n ? one.slice(0, n - 1) + '…' : one;
490
909
  }
491
- /** A path as it was probably typed: relative when it is below here, absolute when it is not. */
492
- function near(dir, cwd) {
493
- const rel = relative(cwd, dir);
494
- return rel && !rel.startsWith('..') ? rel : dir;
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;
495
924
  }
496
925
  //# sourceMappingURL=memory.js.map