@zenera/cli 1.1.0 → 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.
Files changed (61) hide show
  1. package/README.md +88 -11
  2. package/dist/audit.d.ts +8 -6
  3. package/dist/audit.js +14 -22
  4. package/dist/commands/check.js +79 -19
  5. package/dist/commands/init.js +71 -11
  6. package/dist/commands/key.js +126 -36
  7. package/dist/commands/models.js +3 -3
  8. package/dist/commands/open.js +2 -2
  9. package/dist/commands/run.js +3 -0
  10. package/dist/commands/sandbox.js +226 -22
  11. package/dist/engine.d.ts +3 -1
  12. package/dist/engine.js +10 -2
  13. package/dist/image.d.ts +16 -0
  14. package/dist/image.js +85 -0
  15. package/dist/keys.d.ts +95 -12
  16. package/dist/keys.js +175 -34
  17. package/dist/lib.d.ts +2 -2
  18. package/dist/lib.js +2 -2
  19. package/dist/liveness.d.ts +16 -6
  20. package/dist/liveness.js +74 -23
  21. package/dist/main.js +0 -0
  22. package/dist/podman.d.ts +57 -1
  23. package/dist/podman.js +177 -12
  24. package/dist/projects.d.ts +18 -0
  25. package/dist/projects.js +60 -1
  26. package/dist/sandbox.d.ts +14 -1
  27. package/dist/sandbox.js +88 -8
  28. package/dist/scaffold.d.ts +21 -15
  29. package/dist/scaffold.js +133 -167
  30. package/dist/term.d.ts +2 -0
  31. package/dist/term.js +14 -0
  32. package/dist/validate.d.ts +20 -3
  33. package/dist/validate.js +309 -14
  34. package/package.json +2 -18
  35. package/templates/{.github → editor/.github}/copilot-instructions.md +161 -48
  36. package/templates/{.github → editor/.github}/prompts/new-skill.prompt.md +13 -6
  37. package/templates/editor/.github/skills/api-schema-index/SKILL.md +292 -0
  38. package/templates/editor/.github/skills/zen-cli/SKILL.md +74 -0
  39. package/templates/editor/.github/skills/zen-cli/references/check.md +92 -0
  40. package/templates/editor/.github/skills/zen-cli/references/faker.md +111 -0
  41. package/templates/editor/.github/skills/zen-cli/references/frame.md +119 -0
  42. package/templates/editor/.github/skills/zen-cli/references/inspect.md +61 -0
  43. package/templates/editor/.github/skills/zen-cli/references/keys.md +114 -0
  44. package/templates/editor/.github/skills/zen-cli/references/projects.md +99 -0
  45. package/templates/editor/.github/skills/zen-cli/references/rag.md +159 -0
  46. package/templates/editor/.github/skills/zen-cli/references/run.md +104 -0
  47. package/templates/editor/.github/skills/zen-cli/references/sandbox.md +91 -0
  48. package/templates/editor/.vscode/settings.json +6 -0
  49. package/templates/parts/exa.yaml.tmpl +5 -0
  50. package/templates/parts/model.yaml.tmpl +4 -0
  51. package/templates/parts/models.yaml.tmpl +10 -0
  52. package/templates/project/INSTRUCTIONS.md +7 -0
  53. package/templates/project/SPECIFICATION.md +6 -0
  54. package/templates/project/agents/prompts/default.md +15 -0
  55. package/templates/project/agents.yaml.tmpl +44 -0
  56. package/templates/project/assets/README.md +12 -0
  57. package/templates/project/gitignore +9 -0
  58. package/templates/project/sandbox/Dockerfile +21 -0
  59. package/templates/.github/skills/zen-cli/SKILL.md +0 -110
  60. /package/templates/{.github → editor/.github}/prompts/new-agent.prompt.md +0 -0
  61. /package/templates/{.github → editor/.github}/prompts/review-project.prompt.md +0 -0
package/dist/scaffold.js CHANGED
@@ -1,21 +1,64 @@
1
- import { mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  // ---------------------------------------------------------------------------
5
5
  // Scaffolding
6
6
  //
7
- // What `zen init` writes. Deliberately close to empty: a template full of
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 here works as written, and every other knob is in `docs/`.
24
+ // one agent works as written, and every other knob is in `docs/`.
10
25
  // ---------------------------------------------------------------------------
11
- const INSTRUCTIONS_MD = `# House rules
12
-
13
- Everything in this file is prepended to every agent's prompt, so it is the
14
- place for the things that are true regardless of who is answering: tone,
15
- constraints, what to do when the answer is not knowable.
16
-
17
- Replace this with yours.
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,93 +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
- const MODEL_SECTION = (ref, options) => {
70
+ function modelSection(ref, options) {
28
71
  const colon = ref.indexOf(':');
29
72
  if (!options || colon < 0) {
30
- return ('# The model an agent uses when it does not pin its own. Change it here and the\n' +
31
- '# whole project moves. The prefix is the *provider* name, not the vendor — drop\n' +
32
- '# it and the id goes to the default provider, whatever the id looks like.\n' +
33
- `model: ${ref}`);
73
+ return part('model.yaml', { ref });
34
74
  }
35
- const indented = options
36
- .trimEnd()
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
- };
75
+ return part('models.yaml', {
76
+ provider: ref.slice(0, colon),
77
+ id: ref.slice(colon + 1),
78
+ options,
79
+ });
80
+ }
51
81
  /**
52
- * Added above the tool list when the project is scaffolded with web access.
53
- * The group is registered whether or not a key exists, so this only ever
54
- * changes what the agent is allowed to reach for.
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.
55
87
  */
56
- const EXA_NOTE = `
57
- #
58
- # exa:* is web search and page reading, here because this machine has an
59
- # Exa key. The key is read from the environment when a tool is called,
60
- # so a clone of this project without one still loads and only the call
61
- # fails.`;
62
- const AGENTS_YAML = (model, options, web) => `# Who exists, and what they may reach for.
63
- #
64
- version: 1
65
-
66
- ${MODEL_SECTION(model, options)}
67
-
68
- # The container \`sandbox:*\` commands run in. \`persist: true\` keeps it between
69
- # runs instead of throwing it away, so what the agent installs is still there
70
- # next time otherwise only /workspace and its home directory survive, and an
71
- # \`apt-get\` or a root \`pip install\` is repeated on every run. \`zen sandbox
72
- # clean\` removes the ones left behind. Everything else has a default; see the
73
- # sandbox: block in docs/agents-yaml.md to size or pin the image.
74
- sandbox:
75
- persist: true
76
-
77
- agents:
78
- - name: default
79
- description: The entry point.
80
- # Instructions live in agents/prompts/<name>.md and are picked up by
81
- # convention — no need to name the file here.
82
- #
83
- # workspace:* is every file tool at once, sandbox:* is the shell. Name
84
- # them one by one to be narrower, or subtract: [workspace:*, -delete_file]
85
- #
86
- # sandbox:* runs commands in a container, not on this machine, so it
87
- # needs podman — \`zen run\` installs and starts what it can on its own,
88
- # and \`zen sandbox status\` says where that got to. Drop the line if you
89
- # would rather this agent never reached a shell.${web ? EXA_NOTE : ''}
90
- tools:
91
- - workspace:*
92
- - sandbox:*${web ? '\n - exa:*' : ''}
93
- `;
94
- const PROMPT = `You are a helpful assistant working inside a project workspace.
95
-
96
- You have tools to read, search and edit files. The workspace is the only
97
- place you can see; paths are relative to its root.
98
-
99
- You can also run shell commands. They run in a container over the same
100
- workspace, not on the user's machine, so a command that fails there has cost
101
- them nothing — but it is still their work in the directory, so read before you
102
- overwrite and say what you ran.
103
-
104
- Read a file before you change it: \`apply_patch\` matches the surrounding text
105
- exactly, so a patch written from memory will not apply. Use \`apply_patch\` to
106
- change part of a file and \`write_file\` only for a new one.
107
-
108
- Say what you changed.
109
- `;
110
- const GITIGNORE = `# Sessions hold run state, memory, blobs and whatever the agent wrote.
111
- # None of it is source.
112
- sessions/
113
- `;
88
+ function target(name) {
89
+ if (name.endsWith(TEMPLATE)) {
90
+ return name.slice(0, -TEMPLATE.length);
91
+ }
92
+ return name === 'gitignore' ? '.gitignore' : name;
93
+ }
94
+ /**
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.
98
+ */
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
+ }
114
122
  // ---------------------------------------------------------------------------
115
123
  // Telling the editor which instructions are not for it
116
124
  //
@@ -127,94 +135,52 @@ sessions/
127
135
  // up whatever `AGENTS.md` a run happened to leave behind. It is a *restricted*
128
136
  // setting, so it applies only in a trusted workspace; that is the right way
129
137
  // round, since an untrusted folder is not one to run agents in either.
130
- // ---------------------------------------------------------------------------
131
- const VSCODE_SETTINGS = `{
132
- "chat.useNestedAgentsMdFiles": false,
133
- "chat.tools.terminal.autoApprove": {
134
- "zen": true
135
- }
136
- }
137
- `;
138
- /**
139
- * Writes `.vscode/settings.json` under `dir`, replacing what is there. The file
140
- * is ours: it says how the editor is to treat a directory the agents write
141
- * into, and a stale copy of that answer is worse than none. Returns the
142
- * relative path.
143
- */
144
- export function editorSettings(dir) {
145
- const rel = join('.vscode', 'settings.json');
146
- mkdirSync(join(dir, '.vscode'), { recursive: true });
147
- writeFileSync(join(dir, rel), VSCODE_SETTINGS);
148
- return rel;
149
- }
150
- // ---------------------------------------------------------------------------
151
- // The other half of the editor story
152
138
  //
153
139
  // `INSTRUCTIONS.md` addresses the *project's* agents. The editor's assistant
154
140
  // still needs a brief of its own, and what it needs to know is how this kind
155
141
  // of project is put together — the file formats, how a prompt is written, when
156
- // to add a skill rather than an agent. That is a whole `.github/` tree the
157
- // standing brief, plus the prompt files and skills the editor picks up from
158
- // the same place — kept as files rather than template literals in here: they
159
- // are full of backticks and `${...}` examples, which a TS template literal
160
- // cannot hold without escaping every one of them into illegibility.
161
- //
162
- // `templates/.github/` mirrors what lands in the project one for one, so
163
- // 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.
164
145
  // ---------------------------------------------------------------------------
165
- const GITHUB_TEMPLATE = fileURLToPath(new URL('../templates/.github', import.meta.url));
166
- /**
167
- * Writes the `.github/` tree under `dir`, replacing what is there — it
168
- * describes the file formats of the version of `zen` in hand, so the current
169
- * one is the only one worth having. Returns the relative paths written.
170
- */
171
- export function copilotInstructions(dir) {
172
- return copyTree(GITHUB_TEMPLATE, dir, '.github');
173
- }
174
146
  /**
175
- * Copies one template directory into `dir` at `rel`, depth first, sorted so
176
- * the list it returns is the same on every machine.
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.
177
152
  */
178
- function copyTree(from, dir, rel) {
179
- const written = [];
180
- mkdirSync(join(dir, rel), { recursive: true });
181
- const entries = readdirSync(from, { withFileTypes: true });
182
- entries.sort((a, b) => a.name.localeCompare(b.name));
183
- for (const entry of entries) {
184
- const child = join(rel, entry.name);
185
- if (entry.isDirectory()) {
186
- written.push(...copyTree(join(from, entry.name), dir, child));
187
- }
188
- else {
189
- writeFileSync(join(dir, child), readFileSync(join(from, entry.name)));
190
- written.push(child);
191
- }
192
- }
193
- return written;
153
+ export function editorFiles(dir) {
154
+ return copyTree(join(TEMPLATES, 'editor'), dir, '', {});
194
155
  }
195
156
  /**
196
- * Writes a project. Never overwrites the project's own files: the caller
197
- * decides whether it may. The editor files are the exceptionthey are ours,
198
- * 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.
199
165
  */
200
166
  export function scaffold(opts) {
201
- const written = [];
202
- const put = (rel, body) => {
203
- const path = join(opts.dir, rel);
204
- mkdirSync(join(path, '..'), { recursive: true });
205
- writeFileSync(path, body, { flag: 'wx' });
206
- written.push(rel);
207
- };
208
- mkdirSync(join(opts.dir, 'agents', 'prompts'), { recursive: true });
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.
209
179
  mkdirSync(join(opts.dir, 'agents', 'skills'), { recursive: true });
210
180
  mkdirSync(join(opts.dir, 'sessions'), { recursive: true });
211
- put('INSTRUCTIONS.md', INSTRUCTIONS_MD);
212
- put('agents.yaml', AGENTS_YAML(opts.model, opts.modelOptions, opts.web));
213
- put(join('agents', 'prompts', 'default.md'), PROMPT);
214
- put('.gitignore', GITIGNORE);
181
+ mkdirSync(join(opts.dir, '.tmp'), { recursive: true });
215
182
  // The project directory is what `zen open` opens, so this is where the
216
183
  // editor actually reads them.
217
- written.push(editorSettings(opts.dir), ...copilotInstructions(opts.dir));
218
- return written;
184
+ return { files, editor: editorFiles(opts.dir) };
219
185
  }
220
186
  //# sourceMappingURL=scaffold.js.map
package/dist/term.d.ts CHANGED
@@ -66,4 +66,6 @@ export declare function choose<T>(title: string, choices: readonly Choice<T>[]):
66
66
  export declare function readStdin(): Promise<string | undefined>;
67
67
  export declare function ago(iso: string | undefined): string;
68
68
  export declare function count(n: number, singular: string, plural?: string): string;
69
+ /** Powers of 1000, as the container engine prints them, so the two agree. */
70
+ export declare function bytes(n: number): string;
69
71
  //# sourceMappingURL=term.d.ts.map
package/dist/term.js CHANGED
@@ -239,4 +239,18 @@ export function ago(iso) {
239
239
  export function count(n, singular, plural = `${singular}s`) {
240
240
  return `${n} ${n === 1 ? singular : plural}`;
241
241
  }
242
+ /** Powers of 1000, as the container engine prints them, so the two agree. */
243
+ export function bytes(n) {
244
+ if (!Number.isFinite(n) || n <= 0) {
245
+ return '0 B';
246
+ }
247
+ const units = ['B', 'kB', 'MB', 'GB', 'TB'];
248
+ let value = n;
249
+ let unit = 0;
250
+ while (value >= 1000 && unit < units.length - 1) {
251
+ value /= 1000;
252
+ unit++;
253
+ }
254
+ return `${unit > 0 && value < 100 ? value.toFixed(1) : Math.round(value)} ${units[unit]}`;
255
+ }
242
256
  //# sourceMappingURL=term.js.map
@@ -1,4 +1,4 @@
1
- import { type AnyTool, type ProjectConfig } from '@zenera/neo';
1
+ import { type AnyTool, type ProjectConfig, type Runner } from '@zenera/neo';
2
2
  import { type DeclaredRole } from './audit.ts';
3
3
  import { type KeyStore } from './keys.ts';
4
4
  /**
@@ -65,8 +65,8 @@ export interface SkillReport {
65
65
  /** the SKILL.md, relative to the project root */
66
66
  path: string;
67
67
  tools?: string[];
68
- /** other files in the skill folder, which the agent gets as resources */
69
- resources?: string[];
68
+ /** other files in the skill folder, which the agent can reach under /skills */
69
+ files?: string[];
70
70
  /** agents whose binding can see it */
71
71
  usedBy: string[];
72
72
  }
@@ -111,8 +111,12 @@ export interface Report {
111
111
  models: ModelReport[];
112
112
  sandbox: {
113
113
  image: string | null;
114
+ /** the Dockerfile that image is built from, when it is built rather than pulled */
115
+ dockerfile: string | null;
114
116
  declared: boolean;
115
117
  used: boolean;
118
+ /** whether the image was actually built and a container started */
119
+ probed: boolean;
116
120
  };
117
121
  findings: Finding[];
118
122
  counts: {
@@ -139,6 +143,19 @@ export interface ValidateOptions {
139
143
  * pay for it.
140
144
  */
141
145
  keys?: KeyStore;
146
+ /**
147
+ * Whether to build the image and start a container. Everything else here
148
+ * is a reading of files; this is the one part that runs something, so it
149
+ * is named separately and can be turned off.
150
+ */
151
+ sandbox?: {
152
+ enabled: boolean;
153
+ engine?: string;
154
+ /** the test seam, as elsewhere — a check can be watched without an engine */
155
+ exec?: Runner;
156
+ /** called with each slow step, so the caller can narrate one */
157
+ onProgress?: (what: string) => void;
158
+ };
142
159
  }
143
160
  export declare function validateProject(opts: ValidateOptions): Promise<Report>;
144
161
  export declare function availableTools(root: string, config: ProjectConfig): AnyTool<unknown>[];