@zenera/cli 1.1.23 → 1.1.25

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 (184) hide show
  1. package/README.md +88 -2
  2. package/dist/ask.d.ts +51 -0
  3. package/dist/ask.js +166 -0
  4. package/dist/banner.js +2 -2
  5. package/dist/batch.d.ts +83 -0
  6. package/dist/batch.js +413 -0
  7. package/dist/catalog.d.ts +11 -1
  8. package/dist/catalog.js +80 -7
  9. package/dist/command.d.ts +6 -0
  10. package/dist/commands/inspect.d.ts +57 -0
  11. package/dist/commands/inspect.js +539 -91
  12. package/dist/commands/list.js +128 -19
  13. package/dist/commands/memory.js +461 -32
  14. package/dist/commands/meta.js +311 -21
  15. package/dist/commands/models.js +110 -14
  16. package/dist/commands/open.js +4 -4
  17. package/dist/commands/run.js +175 -36
  18. package/dist/commands/sandbox.js +148 -15
  19. package/dist/engine.d.ts +24 -0
  20. package/dist/engine.js +61 -8
  21. package/dist/eventlog.d.ts +59 -0
  22. package/dist/eventlog.js +146 -0
  23. package/dist/liveness.d.ts +7 -0
  24. package/dist/liveness.js +20 -3
  25. package/dist/main.js +5 -2
  26. package/dist/meta/editor.d.ts +9 -0
  27. package/dist/meta/editor.js +60 -0
  28. package/dist/meta/finetune/children.d.ts +8 -0
  29. package/dist/meta/finetune/children.js +76 -0
  30. package/dist/meta/finetune/command.d.ts +30 -0
  31. package/dist/meta/finetune/command.js +338 -0
  32. package/dist/meta/finetune/dataset/anchors.d.ts +41 -0
  33. package/dist/meta/finetune/dataset/anchors.js +239 -0
  34. package/dist/meta/finetune/dataset/changes.d.ts +36 -0
  35. package/dist/meta/finetune/dataset/changes.js +319 -0
  36. package/dist/meta/finetune/dataset/command.d.ts +17 -0
  37. package/dist/meta/finetune/dataset/command.js +1011 -0
  38. package/dist/meta/finetune/dataset/drift.d.ts +33 -0
  39. package/dist/meta/finetune/dataset/drift.js +71 -0
  40. package/dist/meta/finetune/dataset/export.d.ts +26 -0
  41. package/dist/meta/finetune/dataset/export.js +51 -0
  42. package/dist/meta/finetune/dataset/index.d.ts +2 -0
  43. package/dist/meta/finetune/dataset/index.js +2 -0
  44. package/dist/meta/finetune/dataset/sample.d.ts +50 -0
  45. package/dist/meta/finetune/dataset/sample.js +197 -0
  46. package/dist/meta/finetune/dataset/schema.d.ts +33 -0
  47. package/dist/meta/finetune/dataset/schema.js +216 -0
  48. package/dist/meta/finetune/dataset/store.d.ts +35 -0
  49. package/dist/meta/finetune/dataset/store.js +177 -0
  50. package/dist/meta/finetune/dataset/types.d.ts +125 -0
  51. package/dist/meta/finetune/dataset/types.js +39 -0
  52. package/dist/meta/finetune/failure.d.ts +15 -0
  53. package/dist/meta/finetune/failure.js +26 -0
  54. package/dist/meta/finetune/feedback.d.ts +45 -0
  55. package/dist/meta/finetune/feedback.js +85 -0
  56. package/dist/meta/finetune/improvements.d.ts +51 -0
  57. package/dist/meta/finetune/improvements.js +244 -0
  58. package/dist/meta/finetune/index.d.ts +2 -0
  59. package/dist/meta/finetune/index.js +2 -0
  60. package/dist/meta/finetune/journal.d.ts +35 -0
  61. package/dist/meta/finetune/journal.js +135 -0
  62. package/dist/meta/finetune/live.d.ts +74 -0
  63. package/dist/meta/finetune/live.js +226 -0
  64. package/dist/meta/finetune/loop.d.ts +3 -0
  65. package/dist/meta/finetune/loop.js +114 -0
  66. package/dist/meta/finetune/memories.d.ts +47 -0
  67. package/dist/meta/finetune/memories.js +154 -0
  68. package/dist/meta/finetune/report.d.ts +33 -0
  69. package/dist/meta/finetune/report.js +1284 -0
  70. package/dist/meta/finetune/retry.d.ts +24 -0
  71. package/dist/meta/finetune/retry.js +96 -0
  72. package/dist/meta/finetune/sampler.d.ts +18 -0
  73. package/dist/meta/finetune/sampler.js +41 -0
  74. package/dist/meta/finetune/system.d.ts +27 -0
  75. package/dist/meta/finetune/system.js +132 -0
  76. package/dist/meta/finetune/train.d.ts +39 -0
  77. package/dist/meta/finetune/train.js +391 -0
  78. package/dist/meta/finetune/tui.d.ts +16 -0
  79. package/dist/meta/finetune/tui.js +291 -0
  80. package/dist/meta/finetune/tuning.d.ts +167 -0
  81. package/dist/meta/finetune/tuning.js +289 -0
  82. package/dist/meta/finetune/usage.d.ts +62 -0
  83. package/dist/meta/finetune/usage.js +225 -0
  84. package/dist/meta/host.d.ts +14 -0
  85. package/dist/meta/host.js +20 -0
  86. package/dist/meta/index.d.ts +11 -0
  87. package/dist/meta/index.js +11 -0
  88. package/dist/meta/launch.d.ts +81 -0
  89. package/dist/meta/launch.js +286 -0
  90. package/dist/meta/model.d.ts +44 -0
  91. package/dist/meta/model.js +119 -0
  92. package/dist/meta/prompts.d.ts +18 -0
  93. package/dist/meta/prompts.js +61 -0
  94. package/dist/meta/relay.d.ts +20 -0
  95. package/dist/meta/relay.js +123 -0
  96. package/dist/meta/tokens.d.ts +52 -0
  97. package/dist/meta/tokens.js +226 -0
  98. package/dist/meta/window.d.ts +61 -0
  99. package/dist/meta/window.js +198 -0
  100. package/dist/meta/wire.d.ts +42 -0
  101. package/dist/meta/wire.js +129 -0
  102. package/dist/modelref.d.ts +16 -0
  103. package/dist/modelref.js +41 -0
  104. package/dist/narrate.js +5 -2
  105. package/dist/podman.d.ts +42 -0
  106. package/dist/podman.js +198 -5
  107. package/dist/progress.d.ts +33 -0
  108. package/dist/progress.js +402 -0
  109. package/dist/projects.d.ts +0 -5
  110. package/dist/projects.js +2 -15
  111. package/dist/request.d.ts +45 -0
  112. package/dist/request.js +300 -0
  113. package/dist/sandbox.d.ts +2 -0
  114. package/dist/sandbox.js +20 -3
  115. package/dist/scaffold.d.ts +15 -6
  116. package/dist/scaffold.js +5 -60
  117. package/dist/session.d.ts +26 -1
  118. package/dist/session.js +46 -32
  119. package/dist/term.d.ts +5 -0
  120. package/dist/term.js +28 -3
  121. package/dist/trace.d.ts +84 -0
  122. package/dist/trace.js +737 -0
  123. package/dist/tui/models.d.ts +33 -0
  124. package/dist/tui/models.js +389 -0
  125. package/dist/tui/wrap.d.ts +5 -0
  126. package/dist/tui/wrap.js +12 -0
  127. package/dist/usage.d.ts +78 -0
  128. package/dist/usage.js +51 -0
  129. package/dist/validate.js +25 -3
  130. package/package.json +3 -2
  131. package/templates/editor/.github/copilot-instructions.md +41 -107
  132. package/templates/editor/.github/prompts/analyze.prompt.md +31 -0
  133. package/templates/editor/.github/prompts/dataset.prompt.md +108 -0
  134. package/templates/editor/.github/prompts/finetune-apply.prompt.md +91 -0
  135. package/templates/editor/.github/prompts/finetune.prompt.md +744 -0
  136. package/templates/editor/.github/prompts/new-agent.prompt.md +3 -1
  137. package/templates/editor/.github/prompts/new-skill.prompt.md +2 -0
  138. package/templates/editor/.github/prompts/project-review.prompt.md +4 -3
  139. package/templates/editor/.github/skills/zen-analyze-run/SKILL.md +483 -0
  140. package/templates/editor/.github/skills/zen-cli/SKILL.md +18 -4
  141. package/templates/editor/.github/skills/zen-cli/references/check.md +7 -5
  142. package/templates/editor/.github/skills/zen-cli/references/dataset.md +34 -0
  143. package/templates/editor/.github/skills/zen-cli/references/finetune.md +77 -0
  144. package/templates/editor/.github/skills/zen-cli/references/inspect.md +300 -21
  145. package/templates/editor/.github/skills/zen-cli/references/memory.md +121 -28
  146. package/templates/editor/.github/skills/zen-cli/references/meta.md +61 -0
  147. package/templates/editor/.github/skills/zen-cli/references/models.md +6 -2
  148. package/templates/editor/.github/skills/zen-cli/references/projects.md +12 -5
  149. package/templates/editor/.github/skills/zen-cli/references/rag.md +24 -6
  150. package/templates/editor/.github/skills/zen-cli/references/run.md +201 -4
  151. package/templates/editor/.github/skills/zen-cli/references/sandbox.md +10 -1
  152. package/templates/editor/.github/skills/zen-code-python/SKILL.md +123 -0
  153. package/templates/editor/.github/skills/zen-code-python/references/code_python.md +48 -0
  154. package/templates/editor/.github/skills/zen-dataset/SKILL.md +253 -0
  155. package/templates/editor/.github/skills/zen-finetune/SKILL.md +1791 -0
  156. package/templates/editor/.github/skills/zen-finetune/references/readme-template.md +268 -0
  157. package/templates/editor/.github/skills/zen-finetune/scripts/batch.mjs +390 -0
  158. package/templates/editor/.github/skills/zen-finetune/scripts/difficult.mjs +243 -0
  159. package/templates/editor/.github/skills/zen-finetune/scripts/memory.mjs +280 -0
  160. package/templates/editor/.github/skills/zen-finetune/scripts/next.mjs +398 -0
  161. package/templates/editor/.github/skills/zen-finetune/scripts/report.mjs +797 -0
  162. package/templates/editor/.github/skills/zen-finetune/scripts/select.mjs +354 -0
  163. package/templates/editor/.github/skills/zen-finetune/scripts/usage.mjs +722 -0
  164. package/templates/editor/.github/skills/zen-inspect/SKILL.md +431 -0
  165. package/templates/editor/.github/skills/zen-inspect-ask/SKILL.md +270 -0
  166. package/templates/editor/.github/skills/zen-memory/SKILL.md +78 -29
  167. package/templates/editor/.github/skills/zen-memory-warmup/SKILL.md +363 -531
  168. package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +20 -8
  169. package/templates/editor/.github/skills/zen-review/SKILL.md +5 -3
  170. package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +5 -4
  171. package/templates/editor/.github/skills/zen-sandbox/SKILL.md +25 -2
  172. package/templates/editor/.github/skills/zen-sandbox-capacity/SKILL.md +334 -0
  173. package/templates/editor/.github/skills/zen-sandbox-capacity/references/loadtest.md +181 -0
  174. package/templates/editor/.github/skills/zen-sandbox-capacity/scripts/preflight_sandbox.sh +457 -0
  175. package/templates/editor/.github/skills/zen-topology/SKILL.md +283 -0
  176. package/templates/editor/.vscode/agents.schema.json +1 -1
  177. package/templates/editor/.vscode/settings.json +2 -0
  178. package/templates/project/agents/files-instructions.md +35 -13
  179. package/templates/project/agents/fork-instructions.md +27 -0
  180. package/templates/project/agents/memory-instructions.md +106 -34
  181. package/templates/project/agents/tools-instructions.md +7 -4
  182. package/templates/project/gitignore +5 -0
  183. package/dist/meta.d.ts +0 -221
  184. package/dist/meta.js +0 -684
@@ -87,10 +87,5 @@ export declare function lastUsedAt(projectDir: string): string | undefined;
87
87
  export declare function dirSize(dir: string): number;
88
88
  /** True when a session's lock names a process that is still alive. */
89
89
  export declare function isBusy(sessionDir: string): boolean;
90
- /**
91
- * `kill(pid, 0)` sends no signal and only asks whether the process exists.
92
- * EPERM means it exists and belongs to someone else, which still counts.
93
- */
94
- export declare function alive(pid: number): boolean;
95
90
  export declare function projectName(dir: string): string;
96
91
  //# sourceMappingURL=projects.d.ts.map
package/dist/projects.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ASSETS_MOUNT, assetsDir, FILES_DIR, MEMORY_MOUNT, memoryDir, skillDirs, skillMounts, SKILLS_MOUNT, } from '@zenera/neo';
1
+ import { ASSETS_MOUNT, assetsDir, FILES_DIR, MEMORY_MOUNT, memoryDir, pidAlive, skillDirs, skillMounts, SKILLS_MOUNT, } from '@zenera/neo';
2
2
  import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync } from 'node:fs';
3
3
  import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
4
4
  import { paths, readJson, writeJson } from "./home.js";
@@ -290,26 +290,13 @@ export function isBusy(sessionDir) {
290
290
  const path = join(sessionDir, '.lock');
291
291
  try {
292
292
  const { pid } = JSON.parse(readFileSync(path, 'utf8'));
293
- return typeof pid === 'number' && alive(pid);
293
+ return typeof pid === 'number' && pidAlive(pid);
294
294
  }
295
295
  catch {
296
296
  // Absent, unreadable or malformed: nothing is holding it.
297
297
  return false;
298
298
  }
299
299
  }
300
- /**
301
- * `kill(pid, 0)` sends no signal and only asks whether the process exists.
302
- * EPERM means it exists and belongs to someone else, which still counts.
303
- */
304
- export function alive(pid) {
305
- try {
306
- process.kill(pid, 0);
307
- return true;
308
- }
309
- catch (err) {
310
- return err.code === 'EPERM';
311
- }
312
- }
313
300
  export function projectName(dir) {
314
301
  return basename(resolve(dir));
315
302
  }
@@ -0,0 +1,45 @@
1
+ import { type Input } from '@zenera/neo';
2
+ export interface RunRequest {
3
+ /** a registered name, or a directory — paths resolved against the file */
4
+ project?: string;
5
+ input: Input;
6
+ /** absolute */
7
+ workspace?: string;
8
+ /** absolute */
9
+ memory?: string;
10
+ }
11
+ export interface BatchItem {
12
+ /** names this item's directory under the batch dir; defaults to its index */
13
+ id: string;
14
+ index: number;
15
+ input: Input;
16
+ /** absolute; without one the item gets a directory of its own */
17
+ workspace?: string;
18
+ }
19
+ export interface BatchRequest {
20
+ items: BatchItem[];
21
+ }
22
+ /**
23
+ * `-` is stdin, and then there is no file to resolve paths against, so the cwd
24
+ * answers instead — the only place a piped request could have meant.
25
+ */
26
+ export declare function readRequest(file: string, cwd: string, piped?: string): Promise<RunRequest>;
27
+ /**
28
+ * `base` is the directory every relative path in the request is resolved
29
+ * against — the file's own, so a case travels with the images beside it.
30
+ */
31
+ export declare function parseRequest(body: string, base: string, where: string): RunRequest;
32
+ /** The batch file. `-` is stdin, and then the cwd is what paths mean. */
33
+ export declare function readBatch(file: string, cwd: string, piped?: string): Promise<BatchRequest>;
34
+ /**
35
+ * Everything refusable is refused here, before a single model is called: a
36
+ * batch that fails on item 80 has already spent seventy-nine runs finding out
37
+ * something its file could have been read for.
38
+ */
39
+ export declare function parseBatch(body: string, base: string, where: string): BatchRequest;
40
+ /**
41
+ * Canonical `ContentPart[]` out, whatever went in: the shorthands exist for
42
+ * whoever writes the file, and stop existing the moment it is read.
43
+ */
44
+ export declare function toInput(raw: unknown, base: string, where: string): Input;
45
+ //# sourceMappingURL=request.d.ts.map
@@ -0,0 +1,300 @@
1
+ import { media, text } from '@zenera/neo';
2
+ import { readFileSync, statSync } from 'node:fs';
3
+ import { dirname, extname, isAbsolute, resolve } from 'node:path';
4
+ import { readStdin, usageError, warn } from "./term.js";
5
+ /**
6
+ * Local media is read and inlined, so the state a run is resumed from carries
7
+ * the picture rather than a path that meant something on another machine. That
8
+ * also puts the bytes in `state.json`, rewritten at every checkpoint of every
9
+ * later turn — hence a ceiling, and a word of warning well before it.
10
+ */
11
+ const MAX_MEDIA_BYTES = 20 * 1024 * 1024;
12
+ const WARN_MEDIA_BYTES = 2 * 1024 * 1024;
13
+ /** Enough to name what people actually attach; anything else says `mimeType`. */
14
+ const MIME = {
15
+ '.png': 'image/png',
16
+ '.jpg': 'image/jpeg',
17
+ '.jpeg': 'image/jpeg',
18
+ '.gif': 'image/gif',
19
+ '.webp': 'image/webp',
20
+ '.bmp': 'image/bmp',
21
+ '.svg': 'image/svg+xml',
22
+ '.heic': 'image/heic',
23
+ '.avif': 'image/avif',
24
+ '.mp3': 'audio/mpeg',
25
+ '.wav': 'audio/wav',
26
+ '.ogg': 'audio/ogg',
27
+ '.m4a': 'audio/mp4',
28
+ '.flac': 'audio/flac',
29
+ '.aac': 'audio/aac',
30
+ '.mp4': 'video/mp4',
31
+ '.webm': 'video/webm',
32
+ '.mov': 'video/quicktime',
33
+ '.mkv': 'video/x-matroska',
34
+ '.pdf': 'application/pdf',
35
+ '.txt': 'text/plain',
36
+ '.md': 'text/markdown',
37
+ '.csv': 'text/csv',
38
+ '.json': 'application/json',
39
+ '.xml': 'application/xml',
40
+ '.html': 'text/html',
41
+ '.yaml': 'application/yaml',
42
+ '.yml': 'application/yaml',
43
+ };
44
+ const MEDIA_KEYS = ['image', 'audio', 'video', 'file'];
45
+ // ---------------------------------------------------------------------------
46
+ // Reading
47
+ // ---------------------------------------------------------------------------
48
+ /**
49
+ * `-` is stdin, and then there is no file to resolve paths against, so the cwd
50
+ * answers instead — the only place a piped request could have meant.
51
+ */
52
+ export async function readRequest(file, cwd, piped) {
53
+ if (file === '-') {
54
+ const body = piped ?? (await readStdin());
55
+ if (body === undefined) {
56
+ throw usageError('--input - expects the request on stdin', 'pipe a JSON object in');
57
+ }
58
+ return parseRequest(body, cwd, '<stdin>');
59
+ }
60
+ const at = resolve(cwd, file);
61
+ let body;
62
+ try {
63
+ body = readFileSync(at, 'utf8');
64
+ }
65
+ catch {
66
+ throw usageError(`cannot read ${at}`);
67
+ }
68
+ return parseRequest(body, dirname(at), at);
69
+ }
70
+ /**
71
+ * `base` is the directory every relative path in the request is resolved
72
+ * against — the file's own, so a case travels with the images beside it.
73
+ */
74
+ export function parseRequest(body, base, where) {
75
+ let raw;
76
+ try {
77
+ raw = JSON.parse(body);
78
+ }
79
+ catch (err) {
80
+ throw usageError(`${where}: ${err.message}`, 'the request must be JSON');
81
+ }
82
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
83
+ throw usageError(`${where}: expected a JSON object`, 'see: zen run --help');
84
+ }
85
+ if (raw.input === undefined) {
86
+ throw usageError(`${where}: no "input"`, 'a request has to say what to ask');
87
+ }
88
+ return {
89
+ project: project(raw.project, base, where),
90
+ input: toInput(raw.input, base, where),
91
+ workspace: path(raw.workspace, base, where, 'workspace'),
92
+ memory: path(raw.memory, base, where, 'memory'),
93
+ };
94
+ }
95
+ /** The batch file. `-` is stdin, and then the cwd is what paths mean. */
96
+ export async function readBatch(file, cwd, piped) {
97
+ if (file === '-') {
98
+ const body = piped ?? (await readStdin());
99
+ if (body === undefined) {
100
+ throw usageError('--input - expects the batch on stdin', 'pipe a JSON object in');
101
+ }
102
+ return parseBatch(body, cwd, '<stdin>');
103
+ }
104
+ const at = resolve(cwd, file);
105
+ let body;
106
+ try {
107
+ body = readFileSync(at, 'utf8');
108
+ }
109
+ catch {
110
+ throw usageError(`cannot read ${at}`);
111
+ }
112
+ return parseBatch(body, dirname(at), at);
113
+ }
114
+ /**
115
+ * Everything refusable is refused here, before a single model is called: a
116
+ * batch that fails on item 80 has already spent seventy-nine runs finding out
117
+ * something its file could have been read for.
118
+ */
119
+ export function parseBatch(body, base, where) {
120
+ let raw;
121
+ try {
122
+ raw = JSON.parse(body);
123
+ }
124
+ catch (err) {
125
+ throw usageError(`${where}: ${err.message}`, 'the batch must be JSON');
126
+ }
127
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
128
+ throw usageError(`${where}: expected a JSON object`, 'see: zen run batch --help');
129
+ }
130
+ for (const key of ['memory', 'workspace', 'project']) {
131
+ if (raw[key] !== undefined) {
132
+ throw usageError(`${where}: "${key}" is not part of a batch file`, `it applies to every item, so it is a flag: --${key}`);
133
+ }
134
+ }
135
+ if (!Array.isArray(raw.batch)) {
136
+ throw usageError(`${where}: no "batch" array`, 'a batch is a list of requests');
137
+ }
138
+ if (raw.batch.length === 0) {
139
+ throw usageError(`${where}: "batch" is empty`, 'there is nothing to run');
140
+ }
141
+ const items = raw.batch.map((item, index) => toItem(item, index, base, `${where}: batch`));
142
+ unique(items.map((i) => i.id), 'id', where);
143
+ unique(items.flatMap((i) => (i.workspace ? [i.workspace] : [])), 'workspace', where);
144
+ return { items };
145
+ }
146
+ function toItem(raw, index, base, where) {
147
+ const at = `${where}[${index}]`;
148
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
149
+ throw usageError(`${at}: expected an object`, 'see: zen run batch --help');
150
+ }
151
+ const obj = raw;
152
+ for (const key of ['memory', 'project']) {
153
+ if (obj[key] !== undefined) {
154
+ throw usageError(`${at}: "${key}" is not part of a batch item`, `every item shares one, so it is a flag: --${key}`);
155
+ }
156
+ }
157
+ if (obj.input === undefined) {
158
+ throw usageError(`${at}: no "input"`, 'an item has to say what to ask');
159
+ }
160
+ return {
161
+ id: id(obj.id, index, at),
162
+ index,
163
+ input: toInput(obj.input, base, at),
164
+ workspace: path(obj.workspace, base, at, 'workspace'),
165
+ };
166
+ }
167
+ /** An id names a directory, so it has to be one path segment and nothing else. */
168
+ function id(value, index, at) {
169
+ if (value === undefined) {
170
+ return String(index);
171
+ }
172
+ if (typeof value !== 'string' || !value) {
173
+ throw usageError(`${at}: "id" must be a non-empty string`);
174
+ }
175
+ if (!/^[\w.-]+$/.test(value) || value === '.' || value === '..') {
176
+ throw usageError(`${at}: "${value}" cannot be an id`, 'it names a directory: letters, digits, dot, dash and underscore');
177
+ }
178
+ return value;
179
+ }
180
+ function unique(values, what, where) {
181
+ const seen = new Set();
182
+ for (const value of values) {
183
+ if (seen.has(value)) {
184
+ throw usageError(`${where}: two items share the ${what} "${value}"`, 'every item runs at the same time as the others; give each its own');
185
+ }
186
+ seen.add(value);
187
+ }
188
+ }
189
+ /**
190
+ * A registered name is not a path and must not be resolved into one; a name
191
+ * that looks like a path is one. `Projects.open` takes either.
192
+ */
193
+ function project(value, base, where) {
194
+ if (value === undefined) {
195
+ return undefined;
196
+ }
197
+ if (typeof value !== 'string' || !value) {
198
+ throw usageError(`${where}: "project" must be a name or a directory`);
199
+ }
200
+ return value.startsWith('.') || isAbsolute(value) ? resolve(base, value) : value;
201
+ }
202
+ function path(value, base, where, field) {
203
+ if (value === undefined) {
204
+ return undefined;
205
+ }
206
+ if (typeof value !== 'string' || !value) {
207
+ throw usageError(`${where}: "${field}" must be a directory`);
208
+ }
209
+ return resolve(base, value);
210
+ }
211
+ // ---------------------------------------------------------------------------
212
+ // Input
213
+ // ---------------------------------------------------------------------------
214
+ /**
215
+ * Canonical `ContentPart[]` out, whatever went in: the shorthands exist for
216
+ * whoever writes the file, and stop existing the moment it is read.
217
+ */
218
+ export function toInput(raw, base, where) {
219
+ if (typeof raw === 'string') {
220
+ return raw;
221
+ }
222
+ if (!Array.isArray(raw)) {
223
+ throw usageError(`${where}: "input" must be a string or an array of parts`);
224
+ }
225
+ return raw.map((part, i) => toPart(part, base, `${where}: input[${i}]`));
226
+ }
227
+ function toPart(part, base, at) {
228
+ if (typeof part === 'string') {
229
+ return text(part);
230
+ }
231
+ if (part === null || typeof part !== 'object' || Array.isArray(part)) {
232
+ throw usageError(`${at}: expected a string or an object`);
233
+ }
234
+ const obj = part;
235
+ const mimeType = mime(obj.mimeType, at);
236
+ if (typeof obj.type === 'string') {
237
+ if (obj.type === 'text') {
238
+ return text(str(obj.text, at, 'text'));
239
+ }
240
+ if (MEDIA_KEYS.includes(obj.type)) {
241
+ const kind = obj.type;
242
+ return url(kind, str(obj.url, at, 'url'), mimeType, base, at);
243
+ }
244
+ throw usageError(`${at}: unknown part type "${obj.type}"`, `text, ${MEDIA_KEYS.join(', ')}`);
245
+ }
246
+ if (typeof obj.text === 'string') {
247
+ return text(obj.text);
248
+ }
249
+ for (const kind of MEDIA_KEYS) {
250
+ if (obj[kind] !== undefined) {
251
+ return url(kind, str(obj[kind], at, kind), mimeType, base, at);
252
+ }
253
+ }
254
+ throw usageError(`${at}: no text, ${MEDIA_KEYS.join(', ')} or type`, 'see: zen run --help');
255
+ }
256
+ /**
257
+ * A url the model can already fetch is left exactly as it is; anything else is
258
+ * a path on this machine, and a path is only a url once it carries its bytes.
259
+ */
260
+ function url(kind, value, mimeType, base, at) {
261
+ if (/^(https?|data):/.test(value)) {
262
+ return media(kind, value, mimeType);
263
+ }
264
+ const file = resolve(base, value);
265
+ const type = mimeType ?? MIME[extname(file).toLowerCase()];
266
+ if (!type) {
267
+ throw usageError(`${at}: nothing known about "${extname(file) || value}"`, 'name the type with "mimeType"');
268
+ }
269
+ let size;
270
+ try {
271
+ size = statSync(file).size;
272
+ }
273
+ catch {
274
+ throw usageError(`${at}: cannot read ${file}`);
275
+ }
276
+ if (size > MAX_MEDIA_BYTES) {
277
+ throw usageError(`${at}: ${file} is ${Math.round(size / 1024 / 1024)} MB`, `inlined media lives in the run state — the limit is ${MAX_MEDIA_BYTES / 1024 / 1024} MB`);
278
+ }
279
+ if (size > WARN_MEDIA_BYTES) {
280
+ warn(`${file} is large; it is written into the session state on every later turn`);
281
+ }
282
+ const bytes = readFileSync(file);
283
+ return media(kind, `data:${type};base64,${bytes.toString('base64')}`, type);
284
+ }
285
+ function str(value, at, field) {
286
+ if (typeof value !== 'string' || !value) {
287
+ throw usageError(`${at}: "${field}" must be a non-empty string`);
288
+ }
289
+ return value;
290
+ }
291
+ function mime(value, at) {
292
+ if (value === undefined) {
293
+ return undefined;
294
+ }
295
+ if (typeof value !== 'string' || !value) {
296
+ throw usageError(`${at}: "mimeType" must be a string`);
297
+ }
298
+ return value;
299
+ }
300
+ //# sourceMappingURL=request.js.map
package/dist/sandbox.d.ts CHANGED
@@ -39,6 +39,8 @@ export interface SandboxInputs {
39
39
  * promise it does not keep.
40
40
  */
41
41
  env?: readonly string[];
42
+ /** `false` overrides every `persist:` in the config, the agents' included */
43
+ persist?: boolean;
42
44
  }
43
45
  export declare function buildSandbox(opts: SandboxInputs): SandboxSetup;
44
46
  /**
package/dist/sandbox.js CHANGED
@@ -3,7 +3,7 @@ import { mkdirSync } from 'node:fs';
3
3
  import { basename, join } from 'node:path';
4
4
  import { resolveBuild } from "./image.js";
5
5
  import { credentials } from "./keys.js";
6
- import { ensurePodmanReady } from "./podman.js";
6
+ import { assertLocks, ensurePodmanReady } from "./podman.js";
7
7
  import { warn } from "./term.js";
8
8
  // ---------------------------------------------------------------------------
9
9
  // The sandbox, as this CLI wires it
@@ -71,6 +71,7 @@ export function buildSandbox(opts) {
71
71
  ...(opts.config.sandbox ?? {}),
72
72
  ...((opts.image ?? build?.tag) ? { image: opts.image ?? build?.tag } : {}),
73
73
  ...(opts.keys === false ? { keys: false } : {}),
74
+ ...(opts.persist === false ? { persist: false } : {}),
74
75
  };
75
76
  const home = join(opts.session.data, 'sandbox', 'home');
76
77
  const mounts = [{ host: home, at: HOME }, ...(opts.mounts ?? [])];
@@ -81,18 +82,22 @@ export function buildSandbox(opts) {
81
82
  // Skills and assets are mounted read-only, and a python script run from a
82
83
  // read-only directory fails on writing its own `__pycache__` — a confusing
83
84
  // error about a file nobody asked for.
84
- const extra = { HOME, PYTHONDONTWRITEBYTECODE: '1', ...(keys?.env ?? {}) };
85
+ // `ZENERA_AGENT` tells a `zen` run by `run_command` that no person is there.
86
+ const extra = { HOME, PYTHONDONTWRITEBYTECODE: '1', ZENERA_AGENT: '1', ...(keys?.env ?? {}) };
85
87
  const spec = toSpec(base, extra, secretsOf(keys?.secrets, opts.env));
86
88
  const agents = {};
87
89
  for (const agent of opts.config.agents) {
88
90
  if (agent.sandbox) {
89
91
  const merged = merge(base, agent.sandbox);
92
+ if (opts.persist === false) {
93
+ merged.persist = false;
94
+ }
90
95
  // An agent that opts out gets neither the variables nor the mount,
91
96
  // but the mount is on the pool and cannot be taken back per agent —
92
97
  // so the variable is what actually decides, and without it the file
93
98
  // sitting there is not a credential anything will look for.
94
99
  const own = merged.keys === false ? undefined : keys;
95
- agents[agent.name] = toSpec(merged, { HOME, PYTHONDONTWRITEBYTECODE: '1', ...(own?.env ?? {}) }, own && secretsOf(own.secrets, opts.env));
100
+ agents[agent.name] = toSpec(merged, { HOME, PYTHONDONTWRITEBYTECODE: '1', ZENERA_AGENT: '1', ...(own?.env ?? {}) }, own && secretsOf(own.secrets, opts.env));
96
101
  }
97
102
  }
98
103
  const pool = new SandboxPool({
@@ -178,6 +183,12 @@ function toSpec(config, extra, secrets) {
178
183
  export function usesSandbox(project) {
179
184
  return project.agents.some((a) => a.tools.some((t) => t.group === SANDBOX_GROUP));
180
185
  }
186
+ /**
187
+ * Images this process has already settled. A batch opens the same project
188
+ * dozens of times at once, and without this each one shells out to podman and
189
+ * the concurrent copies race each other to pull the very same image.
190
+ */
191
+ const READY = new Set();
181
192
  /**
182
193
  * Asked before the first turn rather than at the first tool call, so a missing
183
194
  * container engine costs nothing instead of costing a round trip and half a
@@ -185,6 +196,9 @@ export function usesSandbox(project) {
185
196
  */
186
197
  export async function preflight(setup, yes) {
187
198
  mkdirSync(setup.home, { recursive: true });
199
+ if (READY.has(setup.image)) {
200
+ return;
201
+ }
188
202
  await ensurePodmanReady({
189
203
  image: setup.image,
190
204
  build: setup.build,
@@ -192,6 +206,9 @@ export async function preflight(setup, yes) {
192
206
  memory: setup.spec.memory,
193
207
  yes,
194
208
  });
209
+ // A resumed session restarts its own stopped container, which needs no lock.
210
+ await assertLocks(1, { reuses: setup.pool.for().name });
211
+ READY.add(setup.image);
195
212
  }
196
213
  /** Best-effort teardown: losing a container must never lose a run. */
197
214
  export async function teardown(pool) {
@@ -1,3 +1,6 @@
1
+ export declare const TEMPLATES: string;
2
+ /** How to use the memory graph: a house rules file, landing under `agents/`. */
3
+ export declare const MEMORY_RULES: string;
1
4
  /**
2
5
  * The project files that are ours rather than the project's.
3
6
  *
@@ -19,14 +22,19 @@ export declare const SHARED_RULES: readonly string[];
19
22
  * scaffold wrote, rather than a second definition of "this version".
20
23
  */
21
24
  export declare function ownVersion(): string;
25
+ type Vars = Record<string, string>;
26
+ export interface CopyOptions {
27
+ /** values for the `{{...}}` in any template under this tree */
28
+ vars?: Vars;
29
+ /** leave a file that is already there alone rather than replacing it */
30
+ keep?: boolean;
31
+ }
22
32
  /**
23
- * Writes the editor's files under `dir`, replacing what is there. They are
24
- * ours: they say how the editor is to treat a directory the agents write into,
25
- * and they describe the file formats of the version of `zen` in hand, so the
26
- * current answer is the only one worth having and a stale one is worse than
27
- * none. Returns the relative paths written.
33
+ * Copies one template directory into `dir` at `rel`, depth first, sorted so the
34
+ * list it returns is the same on every machine. Only a `.tmpl` is read as text;
35
+ * everything else is copied byte for byte.
28
36
  */
29
- export declare function editorFiles(dir: string): string[];
37
+ export declare function copyTree(from: string, dir: string, rel: string, opts: CopyOptions): string[];
30
38
  /**
31
39
  * Replaces every file in a project that nobody is meant to be maintaining:
32
40
  * `SHARED_RULES` in the project tree, and the editor tree whole. Returns the
@@ -83,4 +91,5 @@ export interface Scaffolded {
83
91
  * the first set under twice as many lines about the second.
84
92
  */
85
93
  export declare function scaffold(opts: ScaffoldOptions): Scaffolded;
94
+ export {};
86
95
  //# sourceMappingURL=scaffold.d.ts.map
package/dist/scaffold.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { chmodSync, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from 'node:fs';
2
2
  import { dirname, join, sep } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
+ import { editorFiles } from "./meta/editor.js";
4
5
  // ---------------------------------------------------------------------------
5
6
  // Scaffolding
6
7
  //
@@ -30,13 +31,13 @@ import { fileURLToPath } from 'node:url';
30
31
  // standing next to the files that implement it, which is the shape every
31
32
  // change to the project is then made in.
32
33
  // ---------------------------------------------------------------------------
33
- const TEMPLATES = fileURLToPath(new URL('../templates', import.meta.url));
34
+ export const TEMPLATES = fileURLToPath(new URL('../templates', import.meta.url));
34
35
  /** The suffix on a file with `{{...}}` in it, dropped when the file lands. */
35
36
  const TEMPLATE = '.tmpl';
36
37
  /** How to use file tools: a house rules file, landing under `agents/`. */
37
38
  const FILE_RULES = join('agents', 'files-instructions.md');
38
39
  /** How to use the memory graph: a house rules file, landing under `agents/`. */
39
- const MEMORY_RULES = join('agents', 'memory-instructions.md');
40
+ export const MEMORY_RULES = join('agents', 'memory-instructions.md');
40
41
  /** How tools are called: the same, and equally not the project's to maintain. */
41
42
  const TOOL_RULES = join('agents', 'tools-instructions.md');
42
43
  /** How forking behaves: the same again, reaching only agents that declare `fork:`. */
@@ -54,8 +55,6 @@ const FORK_RULES = join('agents', 'fork-instructions.md');
54
55
  * own beside them.
55
56
  */
56
57
  export const SHARED_RULES = [FILE_RULES, FORK_RULES, MEMORY_RULES, TOOL_RULES];
57
- /** The same bytes, kept in the `zen-memory` skill to restore or diff against. */
58
- const MEMORY_REFERENCE = join('.github', 'skills', 'zen-memory', 'references', 'memory-instructions.md');
59
58
  /**
60
59
  * This `zen`'s own version, which the scaffold pins the sandbox's tools to.
61
60
  * The publishable packages move in lockstep, so one number covers them all.
@@ -149,7 +148,7 @@ function target(name) {
149
148
  * list it returns is the same on every machine. Only a `.tmpl` is read as text;
150
149
  * everything else is copied byte for byte.
151
150
  */
152
- function copyTree(from, dir, rel, opts) {
151
+ export function copyTree(from, dir, rel, opts) {
153
152
  const written = [];
154
153
  mkdirSync(join(dir, rel), { recursive: true });
155
154
  const entries = readdirSync(from, { withFileTypes: true });
@@ -169,67 +168,13 @@ function copyTree(from, dir, rel, opts) {
169
168
  : readFileSync(source);
170
169
  writeFileSync(join(dir, child), body);
171
170
  // The bytes are copied, not the file, so a script arrives unrunnable.
172
- if (child.endsWith('.sh')) {
171
+ if (child.endsWith('.sh') || child.endsWith('.mjs')) {
173
172
  chmodSync(join(dir, child), 0o755);
174
173
  }
175
174
  written.push(child);
176
175
  }
177
176
  return written;
178
177
  }
179
- // ---------------------------------------------------------------------------
180
- // Telling the editor which instructions are not for it
181
- //
182
- // The project's house rules are `agents/instructions.md`, deliberately not
183
- // `AGENTS.md`: every coding assistant now reads that name out of an open
184
- // folder and feeds it to itself as always-on instructions, and `zen open`
185
- // opens exactly this directory. A name nobody else claims means the two are
186
- // never confused, and `chat.useAgentsMdFile` no longer has to be switched off
187
- // to keep them apart.
188
- //
189
- // They sit under `agents/` because there is rarely only one of them. Anything
190
- // named `agents/<topic>-instructions.md` is read too, in filename order, so a
191
- // subject that is true for every agent but is about one capability — memory,
192
- // say — gets its own document instead of another section in a file that keeps
193
- // growing.
194
- //
195
- // `chat.useNestedAgentsMdFiles` is still written. It is already false by
196
- // default, but it is opt-in globally, and this is a directory the agent itself
197
- // writes into — someone who turned it on would otherwise have the editor pick
198
- // up whatever `AGENTS.md` a run happened to leave behind. It is a *restricted*
199
- // setting, so it applies only in a trusted workspace; that is the right way
200
- // round, since an untrusted folder is not one to run agents in either.
201
- //
202
- // `agents/instructions.md` addresses the *project's* agents. The editor's
203
- // assistant still needs a brief of its own, and what it needs to know is how
204
- // this kind of project is put together — the file formats, how a prompt is
205
- // written, when to add a skill rather than an agent. That is what the
206
- // `.github/` tree is: the standing brief, plus the prompt files and skills the
207
- // editor picks up from the same place.
208
- // ---------------------------------------------------------------------------
209
- /**
210
- * Writes the editor's files under `dir`, replacing what is there. They are
211
- * ours: they say how the editor is to treat a directory the agents write into,
212
- * and they describe the file formats of the version of `zen` in hand, so the
213
- * current answer is the only one worth having and a stale one is worse than
214
- * none. Returns the relative paths written.
215
- */
216
- export function editorFiles(dir) {
217
- const written = copyTree(join(TEMPLATES, 'editor'), dir, '', {});
218
- // The one file that belongs to both trees. It is a project file first —
219
- // house rules reaching every agent that can see the graph — and it is
220
- // written once and edited from then on, like every other project file. But
221
- // a project whose copy drifted, or that deleted it and turned memory back
222
- // on later, needs somewhere to get the current text from, and the editor
223
- // tree is replaced on every `init` and `open`. Copying the same bytes into
224
- // the skill's references is what makes `diff` between the two mean
225
- // something, and what stops a second copy of these rules existing in the
226
- // repository to fall out of step with the first.
227
- const reference = join(dir, MEMORY_REFERENCE);
228
- mkdirSync(dirname(reference), { recursive: true });
229
- copyFileSync(join(TEMPLATES, 'project', MEMORY_RULES), reference);
230
- written.push(MEMORY_REFERENCE);
231
- return written;
232
- }
233
178
  /**
234
179
  * Replaces every file in a project that nobody is meant to be maintaining:
235
180
  * `SHARED_RULES` in the project tree, and the editor tree whole. Returns the