@zenera/cli 1.1.16 → 1.1.18
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 +110 -19
- package/dist/banner.d.ts +19 -0
- package/dist/banner.js +12 -9
- package/dist/catalog.js +19 -20
- package/dist/command.d.ts +3 -0
- package/dist/commands/check.js +5 -1
- package/dist/commands/index.js +4 -1
- package/dist/commands/init.js +42 -7
- package/dist/commands/inspect.js +77 -13
- package/dist/commands/key.js +49 -1
- package/dist/commands/memory.js +16 -3
- package/dist/commands/meta.d.ts +3 -0
- package/dist/commands/meta.js +596 -0
- package/dist/commands/run.js +64 -35
- package/dist/engine.d.ts +15 -0
- package/dist/engine.js +26 -1
- package/dist/env.d.ts +20 -0
- package/dist/env.js +65 -0
- package/dist/home.d.ts +2 -0
- package/dist/home.js +2 -0
- package/dist/keys.d.ts +2 -0
- package/dist/keys.js +2 -0
- package/dist/main.js +1 -1
- package/dist/meta.d.ts +221 -0
- package/dist/meta.js +695 -0
- package/dist/podman.js +64 -2
- package/dist/projects.d.ts +9 -1
- package/dist/projects.js +19 -2
- package/dist/resolve.js +7 -1
- package/dist/sandbox.d.ts +11 -0
- package/dist/sandbox.js +24 -4
- package/dist/scaffold.d.ts +6 -0
- package/dist/scaffold.js +56 -15
- package/dist/session.d.ts +10 -0
- package/dist/session.js +19 -1
- package/dist/term.d.ts +7 -0
- package/dist/term.js +8 -1
- package/dist/tui/app.js +33 -5
- package/dist/tui/wrap.d.ts +1 -1
- package/dist/tui/wrap.js +1 -1
- package/dist/validate.js +57 -15
- package/package.json +3 -2
- package/templates/editor/.github/copilot-instructions.md +313 -94
- package/templates/editor/.github/prompts/project-review.prompt.md +74 -0
- package/templates/editor/.github/prompts/spec-apply-feedback.prompt.md +142 -0
- package/templates/editor/.github/prompts/spec-sync-project.prompt.md +392 -0
- package/templates/editor/.github/skills/zen-cli/references/check.md +6 -3
- package/templates/editor/.github/skills/zen-cli/references/frame.md +5 -3
- package/templates/editor/.github/skills/zen-cli/references/inspect.md +4 -3
- package/templates/editor/.github/skills/zen-cli/references/memory.md +14 -0
- package/templates/editor/.github/skills/zen-cli/references/projects.md +19 -2
- package/templates/editor/.github/skills/zen-cli/references/run.md +75 -16
- package/templates/editor/.github/skills/zen-cli/references/sandbox.md +6 -0
- package/templates/editor/.github/skills/zen-memory/SKILL.md +153 -33
- package/templates/editor/.github/skills/zen-memory/scripts/check-instructions.sh +115 -0
- 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 +44 -4
- package/templates/parts/memory-terms.yaml.tmpl +16 -0
- package/templates/parts/memory.yaml.tmpl +12 -0
- package/templates/project/SPECIFICATION.md.tmpl +32 -2
- package/templates/project/agents/memory-instructions.md +278 -0
- package/templates/project/agents.yaml.tmpl +12 -3
- package/templates/project/env +28 -0
- package/templates/project/gitignore +4 -4
- package/templates/editor/.github/prompts/review-project.prompt.md +0 -36
- package/templates/editor/.github/prompts/sync-with-spec.prompt.md +0 -205
- /package/templates/project/{INSTRUCTIONS.md → agents/instructions.md} +0 -0
package/dist/commands/run.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { writeFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
2
3
|
import { parse } from "../args.js";
|
|
3
4
|
import * as Engine from "../engine.js";
|
|
4
5
|
import { duration, Narrator, stopMark, summary } from "../narrate.js";
|
|
@@ -12,29 +13,42 @@ export const run = {
|
|
|
12
13
|
summary: 'Run the project — the TUI on a terminal, one shot otherwise.',
|
|
13
14
|
usage: USAGE,
|
|
14
15
|
details: [
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
' --workspace <dir> What the agent can read and write.',
|
|
19
|
-
' --model <ref> Override the default model.',
|
|
20
|
-
' --image <ref> Override the sandbox image commands run in.',
|
|
21
|
-
' --read-only Give the agent no way to write.',
|
|
22
|
-
' --no-keys Keep the API keys out of the sandbox.',
|
|
23
|
-
' --quiet Answer only; no narration.',
|
|
24
|
-
' --plain One shot, even on a terminal.',
|
|
25
|
-
' --theme <dark|light> Force the palette. Detected otherwise; $ZENERA_THEME.',
|
|
26
|
-
' --out <file> Write the answer here as well as to stdout.',
|
|
27
|
-
' --yes Accept the questions this would otherwise ask.',
|
|
16
|
+
'Arguments:',
|
|
17
|
+
' [project] Name of a project. Default: the one you are in.',
|
|
18
|
+
' [prompt] Your question, in quotes. Without one, the TUI opens.',
|
|
28
19
|
'',
|
|
29
|
-
'
|
|
30
|
-
'
|
|
20
|
+
'Options:',
|
|
21
|
+
' --project <name|dir> Which project to run. Default: the one you are in.',
|
|
22
|
+
' --session <id> Continue this session.',
|
|
23
|
+
' --new Start a new session without asking which one.',
|
|
24
|
+
' --workspace <dir> Directory the agent can read and write.',
|
|
25
|
+
' --memory <dir> Directory the agents remember into.',
|
|
26
|
+
' --model <ref> Use this model instead of the default.',
|
|
27
|
+
' --image <ref> Use this container image to run commands in.',
|
|
28
|
+
' --read-only Take away every tool that can write.',
|
|
29
|
+
' --no-keys Do not pass API keys into the container.',
|
|
30
|
+
' --plain Never open the TUI. Needs a prompt.',
|
|
31
|
+
' --theme <dark|light> Colours for the TUI. Default: auto.',
|
|
32
|
+
' --out <file> Put the answer in this file instead of on screen.',
|
|
33
|
+
' --yes Answer yes to every question.',
|
|
31
34
|
'',
|
|
32
|
-
'The
|
|
33
|
-
'
|
|
35
|
+
'The first word is the project when it names one, otherwise the prompt.',
|
|
36
|
+
'The answer goes to stdout and the progress to stderr, so `> out.md` keeps',
|
|
37
|
+
'the answer alone and `2>/dev/null` hides the progress.',
|
|
34
38
|
'',
|
|
35
|
-
'
|
|
36
|
-
'
|
|
37
|
-
'
|
|
39
|
+
'A prompt always starts a new session, so --new is for the TUI, where the',
|
|
40
|
+
'alternative is being asked which session to continue.',
|
|
41
|
+
'',
|
|
42
|
+
'Examples:',
|
|
43
|
+
' zen run open the TUI in this project',
|
|
44
|
+
' zen run acme open the TUI in the acme project',
|
|
45
|
+
' zen run "what changed?" ask that, print the answer, exit',
|
|
46
|
+
' git diff | zen run take the prompt from stdin',
|
|
47
|
+
' zen run "what changed?" > out.md redirect the answer into a file',
|
|
48
|
+
' zen run --out out.md "what changed?" write the answer to out.md only',
|
|
49
|
+
' zen run --read-only "what changed?" let it read but not write',
|
|
50
|
+
' zen run --memory ./mem remember into ./mem, not the project',
|
|
51
|
+
' zen run --new open the TUI in a new session',
|
|
38
52
|
],
|
|
39
53
|
run: async (ctx) => {
|
|
40
54
|
const { values, positionals } = parse(ctx.args, {
|
|
@@ -42,12 +56,12 @@ export const run = {
|
|
|
42
56
|
session: { type: 'string' },
|
|
43
57
|
new: { type: 'boolean' },
|
|
44
58
|
workspace: { type: 'string' },
|
|
59
|
+
memory: { type: 'string' },
|
|
45
60
|
model: { type: 'string' },
|
|
46
61
|
image: { type: 'string' },
|
|
47
62
|
'no-keys': { type: 'boolean' },
|
|
48
63
|
'read-only': { type: 'boolean' },
|
|
49
64
|
yes: { type: 'boolean' },
|
|
50
|
-
quiet: { type: 'boolean' },
|
|
51
65
|
plain: { type: 'boolean' },
|
|
52
66
|
theme: { type: 'string' },
|
|
53
67
|
out: { type: 'string' },
|
|
@@ -86,6 +100,9 @@ export const run = {
|
|
|
86
100
|
readOnly: values['read-only'],
|
|
87
101
|
model: values.model,
|
|
88
102
|
image: values.image,
|
|
103
|
+
// Relative to where it was typed, like every other path on the line
|
|
104
|
+
// — the project root is not the cwd.
|
|
105
|
+
memoryDir: values.memory ? resolve(ctx.cwd, values.memory) : undefined,
|
|
89
106
|
keys: values['no-keys'] ? false : undefined,
|
|
90
107
|
yes: values.yes || ctx.json,
|
|
91
108
|
});
|
|
@@ -96,7 +113,6 @@ export const run = {
|
|
|
96
113
|
// over it would be worse than not drawing one.
|
|
97
114
|
const drawing = !prompt &&
|
|
98
115
|
!values.plain &&
|
|
99
|
-
!values.quiet &&
|
|
100
116
|
!ctx.json &&
|
|
101
117
|
Boolean(process.stdout.isTTY && process.stdin.isTTY);
|
|
102
118
|
if (drawing) {
|
|
@@ -123,7 +139,7 @@ export const run = {
|
|
|
123
139
|
// ---------------------------------------------------------------------------
|
|
124
140
|
async function once(engine, prompt, values, asJson, cwd, where) {
|
|
125
141
|
const narrator = new Narrator({
|
|
126
|
-
quiet:
|
|
142
|
+
quiet: asJson,
|
|
127
143
|
live: Boolean(process.stderr.isTTY),
|
|
128
144
|
});
|
|
129
145
|
// Ctrl-C asks the run to stop rather than killing the process, so the turn
|
|
@@ -131,7 +147,7 @@ async function once(engine, prompt, values, asJson, cwd, where) {
|
|
|
131
147
|
const stopping = new AbortController();
|
|
132
148
|
const onInterrupt = () => stopping.abort();
|
|
133
149
|
process.once('SIGINT', onInterrupt);
|
|
134
|
-
if (!
|
|
150
|
+
if (!asJson) {
|
|
135
151
|
// Two questions were just answered, possibly without being asked. Say
|
|
136
152
|
// which way they went: a run that quietly resumed the wrong session, or
|
|
137
153
|
// wrote into the directory you were standing in, is only explainable
|
|
@@ -154,25 +170,38 @@ async function once(engine, prompt, values, asJson, cwd, where) {
|
|
|
154
170
|
}
|
|
155
171
|
if (asJson) {
|
|
156
172
|
json({
|
|
157
|
-
session: engine.session.id,
|
|
158
|
-
run:
|
|
173
|
+
session: { id: engine.session.id, dir: engine.session.dir },
|
|
174
|
+
run: {
|
|
175
|
+
id: outcome.run.id,
|
|
176
|
+
dir: outcome.run.dir,
|
|
177
|
+
input: outcome.run.input,
|
|
178
|
+
output: outcome.run.output,
|
|
179
|
+
state: outcome.run.state,
|
|
180
|
+
meta: outcome.run.meta,
|
|
181
|
+
...(outcome.report ? { report: outcome.report } : {}),
|
|
182
|
+
},
|
|
183
|
+
mounts: Engine.mounts(engine),
|
|
159
184
|
agent: outcome.result.agent,
|
|
160
185
|
stopReason: outcome.result.stopReason,
|
|
161
186
|
durationMs: outcome.durationMs,
|
|
162
187
|
usage: outcome.result.usage,
|
|
163
188
|
output: outcome.text,
|
|
164
|
-
report: outcome.run.report,
|
|
165
189
|
});
|
|
166
190
|
return;
|
|
167
191
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
note(
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
192
|
+
// --out is a destination, not a copy: the answer goes there instead of to
|
|
193
|
+
// stdout, so a redirect and a file cannot both end up holding it.
|
|
194
|
+
if (values.out) {
|
|
195
|
+
note(dim(`answer: ${cyan(values.out)}`));
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
write(outcome.text);
|
|
199
|
+
}
|
|
200
|
+
note('');
|
|
201
|
+
note(`${stopMark(outcome.result.stopReason)} ${dim(duration(outcome.durationMs))} ` +
|
|
202
|
+
dim(summary(outcome.result.usage)));
|
|
203
|
+
if (outcome.report) {
|
|
204
|
+
note(dim(`report: ${cyan(display(outcome.report, cwd))}`));
|
|
176
205
|
}
|
|
177
206
|
}
|
|
178
207
|
//# sourceMappingURL=run.js.map
|
package/dist/engine.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export interface EngineOptions {
|
|
|
10
10
|
model?: string;
|
|
11
11
|
/** sandbox image override — `--image` */
|
|
12
12
|
image?: string;
|
|
13
|
+
/** another directory to remember into — `--memory` */
|
|
14
|
+
memoryDir?: string;
|
|
13
15
|
/** whether credentials reach the sandbox — `--no-keys` sets this false */
|
|
14
16
|
keys?: boolean;
|
|
15
17
|
/** answer the sandbox's install question without asking — `--yes` */
|
|
@@ -20,6 +22,10 @@ export interface Engine {
|
|
|
20
22
|
runner: AgentRunner;
|
|
21
23
|
name: string;
|
|
22
24
|
workspace: string;
|
|
25
|
+
/** where the workspace appears inside the container */
|
|
26
|
+
workspaceMount: string;
|
|
27
|
+
/** whether the agent was denied every writing tool */
|
|
28
|
+
readOnly: boolean;
|
|
23
29
|
session: SessionPaths;
|
|
24
30
|
/** the session's accumulated state, when it has one */
|
|
25
31
|
state?: AgentState;
|
|
@@ -34,6 +40,15 @@ export interface Engine {
|
|
|
34
40
|
* and the whole conversation, its memory and its blobs travel with it.
|
|
35
41
|
*/
|
|
36
42
|
export declare function open(opts: EngineOptions): Promise<Engine>;
|
|
43
|
+
export interface Mount {
|
|
44
|
+
/** the directory on this machine */
|
|
45
|
+
host: string;
|
|
46
|
+
/** where it appears to the agent */
|
|
47
|
+
at: string;
|
|
48
|
+
readOnly: boolean;
|
|
49
|
+
}
|
|
50
|
+
/** Every tree the agent can see, the workspace first. */
|
|
51
|
+
export declare function mounts(engine: Engine): Mount[];
|
|
37
52
|
export interface RunOutcome {
|
|
38
53
|
run: RunPaths;
|
|
39
54
|
result: RunResult<string>;
|
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);
|
|
@@ -37,8 +42,13 @@ export async function open(opts) {
|
|
|
37
42
|
let files;
|
|
38
43
|
try {
|
|
39
44
|
const { root, config } = readProjectConfig(opts.project.dir);
|
|
45
|
+
// Naming a directory declares the graph, but it cannot bind anyone to
|
|
46
|
+
// it — so say when the run is pointed at a memory no agent will touch.
|
|
47
|
+
if (opts.memoryDir && !config.agents.some((a) => a.memory)) {
|
|
48
|
+
warn('--memory names a directory no agent uses — none has `memory: true`');
|
|
49
|
+
}
|
|
40
50
|
// Assets and the skill catalog are mounted for both, under one name.
|
|
41
|
-
const mounts = projectMounts(root, config);
|
|
51
|
+
const mounts = projectMounts(root, config, opts.memoryDir);
|
|
42
52
|
sandbox = buildSandbox({
|
|
43
53
|
config,
|
|
44
54
|
root,
|
|
@@ -47,6 +57,7 @@ export async function open(opts) {
|
|
|
47
57
|
readOnly: opts.readOnly,
|
|
48
58
|
image: opts.image,
|
|
49
59
|
keys: opts.keys,
|
|
60
|
+
env: env?.names,
|
|
50
61
|
mounts,
|
|
51
62
|
});
|
|
52
63
|
const workspaceOptions = {
|
|
@@ -72,6 +83,7 @@ export async function open(opts) {
|
|
|
72
83
|
...exaTools(),
|
|
73
84
|
],
|
|
74
85
|
skillsAt: SKILLS_MOUNT,
|
|
86
|
+
memoryDir: opts.memoryDir,
|
|
75
87
|
payloads,
|
|
76
88
|
resolveFile: (path) => files.within(path),
|
|
77
89
|
});
|
|
@@ -103,6 +115,8 @@ export async function open(opts) {
|
|
|
103
115
|
runner,
|
|
104
116
|
name: opts.project.name,
|
|
105
117
|
workspace,
|
|
118
|
+
workspaceMount: sandbox.spec.workdir ?? SANDBOX_MOUNT,
|
|
119
|
+
readOnly: Boolean(opts.readOnly),
|
|
106
120
|
session: opts.session,
|
|
107
121
|
state,
|
|
108
122
|
sandbox,
|
|
@@ -126,6 +140,17 @@ async function loadState(session) {
|
|
|
126
140
|
throw invalidError(`${session.state} is not a usable run state`, 'start a fresh session with --new');
|
|
127
141
|
}
|
|
128
142
|
}
|
|
143
|
+
/** Every tree the agent can see, the workspace first. */
|
|
144
|
+
export function mounts(engine) {
|
|
145
|
+
return [
|
|
146
|
+
{ host: engine.workspace, at: engine.workspaceMount, readOnly: engine.readOnly },
|
|
147
|
+
...engine.sandbox.mounts.map((m) => ({
|
|
148
|
+
host: m.host,
|
|
149
|
+
at: m.at,
|
|
150
|
+
readOnly: Boolean(m.readOnly),
|
|
151
|
+
})),
|
|
152
|
+
];
|
|
153
|
+
}
|
|
129
154
|
/**
|
|
130
155
|
* One turn. The session's state decides whether this starts a run or continues
|
|
131
156
|
* one — which is the whole of what `resume` used to be, and why it is not a
|
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/home.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export declare const paths: {
|
|
|
5
5
|
keys: () => string;
|
|
6
6
|
keyDir: () => string;
|
|
7
7
|
faker: () => string;
|
|
8
|
+
/** what the meta agent runs on, which is a choice about a tool, not a project */
|
|
9
|
+
meta: () => string;
|
|
8
10
|
/** work already done and worth keeping: vectors, parses, model listings */
|
|
9
11
|
cache: () => string;
|
|
10
12
|
};
|
package/dist/home.js
CHANGED
|
@@ -26,6 +26,8 @@ export const paths = {
|
|
|
26
26
|
keys: () => join(home(), 'keys.json'),
|
|
27
27
|
keyDir: () => join(home(), 'keys'),
|
|
28
28
|
faker: () => join(home(), 'faker'),
|
|
29
|
+
/** what the meta agent runs on, which is a choice about a tool, not a project */
|
|
30
|
+
meta: () => join(home(), 'meta.json'),
|
|
29
31
|
/** work already done and worth keeping: vectors, parses, model listings */
|
|
30
32
|
cache: () => join(home(), 'cache'),
|
|
31
33
|
};
|
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
|
@@ -60,7 +60,7 @@ async function main(argv) {
|
|
|
60
60
|
const ragSearch = name === 'rag' && ['docs', 'schema'].includes(rest[0]) && rest[1] === 'search';
|
|
61
61
|
const interactive = rest.includes('--interactive');
|
|
62
62
|
if (!json && (!ragSearch || interactive)) {
|
|
63
|
-
const brand = external && hasExternal(external) ? external.banner :
|
|
63
|
+
const brand = external && hasExternal(external) ? external.banner : command?.banner;
|
|
64
64
|
printBanner(brand ?? NEO_BANNER, { full: ragSearch && interactive });
|
|
65
65
|
}
|
|
66
66
|
if (parts.name === 'help') {
|
package/dist/meta.d.ts
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { spawn as nodeSpawn } from 'node:child_process';
|
|
2
|
+
import { type KeyEntry, type KeyStore, type Provider } from './keys.ts';
|
|
3
|
+
export declare const PROMPT_DIR = ".github/prompts";
|
|
4
|
+
/** The variable a project's `.env` names it with — a zen ref, not a wire id. */
|
|
5
|
+
export declare const MODEL_ENV = "ZENERA_META_MODEL";
|
|
6
|
+
export type ModelSource = 'flag' | 'shell' | 'env' | 'store' | 'project' | 'default';
|
|
7
|
+
export interface ModelChoice {
|
|
8
|
+
/** a zen ref: `vertex/gemini-3.8-flash`, or a bare id for the default provider */
|
|
9
|
+
ref: string;
|
|
10
|
+
from: ModelSource;
|
|
11
|
+
}
|
|
12
|
+
export interface ModelSources {
|
|
13
|
+
flag?: string;
|
|
14
|
+
/** `ZENERA_META_MODEL` as it stood before the project's `.env` was folded in */
|
|
15
|
+
shell?: string;
|
|
16
|
+
/** the same variable after it was, which is the `.env` answer when they differ */
|
|
17
|
+
env?: string;
|
|
18
|
+
stored?: string;
|
|
19
|
+
/** the project's `agents.yaml` `model:` */
|
|
20
|
+
project?: string;
|
|
21
|
+
/** what a held key recommends, when nothing above answered */
|
|
22
|
+
fallback?: string;
|
|
23
|
+
}
|
|
24
|
+
/** Where the answer is looked for, best first. */
|
|
25
|
+
export declare const ORDER: readonly ModelSource[];
|
|
26
|
+
export declare function chooseModel(sources: ModelSources): ModelChoice | undefined;
|
|
27
|
+
export declare const SOURCE_LABELS: Record<ModelSource, string>;
|
|
28
|
+
/** First of each row is the default; the rest are what `--pick` offers beside it. */
|
|
29
|
+
export declare const RECOMMENDED: Partial<Record<Provider, readonly string[]>>;
|
|
30
|
+
/** The recommended ref for the first provider with a key, or for `only`. */
|
|
31
|
+
export declare function defaultRef(store: KeyStore, only?: Provider): string | undefined;
|
|
32
|
+
export interface MetaFile {
|
|
33
|
+
version: 1;
|
|
34
|
+
model?: string;
|
|
35
|
+
}
|
|
36
|
+
export declare const readMeta: () => Promise<MetaFile>;
|
|
37
|
+
export declare function writeMeta(file: MetaFile): void;
|
|
38
|
+
/**
|
|
39
|
+
* `vertex/gemini-3.8-flash` splits; `gpt-5.6-sol` does not. Only a known provider
|
|
40
|
+
* counts as a prefix, because `google/gemini-3.8-flash` is also a perfectly good
|
|
41
|
+
* bare id on an endpoint that speaks publisher names.
|
|
42
|
+
*
|
|
43
|
+
* The colon form is the library's own — `openai:gpt-5.6-sol`, and with an api
|
|
44
|
+
* selector `openai/responses:gpt-5.6-sol` — which is what arrives when the ref
|
|
45
|
+
* came from `agents.yaml`.
|
|
46
|
+
*/
|
|
47
|
+
export declare function splitRef(ref: string): {
|
|
48
|
+
provider?: Provider;
|
|
49
|
+
id: string;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* The provider a misspelt prefix was reaching for, if that is what it is.
|
|
53
|
+
*
|
|
54
|
+
* `vertes/gemini-3.8-flash` is shaped exactly like the OpenRouter id
|
|
55
|
+
* `meta-llama/llama-4`, so an unknown prefix cannot be an error on its own —
|
|
56
|
+
* only one close enough to a real provider name to be a slip of the hand.
|
|
57
|
+
*/
|
|
58
|
+
export declare function misspelledProvider(ref: string): Provider | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* Copilot offers its tools as OpenAI *custom* tools, which the completions API
|
|
61
|
+
* rejects outright — `400 Invalid value: 'custom'`. Only the responses API
|
|
62
|
+
* takes them, and only the reasoning models serve it, so the wire API follows
|
|
63
|
+
* the model rather than being a flag nobody would know to set.
|
|
64
|
+
*/
|
|
65
|
+
export declare function wireApi(provider: Provider, id: string): string | undefined;
|
|
66
|
+
export interface Wiring {
|
|
67
|
+
provider: Provider;
|
|
68
|
+
/** what the model is called on the wire */
|
|
69
|
+
model: string;
|
|
70
|
+
/** every `COPILOT_*` name and value, ready to hand to the child */
|
|
71
|
+
env: Record<string, string>;
|
|
72
|
+
/** names whose values are secret, for `--secret-env-vars` and for masking */
|
|
73
|
+
secret: string[];
|
|
74
|
+
/** anything true but unwelcome: a location that will be slow, a stale default */
|
|
75
|
+
warnings: string[];
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Turns a zen key entry and a model id into copilot's environment.
|
|
79
|
+
*
|
|
80
|
+
* `COPILOT_PROVIDER_BASE_URL` is what activates BYOK at all, so every provider
|
|
81
|
+
* gets one — including OpenAI, whose url copilot would otherwise have reached
|
|
82
|
+
* through a GitHub account we are deliberately not using.
|
|
83
|
+
*/
|
|
84
|
+
export declare function wire(store: KeyStore, entry: KeyEntry, id: string): Wiring;
|
|
85
|
+
/** Values a `--dry-run` may print. A key is four characters and an apology. */
|
|
86
|
+
export declare function masked(env: Record<string, string>, secret: readonly string[]): string[];
|
|
87
|
+
/** `/project-review`, `project-review`, `project-review.prompt.md` or a path. */
|
|
88
|
+
export declare function promptPath(dir: string, name: string): string;
|
|
89
|
+
export interface StoredPrompt {
|
|
90
|
+
name: string;
|
|
91
|
+
path: string;
|
|
92
|
+
description?: string;
|
|
93
|
+
body: string;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Frontmatter is the editor's business — `mode`, `tools`, `description` — and
|
|
97
|
+
* none of it means anything to copilot, so only the body is sent. `description`
|
|
98
|
+
* is kept for the one line of narration that says which prompt is running.
|
|
99
|
+
*/
|
|
100
|
+
export declare function readPrompt(path: string, name: string, text: string): StoredPrompt;
|
|
101
|
+
export declare function loadPrompt(dir: string, name: string): Promise<StoredPrompt>;
|
|
102
|
+
export declare function listPrompts(dir: string): Promise<string[]>;
|
|
103
|
+
export interface Binary {
|
|
104
|
+
command: string;
|
|
105
|
+
args: string[];
|
|
106
|
+
from: 'path' | 'npx';
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* A `copilot` already installed is used as it is; otherwise `npx` fetches one.
|
|
110
|
+
* Note that finding the name on `PATH` is not the same as finding the CLI —
|
|
111
|
+
* an editor may have put a shim there that installs it on first run — so this
|
|
112
|
+
* only decides how to launch, never whether it will work.
|
|
113
|
+
*/
|
|
114
|
+
export declare function locate(has?: (cmd: string) => boolean): Binary;
|
|
115
|
+
interface Event {
|
|
116
|
+
type: string;
|
|
117
|
+
data?: Record<string, unknown>;
|
|
118
|
+
[key: string]: unknown;
|
|
119
|
+
}
|
|
120
|
+
export interface Outcome {
|
|
121
|
+
/** the last thing the agent said that was not a preamble to a tool call */
|
|
122
|
+
answer: string;
|
|
123
|
+
exitCode: number;
|
|
124
|
+
sessionId?: string;
|
|
125
|
+
usage?: Record<string, unknown>;
|
|
126
|
+
events: Event[];
|
|
127
|
+
}
|
|
128
|
+
export interface Sink {
|
|
129
|
+
answer(text: string): void;
|
|
130
|
+
narrate(line: string): void;
|
|
131
|
+
warn(line: string): void;
|
|
132
|
+
/** Kept by the log, never shown: the step-by-step nobody reads while it runs. */
|
|
133
|
+
detail?(line: string): void;
|
|
134
|
+
/** A transient last row, replaced each time and never kept. '' removes it. */
|
|
135
|
+
status?(line: string): void;
|
|
136
|
+
/** Called once when the run ends, so a repainting sink can clear itself. */
|
|
137
|
+
close?(): void;
|
|
138
|
+
}
|
|
139
|
+
export declare const terminalSink: Sink;
|
|
140
|
+
/** Rows of narration the window keeps on screen. */
|
|
141
|
+
export declare const WINDOW_ROWS = 8;
|
|
142
|
+
/** How often the border is redrawn — the wave's frame, not the clock's. */
|
|
143
|
+
export declare const FRAME_MS = 80;
|
|
144
|
+
/** Cells the crest travels per frame — a lap of a wide window in a few seconds. */
|
|
145
|
+
export declare const WAVE_STEP = 3;
|
|
146
|
+
/**
|
|
147
|
+
* How brightly one cell of the border is lit, given where the crest has got to.
|
|
148
|
+
*
|
|
149
|
+
* `position` counts clockwise from the top left corner, so one index walks the
|
|
150
|
+
* whole ring and the wave rounds the corners without a seam. Everything the
|
|
151
|
+
* tail has passed sits at the dimmest tone, which is the border's resting
|
|
152
|
+
* colour: the ring is always drawn, only the crest moves.
|
|
153
|
+
*/
|
|
154
|
+
export declare function toneAt(position: number, head: number, span: number): number;
|
|
155
|
+
/**
|
|
156
|
+
* The bottom rule as cells, with the status label set into it near the left.
|
|
157
|
+
*
|
|
158
|
+
* Waiting belongs to the frame rather than to the narration: a row of its own
|
|
159
|
+
* costs a step of what the agent is doing every time, and it settles just
|
|
160
|
+
* under the last line, which is where the eye already is. Near the left, where
|
|
161
|
+
* reading starts, and not so near that it displaces the corner.
|
|
162
|
+
*/
|
|
163
|
+
export declare function footerCells(label: string, width: number): string[];
|
|
164
|
+
/**
|
|
165
|
+
* Narration held to a fixed number of rows that rewrite themselves in place.
|
|
166
|
+
*
|
|
167
|
+
* A long run is hundreds of tool calls, and printing each as its own line
|
|
168
|
+
* scrolls the question, the model line and every warning off the top of the
|
|
169
|
+
* screen — by the end the terminal holds a transcript nobody asked for and the
|
|
170
|
+
* answer is somewhere in the middle of it. Only the last few steps say what it
|
|
171
|
+
* is doing now, which is the only thing narration is for; the rest is in the
|
|
172
|
+
* session copilot recorded.
|
|
173
|
+
*
|
|
174
|
+
* Warnings are not narration: they leave the window and stay on the screen.
|
|
175
|
+
*
|
|
176
|
+
* Without a terminal there is nothing to rewrite over, so every line is its
|
|
177
|
+
* own — which is what a CI log wants anyway.
|
|
178
|
+
*/
|
|
179
|
+
export declare function windowSink(rows?: number): Sink;
|
|
180
|
+
/**
|
|
181
|
+
* The answer in the same box `zen run` draws it in: bounded width, rounded
|
|
182
|
+
* rule, fenced blocks kept as they were written.
|
|
183
|
+
*
|
|
184
|
+
* Redirected output gets the text and nothing else — `zen meta run ... > out.md`
|
|
185
|
+
* is meant to produce a file you can read, not one with a border down its side.
|
|
186
|
+
*/
|
|
187
|
+
export declare function answerBox(text: string, columns?: number): string[];
|
|
188
|
+
export interface Log {
|
|
189
|
+
readonly path: string;
|
|
190
|
+
/** Where the answer alone goes, beside the log and openable on its own. */
|
|
191
|
+
readonly answerPath: string;
|
|
192
|
+
line(text: string): void;
|
|
193
|
+
saveAnswer(text: string): void;
|
|
194
|
+
close(): void;
|
|
195
|
+
}
|
|
196
|
+
/** `<project>/.tmp/logs/meta.<when>.log`, opened for append. */
|
|
197
|
+
export declare function openLog(dir: string, now?: Date): Log;
|
|
198
|
+
/**
|
|
199
|
+
* Folds one event into the outcome and says what, if anything, to show for it.
|
|
200
|
+
*
|
|
201
|
+
* `assistant.message` carries both the running commentary and the final word;
|
|
202
|
+
* what separates them is `toolRequests`, so the last message that asked for no
|
|
203
|
+
* tool is the answer and the rest are narration.
|
|
204
|
+
*/
|
|
205
|
+
export declare function absorb(event: Event, out: Outcome, sink: Sink, width: number): void;
|
|
206
|
+
export interface Launch {
|
|
207
|
+
binary: Binary;
|
|
208
|
+
args: string[];
|
|
209
|
+
env: Record<string, string>;
|
|
210
|
+
cwd: string;
|
|
211
|
+
sink?: Sink;
|
|
212
|
+
log?: Log;
|
|
213
|
+
width?: number;
|
|
214
|
+
/** injected by the tests, which have no copilot and want none */
|
|
215
|
+
spawn?: typeof nodeSpawn;
|
|
216
|
+
}
|
|
217
|
+
export declare function launch(opts: Launch): Promise<Outcome>;
|
|
218
|
+
/** Non-fatal, said once, because a competing file is a silent override. */
|
|
219
|
+
export declare function providersWarning(): string | undefined;
|
|
220
|
+
export {};
|
|
221
|
+
//# sourceMappingURL=meta.d.ts.map
|