@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
@@ -0,0 +1,33 @@
1
+ import { type Catalog, type CatalogEntry } from '../catalog.ts';
2
+ import type { Provider } from '../keys.ts';
3
+ export interface ProviderRow {
4
+ provider: Provider;
5
+ /** where its credential came from — `environment`, `keyring` — or '' for none */
6
+ credential: string;
7
+ /** what the cached listing holds, read without asking anyone */
8
+ models: number;
9
+ origin: Catalog['origin'];
10
+ fetchedAt: string;
11
+ }
12
+ /** What asking a model one question found out. */
13
+ export interface Verdict {
14
+ state: 'live' | 'dead' | 'blocked' | 'unknown';
15
+ ms?: number;
16
+ detail?: string;
17
+ fix?: string;
18
+ dimensions?: number;
19
+ }
20
+ export interface BrowseOptions {
21
+ providers: readonly ProviderRow[];
22
+ /** opens straight into this provider's listing */
23
+ initial?: Provider;
24
+ theme?: string;
25
+ load(provider: Provider, refresh: boolean): Promise<Catalog>;
26
+ test(entry: CatalogEntry): Promise<Verdict>;
27
+ }
28
+ /**
29
+ * Runs the browser until the person picks a model or leaves. Resolves to the
30
+ * picked entry, or undefined when nothing was picked.
31
+ */
32
+ export declare function browse(options: BrowseOptions): Promise<CatalogEntry | undefined>;
33
+ //# sourceMappingURL=models.d.ts.map
@@ -0,0 +1,389 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ // ---------------------------------------------------------------------------
3
+ // zen models browse
4
+ //
5
+ // The catalog as something to walk rather than something to grep: providers,
6
+ // then what one of them serves, then one model in full — with a key to ask it
7
+ // a real question and a key to take it.
8
+ //
9
+ // Drawn on STDERR, like the banner and every other piece of narration, so the
10
+ // one thing that reaches stdout is the ref picked at the end. That is what
11
+ // makes `ref=$(zen models browse)` work the way `zen models pick` does: the
12
+ // person sees the interface, the shell sees the answer.
13
+ //
14
+ // Nothing here knows how to list or probe a model. Both arrive as callbacks
15
+ // from commands/models.ts, which already owns credentials and the catalog, so
16
+ // this file is layout and keys and nothing else.
17
+ // ---------------------------------------------------------------------------
18
+ import { Box, render, Text, useApp, useInput, useStdout } from 'ink';
19
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
20
+ import { byGroup, isPartner, matches, } from "../catalog.js";
21
+ import { ago } from "../term.js";
22
+ import { resolveTheme } from "./theme.js";
23
+ import { clip, scrollTop, wrap } from "./wrap.js";
24
+ const ROLES = ['chat', 'embedding', 'image', 'audio'];
25
+ const message = (err) => (err instanceof Error ? err.message : String(err));
26
+ const thousands = (n) => n === undefined ? '' : n >= 1000 ? `${Math.round(n / 1000)}k` : String(n);
27
+ const ms = (n) => (n < 1000 ? `${n}ms` : `${(n / 1000).toFixed(1)}s`);
28
+ function freshness(origin, fetchedAt) {
29
+ switch (origin) {
30
+ case 'curated':
31
+ return 'built-in list';
32
+ case 'stale':
33
+ return `stale, ${ago(fetchedAt)}`;
34
+ case 'live':
35
+ return 'fetched now';
36
+ default:
37
+ return `cached ${ago(fetchedAt)}`;
38
+ }
39
+ }
40
+ function said(verdict, theme) {
41
+ if (verdict === undefined) {
42
+ return undefined;
43
+ }
44
+ if (verdict === 'asking') {
45
+ return { text: '… asking', color: theme.warn };
46
+ }
47
+ switch (verdict.state) {
48
+ case 'live':
49
+ return {
50
+ text: `✓ answers ${verdict.ms === undefined ? '' : ms(verdict.ms)}`.trim(),
51
+ color: theme.code.color,
52
+ };
53
+ case 'blocked':
54
+ return { text: '! blocked', color: theme.warn };
55
+ case 'dead':
56
+ return { text: '✗ refused', color: theme.line.error.color };
57
+ default:
58
+ return { text: '? no answer', color: theme.chrome.color };
59
+ }
60
+ }
61
+ /** One row, never wrapped: a wrapped row is a row the frame did not budget for. */
62
+ function Row({ children, ...style }) {
63
+ return (_jsx(Text, { wrap: "truncate-end", ...style, children: children }));
64
+ }
65
+ function Keys({ theme, text }) {
66
+ return _jsx(Row, { color: theme.chrome.color, children: text });
67
+ }
68
+ function Browser({ options, theme, onPick }) {
69
+ const { exit } = useApp();
70
+ const { stdout } = useStdout();
71
+ const [size, setSize] = useState({ columns: stdout.columns || 80, rows: stdout.rows || 24 });
72
+ useEffect(() => {
73
+ const resized = () => setSize({ columns: stdout.columns || 80, rows: stdout.rows || 24 });
74
+ stdout.on('resize', resized);
75
+ return () => {
76
+ stdout.off('resize', resized);
77
+ };
78
+ }, [stdout]);
79
+ const start = options.providers.findIndex((p) => p.provider === options.initial);
80
+ const [screen, setScreen] = useState(start >= 0 ? 'models' : 'providers');
81
+ const [at, setAt] = useState(Math.max(0, start));
82
+ const [catalogs, setCatalogs] = useState(new Map());
83
+ const [loading, setLoading] = useState();
84
+ const [failure, setFailure] = useState();
85
+ const [query, setQuery] = useState('');
86
+ const [typing, setTyping] = useState(false);
87
+ const [role, setRole] = useState(0);
88
+ const [selected, setSelected] = useState(0);
89
+ const [verdicts, setVerdicts] = useState(new Map());
90
+ const [closing, setClosing] = useState(false);
91
+ const top = useRef(0);
92
+ const provider = options.providers[at].provider;
93
+ const catalog = catalogs.get(provider);
94
+ const load = (p, refresh) => {
95
+ setLoading(p);
96
+ setFailure(undefined);
97
+ options.load(p, refresh).then((cat) => {
98
+ setCatalogs((prev) => new Map(prev).set(p, cat));
99
+ setLoading((now) => (now === p ? undefined : now));
100
+ }, (err) => {
101
+ setFailure(message(err));
102
+ setLoading((now) => (now === p ? undefined : now));
103
+ });
104
+ };
105
+ useEffect(() => {
106
+ if (start >= 0) {
107
+ load(provider, false);
108
+ }
109
+ // Once, for `zen models browse <provider>`; every later open is a key.
110
+ }, []);
111
+ useEffect(() => {
112
+ if (closing) {
113
+ exit();
114
+ }
115
+ }, [closing, exit]);
116
+ // Only the roles this listing actually has: cycling through `audio` on a
117
+ // provider with no audio models is a keypress spent on an empty screen.
118
+ const present = useMemo(() => [
119
+ undefined,
120
+ ...ROLES.filter((r) => catalog?.entries.some((e) => e.roles.includes(r))),
121
+ ], [catalog]);
122
+ const only = present[role % present.length];
123
+ const rows = useMemo(() => (catalog?.entries ?? [])
124
+ .filter((e) => matches(e, query, { roles: only ? [only] : [] }))
125
+ .sort(byGroup), [catalog, query, only]);
126
+ const index = Math.min(selected, Math.max(0, rows.length - 1));
127
+ const current = rows[index];
128
+ const open = (next) => {
129
+ const p = options.providers[next].provider;
130
+ setAt(next);
131
+ setScreen('models');
132
+ setQuery('');
133
+ setTyping(false);
134
+ setRole(0);
135
+ setSelected(0);
136
+ top.current = 0;
137
+ if (!catalogs.has(p)) {
138
+ load(p, false);
139
+ }
140
+ };
141
+ const ask = (entry) => {
142
+ if (verdicts.get(entry.ref) === 'asking') {
143
+ return;
144
+ }
145
+ setVerdicts((prev) => new Map(prev).set(entry.ref, 'asking'));
146
+ options.test(entry).then((v) => setVerdicts((prev) => new Map(prev).set(entry.ref, v)), (err) => setVerdicts((prev) => new Map(prev).set(entry.ref, { state: 'unknown', detail: message(err) })));
147
+ };
148
+ const pick = (entry) => {
149
+ onPick(entry);
150
+ setClosing(true);
151
+ };
152
+ // Everything but the list is a fixed number of rows, and the list gets
153
+ // what is left. A row is kept in hand: a frame as tall as the viewport is
154
+ // the one Ink cannot erase (see tui/app.tsx).
155
+ const problem = catalog?.problem;
156
+ const chrome = 6 + (problem ? 1 : 0);
157
+ const height = Math.max(3, size.rows - 1 - chrome);
158
+ top.current = scrollTop(index, top.current, height, rows.length);
159
+ useInput((input, key) => {
160
+ if (closing) {
161
+ return;
162
+ }
163
+ const quit = () => setClosing(true);
164
+ if (screen === 'providers') {
165
+ const n = options.providers.length;
166
+ if (key.upArrow || input === 'k') {
167
+ setAt((i) => (i + n - 1) % n);
168
+ }
169
+ else if (key.downArrow || input === 'j') {
170
+ setAt((i) => (i + 1) % n);
171
+ }
172
+ else if (key.return || key.rightArrow || input === 'l') {
173
+ open(at);
174
+ }
175
+ else if (key.escape || input === 'q') {
176
+ quit();
177
+ }
178
+ return;
179
+ }
180
+ if (screen === 'detail') {
181
+ if (!current) {
182
+ setScreen('models');
183
+ }
184
+ else if (input === 't') {
185
+ ask(current);
186
+ }
187
+ else if (key.return || input === 'p') {
188
+ pick(current);
189
+ }
190
+ else if (key.escape || key.leftArrow || input === 'h') {
191
+ setScreen('models');
192
+ }
193
+ else if (input === 'q') {
194
+ quit();
195
+ }
196
+ return;
197
+ }
198
+ const move = (by) => setSelected(Math.max(0, Math.min(rows.length - 1, index + by)));
199
+ if (key.upArrow) {
200
+ return move(-1);
201
+ }
202
+ if (key.downArrow) {
203
+ return move(1);
204
+ }
205
+ if (key.pageUp) {
206
+ return move(-height);
207
+ }
208
+ if (key.pageDown) {
209
+ return move(height);
210
+ }
211
+ if (typing) {
212
+ if (key.return) {
213
+ setTyping(false);
214
+ }
215
+ else if (key.escape) {
216
+ setQuery('');
217
+ setTyping(false);
218
+ }
219
+ else if (key.backspace || key.delete) {
220
+ setQuery((q) => q.slice(0, -1));
221
+ setSelected(0);
222
+ }
223
+ else if (input && !key.ctrl && !key.meta && !key.tab) {
224
+ // Fast typing or a paste can arrive as one chunk with the Enter
225
+ // inside it: keep the text, honour the Enter, drop the rest.
226
+ const [text = '', ...after] = input.split(/[\r\n]/);
227
+ setQuery((q) => q + text.replace(/[\u0000-\u001f\u007f]/g, ''));
228
+ setSelected(0);
229
+ if (after.length > 0) {
230
+ setTyping(false);
231
+ }
232
+ }
233
+ return;
234
+ }
235
+ if (input === 'k') {
236
+ move(-1);
237
+ }
238
+ else if (input === 'j') {
239
+ move(1);
240
+ }
241
+ else if (key.home || input === 'g') {
242
+ setSelected(0);
243
+ }
244
+ else if (key.end || input === 'G') {
245
+ setSelected(Math.max(0, rows.length - 1));
246
+ }
247
+ else if (input.startsWith('/')) {
248
+ // A pasted `/gpt-5` arrives as one chunk, not as a slash and then
249
+ // the rest.
250
+ setTyping(true);
251
+ setQuery(input.slice(1));
252
+ setSelected(0);
253
+ }
254
+ else if (key.tab) {
255
+ setRole((r) => (r + 1) % present.length);
256
+ setSelected(0);
257
+ }
258
+ else if ((key.return || key.rightArrow || input === 'l') && current) {
259
+ setScreen('detail');
260
+ }
261
+ else if (input === 't' && current) {
262
+ ask(current);
263
+ }
264
+ else if (input === 'p' && current) {
265
+ pick(current);
266
+ }
267
+ else if (input === 'r') {
268
+ load(provider, true);
269
+ }
270
+ else if (key.escape && query) {
271
+ setQuery('');
272
+ setSelected(0);
273
+ }
274
+ else if (key.escape || key.leftArrow || input === 'h') {
275
+ setScreen('providers');
276
+ }
277
+ else if (input === 'q') {
278
+ quit();
279
+ }
280
+ });
281
+ if (closing) {
282
+ // An empty frame erases the last one, so the shell is left as it was
283
+ // found rather than with a stale picture of a list on it.
284
+ return null;
285
+ }
286
+ const width = Math.max(20, size.columns - 1);
287
+ if (screen === 'providers') {
288
+ const nameW = Math.max(...options.providers.map((p) => p.provider.length)) + 2;
289
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Row, { bold: true, color: theme.accent, children: "Providers" }), _jsx(Text, { children: " " }), options.providers.map((p, i) => {
290
+ const here = i === at;
291
+ const cat = catalogs.get(p.provider);
292
+ const count = cat?.entries.length ?? p.models;
293
+ const fresh = cat
294
+ ? freshness(cat.origin, cat.fetchedAt)
295
+ : freshness(p.origin, p.fetchedAt);
296
+ return (_jsxs(Row, { children: [_jsxs(Text, { color: here ? theme.accent : undefined, bold: here, children: [here ? '› ' : ' ', p.provider.padEnd(nameW)] }), p.credential ? (_jsx(Text, { color: theme.code.color, children: p.credential.padEnd(15) })) : (_jsx(Text, { color: theme.line.error.color, children: 'no credential'.padEnd(15) })), _jsxs(Text, { color: theme.chrome.color, children: [`${count} model${count === 1 ? '' : 's'}`.padEnd(13), p.credential ? fresh : `zen key add ${p.provider}`] })] }, p.provider));
297
+ }), _jsx(Text, { children: " " }), _jsx(Keys, { theme: theme, text: "\u2191\u2193 move \u00B7 enter open \u00B7 q quit" })] }));
298
+ }
299
+ if (screen === 'detail' && current) {
300
+ return (_jsx(Detail, { entry: current, catalog: catalog, verdict: verdicts.get(current.ref), theme: theme, width: width, rows: size.rows - 1 }));
301
+ }
302
+ const header = catalog
303
+ ? `${provider} · ${catalog.entries.length} models · ${freshness(catalog.origin, catalog.fetchedAt)}`
304
+ : provider;
305
+ const idW = Math.min(44, Math.max(8, ...rows.map((e) => e.id.length))) + 2;
306
+ const shown = rows.slice(top.current, top.current + height);
307
+ const filter = typing ? `/${query}▌` : query ? `/${query}` : '';
308
+ let body;
309
+ if (failure) {
310
+ body = _jsx(Row, { color: theme.line.error.color, children: failure });
311
+ }
312
+ else if (loading === provider || !catalog) {
313
+ body = _jsxs(Row, { color: theme.warn, children: ["listing ", provider, " \u2026"] });
314
+ }
315
+ else if (rows.length === 0) {
316
+ body = (_jsx(Row, { color: theme.chrome.color, children: query ? `nothing matches "${query}"` : 'nothing listed' }));
317
+ }
318
+ else {
319
+ body = (_jsx(_Fragment, { children: shown.map((e, i) => {
320
+ const here = top.current + i === index;
321
+ const v = said(verdicts.get(e.ref), theme);
322
+ return (_jsxs(Row, { children: [_jsxs(Text, { color: here ? theme.accent : undefined, bold: here, children: [here ? '› ' : ' ', clip(e.id, idW - 2).padEnd(idW)] }), e.publisher ? (_jsx(Text, { color: isPartner(e) ? theme.warn : theme.code.color, children: clip(e.publisher, 11).padEnd(12) })) : null, _jsxs(Text, { color: theme.chrome.color, children: [e.roles.join('+').padEnd(16), thousands(e.contextLength).padStart(6), (e.pricing?.free ? ' free' : '').padEnd(7)] }), v ? _jsx(Text, { color: v.color, children: v.text }) : null] }, e.ref));
323
+ }) }));
324
+ }
325
+ const preview = current ? [current.name, current.description].filter(Boolean).join(' — ') : '';
326
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Row, { children: [_jsx(Text, { bold: true, color: theme.accent, children: header }), _jsxs(Text, { color: theme.chrome.color, children: [' role: ', only ?? 'all', filter ? ' ' : ''] }), _jsx(Text, { color: theme.code.color, children: filter })] }), problem ? (_jsxs(Row, { color: theme.warn, children: ["could not be listed: ", problem.detail ?? 'no reason given'] })) : null, _jsx(Text, { children: " " }), _jsx(Box, { flexDirection: "column", height: height, children: body }), _jsx(Row, { color: theme.chrome.color, children: clip(preview, width) || ' ' }), _jsx(Row, { color: theme.chrome.color, children: rows.length === 0
327
+ ? ' '
328
+ : `${index + 1} of ${rows.length}${rows.length < (catalog?.entries.length ?? 0) ? ` (${catalog.entries.length} in all)` : ''}` }), _jsx(Keys, { theme: theme, text: typing
329
+ ? 'type to filter · enter keep · esc clear · ↑↓ move'
330
+ : '↑↓ · / filter · tab role · ⏎ more · t test · p pick · r reload · esc back' })] }));
331
+ }
332
+ function Detail({ entry, catalog, verdict, theme, width, rows }) {
333
+ const facts = [];
334
+ const add = (label, value) => {
335
+ if (value) {
336
+ facts.push([label, value]);
337
+ }
338
+ };
339
+ add('name', entry.name);
340
+ add('roles', entry.roles.join(', '));
341
+ add('context', entry.contextLength ? `${entry.contextLength.toLocaleString()} tokens` : undefined);
342
+ add('max output', entry.maxOutputTokens ? `${entry.maxOutputTokens.toLocaleString()} tokens` : undefined);
343
+ add('dimensions', entry.dimensions ? String(entry.dimensions) : undefined);
344
+ add('input', entry.modalities?.input?.join(', '));
345
+ add('output', entry.modalities?.output?.join(', '));
346
+ add('supports', Object.entries(entry.supports ?? {})
347
+ .filter(([, on]) => on)
348
+ .map(([k]) => k)
349
+ .join(', '));
350
+ add('pricing', entry.pricing?.free
351
+ ? 'free'
352
+ : entry.pricing?.prompt
353
+ ? `$${entry.pricing.prompt}/token in, $${entry.pricing.completion ?? '?'}/token out`
354
+ : undefined);
355
+ add('released', entry.created);
356
+ add('source', entry.source === 'live' && catalog
357
+ ? `${entry.provider}, ${freshness(catalog.origin, catalog.fetchedAt)}`
358
+ : 'built-in list');
359
+ const v = said(verdict, theme);
360
+ const reason = verdict && verdict !== 'asking'
361
+ ? [
362
+ verdict.dimensions ? `${verdict.dimensions} dims` : '',
363
+ verdict.state === 'live' ? '' : (verdict.detail ?? ''),
364
+ verdict.fix ? `fix: ${verdict.fix}` : '',
365
+ ]
366
+ .filter(Boolean)
367
+ .join(' · ')
368
+ : '';
369
+ // Header, blank, facts, the verdict row, blank, keys — the description has
370
+ // whatever is left, and is cut rather than allowed to push the keys off.
371
+ const room = Math.max(0, rows - facts.length - 6);
372
+ const about = entry.description ? wrap(entry.description, width - 2).slice(0, room) : [];
373
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Row, { bold: true, color: theme.accent, children: entry.ref }), _jsx(Text, { children: " " }), facts.map(([label, value]) => (_jsxs(Row, { children: [_jsx(Text, { color: theme.chrome.color, children: ` ${label.padEnd(12)}` }), _jsx(Text, { children: value })] }, label))), _jsxs(Row, { children: [_jsxs(Text, { color: theme.chrome.color, children: [' ', 'asked'.padEnd(12)] }), v ? (_jsx(Text, { color: v.color, children: v.text })) : (_jsx(Text, { color: theme.chrome.color, children: "not yet \u2014 t to ask it one question" })), reason ? _jsx(Text, { color: v?.color, children: ` ${reason}` }) : null] }), about.length > 0 ? _jsx(Text, { children: " " }) : null, about.map((line, i) => (_jsx(Row, { color: theme.chrome.color, children: ` ${line}` }, i))), _jsx(Text, { children: " " }), _jsx(Keys, { theme: theme, text: "t test \u00B7 p pick (prints the ref) \u00B7 esc back \u00B7 q quit" })] }));
374
+ }
375
+ /**
376
+ * Runs the browser until the person picks a model or leaves. Resolves to the
377
+ * picked entry, or undefined when nothing was picked.
378
+ */
379
+ export async function browse(options) {
380
+ // Asked before Ink takes the terminal, as tui/app.tsx does.
381
+ const theme = await resolveTheme(options.theme);
382
+ let chosen;
383
+ const instance = render(_jsx(Browser, { options: options, theme: theme, onPick: (entry) => {
384
+ chosen = entry;
385
+ } }), { stdout: process.stderr });
386
+ await instance.waitUntilExit();
387
+ return chosen;
388
+ }
389
+ //# sourceMappingURL=models.js.map
@@ -15,6 +15,11 @@ export declare function windowOf(text: string, width: number, rows: number): str
15
15
  export declare function clip(text: string, width: number): string;
16
16
  /** Word wrap. Every returned row is at most `width` columns wide. */
17
17
  export declare function wrap(text: string, width: number): string[];
18
+ /**
19
+ * The first row of a scrolled list, moved as little as possible so that row
20
+ * `selected` is inside a window of `height` rows over `count` of them.
21
+ */
22
+ export declare function scrollTop(selected: number, top: number, height: number, count: number): number;
18
23
  /** The three footer rows, its margin, the prompt, and a row in hand. */
19
24
  export declare const CHROME_ROWS = 7;
20
25
  /**
package/dist/tui/wrap.js CHANGED
@@ -74,6 +74,18 @@ export function wrap(text, width) {
74
74
  }
75
75
  return out;
76
76
  }
77
+ /**
78
+ * The first row of a scrolled list, moved as little as possible so that row
79
+ * `selected` is inside a window of `height` rows over `count` of them.
80
+ */
81
+ export function scrollTop(selected, top, height, count) {
82
+ const h = Math.max(1, height);
83
+ let first = Math.min(top, selected);
84
+ if (selected >= first + h) {
85
+ first = selected - h + 1;
86
+ }
87
+ return Math.max(0, Math.min(first, count - h));
88
+ }
77
89
  // ---------------------------------------------------------------------------
78
90
  // Dividing the frame
79
91
  //
@@ -0,0 +1,78 @@
1
+ import type { ModelUsage, TokenUsage } from '@zenera/neo';
2
+ /** Set by `zen meta` for everything its agent runs, so all of it lands in one ledger. */
3
+ export declare const LEDGER_ENV = "ZENERA_USAGE_LEDGER";
4
+ /** The meta session whose agent started this process, if one did. */
5
+ export declare const META_SESSION_ENV = "ZENERA_META_SESSION";
6
+ /** The stored prompt that meta session was started with, if any. */
7
+ export declare const META_PROMPT_ENV = "ZENERA_META_PROMPT";
8
+ /**
9
+ * Where this project's usage goes: the variable when set, else the fine-tuning's
10
+ * own ledger when the project is being tuned. Elsewhere nothing is recorded.
11
+ */
12
+ export declare function ledgerPath(projectDir: string): string | undefined;
13
+ interface RowBase {
14
+ /** when the thing ended */
15
+ ts: string;
16
+ }
17
+ export interface MetaCallRow extends RowBase {
18
+ kind: 'meta.call';
19
+ session?: string;
20
+ model: string;
21
+ usage: TokenUsage;
22
+ cacheWriteTokens?: number;
23
+ startedAt: string;
24
+ durationMs: number;
25
+ traceId?: string;
26
+ spanId?: string;
27
+ }
28
+ export interface MetaSessionRow extends RowBase {
29
+ kind: 'meta.session';
30
+ session: string;
31
+ /** copilot's own totals for the whole session so far - cumulative across resumes */
32
+ models: ModelUsage[];
33
+ exitCode: number;
34
+ resumes: number;
35
+ startedAt: string;
36
+ durationMs: number;
37
+ }
38
+ export interface RunRow extends RowBase {
39
+ kind: 'run';
40
+ runDir?: string;
41
+ session?: string;
42
+ batchDir?: string;
43
+ item?: string;
44
+ ok: boolean;
45
+ error?: string;
46
+ stopReason?: string;
47
+ durationMs?: number;
48
+ models: ModelUsage[];
49
+ }
50
+ export interface BatchRow extends RowBase {
51
+ kind: 'batch';
52
+ batchDir: string;
53
+ input: string;
54
+ items: number;
55
+ ok: number;
56
+ failed: number;
57
+ concurrency: number;
58
+ memory: string;
59
+ startedAt: string;
60
+ durationMs: number;
61
+ models: ModelUsage[];
62
+ }
63
+ export interface AskRow extends RowBase {
64
+ kind: 'ask';
65
+ runDir: string;
66
+ node: string;
67
+ nodeAgent: string;
68
+ nodeModel: string;
69
+ model: string;
70
+ usage?: TokenUsage;
71
+ startedAt: string;
72
+ durationMs: number;
73
+ }
74
+ export type UsageRow = MetaCallRow | MetaSessionRow | RunRow | BatchRow | AskRow;
75
+ /** Appends one row. Never throws: losing a line of accounting must not fail a run. */
76
+ export declare function appendUsage(projectDir: string, row: UsageRow): void;
77
+ export {};
78
+ //# sourceMappingURL=usage.d.ts.map
package/dist/usage.js ADDED
@@ -0,0 +1,51 @@
1
+ import { appendFileSync, existsSync, mkdirSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { warn } from "./term.js";
4
+ // ---------------------------------------------------------------------------
5
+ // The usage ledger
6
+ //
7
+ // One JSON line per thing that spent tokens: a meta agent call, a run, a batch,
8
+ // an `inspect ask`. Append-only, so several processes - the meta agent, the
9
+ // batch it started, the ask it is grading with - can write to one file at once.
10
+ // Reports are folded out of it later; nothing here aggregates.
11
+ // ---------------------------------------------------------------------------
12
+ /** Set by `zen meta` for everything its agent runs, so all of it lands in one ledger. */
13
+ export const LEDGER_ENV = 'ZENERA_USAGE_LEDGER';
14
+ /** The meta session whose agent started this process, if one did. */
15
+ export const META_SESSION_ENV = 'ZENERA_META_SESSION';
16
+ /** The stored prompt that meta session was started with, if any. */
17
+ export const META_PROMPT_ENV = 'ZENERA_META_PROMPT';
18
+ /**
19
+ * Where this project's usage goes: the variable when set, else the fine-tuning's
20
+ * own ledger when the project is being tuned. Elsewhere nothing is recorded.
21
+ */
22
+ export function ledgerPath(projectDir) {
23
+ const set = process.env[LEDGER_ENV]?.trim();
24
+ if (set) {
25
+ return set;
26
+ }
27
+ return existsSync(join(projectDir, 'finetune'))
28
+ ? join(projectDir, 'finetune', 'usage', 'ledger.jsonl')
29
+ : undefined;
30
+ }
31
+ let warned = false;
32
+ /** Appends one row. Never throws: losing a line of accounting must not fail a run. */
33
+ export function appendUsage(projectDir, row) {
34
+ const path = ledgerPath(projectDir);
35
+ if (!path) {
36
+ return;
37
+ }
38
+ const meta = process.env[META_SESSION_ENV]?.trim();
39
+ const line = { v: 1, ...row, project: projectDir, pid: process.pid, ...(meta ? { meta } : {}) };
40
+ try {
41
+ mkdirSync(dirname(path), { recursive: true });
42
+ appendFileSync(path, `${JSON.stringify(line)}\n`);
43
+ }
44
+ catch (err) {
45
+ if (!warned) {
46
+ warned = true;
47
+ warn(`could not record usage in ${path} - ${err.message}`);
48
+ }
49
+ }
50
+ }
51
+ //# sourceMappingURL=usage.js.map
package/dist/validate.js CHANGED
@@ -1,4 +1,4 @@
1
- import { EXA_GROUP, FileSkillProvider, SANDBOX_MOUNT, Sandbox, exaTools, frontmatter, projectRegistry, readProjectConfig, sandboxTools, selectTools, toList, workspaceTools, } from '@zenera/neo';
1
+ import { EXA_GROUP, FileSkillProvider, SANDBOX_MOUNT, Sandbox, exaTools, fileTools, frontmatter, projectRegistry, readProjectConfig, sandboxTools, selectTools, toList, } from '@zenera/neo';
2
2
  import { existsSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path';
@@ -35,10 +35,17 @@ const FILE_TOOLS = [
35
35
  'find_files',
36
36
  'write_file',
37
37
  'apply_patch',
38
+ 'copy_file',
38
39
  'move_file',
39
40
  'delete_file',
40
41
  ];
41
- const FILE_WRITE_TOOLS = new Set(['write_file', 'apply_patch', 'move_file', 'delete_file']);
42
+ const FILE_WRITE_TOOLS = new Set([
43
+ 'write_file',
44
+ 'apply_patch',
45
+ 'copy_file',
46
+ 'move_file',
47
+ 'delete_file',
48
+ ]);
42
49
  function activeFileTools(selectors) {
43
50
  const active = new Set();
44
51
  if (!selectors?.length) {
@@ -607,7 +614,7 @@ export function availableTools(root, config) {
607
614
  // Constructed, not started: a pool creates its container on the first
608
615
  // command, so naming one here costs nothing and needs no container engine.
609
616
  return [
610
- ...workspaceTools({
617
+ ...fileTools({
611
618
  root,
612
619
  mount: config.sandbox?.workdir ?? SANDBOX_MOUNT,
613
620
  }),
@@ -1543,6 +1550,21 @@ function checkModels(root, config, keys, add, probe = false) {
1543
1550
  if (resolved !== name) {
1544
1551
  report.ref = resolved;
1545
1552
  }
1553
+ // Reported here rather than left to the probe below: a knob for the
1554
+ // wrong vendor is a config mistake that wants naming whether or not
1555
+ // there is a credential to build the model with.
1556
+ if (role === 'model') {
1557
+ for (const stray of registry.knobs(ref)) {
1558
+ add({
1559
+ severity: 'error',
1560
+ code: 'model.knob',
1561
+ where: whereFor(config, role, name, usedBy),
1562
+ message: `\`${stray.knob}\` means nothing to a ${stray.target} model — ` +
1563
+ 'it would never reach the request',
1564
+ fix: `this model takes: ${stray.supported.join(', ')}`,
1565
+ });
1566
+ }
1567
+ }
1546
1568
  }
1547
1569
  catch (err) {
1548
1570
  add({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenera/cli",
3
- "version": "1.1.23",
3
+ "version": "1.1.25",
4
4
  "description": "Command-line front end for @zenera/neo: agentic projects you can run, share and commit.",
5
5
  "keywords": [
6
6
  "agents",
@@ -52,11 +52,12 @@
52
52
  "@google/genai": "^2.18.0",
53
53
  "@inkjs/ui": "^2.0.0",
54
54
  "@openrouter/sdk": "^1.2.80",
55
- "@zenera/neo": "^1.1.23",
55
+ "@zenera/neo": "^1.1.25",
56
56
  "google-auth-library": "^10.0.0",
57
57
  "ink": "^7.1.1",
58
58
  "openai": "^6.0.0",
59
59
  "react": "^19.2.8",
60
+ "yaml": "^2.9.0",
60
61
  "yauzl": "^3.4.0",
61
62
  "yazl": "^3.3.1"
62
63
  },