@zenera/cli 1.1.17 → 1.1.19
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 +99 -16
- package/dist/banner.d.ts +19 -0
- package/dist/banner.js +17 -10
- package/dist/command.d.ts +3 -0
- package/dist/commands/index.js +4 -1
- package/dist/commands/init.js +1 -1
- package/dist/commands/inspect.js +77 -13
- package/dist/commands/key.js +49 -1
- package/dist/commands/memory.js +16 -3
- package/dist/commands/meta.d.ts +3 -0
- package/dist/commands/meta.js +596 -0
- package/dist/commands/run.js +63 -38
- package/dist/engine.d.ts +15 -0
- package/dist/engine.js +20 -1
- package/dist/home.d.ts +2 -0
- package/dist/home.js +2 -0
- package/dist/main.js +1 -1
- package/dist/meta.d.ts +221 -0
- package/dist/meta.js +695 -0
- package/dist/podman.js +64 -2
- package/dist/projects.d.ts +9 -1
- package/dist/projects.js +19 -2
- package/dist/resolve.js +7 -1
- package/dist/sandbox.d.ts +2 -0
- package/dist/sandbox.js +1 -1
- package/dist/scaffold.js +1 -1
- package/dist/session.d.ts +10 -0
- package/dist/session.js +19 -1
- package/dist/term.d.ts +21 -0
- package/dist/term.js +74 -2
- package/dist/tui/app.js +313 -150
- package/dist/tui/theme.d.ts +26 -7
- package/dist/tui/theme.js +62 -36
- package/dist/tui/wrap.d.ts +41 -16
- package/dist/tui/wrap.js +191 -18
- package/dist/validate.js +3 -16
- package/package.json +3 -2
- package/templates/editor/.github/copilot-instructions.md +61 -7
- package/templates/editor/.github/prompts/{review-project.prompt.md → project-review.prompt.md} +0 -1
- package/templates/editor/.github/prompts/{update-spec-from-feedback.prompt.md → spec-apply-feedback.prompt.md} +5 -5
- package/templates/editor/.github/prompts/{sync-with-spec.prompt.md → spec-sync-project.prompt.md} +4 -4
- package/templates/editor/.github/skills/zen-cli/references/check.md +0 -2
- package/templates/editor/.github/skills/zen-cli/references/inspect.md +4 -3
- package/templates/editor/.github/skills/zen-cli/references/memory.md +14 -0
- package/templates/editor/.github/skills/zen-cli/references/projects.md +1 -0
- package/templates/editor/.github/skills/zen-cli/references/run.md +75 -16
- package/templates/editor/.github/skills/zen-memory/SKILL.md +52 -4
- package/templates/editor/.github/skills/zen-review/SKILL.md +2 -2
- package/templates/editor/.github/skills/zen-review/scripts/review.sh +1 -1
- package/templates/editor/.github/skills/zen-spec-sync/SKILL.md +2 -2
- package/templates/editor/.github/skills/zen-spec-sync/scripts/snapshot.sh +1 -1
- package/templates/project/SPECIFICATION.md.tmpl +25 -2
- package/templates/project/agents/tools-instructions.md +8 -0
- package/templates/project/agents.yaml.tmpl +3 -3
- package/templates/project/env +7 -1
- package/templates/project/gitignore +4 -4
package/dist/tui/theme.d.ts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { type Appearance } from '../term.ts';
|
|
2
|
+
export type { Appearance };
|
|
3
|
+
/**
|
|
4
|
+
* The roles a line can play in the transcript.
|
|
5
|
+
*
|
|
6
|
+
* `text` is what the agent said on its way somewhere — prose between one batch
|
|
7
|
+
* of calls and the next — and `agent` is the answer it finished on, which is
|
|
8
|
+
* the only one that gets a box. Reasoning is not among them: it is progress,
|
|
9
|
+
* and progress that has finished is worth no rows at all.
|
|
10
|
+
*/
|
|
11
|
+
export type Kind = 'you' | 'text' | 'agent' | 'tool' | 'note' | 'error';
|
|
4
12
|
export interface LineStyle {
|
|
5
13
|
/** `undefined` means the terminal's own foreground. */
|
|
6
14
|
readonly color?: string;
|
|
7
|
-
readonly dim?: boolean;
|
|
8
15
|
}
|
|
9
16
|
export interface Theme {
|
|
10
17
|
readonly appearance: Appearance;
|
|
@@ -13,13 +20,25 @@ export interface Theme {
|
|
|
13
20
|
readonly accent: string;
|
|
14
21
|
/** Read-only badge, busy label. */
|
|
15
22
|
readonly warn: string;
|
|
23
|
+
/**
|
|
24
|
+
* Box rules, clocks, the footer's numbers, the call rows. One step quieter
|
|
25
|
+
* than the prose and never two — stated as a grey because `dim` is a
|
|
26
|
+
* request the terminal is free to over-honour, and some do.
|
|
27
|
+
*/
|
|
28
|
+
readonly chrome: LineStyle;
|
|
16
29
|
/**
|
|
17
30
|
* Reasoning. It is not a tool call and it is not the answer, and drawn in
|
|
18
|
-
* the same
|
|
19
|
-
*
|
|
20
|
-
*
|
|
31
|
+
* the same grey as the rows around it there was nothing to say which — a
|
|
32
|
+
* wall of one weight reads as one thing. Its own hue is what separates the
|
|
33
|
+
* model's thinking from the work it went on to do.
|
|
21
34
|
*/
|
|
22
35
|
readonly thinking: LineStyle;
|
|
36
|
+
/**
|
|
37
|
+
* The three things a turn spends time on. One word in the footer says which,
|
|
38
|
+
* and it is read at a glance from across a desk — so the word changing is
|
|
39
|
+
* backed by the colour changing, not left to be spelled out.
|
|
40
|
+
*/
|
|
41
|
+
readonly phase: Record<'reasoning' | 'waiting' | 'working', string>;
|
|
23
42
|
/**
|
|
24
43
|
* Branch colours, cycled in order of first sight. A fan-out is the one
|
|
25
44
|
* place where colour carries information rather than decoration: eight
|
package/dist/tui/theme.js
CHANGED
|
@@ -13,47 +13,71 @@
|
|
|
13
13
|
// unreadable on dark and invisible on paper. What is left is the handful of
|
|
14
14
|
// accents that ANSI *does* let a light theme get wrong, so those swap.
|
|
15
15
|
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
16
|
+
// A second weight is spent on *chrome* and on the machinery: box rules,
|
|
17
|
+
// clocks, the footer's numbers, and the call rows that say how an answer was
|
|
18
|
+
// arrived at. What the agent actually said is drawn at full weight in the
|
|
19
|
+
// terminal's own foreground, so a transcript read top to bottom reads as prose
|
|
20
|
+
// with the work annotated beside it — not as one undifferentiated wall.
|
|
20
21
|
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
22
|
+
// That weight used to be SGR 2 (`dim`), and it was the third version of the
|
|
23
|
+
// same bug. `dim` is not a colour, it is an instruction, and how far it is
|
|
24
|
+
// obeyed is the terminal's business: iTerm2 takes a step, Apple Terminal
|
|
25
|
+
// blends most of the way to the background. On Terminal.app the entire margin
|
|
26
|
+
// — every rule, every call row, every number — went out with it.
|
|
27
|
+
//
|
|
28
|
+
// So there is exactly one ramp here, `chrome`, and it is a stated grey rather
|
|
29
|
+
// than an attribute. It is the only number in the file, and it buys the one
|
|
30
|
+
// thing deference cannot: a floor under the quiet half of the screen.
|
|
31
|
+
// Everything else is still named by role and still defers to the user's own
|
|
32
|
+
// scheme — no brand, no second hue, no 256-colour palette.
|
|
24
33
|
// ---------------------------------------------------------------------------
|
|
34
|
+
import { appearance, CHROME } from "../term.js";
|
|
35
|
+
// The two greys, shared with every one-shot command so a `zen check` table and
|
|
36
|
+
// a TUI call row are the same weight. Chalk steps them down for a 16-colour
|
|
37
|
+
// terminal, which is the only place the old `gray` problem is still reachable.
|
|
38
|
+
const DARK_CHROME = { color: `ansi256(${CHROME.dark})` };
|
|
39
|
+
const LIGHT_CHROME = { color: `ansi256(${CHROME.light})` };
|
|
25
40
|
const DARK = {
|
|
26
41
|
appearance: 'dark',
|
|
27
42
|
line: {
|
|
28
43
|
you: { color: 'cyan' },
|
|
44
|
+
text: {},
|
|
29
45
|
agent: {},
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
tool:
|
|
46
|
+
// A call is the machinery, not the work: the transcript is read for
|
|
47
|
+
// what the agent said, and the rows saying how it found out are a
|
|
48
|
+
// margin note beside that.
|
|
49
|
+
tool: DARK_CHROME,
|
|
34
50
|
note: { color: 'cyan' },
|
|
35
51
|
error: { color: 'red' },
|
|
36
52
|
},
|
|
37
53
|
accent: 'cyan',
|
|
38
54
|
warn: 'yellow',
|
|
39
|
-
|
|
55
|
+
chrome: DARK_CHROME,
|
|
56
|
+
// Bright: the gist is one line among many and plain `magenta` on black is
|
|
57
|
+
// the darkest of the six.
|
|
58
|
+
thinking: { color: 'magentaBright' },
|
|
59
|
+
phase: { reasoning: 'magenta', waiting: 'yellow', working: 'cyan' },
|
|
40
60
|
lanes: ['cyan', 'green', 'yellow', 'blue', 'red', 'magenta'],
|
|
41
61
|
};
|
|
42
62
|
// On a light background `cyan` and `yellow` are barely darker than the paper.
|
|
43
|
-
//
|
|
44
|
-
// still legible.
|
|
63
|
+
// Grey and `blue`/`magenta` are the same information, still legible.
|
|
45
64
|
const LIGHT = {
|
|
46
65
|
appearance: 'light',
|
|
47
66
|
line: {
|
|
48
67
|
you: { color: 'blue' },
|
|
68
|
+
text: {},
|
|
49
69
|
agent: {},
|
|
50
|
-
tool:
|
|
70
|
+
tool: LIGHT_CHROME,
|
|
51
71
|
note: { color: 'blue' },
|
|
52
72
|
error: { color: 'red' },
|
|
53
73
|
},
|
|
54
74
|
accent: 'blue',
|
|
55
75
|
warn: 'magenta',
|
|
76
|
+
chrome: LIGHT_CHROME,
|
|
56
77
|
thinking: { color: 'magenta' },
|
|
78
|
+
// No yellow: `waiting` takes blue and `working` green, which are the two
|
|
79
|
+
// that survive paper.
|
|
80
|
+
phase: { reasoning: 'magenta', waiting: 'blue', working: 'green' },
|
|
57
81
|
// No cyan or yellow: on paper they are barely darker than the paper.
|
|
58
82
|
lanes: ['blue', 'green', 'red', 'magenta'],
|
|
59
83
|
};
|
|
@@ -67,23 +91,17 @@ export async function resolveTheme(choice) {
|
|
|
67
91
|
if (asked !== 'auto') {
|
|
68
92
|
return THEMES[asked];
|
|
69
93
|
}
|
|
70
|
-
return THEMES[(await queryBackground()) ??
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* `COLORFGBG` is `fg;bg` or `fg;<something>;bg`; the background is the last
|
|
74
|
-
* field. Anything non-numeric (notably `default`) tells us nothing.
|
|
75
|
-
*/
|
|
76
|
-
function fromColorFgBg() {
|
|
77
|
-
const parts = process.env['COLORFGBG']?.split(';');
|
|
78
|
-
const bg = parts?.[parts.length - 1];
|
|
79
|
-
if (bg === undefined || !/^\d+$/.test(bg)) {
|
|
80
|
-
return undefined;
|
|
81
|
-
}
|
|
82
|
-
const n = Number(bg);
|
|
83
|
-
return n === 7 || n >= 9 ? 'light' : 'dark';
|
|
94
|
+
return THEMES[(await queryBackground()) ?? appearance()];
|
|
84
95
|
}
|
|
85
|
-
|
|
96
|
+
// The DA1 behind the colour query is what makes the answer conclusive. Every
|
|
97
|
+
// terminal replies to DA1, and none replies to it ahead of an OSC 11 it
|
|
98
|
+
// supports — so a DA1 arriving alone means "I do not do that", known in one
|
|
99
|
+
// round trip instead of guessed at the end of a timeout. What is left for the
|
|
100
|
+
// timeout is the terminal that answers neither, which is the only case worth
|
|
101
|
+
// waiting on and now the only one that does.
|
|
102
|
+
const QUERY = '\u001b]11;?\u0007\u001b[c';
|
|
86
103
|
const REPLY = /\u001b\]11;rgb:([\da-f]{1,4})\/([\da-f]{1,4})\/([\da-f]{1,4})/i;
|
|
104
|
+
const DA1 = /\u001b\[\?[\d;]*c/;
|
|
87
105
|
/**
|
|
88
106
|
* Ask the terminal for its background colour and read the answer off stdin.
|
|
89
107
|
*
|
|
@@ -93,7 +111,7 @@ const REPLY = /\u001b\]11;rgb:([\da-f]{1,4})\/([\da-f]{1,4})\/([\da-f]{1,4})/i;
|
|
|
93
111
|
* and nothing else. Anything the user typed in that window is put back, so
|
|
94
112
|
* the first keystroke of a fast start is not eaten by the handshake.
|
|
95
113
|
*/
|
|
96
|
-
async function queryBackground(timeoutMs =
|
|
114
|
+
async function queryBackground(timeoutMs = 300) {
|
|
97
115
|
const { stdin, stdout } = process;
|
|
98
116
|
if (!stdin.isTTY || !stdout.isTTY || typeof stdin.setRawMode !== 'function') {
|
|
99
117
|
return undefined;
|
|
@@ -102,6 +120,7 @@ async function queryBackground(timeoutMs = 120) {
|
|
|
102
120
|
let timer;
|
|
103
121
|
let settled = false;
|
|
104
122
|
let seen = '';
|
|
123
|
+
let found;
|
|
105
124
|
return await new Promise((resolve) => {
|
|
106
125
|
const finish = (result) => {
|
|
107
126
|
if (settled) {
|
|
@@ -114,26 +133,33 @@ async function queryBackground(timeoutMs = 120) {
|
|
|
114
133
|
if (!wasRaw) {
|
|
115
134
|
stdin.pause();
|
|
116
135
|
}
|
|
117
|
-
const typed = seen
|
|
136
|
+
const typed = seen
|
|
137
|
+
.replace(REPLY, '')
|
|
138
|
+
.replace(DA1, '')
|
|
139
|
+
.replace(/\u001b\\|\u0007/g, '');
|
|
118
140
|
if (typed) {
|
|
119
141
|
stdin.unshift(Buffer.from(typed, 'latin1'));
|
|
120
142
|
}
|
|
121
143
|
resolve(result);
|
|
122
144
|
};
|
|
145
|
+
// The DA1 is sent second and answered second, so it is the end of the
|
|
146
|
+
// conversation whether or not the colour came back. Settling on the
|
|
147
|
+
// colour alone leaves the DA1 in the pipe, and it surfaces a moment
|
|
148
|
+
// later as `[?64;1;2;...c` typed into the prompt.
|
|
123
149
|
const onData = (chunk) => {
|
|
124
150
|
seen += chunk.toString('latin1');
|
|
125
151
|
const m = REPLY.exec(seen);
|
|
126
152
|
if (m) {
|
|
127
|
-
|
|
153
|
+
found = appearanceOf(m[1], m[2], m[3]);
|
|
128
154
|
}
|
|
129
|
-
|
|
130
|
-
finish(
|
|
155
|
+
if (DA1.test(seen) || seen.length > 256) {
|
|
156
|
+
finish(found);
|
|
131
157
|
}
|
|
132
158
|
};
|
|
133
159
|
stdin.setRawMode(true);
|
|
134
160
|
stdin.resume();
|
|
135
161
|
stdin.on('data', onData);
|
|
136
|
-
timer = setTimeout(() => finish(
|
|
162
|
+
timer = setTimeout(() => finish(found), timeoutMs);
|
|
137
163
|
timer.unref?.();
|
|
138
164
|
stdout.write(QUERY);
|
|
139
165
|
});
|
package/dist/tui/wrap.d.ts
CHANGED
|
@@ -17,26 +17,33 @@ export declare function clip(text: string, width: number): string;
|
|
|
17
17
|
export declare function wrap(text: string, width: number): string[];
|
|
18
18
|
/** The three footer rows, its margin, the prompt, and a row in hand. */
|
|
19
19
|
export declare const CHROME_ROWS = 7;
|
|
20
|
-
/**
|
|
21
|
-
|
|
20
|
+
/**
|
|
21
|
+
* How much of the reasoning stream is worth showing.
|
|
22
|
+
*
|
|
23
|
+
* One row. A reasoning summary is not a document to be read while it arrives —
|
|
24
|
+
* it is the model saying what it is about to do, and the sentence that says so
|
|
25
|
+
* is the heading it writes for itself. Six rows of it pushed the answer and the
|
|
26
|
+
* work down the screen to make room for a paragraph nobody finishes reading,
|
|
27
|
+
* and the whole chain is in the trajectory either way.
|
|
28
|
+
*/
|
|
29
|
+
export declare const THINKING_ROWS = 1;
|
|
22
30
|
/** How much of the frame the work in flight may take. A branch is a box now,
|
|
23
31
|
* not a row, so a fan-out of two costs fourteen of these. */
|
|
24
32
|
export declare const ACTIVITY_ROWS = 16;
|
|
25
|
-
/** The
|
|
26
|
-
export declare const
|
|
33
|
+
/** The width of the elapsed-time column every call row is drawn behind. */
|
|
34
|
+
export declare const TIME_COL = 5;
|
|
27
35
|
export interface Budget {
|
|
28
36
|
/** rows for the list of what is in flight */
|
|
29
37
|
activity: number;
|
|
30
|
-
/** rows for the
|
|
38
|
+
/** rows for the gist of what the model is reasoning about */
|
|
31
39
|
thinking: number;
|
|
32
40
|
/** rows for the answer as it arrives */
|
|
33
41
|
live: number;
|
|
34
42
|
/**
|
|
35
|
-
* What the three of them occupy together
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* reasoning block.
|
|
43
|
+
* What the three of them occupy together — a constant for a given terminal,
|
|
44
|
+
* whatever is happening inside it. The region grows to this and stops, and
|
|
45
|
+
* never shrinks back while the turn runs, which is what keeps the footer on
|
|
46
|
+
* one row instead of riding up and down on the work.
|
|
40
47
|
*/
|
|
41
48
|
total: number;
|
|
42
49
|
}
|
|
@@ -47,11 +54,8 @@ export interface Budget {
|
|
|
47
54
|
* arrives matters more than the machinery producing it. Reasoning yields to it
|
|
48
55
|
* in turn, because it is a progress indicator and the answer is the point.
|
|
49
56
|
*
|
|
50
|
-
* `thinking` is how many rows of reasoning are wanted
|
|
51
|
-
*
|
|
52
|
-
* settled and its tail is history rather than progress. The two rules are
|
|
53
|
-
* charged on top, so a block too cramped to be worth boxing is not drawn at all
|
|
54
|
-
* rather than drawn as a border with a line in it.
|
|
57
|
+
* `thinking` is how many rows of reasoning are wanted — one while the model is
|
|
58
|
+
* reasoning, none otherwise — and it never takes the answer's last row.
|
|
55
59
|
*/
|
|
56
60
|
export declare function budgetOf(rows: number, activity: number, thinking: number): Budget;
|
|
57
61
|
/** The rows a branch box may spend on its own calls and its reasoning. */
|
|
@@ -71,7 +75,7 @@ export declare function branchRows(count: number, allowance: number): number;
|
|
|
71
75
|
/**
|
|
72
76
|
* How wide the answer is drawn. A line of prose spanning a 200-column terminal
|
|
73
77
|
* is measurably harder to read than one that stops, which is why every demo in
|
|
74
|
-
* `examples/` puts its answer in a box of bounded width — the terminal is the
|
|
78
|
+
* `examples/sdk/` puts its answer in a box of bounded width — the terminal is the
|
|
75
79
|
* page, not the paragraph.
|
|
76
80
|
*/
|
|
77
81
|
export declare function answerWidth(columns: number): number;
|
|
@@ -87,6 +91,27 @@ export declare function answerWidth(columns: number): number;
|
|
|
87
91
|
* that got cut.
|
|
88
92
|
*/
|
|
89
93
|
export declare function readable(preview: string): string;
|
|
94
|
+
/** Every field of a preview, first mention winning. */
|
|
95
|
+
export declare function fieldsOf(preview: string): Map<string, string>;
|
|
96
|
+
/** What a call is doing, and what it is doing it to. */
|
|
97
|
+
export interface CallView {
|
|
98
|
+
/** The verb, drawn first: what kind of thing this is. */
|
|
99
|
+
verb: string;
|
|
100
|
+
/** What identifies this call: a path, a command, a query. May be empty. */
|
|
101
|
+
subject: string;
|
|
102
|
+
}
|
|
103
|
+
export declare function describeCall(name: string, preview: string): CallView;
|
|
104
|
+
export declare function summarise(name: string, preview: string): string;
|
|
105
|
+
/**
|
|
106
|
+
* One line saying where the model has got to.
|
|
107
|
+
*
|
|
108
|
+
* Reasoning summaries arrive as markdown and are structured by their own bold
|
|
109
|
+
* headings — Gemini emits one every few sentences, and they are the shortest
|
|
110
|
+
* true account of the step. The last one is where the model is now. Failing
|
|
111
|
+
* that the raw text is clipped, which is a fragment, but a moving fragment is
|
|
112
|
+
* still proof that something is happening.
|
|
113
|
+
*/
|
|
114
|
+
export declare function gistOf(text: string, width: number): string;
|
|
90
115
|
/** A run of lines from an answer, and whether it was fenced as code. */
|
|
91
116
|
export interface Segment {
|
|
92
117
|
code: boolean;
|
package/dist/tui/wrap.js
CHANGED
|
@@ -82,13 +82,21 @@ export function wrap(text, width) {
|
|
|
82
82
|
// ---------------------------------------------------------------------------
|
|
83
83
|
/** The three footer rows, its margin, the prompt, and a row in hand. */
|
|
84
84
|
export const CHROME_ROWS = 7;
|
|
85
|
-
/**
|
|
86
|
-
|
|
85
|
+
/**
|
|
86
|
+
* How much of the reasoning stream is worth showing.
|
|
87
|
+
*
|
|
88
|
+
* One row. A reasoning summary is not a document to be read while it arrives —
|
|
89
|
+
* it is the model saying what it is about to do, and the sentence that says so
|
|
90
|
+
* is the heading it writes for itself. Six rows of it pushed the answer and the
|
|
91
|
+
* work down the screen to make room for a paragraph nobody finishes reading,
|
|
92
|
+
* and the whole chain is in the trajectory either way.
|
|
93
|
+
*/
|
|
94
|
+
export const THINKING_ROWS = 1;
|
|
87
95
|
/** How much of the frame the work in flight may take. A branch is a box now,
|
|
88
96
|
* not a row, so a fan-out of two costs fourteen of these. */
|
|
89
97
|
export const ACTIVITY_ROWS = 16;
|
|
90
|
-
/** The
|
|
91
|
-
export const
|
|
98
|
+
/** The width of the elapsed-time column every call row is drawn behind. */
|
|
99
|
+
export const TIME_COL = 5;
|
|
92
100
|
/**
|
|
93
101
|
* How many rows each repainting block may draw in a terminal `rows` tall.
|
|
94
102
|
*
|
|
@@ -96,24 +104,15 @@ export const THINKING_CHROME = 2;
|
|
|
96
104
|
* arrives matters more than the machinery producing it. Reasoning yields to it
|
|
97
105
|
* in turn, because it is a progress indicator and the answer is the point.
|
|
98
106
|
*
|
|
99
|
-
* `thinking` is how many rows of reasoning are wanted
|
|
100
|
-
*
|
|
101
|
-
* settled and its tail is history rather than progress. The two rules are
|
|
102
|
-
* charged on top, so a block too cramped to be worth boxing is not drawn at all
|
|
103
|
-
* rather than drawn as a border with a line in it.
|
|
107
|
+
* `thinking` is how many rows of reasoning are wanted — one while the model is
|
|
108
|
+
* reasoning, none otherwise — and it never takes the answer's last row.
|
|
104
109
|
*/
|
|
105
110
|
export function budgetOf(rows, activity, thinking) {
|
|
106
111
|
const total = Math.max(2, rows - CHROME_ROWS);
|
|
107
112
|
const shown = Math.min(Math.max(0, activity), ACTIVITY_ROWS, Math.max(0, total - 2));
|
|
108
113
|
const rest = total - shown;
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
return {
|
|
112
|
-
activity: shown,
|
|
113
|
-
thinking: tail,
|
|
114
|
-
live: rest - tail - (tail ? THINKING_CHROME : 0),
|
|
115
|
-
total,
|
|
116
|
-
};
|
|
114
|
+
const tail = Math.min(Math.max(0, thinking), THINKING_ROWS, Math.max(0, rest - 1));
|
|
115
|
+
return { activity: shown, thinking: tail, live: rest - tail, total };
|
|
117
116
|
}
|
|
118
117
|
/** The rows a branch box may spend on its own calls and its reasoning. */
|
|
119
118
|
export const BRANCH_ROWS = 5;
|
|
@@ -138,7 +137,7 @@ export function branchRows(count, allowance) {
|
|
|
138
137
|
/**
|
|
139
138
|
* How wide the answer is drawn. A line of prose spanning a 200-column terminal
|
|
140
139
|
* is measurably harder to read than one that stops, which is why every demo in
|
|
141
|
-
* `examples/` puts its answer in a box of bounded width — the terminal is the
|
|
140
|
+
* `examples/sdk/` puts its answer in a box of bounded width — the terminal is the
|
|
142
141
|
* page, not the paragraph.
|
|
143
142
|
*/
|
|
144
143
|
export function answerWidth(columns) {
|
|
@@ -176,6 +175,180 @@ function unquote(raw) {
|
|
|
176
175
|
const body = raw.length > 1 && raw.endsWith('"') ? raw.slice(1, -1) : raw.slice(1);
|
|
177
176
|
return body.replace(/\\[nrt]/g, ' ').replace(/\\(["\\/])/g, '$1');
|
|
178
177
|
}
|
|
178
|
+
/** Every field of a preview, first mention winning. */
|
|
179
|
+
export function fieldsOf(preview) {
|
|
180
|
+
const out = new Map();
|
|
181
|
+
for (const m of preview.matchAll(FIELD)) {
|
|
182
|
+
const key = m[1];
|
|
183
|
+
if (!out.has(key)) {
|
|
184
|
+
out.set(key, unquote(m[2]));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
/** The text between `"key":[` and the first `]`, or nothing if there is no such array. */
|
|
190
|
+
function arrayBody(preview, key) {
|
|
191
|
+
const at = preview.indexOf(`"${key}"`);
|
|
192
|
+
if (at < 0) {
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
const open = preview.indexOf('[', at);
|
|
196
|
+
if (open < 0) {
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
const close = preview.indexOf(']', open);
|
|
200
|
+
return preview.slice(open + 1, close < 0 ? undefined : close);
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* An array argument, as the two things worth knowing about it: what is in it,
|
|
204
|
+
* when its items say their own names, and how many there are either way.
|
|
205
|
+
*/
|
|
206
|
+
function itemsOf(preview, key) {
|
|
207
|
+
const body = arrayBody(preview, key);
|
|
208
|
+
if (body === undefined || !body.trim()) {
|
|
209
|
+
return { names: [], total: 0 };
|
|
210
|
+
}
|
|
211
|
+
if (/^\s*\{/.test(body)) {
|
|
212
|
+
const names = [...body.matchAll(/"name"\s*:\s*"([^"]*)"/g)].map((m) => m[1]);
|
|
213
|
+
return { names, total: Math.max(names.length, (body.match(/\{/g) ?? []).length) };
|
|
214
|
+
}
|
|
215
|
+
const names = [...body.matchAll(/"([^"]*)"/g)].map((m) => m[1]).filter(Boolean);
|
|
216
|
+
return { names, total: names.length };
|
|
217
|
+
}
|
|
218
|
+
/** A few things are worth naming; more than a few are worth counting. */
|
|
219
|
+
function named(items, unit) {
|
|
220
|
+
if (!items.total) {
|
|
221
|
+
return '';
|
|
222
|
+
}
|
|
223
|
+
if (items.names.length && items.names.length <= 2) {
|
|
224
|
+
return items.names.join(', ');
|
|
225
|
+
}
|
|
226
|
+
return `${items.total} ${items.total === 1 ? unit : `${unit}s`}`;
|
|
227
|
+
}
|
|
228
|
+
const CALLS = {
|
|
229
|
+
read_file: { verb: 'read', of: ['path'] },
|
|
230
|
+
list_dir: { verb: 'list', of: ['path'] },
|
|
231
|
+
find_files: { verb: 'find', of: ['pattern'] },
|
|
232
|
+
write_file: { verb: 'write', of: ['path'] },
|
|
233
|
+
apply_patch: { verb: 'patch' },
|
|
234
|
+
move_file: { verb: 'move', pair: ['from', 'to'] },
|
|
235
|
+
delete_file: { verb: 'delete', of: ['path'] },
|
|
236
|
+
run_command: { verb: 'run', of: ['command'] },
|
|
237
|
+
run_command_background: { verb: 'start', of: ['command'] },
|
|
238
|
+
read_command_output: { verb: 'output', of: ['job_id'] },
|
|
239
|
+
stop_command: { verb: 'stop', of: ['job_id'] },
|
|
240
|
+
memory_search: { verb: 'recall', of: ['query'] },
|
|
241
|
+
memory_load: { verb: 'read memory', count: ['ids', 'node'] },
|
|
242
|
+
memory_commit: { verb: 'remember', count: ['nodes', 'node'] },
|
|
243
|
+
memory_forget: { verb: 'forget', count: ['ids', 'node'] },
|
|
244
|
+
skill_search: { verb: 'find skill', of: ['query'] },
|
|
245
|
+
skill_load: { verb: 'load skill', count: ['names', 'skill'] },
|
|
246
|
+
fork: { verb: 'fork', count: ['branches', 'branch'] },
|
|
247
|
+
final_output: { verb: 'answer' },
|
|
248
|
+
search_api: { verb: 'search api', of: ['query'] },
|
|
249
|
+
list_api: { verb: 'list api', of: ['name', 'kind'] },
|
|
250
|
+
grep_api: { verb: 'grep api', of: ['pattern'] },
|
|
251
|
+
trace_api: { verb: 'trace api', of: ['pattern'] },
|
|
252
|
+
};
|
|
253
|
+
/** The handoff tools are minted per agent, so they are matched by prefix. */
|
|
254
|
+
const TRANSFER = 'transfer_to_';
|
|
255
|
+
/** A patch names its own files, and that is the only part of it worth a row. */
|
|
256
|
+
const PATCHED = /\*\*\* (?:Add|Update|Delete) File:\s*(.*?)(?:\\[nr]|"|$)/g;
|
|
257
|
+
export function describeCall(name, preview) {
|
|
258
|
+
if (name.startsWith(TRANSFER)) {
|
|
259
|
+
return { verb: 'hand off to', subject: name.slice(TRANSFER.length) };
|
|
260
|
+
}
|
|
261
|
+
const shape = CALLS[name];
|
|
262
|
+
if (!shape) {
|
|
263
|
+
return { verb: name, subject: readable(preview) };
|
|
264
|
+
}
|
|
265
|
+
if (name === 'apply_patch') {
|
|
266
|
+
const files = [...preview.matchAll(PATCHED)].map((m) => m[1].trim());
|
|
267
|
+
return { verb: shape.verb, subject: named({ names: files, total: files.length }, 'file') };
|
|
268
|
+
}
|
|
269
|
+
if (shape.pair) {
|
|
270
|
+
const fields = fieldsOf(preview);
|
|
271
|
+
const [from, to] = shape.pair;
|
|
272
|
+
return { verb: shape.verb, subject: `${fields.get(from) ?? ''} → ${fields.get(to) ?? ''}` };
|
|
273
|
+
}
|
|
274
|
+
if (shape.count) {
|
|
275
|
+
const [key, unit] = shape.count;
|
|
276
|
+
return { verb: shape.verb, subject: named(itemsOf(preview, key), unit) };
|
|
277
|
+
}
|
|
278
|
+
const fields = fieldsOf(preview);
|
|
279
|
+
for (const key of shape.of ?? []) {
|
|
280
|
+
const value = fields.get(key);
|
|
281
|
+
if (value) {
|
|
282
|
+
return { verb: shape.verb, subject: value };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return { verb: shape.verb, subject: '' };
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* What a finished call has to say for itself, in the few words a row has left.
|
|
289
|
+
*
|
|
290
|
+
* A `[]` suffix means "count it": the interesting thing about a listing is how
|
|
291
|
+
* much came back, and printing the first two of forty entries says less than
|
|
292
|
+
* the number does.
|
|
293
|
+
*/
|
|
294
|
+
const RESULTS = {
|
|
295
|
+
read_file: ['lines'],
|
|
296
|
+
list_dir: ['entries[]'],
|
|
297
|
+
find_files: ['matches[]'],
|
|
298
|
+
write_file: ['bytes'],
|
|
299
|
+
apply_patch: ['files[]'],
|
|
300
|
+
move_file: ['to'],
|
|
301
|
+
delete_file: ['path'],
|
|
302
|
+
run_command: ['exit_code'],
|
|
303
|
+
run_command_background: ['job_id'],
|
|
304
|
+
};
|
|
305
|
+
export function summarise(name, preview) {
|
|
306
|
+
const fields = fieldsOf(preview);
|
|
307
|
+
// An error is the whole of what happened, and it is what the next turn
|
|
308
|
+
// will be about. Nothing else on the row competes with it.
|
|
309
|
+
const failed = fields.get('error');
|
|
310
|
+
if (failed) {
|
|
311
|
+
return failed;
|
|
312
|
+
}
|
|
313
|
+
const parts = [];
|
|
314
|
+
for (const key of RESULTS[name] ?? []) {
|
|
315
|
+
if (key.endsWith('[]')) {
|
|
316
|
+
const of = key.slice(0, -2);
|
|
317
|
+
const { total } = itemsOf(preview, of);
|
|
318
|
+
if (total) {
|
|
319
|
+
parts.push(`${total} ${of}`);
|
|
320
|
+
}
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const value = fields.get(key);
|
|
324
|
+
if (value !== undefined && value !== '') {
|
|
325
|
+
parts.push(`${key.replace(/_/g, ' ')} ${value}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return parts.length ? parts.join(' · ') : readable(preview);
|
|
329
|
+
}
|
|
330
|
+
// ---------------------------------------------------------------------------
|
|
331
|
+
// The gist of a reasoning stream
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
/** How much of a stream is looked at for a heading. Its end is where it got to. */
|
|
334
|
+
const GIST_TAIL = 4000;
|
|
335
|
+
/** A reasoning summary writes its own headings, in bold. */
|
|
336
|
+
const BOLD = /\*\*([^*\n]{2,}?)\*\*/g;
|
|
337
|
+
/**
|
|
338
|
+
* One line saying where the model has got to.
|
|
339
|
+
*
|
|
340
|
+
* Reasoning summaries arrive as markdown and are structured by their own bold
|
|
341
|
+
* headings — Gemini emits one every few sentences, and they are the shortest
|
|
342
|
+
* true account of the step. The last one is where the model is now. Failing
|
|
343
|
+
* that the raw text is clipped, which is a fragment, but a moving fragment is
|
|
344
|
+
* still proof that something is happening.
|
|
345
|
+
*/
|
|
346
|
+
export function gistOf(text, width) {
|
|
347
|
+
const tail = text.slice(-GIST_TAIL);
|
|
348
|
+
const heads = [...tail.matchAll(BOLD)];
|
|
349
|
+
const last = heads.at(-1)?.[1];
|
|
350
|
+
return clip(last ?? tail, width);
|
|
351
|
+
}
|
|
179
352
|
/**
|
|
180
353
|
* Splits an answer on ``` fences. Prose is left exactly as it was — the common
|
|
181
354
|
* answer has no fence in it and comes back in one piece — but a fenced block is
|
package/dist/validate.js
CHANGED
|
@@ -14,7 +14,6 @@ const CONFIG_NAMES = ['agents.yaml', 'agents.yml', 'agents/agents.yaml', 'agents
|
|
|
14
14
|
const AGENTS_DIR = 'agents';
|
|
15
15
|
const HOUSE_RULES = 'agents/instructions.md';
|
|
16
16
|
const INSTRUCTIONS_SUFFIX = '-instructions.md';
|
|
17
|
-
const LEGACY_HOUSE_RULES = 'INSTRUCTIONS.md';
|
|
18
17
|
const PROMPTS_DIR = 'agents/prompts';
|
|
19
18
|
const SKILLS_DIR = 'agents/skills';
|
|
20
19
|
const SKILL_FILE = 'SKILL.md';
|
|
@@ -22,9 +21,9 @@ const ASSETS_DIR = 'assets';
|
|
|
22
21
|
/** What `allow:` and `preload:` accept, so a skill outside it cannot be named. */
|
|
23
22
|
const REFERABLE = /^[a-z0-9]+(?:[-_][a-z0-9]+)*$/;
|
|
24
23
|
/**
|
|
25
|
-
* Mirrors `readHouseRules` in the loader, in the same order:
|
|
26
|
-
*
|
|
27
|
-
*
|
|
24
|
+
* Mirrors `readHouseRules` in the loader, in the same order:
|
|
25
|
+
* `agents/instructions.md`, then every `agents/<topic>-instructions.md` by
|
|
26
|
+
* filename.
|
|
28
27
|
*/
|
|
29
28
|
function houseRules(root) {
|
|
30
29
|
const found = [];
|
|
@@ -33,7 +32,6 @@ function houseRules(root) {
|
|
|
33
32
|
found.push(rel);
|
|
34
33
|
}
|
|
35
34
|
};
|
|
36
|
-
take(LEGACY_HOUSE_RULES);
|
|
37
35
|
take(HOUSE_RULES);
|
|
38
36
|
const dir = join(root, AGENTS_DIR);
|
|
39
37
|
if (!existsSync(dir) || !statSync(dir).isDirectory()) {
|
|
@@ -193,17 +191,6 @@ export async function validateProject(opts) {
|
|
|
193
191
|
'prompt alone, with nothing shared between them',
|
|
194
192
|
});
|
|
195
193
|
}
|
|
196
|
-
if (rules.includes(LEGACY_HOUSE_RULES)) {
|
|
197
|
-
add({
|
|
198
|
-
severity: 'warning',
|
|
199
|
-
code: 'house-rules.legacy',
|
|
200
|
-
where: LEGACY_HOUSE_RULES,
|
|
201
|
-
message: `${LEGACY_HOUSE_RULES} is where the house rules used to live. It is still ` +
|
|
202
|
-
`read, and still first, but the layout now keeps them under ${AGENTS_DIR}/ ` +
|
|
203
|
-
'so a project can have more than one of them',
|
|
204
|
-
fix: `move it to ${HOUSE_RULES}, or fold it into the ${AGENTS_DIR}/*${INSTRUCTIONS_SUFFIX} files already there`,
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
194
|
// -----------------------------------------------------------------------
|
|
208
195
|
// Agents
|
|
209
196
|
// -----------------------------------------------------------------------
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zenera/cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.19",
|
|
4
4
|
"description": "Command-line front end for @zenera/neo: agentic projects you can run, share and commit.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -51,10 +51,11 @@
|
|
|
51
51
|
"@inkjs/ui": "^2.0.0",
|
|
52
52
|
"ink": "^7.1.1",
|
|
53
53
|
"react": "^19.2.8",
|
|
54
|
-
"@zenera/neo": "^1.1.
|
|
54
|
+
"@zenera/neo": "^1.1.19",
|
|
55
55
|
"@anthropic-ai/sdk": "^0.120.0",
|
|
56
56
|
"@google/genai": "^2.18.0",
|
|
57
57
|
"@openrouter/sdk": "^1.2.80",
|
|
58
|
+
"google-auth-library": "^10.0.0",
|
|
58
59
|
"openai": "^6.0.0"
|
|
59
60
|
}
|
|
60
61
|
}
|