@zenera/cli 1.1.10 → 1.1.12
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 +69 -15
- package/dist/banner.js +69 -47
- package/dist/cache.d.ts +98 -0
- package/dist/cache.js +301 -0
- package/dist/catalog.d.ts +3 -0
- package/dist/catalog.js +35 -11
- package/dist/commands/cache.d.ts +7 -0
- package/dist/commands/cache.js +245 -0
- package/dist/commands/check.js +6 -3
- package/dist/commands/index.js +5 -0
- package/dist/commands/init.js +8 -4
- package/dist/commands/key.js +68 -14
- package/dist/commands/memory.d.ts +3 -0
- package/dist/commands/memory.js +375 -0
- package/dist/commands/models.js +17 -1
- package/dist/commands/run.js +11 -3
- package/dist/commands/sandbox.js +70 -23
- package/dist/engine.js +14 -9
- package/dist/home.d.ts +2 -2
- package/dist/home.js +2 -2
- package/dist/keys.d.ts +22 -0
- package/dist/keys.js +105 -2
- package/dist/lib.d.ts +1 -0
- package/dist/lib.js +1 -0
- package/dist/liveness.js +11 -0
- package/dist/main.js +9 -0
- package/dist/podman.js +43 -9
- package/dist/projects.js +10 -2
- package/dist/resolve.d.ts +4 -0
- package/dist/resolve.js +43 -17
- package/dist/scaffold.js +22 -1
- package/dist/term.d.ts +23 -2
- package/dist/term.js +215 -8
- package/dist/tui/app.d.ts +10 -0
- package/dist/tui/app.js +478 -54
- package/dist/tui/theme.d.ts +13 -2
- package/dist/tui/theme.js +26 -13
- package/dist/tui/wrap.d.ts +92 -0
- package/dist/tui/wrap.js +147 -2
- package/dist/validate.d.ts +2 -0
- package/dist/validate.js +87 -2
- package/package.json +2 -2
- package/templates/editor/.github/copilot-instructions.md +200 -55
- package/templates/editor/.github/prompts/new-agent.prompt.md +5 -2
- package/templates/editor/.github/prompts/review-project.prompt.md +5 -0
- package/templates/editor/.github/prompts/sync-with-spec.prompt.md +11 -4
- package/templates/editor/.github/skills/zen-cli/SKILL.md +3 -0
- package/templates/editor/.github/skills/zen-cli/references/faker.md +18 -8
- package/templates/editor/.github/skills/zen-cli/references/keys.md +7 -7
- package/templates/editor/.github/skills/zen-cli/references/memory.md +113 -0
- package/templates/editor/.github/skills/zen-cli/references/rag.md +2 -2
- package/templates/editor/.github/skills/zen-cli/references/run.md +3 -1
- package/templates/editor/.github/skills/zen-memory/SKILL.md +555 -0
- package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +575 -0
- package/templates/editor/.github/skills/{api-schema-index → zen-rag-schema}/SKILL.md +4 -4
- package/templates/editor/.vscode/settings.json +4 -2
- package/templates/parts/exa.prompt.md.tmpl +4 -0
- package/templates/parts/exa.spec.md.tmpl +2 -0
- package/templates/project/INSTRUCTIONS.md +8 -0
- package/templates/project/SPECIFICATION.md.tmpl +47 -0
- package/templates/project/agents/prompts/{default.md → default.md.tmpl} +4 -0
- package/templates/project/agents.yaml.tmpl +4 -0
- package/templates/project/gitignore +7 -1
- package/templates/project/scripts/_setup.sh +101 -0
- package/templates/project/SPECIFICATION.md +0 -6
package/dist/commands/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { cache } from "./cache.js";
|
|
1
2
|
import { check } from "./check.js";
|
|
2
3
|
import { init } from "./init.js";
|
|
3
4
|
import { inspect } from "./inspect.js";
|
|
4
5
|
import { key } from "./key.js";
|
|
5
6
|
import { list } from "./list.js";
|
|
7
|
+
import { memory } from "./memory.js";
|
|
6
8
|
import { models } from "./models.js";
|
|
7
9
|
import { open } from "./open.js";
|
|
8
10
|
import { run } from "./run.js";
|
|
@@ -22,7 +24,9 @@ export const COMMANDS = {
|
|
|
22
24
|
models,
|
|
23
25
|
check,
|
|
24
26
|
inspect,
|
|
27
|
+
memory,
|
|
25
28
|
sandbox,
|
|
29
|
+
cache,
|
|
26
30
|
version,
|
|
27
31
|
};
|
|
28
32
|
/** Names that are not listed in help but still work. */
|
|
@@ -34,6 +38,7 @@ export const ALIASES = {
|
|
|
34
38
|
validate: 'check',
|
|
35
39
|
doctor: 'check',
|
|
36
40
|
report: 'inspect',
|
|
41
|
+
mem: 'memory',
|
|
37
42
|
edit: 'open',
|
|
38
43
|
code: 'open',
|
|
39
44
|
mock: 'faker',
|
package/dist/commands/init.js
CHANGED
|
@@ -96,10 +96,14 @@ export const init = {
|
|
|
96
96
|
summary: 'Create a project here, or in <dir>, and register it.',
|
|
97
97
|
usage: USAGE,
|
|
98
98
|
details: [
|
|
99
|
-
'Writes INSTRUCTIONS.md, agents.yaml
|
|
100
|
-
'directory so `zen list` and `zen open` can
|
|
101
|
-
'(.vscode/settings.json and the .github/
|
|
102
|
-
'alongside, replacing any already there.',
|
|
99
|
+
'Writes SPECIFICATION.md, INSTRUCTIONS.md, agents.yaml, agents/ and',
|
|
100
|
+
'scripts/, then records the directory so `zen list` and `zen open` can',
|
|
101
|
+
'find it by name. Editor files (.vscode/settings.json and the .github/',
|
|
102
|
+
'tree) are written alongside, replacing any already there.',
|
|
103
|
+
'',
|
|
104
|
+
'SPECIFICATION.md states what the project is for and what it may do,',
|
|
105
|
+
'and describes the one that was just written. Say what you are building',
|
|
106
|
+
'there, then run /sync-with-spec in your editor to make the rest match.',
|
|
103
107
|
'',
|
|
104
108
|
'The default agent gets the file tools and a sandboxed shell, plus',
|
|
105
109
|
'`exa:*` when the keyring holds an Exa key. Without --model, the',
|
package/dist/commands/key.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { parse } from "../args.js";
|
|
3
3
|
import { ensureHome } from "../home.js";
|
|
4
|
-
import { ambient, ambientId, assertNotEmpty, describe, envNames, envOf, keyId, KeyStore, mask, OWNERS, parseRef, SHAPES, } from "../keys.js";
|
|
4
|
+
import { ambient, ambientId, assertNotEmpty, checkRegion, describe, envNames, envOf, formOf, keyId, KeyStore, mask, OWNERS, parseRef, SHAPES, } from "../keys.js";
|
|
5
5
|
import { probe, probeAll } from "../liveness.js";
|
|
6
6
|
import { ago, ask, askSecret, bold, confirm, credentialError, cyan, dim, green, isInteractive, json, note, progress, readStdin, red, table, usageError, write, writeAll, yellow, } from "../term.js";
|
|
7
7
|
const USAGE = 'zen key <ls|add|use|check|rm|show|env> [ref] [options]';
|
|
@@ -17,6 +17,32 @@ const MARK = {
|
|
|
17
17
|
function state(entry) {
|
|
18
18
|
return entry.check ? MARK[entry.check.state] : dim('unchecked');
|
|
19
19
|
}
|
|
20
|
+
/** Where a Vertex project id came from, since only one of the three was typed. */
|
|
21
|
+
const ORIGIN = {
|
|
22
|
+
env: 'from $GOOGLE_CLOUD_PROJECT',
|
|
23
|
+
stored: 'from --gcp-project',
|
|
24
|
+
file: 'from the key file',
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Only a Vertex service account addresses a project and a region. An unset
|
|
28
|
+
* region is shown as the `global` it will actually be, not left blank: it is
|
|
29
|
+
* the slow endpoint, and a blank column is how it goes unnoticed. The project
|
|
30
|
+
* is shown whether or not one was stored, because the file names one too and a
|
|
31
|
+
* blank there reads as "none" rather than "not written down here". Yellow when
|
|
32
|
+
* the environment supplied either, because then the stored one is not in play.
|
|
33
|
+
*/
|
|
34
|
+
function placed(store, entry) {
|
|
35
|
+
if (entry.provider !== 'vertex' || entry.holds !== 'file') {
|
|
36
|
+
return '';
|
|
37
|
+
}
|
|
38
|
+
const fromEnv = process.env.GOOGLE_CLOUD_LOCATION;
|
|
39
|
+
const where = fromEnv ?? entry.location ?? 'global';
|
|
40
|
+
const project = store.projectOf(entry);
|
|
41
|
+
const id = project ? project.id : '';
|
|
42
|
+
return [fromEnv ? yellow(where) : dim(where), project?.from === 'env' ? yellow(id) : dim(id)]
|
|
43
|
+
.join(' ')
|
|
44
|
+
.trimEnd();
|
|
45
|
+
}
|
|
20
46
|
/**
|
|
21
47
|
* An ambient credential borrowed into the shape the rest of this file works
|
|
22
48
|
* in. It is not in the store and never will be: `store.find` misses it, so
|
|
@@ -35,7 +61,7 @@ function asEntry(cred) {
|
|
|
35
61
|
}
|
|
36
62
|
function rows(store, borrowed) {
|
|
37
63
|
const out = [
|
|
38
|
-
[bold(''), bold('KEY'), bold('VALUE'), bold('STATE'), bold('CHECKED')],
|
|
64
|
+
[bold(''), bold('KEY'), bold('VALUE'), bold('GCP'), bold('STATE'), bold('CHECKED')],
|
|
39
65
|
];
|
|
40
66
|
for (const provider of OWNERS) {
|
|
41
67
|
for (const entry of store.for(provider)) {
|
|
@@ -44,6 +70,7 @@ function rows(store, borrowed) {
|
|
|
44
70
|
store.isActive(entry) ? green('*') : ' ',
|
|
45
71
|
keyId(entry),
|
|
46
72
|
dim(describe(store, entry)),
|
|
73
|
+
placed(store, entry),
|
|
47
74
|
state(entry),
|
|
48
75
|
dim(entry.check ? ago(entry.check.at) : '—') +
|
|
49
76
|
(shadow && store.isActive(entry) ? yellow(` shadowed by $${shadow}`) : ''),
|
|
@@ -54,6 +81,7 @@ function rows(store, borrowed) {
|
|
|
54
81
|
dim('~'),
|
|
55
82
|
dim(ambientId(cred)),
|
|
56
83
|
dim(describe(store, entry)),
|
|
84
|
+
placed(store, entry),
|
|
57
85
|
state(entry),
|
|
58
86
|
dim(cred.env ? 'from the environment' : 'from gcloud'),
|
|
59
87
|
]);
|
|
@@ -133,9 +161,11 @@ const add = async (ctx, args) => {
|
|
|
133
161
|
const { values, positionals } = parse(args, {
|
|
134
162
|
name: { type: 'string' },
|
|
135
163
|
'no-check': { type: 'boolean' },
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
164
|
+
// Prefixed because every other command's --project is a Zenera one.
|
|
165
|
+
'gcp-project': { type: 'string' },
|
|
166
|
+
'gcp-location': { type: 'string' },
|
|
167
|
+
}, 'zen key add <provider>[/name] [--name <name>] [--gcp-project <id>] ' +
|
|
168
|
+
'[--gcp-location <region>] [--no-check]');
|
|
139
169
|
const ref = positionals[0];
|
|
140
170
|
if (!ref) {
|
|
141
171
|
throw usageError('which provider?', `one of: ${OWNERS.join(', ')}`);
|
|
@@ -144,8 +174,14 @@ const add = async (ctx, args) => {
|
|
|
144
174
|
const provider = parsed.provider;
|
|
145
175
|
const name = values.name ?? parsed.name ?? 'default';
|
|
146
176
|
const shape = SHAPES[provider];
|
|
147
|
-
if ((values
|
|
148
|
-
throw usageError(`--project and --location mean nothing to ${shape.label}`, 'they configure a Vertex service account');
|
|
177
|
+
if ((values['gcp-project'] || values['gcp-location']) && provider !== 'vertex') {
|
|
178
|
+
throw usageError(`--gcp-project and --gcp-location mean nothing to ${shape.label}`, 'they configure a Vertex service account');
|
|
179
|
+
}
|
|
180
|
+
// Checked before the prompt, so a typo does not cost you pasting the key.
|
|
181
|
+
const region = values['gcp-location'];
|
|
182
|
+
if (region !== undefined && !checkRegion(region)) {
|
|
183
|
+
note(yellow(`${region} is not a region this build knows; storing it anyway`));
|
|
184
|
+
note(dim(' if it is new, this is fine; if it is a typo, Vertex answers 404'));
|
|
149
185
|
}
|
|
150
186
|
ensureHome();
|
|
151
187
|
const store = await KeyStore.open();
|
|
@@ -164,9 +200,14 @@ const add = async (ctx, args) => {
|
|
|
164
200
|
if (shape.forms.length === 1 && shape.forms[0].holds === 'file' && !existsSync(raw)) {
|
|
165
201
|
throw usageError(`no such file: ${raw}`);
|
|
166
202
|
}
|
|
203
|
+
// Which shape was given is only knowable now, and `add` would drop these
|
|
204
|
+
// two on an express key rather than store a combination Vertex refuses.
|
|
205
|
+
if (formOf(provider, raw).holds !== 'file' && (values['gcp-project'] || region)) {
|
|
206
|
+
throw usageError('--gcp-project and --gcp-location need a service-account file', 'the value given is an express-mode key, which addresses no project or region');
|
|
207
|
+
}
|
|
167
208
|
const entry = store.add(provider, name, raw, {
|
|
168
|
-
project: values
|
|
169
|
-
location: values
|
|
209
|
+
project: values['gcp-project'],
|
|
210
|
+
location: values['gcp-location'],
|
|
170
211
|
});
|
|
171
212
|
// Verified before it is trusted, but stored either way: a key that cannot
|
|
172
213
|
// be checked right now — offline, behind a proxy — is not a key that is
|
|
@@ -186,6 +227,12 @@ const add = async (ctx, args) => {
|
|
|
186
227
|
note(dim(` ${check.fix}`));
|
|
187
228
|
}
|
|
188
229
|
}
|
|
230
|
+
else if (check.state === 'unknown' && check.fix) {
|
|
231
|
+
// Most unknowns are a plane or a proxy and say nothing worth a
|
|
232
|
+
// line. One carrying a fix is different: we know what is wrong.
|
|
233
|
+
note(`${yellow('unverified')} ${check.detail}`);
|
|
234
|
+
note(dim(` ${check.fix}`));
|
|
235
|
+
}
|
|
189
236
|
}
|
|
190
237
|
store.save();
|
|
191
238
|
if (ctx.json) {
|
|
@@ -203,7 +250,10 @@ const add = async (ctx, args) => {
|
|
|
203
250
|
note(yellow(`$${envOf(entry)} is set and will win over this`));
|
|
204
251
|
}
|
|
205
252
|
if (provider === 'vertex' && entry.holds === 'file' && !entry.project) {
|
|
206
|
-
|
|
253
|
+
const found = store.projectOf(entry);
|
|
254
|
+
note(dim(found
|
|
255
|
+
? `no --gcp-project given; project ${found.id}, read from the key file`
|
|
256
|
+
: 'no --gcp-project given, and the key file names no project_id'));
|
|
207
257
|
}
|
|
208
258
|
};
|
|
209
259
|
/**
|
|
@@ -329,12 +379,14 @@ const show = async (ctx, args) => {
|
|
|
329
379
|
throw usageError(`no key ${ref}`, 'see: zen key ls');
|
|
330
380
|
}
|
|
331
381
|
const value = values.reveal ? store.reveal(entry) : describe(store, entry);
|
|
382
|
+
const project = store.projectOf(entry);
|
|
332
383
|
if (ctx.json) {
|
|
333
384
|
json({
|
|
334
385
|
key: keyId(entry),
|
|
335
386
|
env: envOf(entry),
|
|
336
387
|
value,
|
|
337
388
|
revealed: Boolean(values.reveal),
|
|
389
|
+
...(project ? { project: project.id, projectFrom: project.from } : {}),
|
|
338
390
|
});
|
|
339
391
|
return;
|
|
340
392
|
}
|
|
@@ -348,8 +400,10 @@ const show = async (ctx, args) => {
|
|
|
348
400
|
[dim('key'), keyId(entry)],
|
|
349
401
|
[dim('env'), envOf(entry)],
|
|
350
402
|
[dim('value'), value],
|
|
351
|
-
...(
|
|
352
|
-
|
|
403
|
+
...(project
|
|
404
|
+
? [[dim('gcp project'), `${project.id} ${dim(ORIGIN[project.from])}`]]
|
|
405
|
+
: []),
|
|
406
|
+
...(entry.location ? [[dim('gcp location'), entry.location]] : []),
|
|
353
407
|
[dim('state'), state(entry)],
|
|
354
408
|
...(entry.check?.fix ? [[dim('fix'), entry.check.fix]] : []),
|
|
355
409
|
[dim('added'), ago(entry.addedAt)],
|
|
@@ -416,8 +470,8 @@ export const key = {
|
|
|
416
470
|
'Services the tools call: exa.',
|
|
417
471
|
'',
|
|
418
472
|
'Vertex takes either shape: a service-account JSON file, which wants',
|
|
419
|
-
'--project and --location too, or an express-mode API key, which
|
|
420
|
-
'neither. Which one you gave is read off the value.',
|
|
473
|
+
'--gcp-project and --gcp-location too, or an express-mode API key, which',
|
|
474
|
+
'wants neither. Which one you gave is read off the value.',
|
|
421
475
|
'',
|
|
422
476
|
' zen key ls [--check] Everything stored, and its state.',
|
|
423
477
|
' zen key add <provider>[/name] Read a key from stdin, or ask for it.',
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import { ALL_AGENTS, buildMemoryReport, hostPath, memoryDir, MemoryIndex, MemoryStore, readProjectConfig, renderMemoryHtml, } from '@zenera/neo';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import { writeFile } from 'node:fs/promises';
|
|
5
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
6
|
+
import { parse } from "../args.js";
|
|
7
|
+
import { project as resolveProject } from "../resolve.js";
|
|
8
|
+
import { ago, bold, bytes, confirm, count, cyan, dim, green, isInteractive, json, note, table, usageError, write, writeAll, yellow, } from "../term.js";
|
|
9
|
+
const USAGE = 'zen memory [stats|ls|show|export|forget] [args] [options]';
|
|
10
|
+
const SUBCOMMANDS = ['stats', 'ls', 'show', 'export', 'forget'];
|
|
11
|
+
/** Enough to see the shape of it; the rest is a number. */
|
|
12
|
+
const LISTED = 30;
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// zen memory
|
|
15
|
+
//
|
|
16
|
+
// The graph from outside the agents, and the reason it needs a command is that
|
|
17
|
+
// there is otherwise no way to answer the question memory always eventually
|
|
18
|
+
// raises: *why did it recall that?* Recall is masked, ranked and truncated by
|
|
19
|
+
// design, so what an agent sees is never the whole picture — and when the
|
|
20
|
+
// picture is what is wrong, you need the part that was hidden.
|
|
21
|
+
//
|
|
22
|
+
// So everything here reads the graph unmasked. That is safe because it is
|
|
23
|
+
// local: this command is a person at a terminal in the project directory, not
|
|
24
|
+
// an agent inside a run, and the mask exists to keep agents apart rather than
|
|
25
|
+
// to keep secrets from the person who owns the files.
|
|
26
|
+
//
|
|
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.
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
export const memory = {
|
|
32
|
+
summary: 'What the agents remember, and getting rid of it.',
|
|
33
|
+
usage: USAGE,
|
|
34
|
+
details: [
|
|
35
|
+
' stats Size, vocabulary, and whether it is embedded.',
|
|
36
|
+
' ls Nodes, newest first. Changes nothing.',
|
|
37
|
+
' show <id> One node in full, with what it links to.',
|
|
38
|
+
' export [file] The whole graph as one HTML page.',
|
|
39
|
+
' forget <id...> Remove nodes, their vectors and their files.',
|
|
40
|
+
'',
|
|
41
|
+
' --project <name|dir> Which project. Defaults to the one you are in.',
|
|
42
|
+
' --kind <name> Only this kind of node.',
|
|
43
|
+
' --audience <name> Only nodes committed under this label.',
|
|
44
|
+
' --files Only nodes that remember a file.',
|
|
45
|
+
' --stale Only nodes something has superseded.',
|
|
46
|
+
' --limit <n> Rows to list. Default 30.',
|
|
47
|
+
' --out <file> Where `export` writes. Default memory.html.',
|
|
48
|
+
' --open Open the exported page.',
|
|
49
|
+
' --yes Do not ask before removing.',
|
|
50
|
+
'',
|
|
51
|
+
'Everything here reads the graph *unmasked* — every audience, including',
|
|
52
|
+
'what no agent can see. That is the point: when a mask is the thing',
|
|
53
|
+
'that is wrong, the hidden part is what you need to look at.',
|
|
54
|
+
'',
|
|
55
|
+
'`export` is the one to reach for. It writes a single self-contained',
|
|
56
|
+
'page: the node list on the left, the graph in the middle, and whatever',
|
|
57
|
+
'you click on the right, file contents and all.',
|
|
58
|
+
],
|
|
59
|
+
run: async (ctx) => {
|
|
60
|
+
const { values, positionals } = parse(ctx.args, {
|
|
61
|
+
project: { type: 'string' },
|
|
62
|
+
kind: { type: 'string' },
|
|
63
|
+
audience: { type: 'string' },
|
|
64
|
+
files: { type: 'boolean' },
|
|
65
|
+
stale: { type: 'boolean' },
|
|
66
|
+
limit: { type: 'string' },
|
|
67
|
+
out: { type: 'string' },
|
|
68
|
+
open: { type: 'boolean' },
|
|
69
|
+
yes: { type: 'boolean' },
|
|
70
|
+
}, USAGE);
|
|
71
|
+
const what = positionals[0] ?? 'stats';
|
|
72
|
+
if (!SUBCOMMANDS.includes(what)) {
|
|
73
|
+
throw usageError(`unknown subcommand: ${what}`, USAGE);
|
|
74
|
+
}
|
|
75
|
+
const rest = positionals.slice(1);
|
|
76
|
+
const opened = await open(ctx.cwd, values.project);
|
|
77
|
+
try {
|
|
78
|
+
switch (what) {
|
|
79
|
+
case 'stats':
|
|
80
|
+
return stats(opened, ctx.json);
|
|
81
|
+
case 'ls':
|
|
82
|
+
return list(opened, values, ctx.json);
|
|
83
|
+
case 'show':
|
|
84
|
+
return show(opened, rest, ctx.json);
|
|
85
|
+
case 'export':
|
|
86
|
+
return await write_(opened, rest, values, ctx.cwd, ctx.json);
|
|
87
|
+
default:
|
|
88
|
+
return await forget(opened, rest, values, ctx.json);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
opened.store.release();
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* The store, opened directly rather than through `loadProject`. Loading the
|
|
98
|
+
* project would resolve models and read every prompt file to inspect a graph
|
|
99
|
+
* that needs none of it — and would fail on a project whose credentials are
|
|
100
|
+
* missing, which is exactly when someone is debugging.
|
|
101
|
+
*/
|
|
102
|
+
async function open(cwd, want) {
|
|
103
|
+
const project = await resolveProject({ cwd, project: want });
|
|
104
|
+
const { config } = readProjectConfig(project.dir);
|
|
105
|
+
const dir = memoryDir(project.dir, config);
|
|
106
|
+
if (!dir) {
|
|
107
|
+
throw usageError(`${project.name} has no memory`, 'give an agent `memory: true` in agents.yaml, or add a `memory:` block');
|
|
108
|
+
}
|
|
109
|
+
if (!existsSync(join(dir, 'manifest.json'))) {
|
|
110
|
+
throw usageError(`${project.name} has a memory configured but nothing in it yet`, 'it is written the first time an agent commits something');
|
|
111
|
+
}
|
|
112
|
+
return { store: await MemoryStore.open(dir), dir, project: project.name };
|
|
113
|
+
}
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// stats
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
function stats(o, asJson) {
|
|
118
|
+
const nodes = o.store.graph.nodes();
|
|
119
|
+
const edges = o.store.graph.edges();
|
|
120
|
+
const stale = o.store.graph.superseded();
|
|
121
|
+
const vectors = o.store.vectors;
|
|
122
|
+
const files = nodes.filter((n) => n.file);
|
|
123
|
+
const byKind = tally(nodes.map((n) => n.kind));
|
|
124
|
+
const byRelation = tally(edges.map((e) => e.relation));
|
|
125
|
+
const byAudience = tally(nodes.flatMap((n) => n.audience));
|
|
126
|
+
if (asJson) {
|
|
127
|
+
json({
|
|
128
|
+
dir: o.dir,
|
|
129
|
+
nodes: nodes.length,
|
|
130
|
+
edges: edges.length,
|
|
131
|
+
superseded: stale.size,
|
|
132
|
+
files: {
|
|
133
|
+
count: files.length,
|
|
134
|
+
bytes: files.reduce((t, n) => t + (n.file?.bytes ?? 0), 0),
|
|
135
|
+
},
|
|
136
|
+
embedding: o.store.embedding,
|
|
137
|
+
vectors: vectors ? { rows: vectors.rows, dims: vectors.dims } : undefined,
|
|
138
|
+
kinds: byKind,
|
|
139
|
+
relations: byRelation,
|
|
140
|
+
audiences: byAudience,
|
|
141
|
+
});
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (!nodes.length) {
|
|
145
|
+
note(`nothing remembered yet ${dim(o.dir)}`);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
write(bold(o.project) + dim(' · ' + o.dir));
|
|
149
|
+
write();
|
|
150
|
+
writeAll(table([
|
|
151
|
+
[dim('nodes'), String(nodes.length)],
|
|
152
|
+
[dim('edges'), String(edges.length)],
|
|
153
|
+
[dim('superseded'), String(stale.size)],
|
|
154
|
+
[
|
|
155
|
+
dim('files'),
|
|
156
|
+
files.length
|
|
157
|
+
? `${files.length} · ${bytes(files.reduce((t, n) => t + (n.file?.bytes ?? 0), 0))}`
|
|
158
|
+
: '0',
|
|
159
|
+
],
|
|
160
|
+
[
|
|
161
|
+
dim('embedding'),
|
|
162
|
+
o.store.embedding
|
|
163
|
+
? `${o.store.embedding.model} ${dim(o.store.embedding.dimensions + 'd')}`
|
|
164
|
+
: yellow('none — recall falls back to term overlap'),
|
|
165
|
+
],
|
|
166
|
+
[dim('vectors'), vectors ? `${vectors.rows} rows` : dim('—')],
|
|
167
|
+
]));
|
|
168
|
+
// A node without a vector cannot be found by meaning, only by luck. It is
|
|
169
|
+
// the one silent failure this store has, so it is called out by name.
|
|
170
|
+
if (vectors && vectors.rows < nodes.length) {
|
|
171
|
+
write();
|
|
172
|
+
note(yellow(`${count(nodes.length - vectors.rows, 'node')} with no vector`));
|
|
173
|
+
}
|
|
174
|
+
section('kinds', byKind);
|
|
175
|
+
section('relations', byRelation);
|
|
176
|
+
section('audiences', byAudience);
|
|
177
|
+
}
|
|
178
|
+
function section(title, counts) {
|
|
179
|
+
const rows = Object.entries(counts).sort((a, b) => b[1] - a[1]);
|
|
180
|
+
if (!rows.length) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
write();
|
|
184
|
+
write(dim(title));
|
|
185
|
+
writeAll(table(rows.map(([k, n]) => [` ${cyan(k)}`, String(n)])));
|
|
186
|
+
}
|
|
187
|
+
function tally(values) {
|
|
188
|
+
const out = {};
|
|
189
|
+
for (const v of values) {
|
|
190
|
+
out[v] = (out[v] ?? 0) + 1;
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
// ls
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
function list(o, flags, asJson) {
|
|
198
|
+
const stale = o.store.graph.superseded();
|
|
199
|
+
const limit = Number(flags.limit ?? LISTED);
|
|
200
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
201
|
+
throw usageError(`--limit wants a positive whole number, got ${flags.limit}`, USAGE);
|
|
202
|
+
}
|
|
203
|
+
const all = o.store.graph
|
|
204
|
+
.nodes()
|
|
205
|
+
.filter((n) => !flags.kind || n.kind === flags.kind)
|
|
206
|
+
.filter((n) => !flags.audience || n.audience.includes(flags.audience))
|
|
207
|
+
.filter((n) => !flags.files || n.file)
|
|
208
|
+
.filter((n) => !flags.stale || stale.has(n.id))
|
|
209
|
+
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
210
|
+
if (asJson) {
|
|
211
|
+
json({ total: all.length, nodes: all.slice(0, limit) });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
if (!all.length) {
|
|
215
|
+
note('nothing matches');
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
writeAll(table(all
|
|
219
|
+
.slice(0, limit)
|
|
220
|
+
.map((n) => [
|
|
221
|
+
cyan(n.id),
|
|
222
|
+
n.kind,
|
|
223
|
+
n.file ? dim('⎘ ' + n.file.format) : '',
|
|
224
|
+
clip(n.text, 56),
|
|
225
|
+
dim(ago(n.createdAt)),
|
|
226
|
+
stale.has(n.id) ? yellow('superseded') : '',
|
|
227
|
+
])));
|
|
228
|
+
if (all.length > limit) {
|
|
229
|
+
write();
|
|
230
|
+
note(dim(`${all.length - limit} more — raise --limit, or use \`zen memory export\``));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
// show
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
function show(o, ids, asJson) {
|
|
237
|
+
const id = ids[0];
|
|
238
|
+
if (!id || ids.length > 1) {
|
|
239
|
+
throw usageError('show takes exactly one id', 'zen memory show <id>');
|
|
240
|
+
}
|
|
241
|
+
const node = o.store.graph.get(id);
|
|
242
|
+
if (!node) {
|
|
243
|
+
throw usageError(`no node ${id}`, 'list them with `zen memory ls`');
|
|
244
|
+
}
|
|
245
|
+
const links = o.store.graph.neighbors(id);
|
|
246
|
+
if (asJson) {
|
|
247
|
+
json({ node, links, file: node.file ? hostPath(o.dir, node.file) : undefined });
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
write(bold(node.id) + ' ' + cyan(node.kind));
|
|
251
|
+
write();
|
|
252
|
+
writeAll(table([
|
|
253
|
+
[dim('revision'), String(node.revision)],
|
|
254
|
+
[dim('used'), `${node.useCount}× · last ${ago(node.lastUsedAt)}`],
|
|
255
|
+
[dim('created'), `${node.createdAt} ${dim(ago(node.createdAt))}`],
|
|
256
|
+
[dim('updated'), node.updatedAt],
|
|
257
|
+
[dim('audience'), node.audience.join(', ')],
|
|
258
|
+
]));
|
|
259
|
+
write();
|
|
260
|
+
write(node.text);
|
|
261
|
+
if (node.metadata) {
|
|
262
|
+
write();
|
|
263
|
+
write(dim('metadata'));
|
|
264
|
+
write(JSON.stringify(node.metadata, null, 2));
|
|
265
|
+
}
|
|
266
|
+
if (node.file) {
|
|
267
|
+
write();
|
|
268
|
+
write(dim('file'));
|
|
269
|
+
writeAll(table([
|
|
270
|
+
[dim(' path'), hostPath(o.dir, node.file)],
|
|
271
|
+
[dim(' size'), bytes(node.file.bytes)],
|
|
272
|
+
[dim(' sha256'), node.file.sha256],
|
|
273
|
+
]));
|
|
274
|
+
}
|
|
275
|
+
write();
|
|
276
|
+
write(dim(`links (${links.length})`));
|
|
277
|
+
if (!links.length) {
|
|
278
|
+
note(dim(' nothing points here and it points nowhere'));
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
writeAll(table(links.map((e) => {
|
|
282
|
+
const out = e.source === id;
|
|
283
|
+
const other = out ? e.target : e.source;
|
|
284
|
+
return [
|
|
285
|
+
` ${out ? '→' : '←'} ${yellow(e.relation)}`,
|
|
286
|
+
cyan(other),
|
|
287
|
+
clip(o.store.graph.get(other)?.text ?? '', 48),
|
|
288
|
+
];
|
|
289
|
+
})));
|
|
290
|
+
}
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
// export
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
async function write_(o, rest, flags, cwd, asJson) {
|
|
295
|
+
if (rest.length > 1) {
|
|
296
|
+
throw usageError('export takes at most one file name', 'zen memory export [file]');
|
|
297
|
+
}
|
|
298
|
+
const named = rest[0] ?? flags.out ?? 'memory.html';
|
|
299
|
+
const target = isAbsolute(named) ? named : resolve(cwd, named);
|
|
300
|
+
const report = await buildMemoryReport(o.store, { title: `${o.project} · memory` });
|
|
301
|
+
await writeFile(target, renderMemoryHtml(report), 'utf8');
|
|
302
|
+
if (asJson) {
|
|
303
|
+
json({ path: target, nodes: report.nodes.length, edges: report.edges.length });
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
note(`${green('wrote')} ${target} ${dim(`· ${count(report.nodes.length, 'node')}, ${count(report.edges.length, 'edge')}`)}`);
|
|
307
|
+
}
|
|
308
|
+
if (flags.open) {
|
|
309
|
+
reveal(target);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Handing a path to the platform opener. `spawn` without a shell, so the path
|
|
314
|
+
* is an argument rather than something a shell gets to interpret.
|
|
315
|
+
*/
|
|
316
|
+
function reveal(target) {
|
|
317
|
+
const command = process.platform === 'darwin'
|
|
318
|
+
? 'open'
|
|
319
|
+
: process.platform === 'win32'
|
|
320
|
+
? 'explorer'
|
|
321
|
+
: 'xdg-open';
|
|
322
|
+
spawn(command, [target], { stdio: 'ignore', detached: true }).unref();
|
|
323
|
+
}
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
// forget
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
/**
|
|
328
|
+
* The only destructive thing here, and it is deliberately not the way to fix a
|
|
329
|
+
* memory that turned out wrong — that is a new node and a `SUPERSEDES` edge,
|
|
330
|
+
* which keeps the reason it changed. This is for what should never have been
|
|
331
|
+
* written down: it takes the node, its vector and its file bytes together.
|
|
332
|
+
*/
|
|
333
|
+
async function forget(o, ids, flags, asJson) {
|
|
334
|
+
if (!ids.length) {
|
|
335
|
+
throw usageError('forget takes at least one id', 'zen memory forget <id...>');
|
|
336
|
+
}
|
|
337
|
+
const found = [];
|
|
338
|
+
for (const id of ids) {
|
|
339
|
+
const node = o.store.graph.get(id);
|
|
340
|
+
if (!node) {
|
|
341
|
+
throw usageError(`no node ${id}`, 'list them with `zen memory ls`');
|
|
342
|
+
}
|
|
343
|
+
found.push(node);
|
|
344
|
+
}
|
|
345
|
+
if (!flags.yes) {
|
|
346
|
+
if (!isInteractive()) {
|
|
347
|
+
throw usageError('refusing to remove without confirmation', 'pass --yes');
|
|
348
|
+
}
|
|
349
|
+
writeAll(table(found.map((n) => [cyan(n.id), n.kind, clip(n.text, 56)])));
|
|
350
|
+
write();
|
|
351
|
+
if (!(await confirm(`Forget ${count(found.length, 'node')}?`))) {
|
|
352
|
+
note('nothing removed');
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// The vector and the bytes go with the node, which `MemoryIndex` already
|
|
357
|
+
// guarantees; `sees` is every audience in the graph, because this command
|
|
358
|
+
// is the person who owns the files rather than an agent inside a run.
|
|
359
|
+
const index = new MemoryIndex({ store: o.store });
|
|
360
|
+
await index.forget(found.map((n) => n.id), [ALL_AGENTS, ...new Set(o.store.graph.nodes().flatMap((n) => n.audience))]);
|
|
361
|
+
if (asJson) {
|
|
362
|
+
json({ forgotten: found.map((n) => n.id) });
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
note(`${green('forgot')} ${count(found.length, 'node')}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// ---------------------------------------------------------------------------
|
|
369
|
+
// Shared
|
|
370
|
+
// ---------------------------------------------------------------------------
|
|
371
|
+
function clip(text, n) {
|
|
372
|
+
const one = text.replace(/\s+/g, ' ').trim();
|
|
373
|
+
return one.length > n ? one.slice(0, n - 1) + '…' : one;
|
|
374
|
+
}
|
|
375
|
+
//# sourceMappingURL=memory.js.map
|
package/dist/commands/models.js
CHANGED
|
@@ -393,6 +393,17 @@ function verdict(probe) {
|
|
|
393
393
|
return dim('no answer');
|
|
394
394
|
}
|
|
395
395
|
}
|
|
396
|
+
/**
|
|
397
|
+
* Which project and region a Vertex call was actually made in. A publisher
|
|
398
|
+
* model is only missing *somewhere*, and the somewhere is the part nobody
|
|
399
|
+
* typed: it comes off the key, or out of the service-account file itself.
|
|
400
|
+
*/
|
|
401
|
+
function vertexAt(where) {
|
|
402
|
+
const entry = where.fromEnv.has('vertex') ? undefined : where.store.active('vertex');
|
|
403
|
+
const id = entry ? where.store.projectOf(entry)?.id : process.env.GOOGLE_CLOUD_PROJECT;
|
|
404
|
+
const location = process.env.GOOGLE_CLOUD_LOCATION ?? entry?.location ?? 'global';
|
|
405
|
+
return `vertex: project ${id ?? 'unset'} · location ${location}`;
|
|
406
|
+
}
|
|
396
407
|
const test = async (ctx, args) => {
|
|
397
408
|
const { values, positionals } = parse(args, ROLE_OPTIONS, 'zen models test <provider:model> … [--chat|--embedding]');
|
|
398
409
|
if (positionals.length === 0) {
|
|
@@ -402,7 +413,7 @@ const test = async (ctx, args) => {
|
|
|
402
413
|
// Every ref is split before anything is built, so a typo in the third one
|
|
403
414
|
// does not arrive after two billable calls.
|
|
404
415
|
const parsed = positionals.map((ref) => ({ ref, ...split(ref) }));
|
|
405
|
-
await credentials();
|
|
416
|
+
const where = await credentials();
|
|
406
417
|
const probes = [];
|
|
407
418
|
const bar = ctx.json ? undefined : progress();
|
|
408
419
|
for (const { ref, provider, id } of parsed) {
|
|
@@ -425,6 +436,11 @@ const test = async (ctx, args) => {
|
|
|
425
436
|
note(dim(`${p.ref}: ${p.check.fix}`));
|
|
426
437
|
}
|
|
427
438
|
}
|
|
439
|
+
// A Vertex refusal is about a model *in a project*, and the project is
|
|
440
|
+
// the half of that nobody typed.
|
|
441
|
+
if (parsed.some((p) => p.provider === 'vertex')) {
|
|
442
|
+
note(dim(vertexAt(where)));
|
|
443
|
+
}
|
|
428
444
|
}
|
|
429
445
|
const failed = probes.filter((p) => p.check.state !== 'live');
|
|
430
446
|
if (failed.length > 0) {
|