@zenera/cli 1.1.2 → 1.1.3
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 +88 -11
- package/dist/audit.d.ts +8 -6
- package/dist/audit.js +14 -22
- package/dist/commands/check.js +34 -7
- package/dist/commands/init.js +71 -11
- package/dist/commands/key.js +126 -36
- package/dist/commands/models.js +3 -3
- package/dist/commands/open.js +2 -2
- package/dist/commands/run.js +3 -0
- package/dist/engine.d.ts +2 -0
- package/dist/engine.js +1 -0
- package/dist/keys.d.ts +95 -12
- package/dist/keys.js +175 -34
- package/dist/lib.d.ts +1 -1
- package/dist/lib.js +1 -1
- package/dist/liveness.d.ts +16 -6
- package/dist/liveness.js +74 -23
- package/dist/sandbox.d.ts +2 -0
- package/dist/sandbox.js +58 -7
- package/dist/scaffold.d.ts +21 -21
- package/dist/scaffold.js +132 -204
- package/dist/validate.js +3 -3
- package/package.json +2 -18
- package/templates/{.github → editor/.github}/copilot-instructions.md +7 -6
- package/templates/editor/.github/skills/api-schema-index/SKILL.md +292 -0
- package/templates/editor/.github/skills/zen-cli/SKILL.md +74 -0
- package/templates/editor/.github/skills/zen-cli/references/check.md +92 -0
- package/templates/editor/.github/skills/zen-cli/references/faker.md +111 -0
- package/templates/editor/.github/skills/zen-cli/references/frame.md +119 -0
- package/templates/editor/.github/skills/zen-cli/references/inspect.md +61 -0
- package/templates/editor/.github/skills/zen-cli/references/keys.md +114 -0
- package/templates/editor/.github/skills/zen-cli/references/projects.md +99 -0
- package/templates/editor/.github/skills/zen-cli/references/rag.md +159 -0
- package/templates/editor/.github/skills/zen-cli/references/run.md +104 -0
- package/templates/editor/.github/skills/zen-cli/references/sandbox.md +91 -0
- package/templates/editor/.vscode/settings.json +6 -0
- package/templates/parts/exa.yaml.tmpl +5 -0
- package/templates/parts/model.yaml.tmpl +4 -0
- package/templates/parts/models.yaml.tmpl +10 -0
- package/templates/project/INSTRUCTIONS.md +7 -0
- package/templates/project/SPECIFICATION.md +6 -0
- package/templates/project/agents/prompts/default.md +15 -0
- package/templates/project/agents.yaml.tmpl +44 -0
- package/templates/project/assets/README.md +12 -0
- package/templates/project/gitignore +9 -0
- package/templates/{sandbox → project/sandbox}/Dockerfile +2 -0
- package/templates/.github/skills/zen-cli/SKILL.md +0 -110
- /package/templates/{.github → editor/.github}/prompts/new-agent.prompt.md +0 -0
- /package/templates/{.github → editor/.github}/prompts/new-skill.prompt.md +0 -0
- /package/templates/{.github → editor/.github}/prompts/review-project.prompt.md +0 -0
package/dist/scaffold.js
CHANGED
|
@@ -4,18 +4,61 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
// ---------------------------------------------------------------------------
|
|
5
5
|
// Scaffolding
|
|
6
6
|
//
|
|
7
|
-
// What `zen init` writes
|
|
7
|
+
// What `zen init` writes and `zen open` refreshes — none of which is in this
|
|
8
|
+
// file. `templates/` holds the real thing, laid out the way it lands, so
|
|
9
|
+
// changing what a project starts life as is editing a file rather than a string
|
|
10
|
+
// literal escaping every backtick and `${...}` it contains:
|
|
11
|
+
//
|
|
12
|
+
// templates/project/ the project's own files. Written once and edited from
|
|
13
|
+
// then on, so anything already there is left alone.
|
|
14
|
+
// templates/editor/ ours: `.vscode/settings.json` and the `.github/` tree,
|
|
15
|
+
// which describe this version of `zen` to the editor and
|
|
16
|
+
// are replaced every time.
|
|
17
|
+
// templates/parts/ fragments spliced into a template above.
|
|
18
|
+
//
|
|
19
|
+
// The trees are copied whole and nothing enumerates them, so adding a file to
|
|
20
|
+
// a new project is adding a file to `templates/project/` and nothing else.
|
|
21
|
+
//
|
|
22
|
+
// What is there is deliberately close to empty: a template full of
|
|
8
23
|
// commented-out options is a template nobody reads and everybody deletes. The
|
|
9
|
-
// one agent
|
|
24
|
+
// one agent works as written, and every other knob is in `docs/`.
|
|
10
25
|
// ---------------------------------------------------------------------------
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
26
|
+
const TEMPLATES = fileURLToPath(new URL('../templates', import.meta.url));
|
|
27
|
+
/** The suffix on a file with `{{...}}` in it, dropped when the file lands. */
|
|
28
|
+
const TEMPLATE = '.tmpl';
|
|
29
|
+
/**
|
|
30
|
+
* Fills the `{{name}}` in a template, in the two shapes templates use.
|
|
31
|
+
*
|
|
32
|
+
* A placeholder alone on a line takes a whole fragment: its own indentation is
|
|
33
|
+
* applied to every line of the value, and an empty value takes the line with
|
|
34
|
+
* it — which is how an optional block leaves nothing behind. Anywhere else it
|
|
35
|
+
* takes a word. A name nothing supplies throws, so a typo in a template is a
|
|
36
|
+
* failing test rather than a `{{provider}}` sitting in somebody's agents.yaml.
|
|
37
|
+
*/
|
|
38
|
+
function render(text, vars) {
|
|
39
|
+
const value = (name) => {
|
|
40
|
+
const found = vars[name];
|
|
41
|
+
if (found === undefined) {
|
|
42
|
+
throw new Error(`template asks for {{${name}}}, which nothing supplies`);
|
|
43
|
+
}
|
|
44
|
+
return found;
|
|
45
|
+
};
|
|
46
|
+
return text
|
|
47
|
+
.replace(/^([ \t]*)\{\{(\w+)\}\}[ \t]*\r?\n/gm, (_, indent, name) => {
|
|
48
|
+
const body = value(name).trimEnd();
|
|
49
|
+
if (!body) {
|
|
50
|
+
return '';
|
|
51
|
+
}
|
|
52
|
+
const lines = body.split('\n').map((line) => (line ? indent + line : ''));
|
|
53
|
+
return `${lines.join('\n')}\n`;
|
|
54
|
+
})
|
|
55
|
+
.replace(/\{\{(\w+)\}\}/g, (_, name) => value(name));
|
|
56
|
+
}
|
|
57
|
+
/** Reads one fragment from `templates/parts/`, without its trailing newline. */
|
|
58
|
+
function part(name, vars = {}) {
|
|
59
|
+
const text = readFileSync(join(TEMPLATES, 'parts', `${name}${TEMPLATE}`), 'utf8');
|
|
60
|
+
return render(text, vars).trimEnd();
|
|
61
|
+
}
|
|
19
62
|
/**
|
|
20
63
|
* The `model:` section, which is one line until it has to say more.
|
|
21
64
|
*
|
|
@@ -24,118 +67,58 @@ Replace this with yours.
|
|
|
24
67
|
* reasoning means splitting the ref back into the two fields and giving the
|
|
25
68
|
* configuration a name to be referred to by.
|
|
26
69
|
*/
|
|
27
|
-
|
|
70
|
+
function modelSection(ref, options) {
|
|
28
71
|
const colon = ref.indexOf(':');
|
|
29
72
|
if (!options || colon < 0) {
|
|
30
|
-
return ('
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
73
|
+
return part('model.yaml', { ref });
|
|
74
|
+
}
|
|
75
|
+
return part('models.yaml', {
|
|
76
|
+
provider: ref.slice(0, colon),
|
|
77
|
+
id: ref.slice(colon + 1),
|
|
78
|
+
options,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The name a template file lands under.
|
|
83
|
+
*
|
|
84
|
+
* `gitignore` gains its dot here because it cannot have one in the repository:
|
|
85
|
+
* npm strips a `.gitignore` out of a published tarball, and git would read this
|
|
86
|
+
* one as rules about `packages/cli/templates/` rather than as content.
|
|
87
|
+
*/
|
|
88
|
+
function target(name) {
|
|
89
|
+
if (name.endsWith(TEMPLATE)) {
|
|
90
|
+
return name.slice(0, -TEMPLATE.length);
|
|
34
91
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
.split('\n')
|
|
38
|
-
.map((line) => (line ? ` ${line}` : ''))
|
|
39
|
-
.join('\n');
|
|
40
|
-
return ('# The model an agent uses when it does not pin its own. Change it here and the\n' +
|
|
41
|
-
'# whole project moves. A named configuration is what gives the knobs below\n' +
|
|
42
|
-
'# somewhere to live; a bare `model: <provider>:<id>` works when there are none.\n' +
|
|
43
|
-
'models:\n' +
|
|
44
|
-
' main:\n' +
|
|
45
|
-
` provider: ${ref.slice(0, colon)}\n` +
|
|
46
|
-
` model: ${ref.slice(colon + 1)}\n` +
|
|
47
|
-
`${indented}\n` +
|
|
48
|
-
'\n' +
|
|
49
|
-
'model: main');
|
|
50
|
-
};
|
|
92
|
+
return name === 'gitignore' ? '.gitignore' : name;
|
|
93
|
+
}
|
|
51
94
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
95
|
+
* Copies one template directory into `dir` at `rel`, depth first, sorted so the
|
|
96
|
+
* list it returns is the same on every machine. Only a `.tmpl` is read as text;
|
|
97
|
+
* everything else is copied byte for byte.
|
|
55
98
|
*/
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
# \`persist: true\` keeps the container between runs rather than throwing it
|
|
80
|
-
# away, so what an agent installs for itself is still there next time —
|
|
81
|
-
# otherwise only /workspace and its home directory survive. \`zen sandbox
|
|
82
|
-
# clean\` removes the ones left behind. Everything else has a default; see the
|
|
83
|
-
# sandbox: block in docs/agents-yaml.md to size it.
|
|
84
|
-
sandbox:
|
|
85
|
-
persist: true
|
|
86
|
-
build:
|
|
87
|
-
dockerfile: sandbox/Dockerfile
|
|
88
|
-
|
|
89
|
-
agents:
|
|
90
|
-
- name: default
|
|
91
|
-
description: The entry point.
|
|
92
|
-
# Instructions live in agents/prompts/<name>.md and are picked up by
|
|
93
|
-
# convention — no need to name the file here.
|
|
94
|
-
#
|
|
95
|
-
# workspace:* is every file tool at once, sandbox:* is the shell. Name
|
|
96
|
-
# them one by one to be narrower, or subtract: [workspace:*, -delete_file]
|
|
97
|
-
#
|
|
98
|
-
# sandbox:* runs commands in a container, not on this machine, so it
|
|
99
|
-
# needs podman — \`zen run\` installs and starts what it can on its own,
|
|
100
|
-
# and \`zen sandbox status\` says where that got to. Drop the line if you
|
|
101
|
-
# would rather this agent never reached a shell.${web ? EXA_NOTE : ''}
|
|
102
|
-
tools:
|
|
103
|
-
- workspace:*
|
|
104
|
-
- sandbox:*${web ? '\n - exa:*' : ''}
|
|
105
|
-
`;
|
|
106
|
-
const PROMPT = `You are a helpful assistant working inside a project workspace.
|
|
107
|
-
|
|
108
|
-
You have tools to read, search and edit files. The workspace is the only
|
|
109
|
-
place you can see; paths are relative to its root.
|
|
110
|
-
|
|
111
|
-
You can also run shell commands. They run in a container over the same
|
|
112
|
-
workspace, not on the user's machine, so a command that fails there has cost
|
|
113
|
-
them nothing — but it is still their work in the directory, so read before you
|
|
114
|
-
overwrite and say what you ran.
|
|
115
|
-
|
|
116
|
-
Read a file before you change it: \`apply_patch\` matches the surrounding text
|
|
117
|
-
exactly, so a patch written from memory will not apply. Use \`apply_patch\` to
|
|
118
|
-
change part of a file and \`write_file\` only for a new one.
|
|
119
|
-
|
|
120
|
-
Say what you changed.
|
|
121
|
-
`;
|
|
122
|
-
const GITIGNORE = `# Sessions hold run state, memory, blobs and whatever the agent wrote.
|
|
123
|
-
# None of it is source.
|
|
124
|
-
sessions/
|
|
125
|
-
`;
|
|
126
|
-
const ASSETS_README = `# assets
|
|
127
|
-
|
|
128
|
-
Everything in this folder is mounted at /assets when an agent runs. Every agent
|
|
129
|
-
in this project can read, list and search it, and no tool of theirs can change
|
|
130
|
-
it — so this is where reference material goes: handbooks, specifications,
|
|
131
|
-
schemas, worked examples, the style guide the output is supposed to follow.
|
|
132
|
-
|
|
133
|
-
It is the project's own files that agents get without being asked. The
|
|
134
|
-
workspace they are pointed at is the work; this is what they consult while
|
|
135
|
-
doing it.
|
|
136
|
-
|
|
137
|
-
Delete this file once there is something here to read.
|
|
138
|
-
`;
|
|
99
|
+
function copyTree(from, dir, rel, opts) {
|
|
100
|
+
const written = [];
|
|
101
|
+
mkdirSync(join(dir, rel), { recursive: true });
|
|
102
|
+
const entries = readdirSync(from, { withFileTypes: true });
|
|
103
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
const source = join(from, entry.name);
|
|
106
|
+
if (entry.isDirectory()) {
|
|
107
|
+
written.push(...copyTree(source, dir, join(rel, entry.name), opts));
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const child = join(rel, target(entry.name));
|
|
111
|
+
if (opts.keep && existsSync(join(dir, child))) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const body = entry.name.endsWith(TEMPLATE)
|
|
115
|
+
? render(readFileSync(source, 'utf8'), opts.vars ?? {})
|
|
116
|
+
: readFileSync(source);
|
|
117
|
+
writeFileSync(join(dir, child), body);
|
|
118
|
+
written.push(child);
|
|
119
|
+
}
|
|
120
|
+
return written;
|
|
121
|
+
}
|
|
139
122
|
// ---------------------------------------------------------------------------
|
|
140
123
|
// Telling the editor which instructions are not for it
|
|
141
124
|
//
|
|
@@ -152,107 +135,52 @@ Delete this file once there is something here to read.
|
|
|
152
135
|
// up whatever `AGENTS.md` a run happened to leave behind. It is a *restricted*
|
|
153
136
|
// setting, so it applies only in a trusted workspace; that is the right way
|
|
154
137
|
// round, since an untrusted folder is not one to run agents in either.
|
|
155
|
-
// ---------------------------------------------------------------------------
|
|
156
|
-
const VSCODE_SETTINGS = `{
|
|
157
|
-
"chat.useNestedAgentsMdFiles": false,
|
|
158
|
-
"chat.tools.terminal.autoApprove": {
|
|
159
|
-
"zen": true
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
`;
|
|
163
|
-
/**
|
|
164
|
-
* Writes `.vscode/settings.json` under `dir`, replacing what is there. The file
|
|
165
|
-
* is ours: it says how the editor is to treat a directory the agents write
|
|
166
|
-
* into, and a stale copy of that answer is worse than none. Returns the
|
|
167
|
-
* relative path.
|
|
168
|
-
*/
|
|
169
|
-
export function editorSettings(dir) {
|
|
170
|
-
const rel = join('.vscode', 'settings.json');
|
|
171
|
-
mkdirSync(join(dir, '.vscode'), { recursive: true });
|
|
172
|
-
writeFileSync(join(dir, rel), VSCODE_SETTINGS);
|
|
173
|
-
return rel;
|
|
174
|
-
}
|
|
175
|
-
// ---------------------------------------------------------------------------
|
|
176
|
-
// The other half of the editor story
|
|
177
138
|
//
|
|
178
139
|
// `INSTRUCTIONS.md` addresses the *project's* agents. The editor's assistant
|
|
179
140
|
// still needs a brief of its own, and what it needs to know is how this kind
|
|
180
141
|
// of project is put together — the file formats, how a prompt is written, when
|
|
181
|
-
// to add a skill rather than an agent. That is
|
|
182
|
-
// standing brief, plus the prompt files and skills the editor picks up from
|
|
183
|
-
//
|
|
184
|
-
// are full of backticks and `${...}` examples, which a TS template literal
|
|
185
|
-
// cannot hold without escaping every one of them into illegibility.
|
|
186
|
-
//
|
|
187
|
-
// `templates/.github/` mirrors what lands in the project one for one, so
|
|
188
|
-
// adding a skill or a prompt file is adding a file there and nothing else.
|
|
142
|
+
// to add a skill rather than an agent. That is what the `.github/` tree is: the
|
|
143
|
+
// standing brief, plus the prompt files and skills the editor picks up from the
|
|
144
|
+
// same place.
|
|
189
145
|
// ---------------------------------------------------------------------------
|
|
190
|
-
const GITHUB_TEMPLATE = fileURLToPath(new URL('../templates/.github', import.meta.url));
|
|
191
|
-
const SANDBOX_TEMPLATE = fileURLToPath(new URL('../templates/sandbox', import.meta.url));
|
|
192
146
|
/**
|
|
193
|
-
* Writes the
|
|
194
|
-
*
|
|
195
|
-
*
|
|
147
|
+
* Writes the editor's files under `dir`, replacing what is there. They are
|
|
148
|
+
* ours: they say how the editor is to treat a directory the agents write into,
|
|
149
|
+
* and they describe the file formats of the version of `zen` in hand, so the
|
|
150
|
+
* current answer is the only one worth having and a stale one is worse than
|
|
151
|
+
* none. Returns the relative paths written.
|
|
196
152
|
*/
|
|
197
|
-
export function
|
|
198
|
-
return copyTree(
|
|
199
|
-
}
|
|
200
|
-
/**
|
|
201
|
-
* Writes `sandbox/`, the Dockerfile the scaffolded `agents.yaml` builds. Unlike
|
|
202
|
-
* the editor files this becomes the project's own — it is meant to be edited —
|
|
203
|
-
* so anything already there is left alone.
|
|
204
|
-
*/
|
|
205
|
-
export function sandboxTemplate(dir) {
|
|
206
|
-
return copyTree(SANDBOX_TEMPLATE, dir, 'sandbox', { keep: true });
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* Copies one template directory into `dir` at `rel`, depth first, sorted so
|
|
210
|
-
* the list it returns is the same on every machine.
|
|
211
|
-
*/
|
|
212
|
-
function copyTree(from, dir, rel, opts) {
|
|
213
|
-
const written = [];
|
|
214
|
-
mkdirSync(join(dir, rel), { recursive: true });
|
|
215
|
-
const entries = readdirSync(from, { withFileTypes: true });
|
|
216
|
-
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
217
|
-
for (const entry of entries) {
|
|
218
|
-
const child = join(rel, entry.name);
|
|
219
|
-
if (entry.isDirectory()) {
|
|
220
|
-
written.push(...copyTree(join(from, entry.name), dir, child, opts));
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
|
-
if (opts?.keep && existsSync(join(dir, child))) {
|
|
224
|
-
continue;
|
|
225
|
-
}
|
|
226
|
-
writeFileSync(join(dir, child), readFileSync(join(from, entry.name)));
|
|
227
|
-
written.push(child);
|
|
228
|
-
}
|
|
229
|
-
return written;
|
|
153
|
+
export function editorFiles(dir) {
|
|
154
|
+
return copyTree(join(TEMPLATES, 'editor'), dir, '', {});
|
|
230
155
|
}
|
|
231
156
|
/**
|
|
232
|
-
* Writes a project. Never overwrites the project's own files
|
|
233
|
-
*
|
|
234
|
-
* and are replaced.
|
|
157
|
+
* Writes a project. Never overwrites the project's own files — a second `init`
|
|
158
|
+
* over a directory fills in what is missing and leaves the rest alone — but the
|
|
159
|
+
* editor files are ours, and are replaced.
|
|
160
|
+
*
|
|
161
|
+
* The two are returned apart because they are read differently: the project's
|
|
162
|
+
* files are the thing that was just made, and worth listing; the editor's are
|
|
163
|
+
* plumbing for a tool that may not even be installed, and listing them buries
|
|
164
|
+
* the first set under twice as many lines about the second.
|
|
235
165
|
*/
|
|
236
166
|
export function scaffold(opts) {
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
};
|
|
244
|
-
|
|
167
|
+
const files = copyTree(join(TEMPLATES, 'project'), opts.dir, '', {
|
|
168
|
+
keep: true,
|
|
169
|
+
vars: {
|
|
170
|
+
model: modelSection(opts.model, opts.modelOptions),
|
|
171
|
+
exa: opts.web ? part('exa.yaml') : '',
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
// The directories with no file to put in them: a skill is a folder someone
|
|
175
|
+
// adds, sessions is written into on the first run, and `.tmp` is scratch —
|
|
176
|
+
// there so an agent has somewhere inside the workspace to put a working
|
|
177
|
+
// file, which is somewhere the sandbox can still reach after the container
|
|
178
|
+
// it was written from is gone.
|
|
245
179
|
mkdirSync(join(opts.dir, 'agents', 'skills'), { recursive: true });
|
|
246
180
|
mkdirSync(join(opts.dir, 'sessions'), { recursive: true });
|
|
247
|
-
|
|
248
|
-
put('agents.yaml', AGENTS_YAML(opts.model, opts.modelOptions, opts.web));
|
|
249
|
-
put(join('agents', 'prompts', 'default.md'), PROMPT);
|
|
250
|
-
put(join('assets', 'README.md'), ASSETS_README);
|
|
251
|
-
put('.gitignore', GITIGNORE);
|
|
252
|
-
written.push(...sandboxTemplate(opts.dir));
|
|
181
|
+
mkdirSync(join(opts.dir, '.tmp'), { recursive: true });
|
|
253
182
|
// The project directory is what `zen open` opens, so this is where the
|
|
254
183
|
// editor actually reads them.
|
|
255
|
-
|
|
256
|
-
return written;
|
|
184
|
+
return { files, editor: editorFiles(opts.dir) };
|
|
257
185
|
}
|
|
258
186
|
//# sourceMappingURL=scaffold.js.map
|
package/dist/validate.js
CHANGED
|
@@ -4,7 +4,7 @@ import { tmpdir } from 'node:os';
|
|
|
4
4
|
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
5
5
|
import { auditModels, credentialFor } from "./audit.js";
|
|
6
6
|
import { resolveBuild } from "./image.js";
|
|
7
|
-
import { SHAPES } from "./keys.js";
|
|
7
|
+
import { SHAPES, envNames, form } from "./keys.js";
|
|
8
8
|
import { BuildError, ensurePodmanReady } from "./podman.js";
|
|
9
9
|
// Mirrors the loader's own constants (`packages/neo/src/project/load.ts`).
|
|
10
10
|
// Duplicated rather than exported, because a check that agreed with the loader
|
|
@@ -230,7 +230,7 @@ function checkServices(agents, available, keys, add) {
|
|
|
230
230
|
continue;
|
|
231
231
|
}
|
|
232
232
|
const shape = SHAPES[service];
|
|
233
|
-
if (process.env[
|
|
233
|
+
if (envNames(service).some((name) => process.env[name]) || keys.active(service)) {
|
|
234
234
|
continue;
|
|
235
235
|
}
|
|
236
236
|
add({
|
|
@@ -239,7 +239,7 @@ function checkServices(agents, available, keys, add) {
|
|
|
239
239
|
where: users.map((a) => `agents.${a.name}`).join(', '),
|
|
240
240
|
message: `uses the ${shape.label} tools, and nothing on this machine holds a ` +
|
|
241
241
|
`${shape.label} key — those tools will refuse every call`,
|
|
242
|
-
fix: `zen key add ${service}, or set $${
|
|
242
|
+
fix: `zen key add ${service}, or set $${form(service).env}`,
|
|
243
243
|
});
|
|
244
244
|
}
|
|
245
245
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenera/cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "Command-line front end for @zenera/neo: agentic projects you can run, share and commit.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -51,26 +51,10 @@
|
|
|
51
51
|
"@inkjs/ui": "^2.0.0",
|
|
52
52
|
"ink": "^7.1.1",
|
|
53
53
|
"react": "^19.2.8",
|
|
54
|
-
"@zenera/neo": "^1.1.
|
|
55
|
-
},
|
|
56
|
-
"peerDependencies": {
|
|
54
|
+
"@zenera/neo": "^1.1.3",
|
|
57
55
|
"@anthropic-ai/sdk": "^0.120.0",
|
|
58
56
|
"@google/genai": "^2.18.0",
|
|
59
57
|
"@openrouter/sdk": "^1.2.80",
|
|
60
58
|
"openai": "^6.0.0"
|
|
61
|
-
},
|
|
62
|
-
"peerDependenciesMeta": {
|
|
63
|
-
"@anthropic-ai/sdk": {
|
|
64
|
-
"optional": true
|
|
65
|
-
},
|
|
66
|
-
"@google/genai": {
|
|
67
|
-
"optional": true
|
|
68
|
-
},
|
|
69
|
-
"@openrouter/sdk": {
|
|
70
|
-
"optional": true
|
|
71
|
-
},
|
|
72
|
-
"openai": {
|
|
73
|
-
"optional": true
|
|
74
|
-
}
|
|
75
59
|
}
|
|
76
60
|
}
|
|
@@ -621,12 +621,13 @@ container involved there is no second name and everything stays relative.
|
|
|
621
621
|
|
|
622
622
|
**Selecting them.** A `tools:` entry is a selector, not only a name:
|
|
623
623
|
|
|
624
|
-
| Selector
|
|
625
|
-
|
|
|
626
|
-
| `read_file`
|
|
627
|
-
| `workspace
|
|
628
|
-
| `
|
|
629
|
-
|
|
|
624
|
+
| Selector | Selects |
|
|
625
|
+
| --------------------- | ------------------------------------------------- |
|
|
626
|
+
| `read_file` | that one tool |
|
|
627
|
+
| `workspace:read_file` | the same tool, written out in full |
|
|
628
|
+
| `workspace:*` | every tool in the group |
|
|
629
|
+
| `'*'` | every tool the runtime provides |
|
|
630
|
+
| `-<any>` | removes what it matches from the selection so far |
|
|
630
631
|
|
|
631
632
|
```yaml
|
|
632
633
|
agents:
|