@zenera/cli 1.1.15 → 1.1.17
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 +201 -129
- package/dist/banner.d.ts +5 -1
- package/dist/banner.js +2 -2
- package/dist/catalog.js +19 -20
- package/dist/command.d.ts +5 -0
- package/dist/commands/check.js +5 -1
- package/dist/commands/init.js +41 -6
- package/dist/commands/run.js +4 -0
- package/dist/engine.js +6 -0
- package/dist/env.d.ts +20 -0
- package/dist/env.js +65 -0
- package/dist/keys.d.ts +2 -0
- package/dist/keys.js +2 -0
- package/dist/main.js +10 -6
- package/dist/sandbox.d.ts +9 -0
- package/dist/sandbox.js +23 -3
- package/dist/scaffold.d.ts +6 -0
- package/dist/scaffold.js +56 -15
- package/dist/validate.js +57 -15
- package/package.json +2 -2
- package/templates/editor/.github/copilot-instructions.md +501 -301
- package/templates/editor/.github/prompts/new-agent.prompt.md +6 -6
- package/templates/editor/.github/prompts/new-skill.prompt.md +7 -7
- package/templates/editor/.github/prompts/review-project.prompt.md +54 -15
- package/templates/editor/.github/prompts/sync-with-spec.prompt.md +229 -42
- package/templates/editor/.github/prompts/update-spec-from-feedback.prompt.md +142 -0
- package/templates/editor/.github/skills/zen-cli/SKILL.md +62 -6
- package/templates/editor/.github/skills/zen-cli/references/check.md +9 -6
- package/templates/editor/.github/skills/zen-cli/references/faker.md +14 -14
- package/templates/editor/.github/skills/zen-cli/references/frame.md +11 -9
- package/templates/editor/.github/skills/zen-cli/references/inspect.md +3 -3
- package/templates/editor/.github/skills/zen-cli/references/keys.md +9 -9
- package/templates/editor/.github/skills/zen-cli/references/memory.md +11 -11
- package/templates/editor/.github/skills/zen-cli/references/models.md +8 -8
- package/templates/editor/.github/skills/zen-cli/references/projects.md +23 -6
- package/templates/editor/.github/skills/zen-cli/references/rag.md +191 -56
- package/templates/editor/.github/skills/zen-cli/references/run.md +3 -3
- package/templates/editor/.github/skills/zen-cli/references/sandbox.md +14 -8
- package/templates/editor/.github/skills/zen-memory/SKILL.md +144 -72
- package/templates/editor/.github/skills/zen-memory/scripts/check-instructions.sh +115 -0
- package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +71 -48
- package/templates/editor/.github/skills/zen-rag-schema/SKILL.md +67 -67
- package/templates/editor/.github/skills/zen-review/SKILL.md +123 -0
- package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +119 -0
- package/templates/editor/.github/skills/zen-review/scripts/review.sh +104 -0
- package/templates/editor/.github/skills/zen-spec-sync/SKILL.md +197 -0
- package/templates/editor/.github/skills/zen-spec-sync/scripts/snapshot.sh +200 -0
- package/templates/editor/.vscode/settings.json +46 -3
- package/templates/parts/memory-terms.yaml.tmpl +16 -0
- package/templates/parts/memory.yaml.tmpl +12 -0
- package/templates/project/SPECIFICATION.md.tmpl +7 -0
- package/templates/project/{INSTRUCTIONS.md → agents/instructions.md} +1 -1
- package/templates/project/agents/memory-instructions.md +278 -0
- package/templates/project/agents.yaml.tmpl +9 -0
- package/templates/project/assets/README.md +1 -1
- package/templates/project/env +22 -0
- package/templates/project/sandbox/Dockerfile.tmpl +3 -0
package/dist/banner.d.ts
CHANGED
|
@@ -6,8 +6,12 @@ export interface BannerText {
|
|
|
6
6
|
/** the line underneath, dim */
|
|
7
7
|
subtitle: string;
|
|
8
8
|
}
|
|
9
|
+
export interface PrintBannerOptions {
|
|
10
|
+
/** Render the block face even when the terminal is narrower than it. */
|
|
11
|
+
readonly full?: boolean;
|
|
12
|
+
}
|
|
9
13
|
export declare const NEO_BANNER: BannerText;
|
|
10
14
|
export declare function bannerLines(text: BannerText, columns?: number): string[];
|
|
11
15
|
/** Narration, and only for someone watching. */
|
|
12
|
-
export declare function printBanner(text: BannerText): void;
|
|
16
|
+
export declare function printBanner(text: BannerText, options?: PrintBannerOptions): void;
|
|
13
17
|
//# sourceMappingURL=banner.d.ts.map
|
package/dist/banner.js
CHANGED
|
@@ -112,12 +112,12 @@ export function bannerLines(text, columns = process.stderr.columns || 80) {
|
|
|
112
112
|
return lines;
|
|
113
113
|
}
|
|
114
114
|
/** Narration, and only for someone watching. */
|
|
115
|
-
export function printBanner(text) {
|
|
115
|
+
export function printBanner(text, options = {}) {
|
|
116
116
|
if (!process.stderr.isTTY) {
|
|
117
117
|
return;
|
|
118
118
|
}
|
|
119
119
|
note('');
|
|
120
|
-
for (const line of bannerLines(text)) {
|
|
120
|
+
for (const line of bannerLines(text, options.full ? Infinity : undefined)) {
|
|
121
121
|
note(line);
|
|
122
122
|
}
|
|
123
123
|
note('');
|
package/dist/catalog.js
CHANGED
|
@@ -38,29 +38,28 @@ const CATALOG_VERSION = 1;
|
|
|
38
38
|
*/
|
|
39
39
|
export const CURATED = {
|
|
40
40
|
openai: [
|
|
41
|
-
{ id: 'gpt-
|
|
42
|
-
{ id: 'gpt-
|
|
41
|
+
{ id: 'gpt-5.6-luna', roles: ['chat'], contextLength: 1_050_000 },
|
|
42
|
+
{ id: 'gpt-5.6-terra', roles: ['chat'], contextLength: 1_050_000 },
|
|
43
43
|
{ id: 'text-embedding-3-small', roles: ['embedding'], dimensions: 1536 },
|
|
44
44
|
{ id: 'text-embedding-3-large', roles: ['embedding'], dimensions: 3072 },
|
|
45
45
|
],
|
|
46
46
|
anthropic: [
|
|
47
|
-
{ id: 'claude-
|
|
48
|
-
{ id: 'claude-
|
|
47
|
+
{ id: 'claude-sonnet-5', roles: ['chat'], contextLength: 1_000_000 },
|
|
48
|
+
{ id: 'claude-opus-5', roles: ['chat'], contextLength: 1_000_000 },
|
|
49
49
|
],
|
|
50
50
|
google: [
|
|
51
|
-
{ id: 'gemini-
|
|
52
|
-
{ id: 'gemini-
|
|
53
|
-
{ id: 'gemini-embedding-
|
|
51
|
+
{ id: 'gemini-3.5-flash-lite', roles: ['chat'], contextLength: 1_048_576 },
|
|
52
|
+
{ id: 'gemini-3.8-flash', roles: ['chat'], contextLength: 1_048_576 },
|
|
53
|
+
{ id: 'gemini-embedding-2', roles: ['embedding'] },
|
|
54
54
|
],
|
|
55
55
|
vertex: [
|
|
56
|
-
{ id: 'gemini-
|
|
57
|
-
{ id: 'gemini-
|
|
58
|
-
{ id: 'gemini-embedding-
|
|
59
|
-
{ id: 'text-embedding-005', roles: ['embedding'], dimensions: 768 },
|
|
56
|
+
{ id: 'gemini-3.5-flash-lite', roles: ['chat'], contextLength: 1_048_576 },
|
|
57
|
+
{ id: 'gemini-3.8-flash', roles: ['chat'], contextLength: 1_048_576 },
|
|
58
|
+
{ id: 'gemini-embedding-2', roles: ['embedding'] },
|
|
60
59
|
],
|
|
61
60
|
openrouter: [
|
|
62
|
-
{ id: 'openai/gpt-
|
|
63
|
-
{ id: 'anthropic/claude-
|
|
61
|
+
{ id: 'openai/gpt-5.6-luna', roles: ['chat'], contextLength: 1_050_000 },
|
|
62
|
+
{ id: 'anthropic/claude-sonnet-5', roles: ['chat'], contextLength: 1_000_000 },
|
|
64
63
|
{ id: 'openai/text-embedding-3-small', roles: ['embedding'], dimensions: 1536 },
|
|
65
64
|
],
|
|
66
65
|
};
|
|
@@ -73,23 +72,23 @@ export const CURATED = {
|
|
|
73
72
|
*/
|
|
74
73
|
export const PREFERRED = {
|
|
75
74
|
openai: {
|
|
76
|
-
chat: ['gpt-
|
|
75
|
+
chat: ['gpt-5.6-luna', 'gpt-5.6-terra'],
|
|
77
76
|
embedding: ['text-embedding-3-small', 'text-embedding-3-large'],
|
|
78
77
|
},
|
|
79
78
|
anthropic: {
|
|
80
|
-
chat: ['claude-
|
|
79
|
+
chat: ['claude-sonnet-5', 'claude-opus-5'],
|
|
81
80
|
embedding: [],
|
|
82
81
|
},
|
|
83
82
|
google: {
|
|
84
|
-
chat: ['gemini-
|
|
85
|
-
embedding: ['gemini-embedding-
|
|
83
|
+
chat: ['gemini-3.5-flash-lite', 'gemini-3.5-flash'],
|
|
84
|
+
embedding: ['gemini-embedding-2'],
|
|
86
85
|
},
|
|
87
86
|
vertex: {
|
|
88
|
-
chat: ['gemini-
|
|
89
|
-
embedding: ['
|
|
87
|
+
chat: ['gemini-3.5-flash-lite', 'gemini-3.5-flash'],
|
|
88
|
+
embedding: ['gemini-embedding-2'],
|
|
90
89
|
},
|
|
91
90
|
openrouter: {
|
|
92
|
-
chat: ['openai/gpt-
|
|
91
|
+
chat: ['openai/gpt-5.6-luna', 'anthropic/claude-sonnet-5'],
|
|
93
92
|
embedding: ['openai/text-embedding-3-small'],
|
|
94
93
|
},
|
|
95
94
|
};
|
package/dist/command.d.ts
CHANGED
|
@@ -9,6 +9,11 @@ export interface Command {
|
|
|
9
9
|
readonly usage: string;
|
|
10
10
|
/** lines printed under the usage line by `zen help <command>` */
|
|
11
11
|
readonly details?: readonly string[];
|
|
12
|
+
/**
|
|
13
|
+
* Answers `--help` itself, for a command whose arguments select a page the
|
|
14
|
+
* frame cannot see. Without it the frame prints this command's own page.
|
|
15
|
+
*/
|
|
16
|
+
help?(ctx: Context): Promise<void> | void;
|
|
12
17
|
run(ctx: Context): Promise<void>;
|
|
13
18
|
}
|
|
14
19
|
//# sourceMappingURL=command.d.ts.map
|
package/dist/commands/check.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, statSync } from 'node:fs';
|
|
2
2
|
import { basename, dirname, resolve } from 'node:path';
|
|
3
3
|
import { one, parse } from "../args.js";
|
|
4
|
+
import { loadProjectEnv } from "../env.js";
|
|
4
5
|
import { KeyStore } from "../keys.js";
|
|
5
6
|
import { duration } from "../narrate.js";
|
|
6
7
|
import { Registry } from "../projects.js";
|
|
@@ -75,7 +76,10 @@ export const check = {
|
|
|
75
76
|
const name = entry?.name ?? basename(dir);
|
|
76
77
|
// Materialised first, so the credential verdicts are the ones a run
|
|
77
78
|
// would reach: a key in the environment and a key in the keyring are
|
|
78
|
-
// the same key by the time the library asks.
|
|
79
|
+
// the same key by the time the library asks. The project's own `.env`
|
|
80
|
+
// goes first for the same reason — `zen run` reads it, so a check that
|
|
81
|
+
// did not would report a missing credential for a project that runs.
|
|
82
|
+
loadProjectEnv(dir);
|
|
79
83
|
const keys = await KeyStore.open();
|
|
80
84
|
keys.materialize();
|
|
81
85
|
const bar = progress();
|
package/dist/commands/init.js
CHANGED
|
@@ -2,6 +2,7 @@ import { readProjectConfig } from '@zenera/neo';
|
|
|
2
2
|
import { existsSync, readdirSync } from 'node:fs';
|
|
3
3
|
import { basename, resolve } from 'node:path';
|
|
4
4
|
import { one, parse } from "../args.js";
|
|
5
|
+
import { PREFERRED } from "../catalog.js";
|
|
5
6
|
import { ensureHome } from "../home.js";
|
|
6
7
|
import { resolveBuild } from "../image.js";
|
|
7
8
|
import { envNames, isProvider, keyId, KeyStore, PROVIDERS } from "../keys.js";
|
|
@@ -43,6 +44,36 @@ const DEFAULT_MODEL = {
|
|
|
43
44
|
vertex: { ref: 'vertex:gemini-3.5-flash' },
|
|
44
45
|
openrouter: { ref: 'openrouter:inclusionai/ling-3.0-flash-fin:free' },
|
|
45
46
|
};
|
|
47
|
+
/**
|
|
48
|
+
* What vectorises the project's memory, given the provider it ends up on.
|
|
49
|
+
*
|
|
50
|
+
* Taken from the same table `zen models pick` ranks by rather than written out
|
|
51
|
+
* again here, so the scaffold cannot drift from what the catalogue calls
|
|
52
|
+
* current. Anthropic publishes no embeddings API, and a `--model` naming a
|
|
53
|
+
* provider we do not ship gives nothing to guess from; both scaffold without
|
|
54
|
+
* one, and memory still works by ranking recall on term overlap. An
|
|
55
|
+
* `embeddings:` entry pointing at a provider that has none is not a degraded
|
|
56
|
+
* project, it is one that fails to load.
|
|
57
|
+
*/
|
|
58
|
+
function defaultEmbedding(provider) {
|
|
59
|
+
if (provider === undefined) {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
const id = PREFERRED[provider].embedding[0];
|
|
63
|
+
return id === undefined ? undefined : `${provider}:${id}`;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The provider a model ref names, when it names one we know.
|
|
67
|
+
*
|
|
68
|
+
* Every ref the scaffold writes carries its provider, so this is how the
|
|
69
|
+
* memory store ends up on the same credential as the chat model — including
|
|
70
|
+
* for an explicit `--model`, where nothing else says which vendor was meant.
|
|
71
|
+
*/
|
|
72
|
+
function providerOf(ref) {
|
|
73
|
+
const colon = ref.indexOf(':');
|
|
74
|
+
const head = colon < 0 ? '' : ref.slice(0, colon);
|
|
75
|
+
return isProvider(head) ? head : undefined;
|
|
76
|
+
}
|
|
46
77
|
/**
|
|
47
78
|
* The provider this machine can actually reach.
|
|
48
79
|
*
|
|
@@ -96,10 +127,11 @@ export const init = {
|
|
|
96
127
|
summary: 'Create a project here, or in <dir>, and register it.',
|
|
97
128
|
usage: USAGE,
|
|
98
129
|
details: [
|
|
99
|
-
'Writes SPECIFICATION.md,
|
|
100
|
-
'scripts/, then records the directory so
|
|
101
|
-
'find it by name. Editor files
|
|
102
|
-
'tree) are written alongside,
|
|
130
|
+
'Writes SPECIFICATION.md, agents.yaml, agents/ (house rules, the memory',
|
|
131
|
+
'rules and one role prompt) and scripts/, then records the directory so',
|
|
132
|
+
'`zen list` and `zen open` can find it by name. Editor files',
|
|
133
|
+
'(.vscode/settings.json and the .github/ tree) are written alongside,',
|
|
134
|
+
'replacing any already there.',
|
|
103
135
|
'',
|
|
104
136
|
'A directory that is already a project is adopted rather than rebuilt:',
|
|
105
137
|
'it is registered under its own name, and nothing in it is touched.',
|
|
@@ -111,7 +143,9 @@ export const init = {
|
|
|
111
143
|
'there, then run /sync-with-spec in your editor to make the rest match.',
|
|
112
144
|
'',
|
|
113
145
|
'The default agent gets the file tools and a sandboxed shell, plus',
|
|
114
|
-
'`exa:*` when the keyring holds an Exa key.
|
|
146
|
+
'`exa:*` when the keyring holds an Exa key. It keeps a memory of its',
|
|
147
|
+
'own work in memory/, vectorised by the same provider as the model',
|
|
148
|
+
'when that provider publishes embeddings. Without --model, the',
|
|
115
149
|
'keyring is checked and the model is picked from a credential the',
|
|
116
150
|
'provider accepts.',
|
|
117
151
|
'',
|
|
@@ -143,7 +177,8 @@ export const init = {
|
|
|
143
177
|
: DEFAULT_MODEL[provider ?? 'openai'];
|
|
144
178
|
const model = choice.ref;
|
|
145
179
|
const web = hasExa(store);
|
|
146
|
-
const
|
|
180
|
+
const embedding = defaultEmbedding(providerOf(model));
|
|
181
|
+
const written = scaffold({ dir, model, modelOptions: choice.options, web, embedding });
|
|
147
182
|
const registry = await Registry.open();
|
|
148
183
|
registry.add(name, dir);
|
|
149
184
|
registry.save();
|
package/dist/commands/run.js
CHANGED
|
@@ -29,6 +29,10 @@ export const run = {
|
|
|
29
29
|
'The first word is the project when it names one, as in `zen run acme`,',
|
|
30
30
|
'and the first word of the prompt when it does not. --project settles it.',
|
|
31
31
|
'',
|
|
32
|
+
"The project's .env is read before it loads, and every name in it is",
|
|
33
|
+
'forwarded into the sandbox. Your shell wins over the file, and the',
|
|
34
|
+
'keyring fills in the rest; --no-keys withholds all of it.',
|
|
35
|
+
'',
|
|
32
36
|
'The prompt comes from the argument, or stdin, or the TUI. There is no',
|
|
33
37
|
'`resume`: a session continues itself, because its state is what it is.',
|
|
34
38
|
'',
|
package/dist/engine.js
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync } from 'node:fs';
|
|
|
3
3
|
import { writeFile } from 'node:fs/promises';
|
|
4
4
|
import { resolve } from 'node:path';
|
|
5
5
|
import { auditModels, describeIssue } from "./audit.js";
|
|
6
|
+
import { loadProjectEnv } from "./env.js";
|
|
6
7
|
import { readJson, writeJson } from "./home.js";
|
|
7
8
|
import { KeyStore, assertUsable } from "./keys.js";
|
|
8
9
|
import { projectMounts } from "./projects.js";
|
|
@@ -15,6 +16,10 @@ import { CliError, EXIT, invalidError, warn } from "./term.js";
|
|
|
15
16
|
* and the whole conversation, its memory and its blobs travel with it.
|
|
16
17
|
*/
|
|
17
18
|
export async function open(opts) {
|
|
19
|
+
// Before the keyring, so the project's own file wins over the machine's
|
|
20
|
+
// keys, and before the load, so `${VAR}` in agents.yaml can name anything
|
|
21
|
+
// in it. The real environment still beats both.
|
|
22
|
+
const env = loadProjectEnv(opts.project.dir);
|
|
18
23
|
const keys = await KeyStore.open();
|
|
19
24
|
keys.materialize();
|
|
20
25
|
assertUsable(keys);
|
|
@@ -47,6 +52,7 @@ export async function open(opts) {
|
|
|
47
52
|
readOnly: opts.readOnly,
|
|
48
53
|
image: opts.image,
|
|
49
54
|
keys: opts.keys,
|
|
55
|
+
env: env?.names,
|
|
50
56
|
mounts,
|
|
51
57
|
});
|
|
52
58
|
const workspaceOptions = {
|
package/dist/env.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** The file, relative to the project directory. */
|
|
2
|
+
export declare const ENV_FILE = ".env";
|
|
3
|
+
export interface ProjectEnv {
|
|
4
|
+
/** the file it was read from */
|
|
5
|
+
path: string;
|
|
6
|
+
/** every name the file declares, whether or not its value won */
|
|
7
|
+
names: string[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Reads `<dir>/.env` into `process.env`, without overwriting anything already
|
|
11
|
+
* set, and returns the names it found. A project with no such file is not a
|
|
12
|
+
* problem to report — most have none — so the answer is simply `undefined`.
|
|
13
|
+
*
|
|
14
|
+
* The names are returned in full, including the ones the environment already
|
|
15
|
+
* answered: the variable is in play either way, and the sandbox forwards by
|
|
16
|
+
* name, so what it needs to know is which names matter and not which of the
|
|
17
|
+
* two sources supplied them.
|
|
18
|
+
*/
|
|
19
|
+
export declare function loadProjectEnv(dir: string): ProjectEnv | undefined;
|
|
20
|
+
//# sourceMappingURL=env.d.ts.map
|
package/dist/env.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
3
|
+
import { parseEnv } from 'node:util';
|
|
4
|
+
import { FILE_ENVS } from "./keys.js";
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// The project's `.env`
|
|
7
|
+
//
|
|
8
|
+
// A project is a directory, and some of what it needs to run is particular to
|
|
9
|
+
// it: the token for the service it talks to, the base url of the environment
|
|
10
|
+
// it talks to it in. The keyring answers "what can this machine pay for"; this
|
|
11
|
+
// answers "what does this project need", and the two are different questions
|
|
12
|
+
// often enough to be worth two places.
|
|
13
|
+
//
|
|
14
|
+
// Precedence is the same one everything else here uses — the real environment
|
|
15
|
+
// always wins. What is already set in the shell is left alone, this file fills
|
|
16
|
+
// the gaps, and the keyring fills whatever is still missing after that. So a
|
|
17
|
+
// variable exported for one command still overrides the file, which is the
|
|
18
|
+
// behaviour anybody typing `FOO=bar zen run` is expecting.
|
|
19
|
+
//
|
|
20
|
+
// The names, not the values, are what leaves this module: they are what the
|
|
21
|
+
// sandbox forwards, and forwarding by name is what keeps a secret out of an
|
|
22
|
+
// argv. See `forwarded()` in sandbox.ts for the rest of that story.
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
/** The file, relative to the project directory. */
|
|
25
|
+
export const ENV_FILE = '.env';
|
|
26
|
+
/**
|
|
27
|
+
* Reads `<dir>/.env` into `process.env`, without overwriting anything already
|
|
28
|
+
* set, and returns the names it found. A project with no such file is not a
|
|
29
|
+
* problem to report — most have none — so the answer is simply `undefined`.
|
|
30
|
+
*
|
|
31
|
+
* The names are returned in full, including the ones the environment already
|
|
32
|
+
* answered: the variable is in play either way, and the sandbox forwards by
|
|
33
|
+
* name, so what it needs to know is which names matter and not which of the
|
|
34
|
+
* two sources supplied them.
|
|
35
|
+
*/
|
|
36
|
+
export function loadProjectEnv(dir) {
|
|
37
|
+
const path = join(dir, ENV_FILE);
|
|
38
|
+
if (!existsSync(path)) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
const names = [];
|
|
42
|
+
for (const [name, value] of Object.entries(parseEnv(readFileSync(path, 'utf8')))) {
|
|
43
|
+
if (value === undefined) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
names.push(name);
|
|
47
|
+
// An empty string is not an answer: tools test for absence, and a
|
|
48
|
+
// commented-out line someone half-filled in should not shadow a key
|
|
49
|
+
// the keyring has.
|
|
50
|
+
if (!process.env[name]) {
|
|
51
|
+
process.env[name] = located(dir, name, value);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { path, names };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* A service-account file is normally kept beside the `.env` that names it, so
|
|
58
|
+
* a relative path there means relative to the *project* — not to wherever the
|
|
59
|
+
* command happened to be run, which is what both the Google SDK and podman's
|
|
60
|
+
* bind mount would otherwise make of it.
|
|
61
|
+
*/
|
|
62
|
+
function located(dir, name, value) {
|
|
63
|
+
return FILE_ENVS.includes(name) && !isAbsolute(value) ? resolve(dir, value) : value;
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=env.js.map
|
package/dist/keys.d.ts
CHANGED
|
@@ -58,6 +58,8 @@ export declare const SHAPES: Record<KeyOwner, ProviderShape>;
|
|
|
58
58
|
export declare function form(provider: KeyOwner): CredentialForm;
|
|
59
59
|
/** Every variable a provider's credential could arrive in, usual one first. */
|
|
60
60
|
export declare function envNames(provider: KeyOwner): string[];
|
|
61
|
+
/** Variables that name a credential *file* rather than carrying the secret. */
|
|
62
|
+
export declare const FILE_ENVS: string[];
|
|
61
63
|
/** The variable this particular credential occupies. */
|
|
62
64
|
export declare function envOf(entry: Pick<KeyEntry, 'provider' | 'holds' | 'env'>): string;
|
|
63
65
|
export declare function isProvider(name: string): name is Provider;
|
package/dist/keys.js
CHANGED
|
@@ -127,6 +127,8 @@ export function form(provider) {
|
|
|
127
127
|
export function envNames(provider) {
|
|
128
128
|
return SHAPES[provider].forms.map((f) => f.env);
|
|
129
129
|
}
|
|
130
|
+
/** Variables that name a credential *file* rather than carrying the secret. */
|
|
131
|
+
export const FILE_ENVS = OWNERS.flatMap((owner) => SHAPES[owner].forms.filter((f) => f.holds === 'file').map((f) => f.env));
|
|
130
132
|
/** The variable this particular credential occupies. */
|
|
131
133
|
export function envOf(entry) {
|
|
132
134
|
if (entry.env) {
|
package/dist/main.js
CHANGED
|
@@ -57,9 +57,11 @@ async function main(argv) {
|
|
|
57
57
|
// Narration, so `--json` and every pipe are untouched by it. A command
|
|
58
58
|
// living in another package brings its own brand, but only once it is
|
|
59
59
|
// actually there — a banner over "not installed" is a claim about nothing.
|
|
60
|
-
|
|
60
|
+
const ragSearch = name === 'rag' && ['docs', 'schema'].includes(rest[0]) && rest[1] === 'search';
|
|
61
|
+
const interactive = rest.includes('--interactive');
|
|
62
|
+
if (!json && (!ragSearch || interactive)) {
|
|
61
63
|
const brand = external && hasExternal(external) ? external.banner : undefined;
|
|
62
|
-
printBanner(brand ?? NEO_BANNER);
|
|
64
|
+
printBanner(brand ?? NEO_BANNER, { full: ragSearch && interactive });
|
|
63
65
|
}
|
|
64
66
|
if (parts.name === 'help') {
|
|
65
67
|
await usage(rest[0]);
|
|
@@ -73,15 +75,17 @@ async function main(argv) {
|
|
|
73
75
|
fail(`unknown command "${parts.name}"`, `run ${bold(`${NAME} --help`)} for the list`);
|
|
74
76
|
return EXIT.usage;
|
|
75
77
|
}
|
|
76
|
-
if (values.help) {
|
|
77
|
-
await usage(name);
|
|
78
|
-
return EXIT.ok;
|
|
79
|
-
}
|
|
80
78
|
// `-C` is consumed here so no command ever reads `process.cwd()` itself,
|
|
81
79
|
// and every relative path in every command means the same thing.
|
|
82
80
|
const cwd = resolve(values.directory ?? process.cwd());
|
|
83
81
|
try {
|
|
84
82
|
const one = command ?? (await loadExternal(name, external));
|
|
83
|
+
if (values.help) {
|
|
84
|
+
// A command that nests its own pages answers for them; the frame
|
|
85
|
+
// only knows the one page it printed the command's name on.
|
|
86
|
+
await (one.help ? one.help({ args: rest, json, cwd }) : usage(name));
|
|
87
|
+
return EXIT.ok;
|
|
88
|
+
}
|
|
85
89
|
await one.run({ args: rest, json, cwd });
|
|
86
90
|
return EXIT.ok;
|
|
87
91
|
}
|
package/dist/sandbox.d.ts
CHANGED
|
@@ -28,6 +28,15 @@ export interface SandboxInputs {
|
|
|
28
28
|
image?: string;
|
|
29
29
|
/** `--no-keys`: refuse to forward credentials, whatever the config says */
|
|
30
30
|
keys?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Names from the project's `.env`, forwarded into the container the same
|
|
33
|
+
* way a credential is — by name, so the value stays out of the argv.
|
|
34
|
+
*
|
|
35
|
+
* They travel with the keys rather than beside them: a `.env` is where an
|
|
36
|
+
* api token lives, so `--no-keys` has to withhold it too or the flag is a
|
|
37
|
+
* promise it does not keep.
|
|
38
|
+
*/
|
|
39
|
+
env?: readonly string[];
|
|
31
40
|
}
|
|
32
41
|
export declare function buildSandbox(opts: SandboxInputs): SandboxSetup;
|
|
33
42
|
/**
|
package/dist/sandbox.js
CHANGED
|
@@ -82,7 +82,7 @@ export function buildSandbox(opts) {
|
|
|
82
82
|
// read-only directory fails on writing its own `__pycache__` — a confusing
|
|
83
83
|
// error about a file nobody asked for.
|
|
84
84
|
const extra = { HOME, PYTHONDONTWRITEBYTECODE: '1', ...(keys?.env ?? {}) };
|
|
85
|
-
const spec = toSpec(base, extra, keys?.secrets);
|
|
85
|
+
const spec = toSpec(base, extra, secretsOf(keys?.secrets, opts.env));
|
|
86
86
|
const agents = {};
|
|
87
87
|
for (const agent of opts.config.agents) {
|
|
88
88
|
if (agent.sandbox) {
|
|
@@ -92,7 +92,7 @@ export function buildSandbox(opts) {
|
|
|
92
92
|
// so the variable is what actually decides, and without it the file
|
|
93
93
|
// sitting there is not a credential anything will look for.
|
|
94
94
|
const own = merged.keys === false ? undefined : keys;
|
|
95
|
-
agents[agent.name] = toSpec(merged, { HOME, PYTHONDONTWRITEBYTECODE: '1', ...(own?.env ?? {}) }, own
|
|
95
|
+
agents[agent.name] = toSpec(merged, { HOME, PYTHONDONTWRITEBYTECODE: '1', ...(own?.env ?? {}) }, own && secretsOf(own.secrets, opts.env));
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
const pool = new SandboxPool({
|
|
@@ -121,10 +121,30 @@ function merge(base, agent) {
|
|
|
121
121
|
}
|
|
122
122
|
return merged;
|
|
123
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* The two sources of forwarded names, as one list: the credentials the keyring
|
|
126
|
+
* selected, and whatever the project's `.env` declares. Deduped, because the
|
|
127
|
+
* same variable named twice is the same `--env` twice.
|
|
128
|
+
*
|
|
129
|
+
* `undefined` in means keys are withheld, and the file goes with them.
|
|
130
|
+
*/
|
|
131
|
+
function secretsOf(secrets, env) {
|
|
132
|
+
if (!secrets) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
return [...new Set([...secrets, ...(env ?? [])])];
|
|
136
|
+
}
|
|
124
137
|
/**
|
|
125
138
|
* Config names a variable; this reads it. A name that is not set on this host
|
|
126
139
|
* is simply not forwarded — an empty string in the container is a different
|
|
127
140
|
* thing from an absent one, and tools test for absence.
|
|
141
|
+
*
|
|
142
|
+
* A name that has already been answered with a value drops off the forwarded
|
|
143
|
+
* list: `--env NAME=value` and `--env NAME` are the same variable twice, and
|
|
144
|
+
* the one that carries a value is the one that was resolved on purpose. That
|
|
145
|
+
* matters for `GOOGLE_APPLICATION_CREDENTIALS`, which is rewritten here to
|
|
146
|
+
* where the key file was mounted — a `.env` naming it must not put the host's
|
|
147
|
+
* path back, since that path does not exist inside the container.
|
|
128
148
|
*/
|
|
129
149
|
function toSpec(config, extra, secrets) {
|
|
130
150
|
const env = { ...extra };
|
|
@@ -144,7 +164,7 @@ function toSpec(config, extra, secrets) {
|
|
|
144
164
|
user: config.user,
|
|
145
165
|
persist: config.persist,
|
|
146
166
|
env,
|
|
147
|
-
secrets,
|
|
167
|
+
secrets: secrets?.filter((name) => !(name in env)),
|
|
148
168
|
};
|
|
149
169
|
}
|
|
150
170
|
/**
|
package/dist/scaffold.d.ts
CHANGED
|
@@ -14,6 +14,12 @@ export interface ScaffoldOptions {
|
|
|
14
14
|
modelOptions?: string;
|
|
15
15
|
/** give the default agent `exa:*` — set when a key for it is on hand */
|
|
16
16
|
web?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* What vectorises the memory graph, as a provider-prefixed ref. Left out
|
|
19
|
+
* when the project's provider publishes no embeddings API — memory still
|
|
20
|
+
* works, ranking recall by term overlap instead of by meaning.
|
|
21
|
+
*/
|
|
22
|
+
embedding?: string;
|
|
17
23
|
}
|
|
18
24
|
export interface Scaffolded {
|
|
19
25
|
/** the project's own files, in the order they were written */
|
package/dist/scaffold.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
1
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
// ---------------------------------------------------------------------------
|
|
5
5
|
// Scaffolding
|
|
@@ -18,6 +18,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
18
18
|
//
|
|
19
19
|
// The trees are copied whole and nothing enumerates them, so adding a file to
|
|
20
20
|
// a new project is adding a file to `templates/project/` and nothing else.
|
|
21
|
+
// `MEMORY_RULES` is the single exception, and it is named below.
|
|
21
22
|
//
|
|
22
23
|
// What is there is deliberately close to empty: a template full of
|
|
23
24
|
// commented-out options is a template nobody reads and everybody deletes. The
|
|
@@ -32,6 +33,10 @@ import { fileURLToPath } from 'node:url';
|
|
|
32
33
|
const TEMPLATES = fileURLToPath(new URL('../templates', import.meta.url));
|
|
33
34
|
/** The suffix on a file with `{{...}}` in it, dropped when the file lands. */
|
|
34
35
|
const TEMPLATE = '.tmpl';
|
|
36
|
+
/** How to use the memory graph: a house rules file, landing under `agents/`. */
|
|
37
|
+
const MEMORY_RULES = join('agents', 'memory-instructions.md');
|
|
38
|
+
/** The same bytes, kept in the `zen-memory` skill to restore or diff against. */
|
|
39
|
+
const MEMORY_REFERENCE = join('.github', 'skills', 'zen-memory', 'references', 'memory-instructions.md');
|
|
35
40
|
/**
|
|
36
41
|
* This `zen`'s own version, which the scaffold pins the sandbox's tools to.
|
|
37
42
|
* The publishable packages move in lockstep, so one number covers them all.
|
|
@@ -98,17 +103,24 @@ function modelSection(ref, options) {
|
|
|
98
103
|
});
|
|
99
104
|
}
|
|
100
105
|
/**
|
|
101
|
-
*
|
|
106
|
+
* Template files that land under a name they cannot be stored under.
|
|
102
107
|
*
|
|
103
|
-
* `gitignore`
|
|
104
|
-
*
|
|
105
|
-
*
|
|
108
|
+
* `gitignore` cannot have its dot in the repository: npm strips a `.gitignore`
|
|
109
|
+
* out of a published tarball, and git would read this one as rules about
|
|
110
|
+
* `packages/cli/templates/` rather than as content. `env` cannot have its dot
|
|
111
|
+
* for a plainer reason — this repository ignores `.env` everywhere, as every
|
|
112
|
+
* repository should, so the template would never be committed at all.
|
|
106
113
|
*/
|
|
114
|
+
const DOTFILES = {
|
|
115
|
+
gitignore: '.gitignore',
|
|
116
|
+
env: '.env',
|
|
117
|
+
};
|
|
118
|
+
/** The name a template file lands under. */
|
|
107
119
|
function target(name) {
|
|
108
120
|
if (name.endsWith(TEMPLATE)) {
|
|
109
121
|
return name.slice(0, -TEMPLATE.length);
|
|
110
122
|
}
|
|
111
|
-
return name
|
|
123
|
+
return DOTFILES[name] ?? name;
|
|
112
124
|
}
|
|
113
125
|
/**
|
|
114
126
|
* Copies one template directory into `dir` at `rel`, depth first, sorted so the
|
|
@@ -145,13 +157,19 @@ function copyTree(from, dir, rel, opts) {
|
|
|
145
157
|
// ---------------------------------------------------------------------------
|
|
146
158
|
// Telling the editor which instructions are not for it
|
|
147
159
|
//
|
|
148
|
-
// The project's house rules are `
|
|
160
|
+
// The project's house rules are `agents/instructions.md`, deliberately not
|
|
149
161
|
// `AGENTS.md`: every coding assistant now reads that name out of an open
|
|
150
162
|
// folder and feeds it to itself as always-on instructions, and `zen open`
|
|
151
163
|
// opens exactly this directory. A name nobody else claims means the two are
|
|
152
164
|
// never confused, and `chat.useAgentsMdFile` no longer has to be switched off
|
|
153
165
|
// to keep them apart.
|
|
154
166
|
//
|
|
167
|
+
// They sit under `agents/` because there is rarely only one of them. Anything
|
|
168
|
+
// named `agents/<topic>-instructions.md` is read too, in filename order, so a
|
|
169
|
+
// subject that is true for every agent but is about one capability — memory,
|
|
170
|
+
// say — gets its own document instead of another section in a file that keeps
|
|
171
|
+
// growing.
|
|
172
|
+
//
|
|
155
173
|
// `chat.useNestedAgentsMdFiles` is still written. It is already false by
|
|
156
174
|
// default, but it is opt-in globally, and this is a directory the agent itself
|
|
157
175
|
// writes into — someone who turned it on would otherwise have the editor pick
|
|
@@ -159,12 +177,12 @@ function copyTree(from, dir, rel, opts) {
|
|
|
159
177
|
// setting, so it applies only in a trusted workspace; that is the right way
|
|
160
178
|
// round, since an untrusted folder is not one to run agents in either.
|
|
161
179
|
//
|
|
162
|
-
// `
|
|
163
|
-
// still needs a brief of its own, and what it needs to know is how
|
|
164
|
-
// of project is put together — the file formats, how a prompt is
|
|
165
|
-
// to add a skill rather than an agent. That is what the
|
|
166
|
-
// standing brief, plus the prompt files and skills the
|
|
167
|
-
// same place.
|
|
180
|
+
// `agents/instructions.md` addresses the *project's* agents. The editor's
|
|
181
|
+
// assistant still needs a brief of its own, and what it needs to know is how
|
|
182
|
+
// this kind of project is put together — the file formats, how a prompt is
|
|
183
|
+
// written, when to add a skill rather than an agent. That is what the
|
|
184
|
+
// `.github/` tree is: the standing brief, plus the prompt files and skills the
|
|
185
|
+
// editor picks up from the same place.
|
|
168
186
|
// ---------------------------------------------------------------------------
|
|
169
187
|
/**
|
|
170
188
|
* Writes the editor's files under `dir`, replacing what is there. They are
|
|
@@ -174,7 +192,21 @@ function copyTree(from, dir, rel, opts) {
|
|
|
174
192
|
* none. Returns the relative paths written.
|
|
175
193
|
*/
|
|
176
194
|
export function editorFiles(dir) {
|
|
177
|
-
|
|
195
|
+
const written = copyTree(join(TEMPLATES, 'editor'), dir, '', {});
|
|
196
|
+
// The one file that belongs to both trees. It is a project file first —
|
|
197
|
+
// house rules prepended to every agent that can see the graph — and it is
|
|
198
|
+
// written once and edited from then on, like every other project file. But
|
|
199
|
+
// a project whose copy drifted, or that deleted it and turned memory back
|
|
200
|
+
// on later, needs somewhere to get the current text from, and the editor
|
|
201
|
+
// tree is replaced on every `init` and `open`. Copying the same bytes into
|
|
202
|
+
// the skill's references is what makes `diff` between the two mean
|
|
203
|
+
// something, and what stops a second copy of these rules existing in the
|
|
204
|
+
// repository to fall out of step with the first.
|
|
205
|
+
const reference = join(dir, MEMORY_REFERENCE);
|
|
206
|
+
mkdirSync(dirname(reference), { recursive: true });
|
|
207
|
+
copyFileSync(join(TEMPLATES, 'project', MEMORY_RULES), reference);
|
|
208
|
+
written.push(MEMORY_REFERENCE);
|
|
209
|
+
return written;
|
|
178
210
|
}
|
|
179
211
|
/**
|
|
180
212
|
* Writes a project. Never overwrites the project's own files — a second `init`
|
|
@@ -198,6 +230,9 @@ export function scaffold(opts) {
|
|
|
198
230
|
// different readers — and a tool nothing says to use is not one.
|
|
199
231
|
web: opts.web ? part('exa.spec.md') : '',
|
|
200
232
|
webPrompt: opts.web ? part('exa.prompt.md') : '',
|
|
233
|
+
memory: opts.embedding
|
|
234
|
+
? part('memory.yaml', { embedding: opts.embedding })
|
|
235
|
+
: part('memory-terms.yaml'),
|
|
201
236
|
version: ownVersion(),
|
|
202
237
|
},
|
|
203
238
|
});
|
|
@@ -209,6 +244,12 @@ export function scaffold(opts) {
|
|
|
209
244
|
mkdirSync(join(opts.dir, 'agents', 'skills'), { recursive: true });
|
|
210
245
|
mkdirSync(join(opts.dir, 'sessions'), { recursive: true });
|
|
211
246
|
mkdirSync(join(opts.dir, '.tmp'), { recursive: true });
|
|
247
|
+
// Where `/sync-with-spec` records what it applied, so the next pass works
|
|
248
|
+
// the difference rather than the whole specification again. Empty here, and
|
|
249
|
+
// git does not carry an empty directory: what says a pass has completed is
|
|
250
|
+
// `baseline/manifest.txt`, never the directory itself.
|
|
251
|
+
mkdirSync(join(opts.dir, '.spec-sync', 'baseline'), { recursive: true });
|
|
252
|
+
mkdirSync(join(opts.dir, '.spec-sync', 'history'), { recursive: true });
|
|
212
253
|
// The project directory is what `zen open` opens, so this is where the
|
|
213
254
|
// editor actually reads them.
|
|
214
255
|
return { files, editor: editorFiles(opts.dir) };
|