@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.
- package/README.md +40 -3
- package/dist/archive.d.ts +139 -0
- package/dist/archive.js +376 -0
- package/dist/banner.d.ts +33 -24
- package/dist/banner.js +25 -6
- package/dist/commands/check.js +1 -0
- package/dist/commands/export.d.ts +3 -0
- package/dist/commands/export.js +255 -0
- package/dist/commands/import.d.ts +3 -0
- package/dist/commands/import.js +173 -0
- package/dist/commands/index.d.ts +5 -1
- package/dist/commands/index.js +13 -1
- package/dist/commands/init.js +1 -0
- package/dist/commands/inspect.js +1 -0
- package/dist/commands/key.js +1 -0
- package/dist/commands/list.js +1 -0
- package/dist/commands/memory.js +118 -10
- package/dist/commands/open.js +1 -0
- package/dist/commands/run.js +3 -0
- package/dist/commands/sandbox.js +1 -0
- package/dist/commands/version.js +1 -0
- package/dist/lib.d.ts +2 -1
- package/dist/lib.js +2 -1
- package/dist/meta.d.ts +1 -1
- package/dist/meta.js +5 -28
- package/dist/resolve.js +19 -12
- package/dist/sandbox.js +1 -0
- package/dist/scaffold.d.ts +8 -0
- package/dist/scaffold.js +7 -2
- package/dist/term.d.ts +2 -0
- package/dist/term.js +2 -0
- package/dist/tui/markdown.d.ts +16 -0
- package/dist/tui/markdown.js +259 -0
- package/dist/tui/theme.js +1 -1
- package/dist/tui/wrap.d.ts +1 -1
- package/dist/validate.js +150 -4
- package/package.json +12 -6
- package/templates/editor/.github/copilot-instructions.md +62 -33
- package/templates/editor/.github/prompts/spec-sync-project.prompt.md +56 -3
- package/templates/editor/.github/skills/zen-cli/SKILL.md +21 -16
- package/templates/editor/.github/skills/zen-cli/references/check.md +6 -0
- package/templates/editor/.github/skills/zen-cli/references/export.md +107 -0
- package/templates/editor/.github/skills/zen-cli/references/memory.md +51 -13
- package/templates/editor/.github/skills/zen-cli/references/projects.md +1 -0
- package/templates/editor/.github/skills/zen-cli/references/rag.md +43 -3
- package/templates/editor/.github/skills/zen-cli/references/sandbox.md +30 -2
- package/templates/editor/.github/skills/zen-instructions/SKILL.md +232 -0
- package/templates/editor/.github/skills/zen-memory/SKILL.md +2 -1
- package/templates/editor/.github/skills/zen-memory-warmup/SKILL.md +684 -0
- package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +56 -23
- package/templates/editor/.github/skills/zen-rag-schema/SKILL.md +55 -28
- package/templates/editor/.github/skills/zen-review/SKILL.md +17 -1
- package/templates/editor/.github/skills/zen-sandbox/SKILL.md +257 -0
- package/templates/editor/.github/skills/zen-spec-sync/SKILL.md +16 -3
- package/templates/editor/.vscode/agents.schema.json +8 -3
- package/templates/project/SPECIFICATION.md.tmpl +4 -1
- package/templates/project/agents/files-instructions.md +86 -0
- package/templates/project/agents/fork-instructions.md +17 -0
- package/templates/project/agents/memory-instructions.md +37 -0
- package/templates/project/agents.yaml.tmpl +3 -3
- package/templates/project/gitignore +12 -1
- 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
|
|
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);
|
package/dist/tui/wrap.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
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
|
|
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.
|
|
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
|
-
"
|
|
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
|
}
|