@zenera/cli 1.1.21 → 1.1.23

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 (62) hide show
  1. package/README.md +40 -3
  2. package/dist/archive.d.ts +139 -0
  3. package/dist/archive.js +376 -0
  4. package/dist/banner.d.ts +33 -24
  5. package/dist/banner.js +25 -6
  6. package/dist/commands/check.js +1 -0
  7. package/dist/commands/export.d.ts +3 -0
  8. package/dist/commands/export.js +255 -0
  9. package/dist/commands/import.d.ts +3 -0
  10. package/dist/commands/import.js +173 -0
  11. package/dist/commands/index.d.ts +5 -1
  12. package/dist/commands/index.js +13 -1
  13. package/dist/commands/init.js +1 -0
  14. package/dist/commands/inspect.js +1 -0
  15. package/dist/commands/key.js +1 -0
  16. package/dist/commands/list.js +1 -0
  17. package/dist/commands/memory.js +118 -10
  18. package/dist/commands/open.js +1 -0
  19. package/dist/commands/run.js +3 -0
  20. package/dist/commands/sandbox.js +1 -0
  21. package/dist/commands/version.js +1 -0
  22. package/dist/lib.d.ts +2 -1
  23. package/dist/lib.js +2 -1
  24. package/dist/meta.d.ts +1 -1
  25. package/dist/meta.js +5 -28
  26. package/dist/resolve.js +19 -12
  27. package/dist/sandbox.js +1 -0
  28. package/dist/scaffold.d.ts +8 -0
  29. package/dist/scaffold.js +7 -2
  30. package/dist/term.d.ts +2 -0
  31. package/dist/term.js +2 -0
  32. package/dist/tui/markdown.d.ts +16 -0
  33. package/dist/tui/markdown.js +259 -0
  34. package/dist/tui/theme.js +1 -1
  35. package/dist/tui/wrap.d.ts +1 -1
  36. package/dist/validate.js +150 -4
  37. package/package.json +12 -6
  38. package/templates/editor/.github/copilot-instructions.md +62 -33
  39. package/templates/editor/.github/prompts/spec-sync-project.prompt.md +56 -3
  40. package/templates/editor/.github/skills/zen-cli/SKILL.md +21 -16
  41. package/templates/editor/.github/skills/zen-cli/references/check.md +6 -0
  42. package/templates/editor/.github/skills/zen-cli/references/export.md +107 -0
  43. package/templates/editor/.github/skills/zen-cli/references/memory.md +51 -13
  44. package/templates/editor/.github/skills/zen-cli/references/projects.md +1 -0
  45. package/templates/editor/.github/skills/zen-cli/references/rag.md +43 -3
  46. package/templates/editor/.github/skills/zen-cli/references/sandbox.md +30 -2
  47. package/templates/editor/.github/skills/zen-instructions/SKILL.md +232 -0
  48. package/templates/editor/.github/skills/zen-memory/SKILL.md +2 -1
  49. package/templates/editor/.github/skills/zen-memory-warmup/SKILL.md +684 -0
  50. package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +56 -23
  51. package/templates/editor/.github/skills/zen-rag-schema/SKILL.md +55 -28
  52. package/templates/editor/.github/skills/zen-review/SKILL.md +17 -1
  53. package/templates/editor/.github/skills/zen-sandbox/SKILL.md +257 -0
  54. package/templates/editor/.github/skills/zen-spec-sync/SKILL.md +16 -3
  55. package/templates/editor/.vscode/agents.schema.json +8 -3
  56. package/templates/project/SPECIFICATION.md.tmpl +4 -1
  57. package/templates/project/agents/files-instructions.md +86 -0
  58. package/templates/project/agents/fork-instructions.md +17 -0
  59. package/templates/project/agents/memory-instructions.md +37 -0
  60. package/templates/project/agents.yaml.tmpl +3 -3
  61. package/templates/project/gitignore +12 -1
  62. package/templates/project/scripts/_setup.sh +68 -11
@@ -0,0 +1,259 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Terminal Markdown Formatter
3
+ //
4
+ // Formats markdown text into styled ANSI terminal lines wrapped to a width.
5
+ //
6
+ // Consumes the block and span AST from `wrap.ts` and applies terminal styling
7
+ // from `term.ts` (bold, italic, green for code, underline for links, dimmed
8
+ // borders and markers). Line wrapping measures plain characters, so ANSI
9
+ // escape codes never cause lines to wrap early or misalign borders.
10
+ // ---------------------------------------------------------------------------
11
+ import { bold, cut, dim, green, italic, plain, styled, underline } from "../term.js";
12
+ import { blocksOf, gridOf, inlineOf } from "./wrap.js";
13
+ function renderPiece(piece) {
14
+ if (!styled()) {
15
+ return piece.text;
16
+ }
17
+ let t = piece.text;
18
+ if (piece.code) {
19
+ t = green(t);
20
+ }
21
+ if (piece.italic) {
22
+ t = italic(t);
23
+ }
24
+ if (piece.bold) {
25
+ t = bold(t);
26
+ }
27
+ if (piece.href) {
28
+ t = underline(t);
29
+ }
30
+ return t;
31
+ }
32
+ function sameStyle(a, b) {
33
+ return (!!a.bold === !!b.bold &&
34
+ !!a.italic === !!b.italic &&
35
+ !!a.code === !!b.code &&
36
+ a.href === b.href);
37
+ }
38
+ function mergePieces(pieces) {
39
+ const out = [];
40
+ for (const p of pieces) {
41
+ if (!p.text)
42
+ continue;
43
+ const last = out[out.length - 1];
44
+ if (last && sameStyle(last, p)) {
45
+ last.text += p.text;
46
+ }
47
+ else {
48
+ out.push({ ...p });
49
+ }
50
+ }
51
+ return out;
52
+ }
53
+ function wordsOf(spans) {
54
+ const words = [];
55
+ let currentPieces = [];
56
+ let currentLen = 0;
57
+ for (const span of spans) {
58
+ const style = {
59
+ ...(span.bold ? { bold: true } : {}),
60
+ ...(span.italic ? { italic: true } : {}),
61
+ ...(span.code ? { code: true } : {}),
62
+ ...(span.href ? { href: span.href } : {}),
63
+ };
64
+ const parts = span.text.split(/(\s+)/);
65
+ for (const part of parts) {
66
+ if (!part)
67
+ continue;
68
+ if (/^\s+$/.test(part)) {
69
+ if (currentPieces.length > 0) {
70
+ words.push({ pieces: currentPieces, length: currentLen });
71
+ currentPieces = [];
72
+ currentLen = 0;
73
+ }
74
+ }
75
+ else {
76
+ currentPieces.push({ text: part, ...style });
77
+ currentLen += part.length;
78
+ }
79
+ }
80
+ }
81
+ if (currentPieces.length > 0) {
82
+ words.push({ pieces: currentPieces, length: currentLen });
83
+ }
84
+ return words;
85
+ }
86
+ function sliceWord(word, at) {
87
+ const leftPieces = [];
88
+ const rightPieces = [];
89
+ let taken = 0;
90
+ for (const p of word.pieces) {
91
+ if (taken + p.text.length <= at) {
92
+ leftPieces.push(p);
93
+ taken += p.text.length;
94
+ }
95
+ else if (taken < at) {
96
+ const cutIndex = at - taken;
97
+ leftPieces.push({ ...p, text: p.text.slice(0, cutIndex) });
98
+ rightPieces.push({ ...p, text: p.text.slice(cutIndex) });
99
+ taken = at;
100
+ }
101
+ else {
102
+ rightPieces.push(p);
103
+ }
104
+ }
105
+ return [
106
+ { pieces: leftPieces, length: at },
107
+ { pieces: rightPieces, length: word.length - at },
108
+ ];
109
+ }
110
+ export function wrapSpans(spans, width, prefix = '', subsequentPrefix = '') {
111
+ const w = Math.max(1, width);
112
+ const words = wordsOf(spans);
113
+ if (words.length === 0) {
114
+ return prefix ? [prefix] : [''];
115
+ }
116
+ const lines = [];
117
+ let currentWords = [];
118
+ const prefixLen = plain(prefix).length;
119
+ const subLen = plain(subsequentPrefix).length;
120
+ let currentLen = prefixLen;
121
+ const flush = () => {
122
+ if (currentWords.length === 0)
123
+ return;
124
+ const linePieces = [];
125
+ for (let i = 0; i < currentWords.length; i++) {
126
+ const word = currentWords[i];
127
+ if (i > 0) {
128
+ const prev = currentWords[i - 1];
129
+ const prevPiece = prev.pieces[prev.pieces.length - 1];
130
+ const nextPiece = word.pieces[0];
131
+ const sharedStyle = prevPiece && nextPiece && sameStyle(prevPiece, nextPiece)
132
+ ? { ...prevPiece, text: ' ' }
133
+ : { text: ' ' };
134
+ linePieces.push(sharedStyle);
135
+ }
136
+ linePieces.push(...word.pieces);
137
+ }
138
+ const p = lines.length === 0 ? prefix : subsequentPrefix;
139
+ lines.push(p + mergePieces(linePieces).map(renderPiece).join(''));
140
+ currentWords = [];
141
+ currentLen = subLen;
142
+ };
143
+ for (let word of words) {
144
+ const available = lines.length === 0 ? w - prefixLen : w - subLen;
145
+ while (word.length > available && available > 0) {
146
+ if (currentWords.length > 0) {
147
+ flush();
148
+ }
149
+ const [head, tail] = sliceWord(word, available);
150
+ const p = lines.length === 0 ? prefix : subsequentPrefix;
151
+ lines.push(p + mergePieces(head.pieces).map(renderPiece).join(''));
152
+ word = tail;
153
+ currentLen = subLen;
154
+ }
155
+ const spaceNeeded = currentWords.length > 0 ? 1 : 0;
156
+ if (currentLen + spaceNeeded + word.length <= w) {
157
+ currentWords.push(word);
158
+ currentLen += spaceNeeded + word.length;
159
+ }
160
+ else {
161
+ flush();
162
+ currentWords.push(word);
163
+ const p = lines.length === 0 ? prefix : subsequentPrefix;
164
+ currentLen = plain(p).length + word.length;
165
+ }
166
+ }
167
+ flush();
168
+ return lines;
169
+ }
170
+ /**
171
+ * Render a sequence of AST `Block`s into styled terminal lines wrapped to `width`.
172
+ */
173
+ export function formatBlocks(blocks, width = 80) {
174
+ const lines = [];
175
+ let previous;
176
+ for (const block of blocks) {
177
+ if (previous && !(block.kind === 'item' && previous.kind === 'item')) {
178
+ lines.push('');
179
+ }
180
+ previous = block;
181
+ if (block.kind === 'code') {
182
+ lines.push(dim(`\u250c\u2500${block.title ? ` ${block.title}` : ''}`));
183
+ lines.push(...(block.lines ?? []).map((l) => `${dim('\u2502')} ${cut(l, Math.max(0, width - 2))}`));
184
+ lines.push(dim('\u2514\u2500'));
185
+ }
186
+ else if (block.kind === 'table') {
187
+ for (const [j, row] of gridOf(block).entries()) {
188
+ let formatted;
189
+ if (block.align !== undefined && j === 0) {
190
+ formatted = row
191
+ .map((s) => s.text === '| ' || s.text === ' | ' || s.text === ' |'
192
+ ? dim(s.text)
193
+ : renderPiece({ ...s, bold: true }))
194
+ .join('');
195
+ }
196
+ else if (block.align !== undefined && j === 1) {
197
+ formatted = dim(row.map((s) => s.text).join(''));
198
+ }
199
+ else {
200
+ formatted = row
201
+ .map((s) => s.text === '| ' || s.text === ' | ' || s.text === ' |'
202
+ ? dim(s.text)
203
+ : renderPiece(s))
204
+ .join('');
205
+ }
206
+ lines.push(cut(formatted, width));
207
+ }
208
+ }
209
+ else if (block.kind === 'heading') {
210
+ const headingSpans = (block.spans ?? []).map((s) => ({ ...s, bold: true }));
211
+ lines.push(...wrapSpans(headingSpans, width));
212
+ }
213
+ else if (block.kind === 'item') {
214
+ const indentLevel = block.level ?? 0;
215
+ const marker = block.marker ?? '\u2022';
216
+ const leadSpaces = ' '.repeat(indentLevel);
217
+ const prefix = `${leadSpaces}${dim(marker)} `;
218
+ const subPrefix = `${leadSpaces}${' '.repeat(marker.length + 1)}`;
219
+ lines.push(...wrapSpans(block.spans ?? [], width, prefix, subPrefix));
220
+ }
221
+ else if (block.kind === 'quote') {
222
+ const quoteSpans = (block.spans ?? []).map((s) => ({ ...s, italic: true }));
223
+ lines.push(...wrapSpans(quoteSpans, width, `${dim('\u2502')} `, `${dim('\u2502')} `));
224
+ }
225
+ else if (block.kind === 'rule') {
226
+ lines.push(dim('\u2500'.repeat(width)));
227
+ }
228
+ else if (block.lines) {
229
+ lines.push(...block.lines.map((l) => cut(l, width)));
230
+ }
231
+ else {
232
+ lines.push(...wrapSpans(block.spans ?? [], width));
233
+ }
234
+ }
235
+ return lines;
236
+ }
237
+ /**
238
+ * Format markdown text into styled ANSI terminal lines wrapped to `width`.
239
+ */
240
+ export function formatMarkdown(text, width = 80) {
241
+ return formatBlocks(blocksOf(text), width);
242
+ }
243
+ /**
244
+ * Format inline markdown text (bold, italic, code, links) into a styled ANSI string.
245
+ * Markers like backticks and asterisks are resolved and removed.
246
+ */
247
+ export function formatInline(text) {
248
+ const spans = inlineOf(text);
249
+ return mergePieces(spans.map((s) => ({
250
+ text: s.text,
251
+ ...(s.bold ? { bold: true } : {}),
252
+ ...(s.italic ? { italic: true } : {}),
253
+ ...(s.code ? { code: true } : {}),
254
+ ...(s.href ? { href: s.href } : {}),
255
+ })))
256
+ .map(renderPiece)
257
+ .join('');
258
+ }
259
+ //# sourceMappingURL=markdown.js.map
package/dist/tui/theme.js CHANGED
@@ -168,7 +168,7 @@ async function queryBackground(timeoutMs = 300) {
168
168
  stdout.write(QUERY);
169
169
  });
170
170
  }
171
- /** Components come back as 1–4 hex digits, so each is scaled by its own width. */
171
+ /** Components come back as 1-4 hex digits, so each is scaled by its own width. */
172
172
  function appearanceOf(r, g, b) {
173
173
  const channel = (hex) => parseInt(hex, 16) / (16 ** hex.length - 1);
174
174
  const luminance = 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b);
@@ -154,7 +154,7 @@ export type Align = 'left' | 'right' | 'center';
154
154
  */
155
155
  export interface Block {
156
156
  kind: BlockKind;
157
- /** `heading`: its depth, 1–6. `item`: how deep it is nested. */
157
+ /** `heading`: its depth, 1-6. `item`: how deep it is nested. */
158
158
  level?: number;
159
159
  /** the fence's info string, when it had one */
160
160
  title?: string;
package/dist/validate.js CHANGED
@@ -7,7 +7,7 @@ import { resolveBuild } from "./image.js";
7
7
  import { SHAPES, envNames, form } from "./keys.js";
8
8
  import { probeModels } from "./liveness.js";
9
9
  import { BuildError, ensurePodmanReady } from "./podman.js";
10
- import { staleShared } from "./scaffold.js";
10
+ import { ownVersion, staleShared } from "./scaffold.js";
11
11
  // Mirrors the loader's own constants (`packages/neo/src/project/load.ts`).
12
12
  // Duplicated rather than exported, because a check that agreed with the loader
13
13
  // by construction could not report that the two had diverged.
@@ -15,24 +15,87 @@ const CONFIG_NAMES = ['agents.yaml', 'agents.yml', 'agents/agents.yaml', 'agents
15
15
  const AGENTS_DIR = 'agents';
16
16
  const HOUSE_RULES = 'agents/instructions.md';
17
17
  const INSTRUCTIONS_SUFFIX = '-instructions.md';
18
+ /** How file tools are used; without it the model has schemas only. */
19
+ const FILE_RULES = 'agents/files-instructions.md';
18
20
  /** The only place memory is explained to a model; the runtime composes none of it. */
19
21
  const MEMORY_RULES = 'agents/memory-instructions.md';
20
22
  /** The only place forking is explained to a model; the same holds. */
21
23
  const FORK_RULES = 'agents/fork-instructions.md';
22
24
  /** The conditioned documents `zen` ships, which land whether or not they apply. */
23
- const OURS = new Set([MEMORY_RULES, FORK_RULES]);
25
+ const OURS = new Set([FILE_RULES, MEMORY_RULES, FORK_RULES]);
24
26
  const PROMPTS_DIR = 'agents/prompts';
25
27
  const SKILLS_DIR = 'agents/skills';
26
28
  const SKILL_FILE = 'SKILL.md';
27
29
  const ASSETS_DIR = 'assets';
28
30
  /** What `allow:` and `preload:` accept, so a skill outside it cannot be named. */
29
31
  const REFERABLE = /^[a-z0-9]+(?:[-_][a-z0-9]+)*$/;
32
+ const FILE_TOOLS = [
33
+ 'read_file',
34
+ 'list_dir',
35
+ 'find_files',
36
+ 'write_file',
37
+ 'apply_patch',
38
+ 'move_file',
39
+ 'delete_file',
40
+ ];
41
+ const FILE_WRITE_TOOLS = new Set(['write_file', 'apply_patch', 'move_file', 'delete_file']);
42
+ function activeFileTools(selectors) {
43
+ const active = new Set();
44
+ if (!selectors?.length) {
45
+ return active;
46
+ }
47
+ for (const raw of selectors) {
48
+ const drop = raw.startsWith('-');
49
+ const selector = (drop ? raw.slice(1) : raw).trim();
50
+ if (selector === '*' || selector === 'files:*' || selector === 'workspace:*') {
51
+ for (const t of FILE_TOOLS) {
52
+ if (drop) {
53
+ active.delete(t);
54
+ }
55
+ else {
56
+ active.add(t);
57
+ }
58
+ }
59
+ continue;
60
+ }
61
+ const colon = selector.lastIndexOf(':');
62
+ const name = colon >= 0 ? selector.slice(colon + 1) : selector;
63
+ const group = colon >= 0 ? selector.slice(0, colon) : undefined;
64
+ if (FILE_TOOLS.includes(name)) {
65
+ if (!group || group === 'files' || group === 'workspace') {
66
+ if (drop) {
67
+ active.delete(name);
68
+ }
69
+ else {
70
+ active.add(name);
71
+ }
72
+ }
73
+ }
74
+ }
75
+ return active;
76
+ }
77
+ function hasFileTools(spec) {
78
+ return activeFileTools(spec.tools).size > 0;
79
+ }
80
+ function hasFileWriteTools(spec) {
81
+ const active = activeFileTools(spec.tools);
82
+ for (const t of FILE_WRITE_TOOLS) {
83
+ if (active.has(t)) {
84
+ return true;
85
+ }
86
+ }
87
+ return false;
88
+ }
30
89
  /** What `requires:` may name. Mirrors `CAPABILITIES` in the loader. */
31
90
  const CAPABILITIES = {
32
91
  fork: (spec) => Boolean(spec.fork),
33
92
  memory: (spec) => Boolean(spec.memory),
34
93
  'memory-write': (spec) => spec.memory?.access === 'read-write' || spec.memory?.access === 'full',
35
94
  'memory-forget': (spec) => spec.memory?.access === 'full',
95
+ files: (spec) => hasFileTools(spec),
96
+ 'files-write': (spec) => hasFileWriteTools(spec),
97
+ workspace: (spec) => hasFileTools(spec),
98
+ 'workspace-write': (spec) => hasFileWriteTools(spec),
36
99
  };
37
100
  /**
38
101
  * Mirrors `readHouseRules` in the loader, in the same order:
@@ -276,7 +339,23 @@ export async function validateProject(opts) {
276
339
  });
277
340
  }
278
341
  }
279
- // Present is not the same as current. These three files are copies of ours
342
+ // The same again for file operations: the file tools need instructions on
343
+ // discovery, safe reading, and patching. Without them the tools are granted
344
+ // with only their schemas to explain them.
345
+ if (config.agents.some((a) => hasFileTools(a))) {
346
+ const path = join(root, FILE_RULES);
347
+ if (!existsSync(path) || empty(path)) {
348
+ add({
349
+ severity: 'error',
350
+ code: 'files.uninstructed',
351
+ where: FILE_RULES,
352
+ message: 'an agent has file tools, but nothing tells it how to use them — ' +
353
+ 'the tools are granted with only their schemas to explain them',
354
+ fix: 'zen check --fix, which writes the current rules back',
355
+ });
356
+ }
357
+ }
358
+ // Present is not the same as current. These files are copies of ours
280
359
  // and nobody is meant to be maintaining them, so bytes that differ are an
281
360
  // older `zen`'s — prose about a runtime that has since moved, which reads
282
361
  // exactly as authoritative as the version that is true. The frontmatter
@@ -605,7 +684,7 @@ function checkAgent(root, config, spec, entry, available, rules, record, add) {
605
684
  try {
606
685
  tools = selectTools(available, [...selectors], {
607
686
  where: `${where}.tools`,
608
- hint: 'this CLI provides the workspace and sandbox groups',
687
+ hint: 'this CLI provides the files and sandbox groups',
609
688
  }).map((t) => t.name);
610
689
  }
611
690
  catch (err) {
@@ -1173,6 +1252,7 @@ function checkSandbox(root, config, record, add) {
1173
1252
  if (!ok) {
1174
1253
  return undefined;
1175
1254
  }
1255
+ checkPin(join(root, file), file, add);
1176
1256
  try {
1177
1257
  return resolveBuild(root, config.sandbox);
1178
1258
  }
@@ -1186,6 +1266,72 @@ function checkSandbox(root, config, record, add) {
1186
1266
  return undefined;
1187
1267
  }
1188
1268
  }
1269
+ /** `@zenera/cli`, and the version after it when the line pins one. */
1270
+ const ZENERA_INSTALL = /@zenera\/([a-z0-9-]+)(?:@([^\s\\'"]+))?/g;
1271
+ /**
1272
+ * Whether the Dockerfile's `@zenera/*` installs name this `zen`.
1273
+ *
1274
+ * `zen init` writes the version it was run by and never rewrites the file
1275
+ * again — the scaffold keeps what the project already has — so the pin is fixed
1276
+ * at creation and falls behind the host on the next upgrade. Nothing else
1277
+ * notices: the file parses, the image builds, and the container simply holds a
1278
+ * different `zen` than the session outside it, which is a mismatch that shows
1279
+ * up as a tool behaving unlike its documentation.
1280
+ *
1281
+ * Read rather than run, so it is true on the laptop with no container engine —
1282
+ * the one `--no-sandbox` leaves with no other word on the subject.
1283
+ */
1284
+ function checkPin(path, rel, add) {
1285
+ let text;
1286
+ try {
1287
+ // Existence is already a finding; an unreadable file is not worth a
1288
+ // second one, and a throw here would cost every check after it.
1289
+ text = readFileSync(path, 'utf8');
1290
+ }
1291
+ catch {
1292
+ return;
1293
+ }
1294
+ const mine = ownVersion();
1295
+ const stale = new Set();
1296
+ const loose = new Set();
1297
+ for (const line of text.split('\n')) {
1298
+ // A comment naming the package is prose about the pin, not the pin.
1299
+ if (/^\s*#/.test(line)) {
1300
+ continue;
1301
+ }
1302
+ for (const match of line.matchAll(ZENERA_INSTALL)) {
1303
+ const name = `@zenera/${match[1]}`;
1304
+ const pinned = match[2];
1305
+ if (pinned === undefined) {
1306
+ loose.add(name);
1307
+ }
1308
+ else if (pinned !== mine) {
1309
+ stale.add(`${name}@${pinned}`);
1310
+ }
1311
+ }
1312
+ }
1313
+ const fix = `pin them to ${mine}, the version \`zen --version\` reports`;
1314
+ if (stale.size > 0) {
1315
+ add({
1316
+ severity: 'warning',
1317
+ code: 'sandbox.pin.stale',
1318
+ where: rel,
1319
+ message: `installs ${[...stale].join(', ')}, but this \`zen\` is ${mine} — the CLI an ` +
1320
+ 'agent runs inside the container is not the one that built the session',
1321
+ fix,
1322
+ });
1323
+ }
1324
+ if (loose.size > 0) {
1325
+ add({
1326
+ severity: 'warning',
1327
+ code: 'sandbox.pin.absent',
1328
+ where: rel,
1329
+ message: `installs ${[...loose].join(', ')} unpinned, so the next image build takes ` +
1330
+ 'whatever is current and drifts away from the host without saying so',
1331
+ fix,
1332
+ });
1333
+ }
1334
+ }
1189
1335
  /** Written by the smoke test and read back, to prove the mount is real. */
1190
1336
  const SMOKE = 'zen-check';
1191
1337
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenera/cli",
3
- "version": "1.1.21",
3
+ "version": "1.1.23",
4
4
  "description": "Command-line front end for @zenera/neo: agentic projects you can run, share and commit.",
5
5
  "keywords": [
6
6
  "agents",
@@ -48,14 +48,20 @@
48
48
  "prepack": "tsc -b"
49
49
  },
50
50
  "dependencies": {
51
- "@inkjs/ui": "^2.0.0",
52
- "ink": "^7.1.1",
53
- "react": "^19.2.8",
54
- "@zenera/neo": "^1.1.21",
55
51
  "@anthropic-ai/sdk": "^0.120.0",
56
52
  "@google/genai": "^2.18.0",
53
+ "@inkjs/ui": "^2.0.0",
57
54
  "@openrouter/sdk": "^1.2.80",
55
+ "@zenera/neo": "^1.1.23",
58
56
  "google-auth-library": "^10.0.0",
59
- "openai": "^6.0.0"
57
+ "ink": "^7.1.1",
58
+ "openai": "^6.0.0",
59
+ "react": "^19.2.8",
60
+ "yauzl": "^3.4.0",
61
+ "yazl": "^3.3.1"
62
+ },
63
+ "devDependencies": {
64
+ "@types/yauzl": "^3.4.0",
65
+ "@types/yazl": "^3.3.1"
60
66
  }
61
67
  }