@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,225 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ export const STAGES = ['run', 'analyze', 'apply'];
4
+ const zero = () => ({ calls: 0, input: 0, cached: 0, output: 0, reasoning: 0 });
5
+ function addTo(into, model, t) {
6
+ const m = (into[model] ??= zero());
7
+ m.calls += t.calls;
8
+ m.input += t.input;
9
+ m.cached += t.cached;
10
+ m.output += t.output;
11
+ m.reasoning += t.reasoning;
12
+ }
13
+ export function mergeModels(...all) {
14
+ const out = {};
15
+ for (const by of all) {
16
+ for (const [model, t] of Object.entries(by ?? {})) {
17
+ addTo(out, model, t);
18
+ }
19
+ }
20
+ return out;
21
+ }
22
+ export function mergeStages(...all) {
23
+ const out = {};
24
+ for (const stage of STAGES) {
25
+ const parts = all.map((s) => s?.[stage]).filter((p) => !!p);
26
+ if (parts.length > 0) {
27
+ out[stage] = mergeModels(...parts);
28
+ }
29
+ }
30
+ return out;
31
+ }
32
+ export function total(by) {
33
+ const sum = {};
34
+ for (const t of Object.values(by ?? {})) {
35
+ addTo(sum, 'all', t);
36
+ }
37
+ return sum.all ?? zero();
38
+ }
39
+ /** Input plus output, the input counting cached tokens. */
40
+ export function spent(by) {
41
+ const s = total(by);
42
+ return s.input + s.output;
43
+ }
44
+ const fromUsage = (u, calls) => ({
45
+ calls,
46
+ input: u.inputTokens ?? 0,
47
+ cached: u.cachedInputTokens ?? 0,
48
+ output: u.outputTokens ?? 0,
49
+ reasoning: u.reasoningTokens ?? 0,
50
+ });
51
+ /** `vertex/gemini-3.8-flash` and `gemini-3.8-flash` are one model in a table. */
52
+ export const modelName = (m) => m ? m.replace(/^[^/:]+[/:]/, '') : '?';
53
+ /** `SandboxError`s the engine causes; a path outside the workspace is the model's own doing. */
54
+ const SANDBOX_DOWN = /could not (create|start) container|the sandbox container \S+ stopped|could not (write|start) the job|is not installed, or not on PATH|exceeded num_locks|cannot connect to podman/i;
55
+ const cache = new Map();
56
+ /**
57
+ * One walk of a run's `state.json`, branches included. Nested records repeat a
58
+ * node, so nodes are counted by id. A node is stamped when the work behind it
59
+ * finished, so the gap to the node before it in the same lane is that work -
60
+ * the same timing as the Stats tab of report.html.
61
+ */
62
+ export function runMetrics(runDir) {
63
+ const file = join(runDir, 'state.json');
64
+ if (!existsSync(file)) {
65
+ return undefined;
66
+ }
67
+ const st = statSync(file);
68
+ const key = `${st.mtimeMs}:${st.size}`;
69
+ const hit = cache.get(file);
70
+ if (hit?.key === key) {
71
+ return hit.metrics;
72
+ }
73
+ let root;
74
+ try {
75
+ root = JSON.parse(readFileSync(file, 'utf8'));
76
+ }
77
+ catch {
78
+ return undefined;
79
+ }
80
+ const m = {
81
+ models: {},
82
+ llmMs: 0,
83
+ toolMs: 0,
84
+ toolCalls: 0,
85
+ toolErrors: 0,
86
+ memory: { recall: 0, load: 0, grep: 0, commit: 0, forget: 0 },
87
+ forks: 0,
88
+ branches: 0,
89
+ compactions: 0,
90
+ handoffs: 0,
91
+ };
92
+ const seen = new Set();
93
+ const count = (o, ms) => {
94
+ switch (o.type) {
95
+ case 'llm_call':
96
+ case 'compaction': {
97
+ if (!o.usage) {
98
+ return;
99
+ }
100
+ const model = o.type === 'compaction' ? 'summarizer' : modelName(o.model);
101
+ const t = fromUsage(o.usage, 1);
102
+ const into = (m.models[model] ??= { ...zero(), peak: 0, ms: 0 });
103
+ into.calls += 1;
104
+ into.input += t.input;
105
+ into.cached += t.cached;
106
+ into.output += t.output;
107
+ into.reasoning += t.reasoning;
108
+ into.peak = Math.max(into.peak, t.input);
109
+ into.ms += ms;
110
+ m.llmMs += ms;
111
+ if (o.type === 'compaction') {
112
+ m.compactions++;
113
+ }
114
+ return;
115
+ }
116
+ case 'tool_result': {
117
+ m.toolCalls++;
118
+ m.toolErrors += o.isError ? 1 : 0;
119
+ m.toolMs += typeof o.durationMs === 'number' ? o.durationMs : ms;
120
+ const said = o.result?.preview;
121
+ if (!m.sandbox && typeof said === 'string' && SANDBOX_DOWN.test(said)) {
122
+ m.sandbox = /"error":\s*"([^"]+)/.exec(said)?.[1] ?? said;
123
+ }
124
+ return;
125
+ }
126
+ case 'memory_recall':
127
+ m.memory.recall++;
128
+ return;
129
+ case 'memory_op': {
130
+ const op = o.op;
131
+ if (op in m.memory) {
132
+ m.memory[op]++;
133
+ }
134
+ return;
135
+ }
136
+ case 'fork':
137
+ m.forks++;
138
+ m.branches += Array.isArray(o.branches) ? o.branches.length : 0;
139
+ return;
140
+ case 'handoff':
141
+ m.handoffs++;
142
+ return;
143
+ }
144
+ };
145
+ // A branch lane starts where its parent lane was when the fork began.
146
+ const walk = (v, lanePrev) => {
147
+ if (Array.isArray(v)) {
148
+ let prev = lanePrev;
149
+ for (const item of v) {
150
+ const o = item;
151
+ const ts = o && typeof o.ts === 'string' ? Date.parse(o.ts) : NaN;
152
+ if (o &&
153
+ typeof o.type === 'string' &&
154
+ typeof o.id === 'string' &&
155
+ !seen.has(o.id)) {
156
+ seen.add(o.id);
157
+ count(o, prev !== undefined && !Number.isNaN(ts) ? Math.max(0, ts - prev) : 0);
158
+ }
159
+ walk(item, prev);
160
+ if (!Number.isNaN(ts)) {
161
+ prev = ts;
162
+ }
163
+ }
164
+ }
165
+ else if (v && typeof v === 'object') {
166
+ for (const x of Object.values(v)) {
167
+ walk(x, lanePrev);
168
+ }
169
+ }
170
+ };
171
+ walk(root, undefined);
172
+ cache.set(file, { key, metrics: m });
173
+ return m;
174
+ }
175
+ /** The token part of a run's metrics, per model. */
176
+ export function tokensOf(m) {
177
+ return Object.fromEntries(Object.entries(m.models).map(([model, { calls, input, cached, output, reasoning }]) => [
178
+ model,
179
+ { calls, input, cached, output, reasoning },
180
+ ]));
181
+ }
182
+ export function runTokens(runDir) {
183
+ const m = runMetrics(runDir);
184
+ return m ? tokensOf(m) : undefined;
185
+ }
186
+ /** One `zen meta run --json` envelope: `{ model, tokens: { calls, inputTokens, … } }`. */
187
+ export function envelopeTokens(file) {
188
+ if (!existsSync(file)) {
189
+ return undefined;
190
+ }
191
+ let env;
192
+ try {
193
+ env = JSON.parse(readFileSync(file, 'utf8'));
194
+ }
195
+ catch {
196
+ return undefined;
197
+ }
198
+ if (!env.tokens) {
199
+ return undefined;
200
+ }
201
+ return { [modelName(env.model)]: fromUsage(env.tokens, env.tokens.calls ?? 0) };
202
+ }
203
+ /** Every envelope in `dir` named `<stem>.json` or `<stem>-<n>.json` - retries keep their own. */
204
+ export function envelopesIn(dir, stem) {
205
+ let names = [];
206
+ try {
207
+ names = readdirSync(dir).filter((n) => new RegExp(`^${stem}(-\\d+)?\\.json$`).test(n));
208
+ }
209
+ catch {
210
+ return undefined;
211
+ }
212
+ const parts = names.map((n) => envelopeTokens(join(dir, n))).filter((p) => !!p);
213
+ return parts.length > 0 ? mergeModels(...parts) : undefined;
214
+ }
215
+ /** 1234 → 1.2k, 2184341 → 2.2M. */
216
+ export function short(n) {
217
+ if (n >= 1_000_000) {
218
+ return `${(n / 1_000_000).toFixed(1)}M`;
219
+ }
220
+ if (n >= 1000) {
221
+ return `${Math.round(n / 1000)}k`;
222
+ }
223
+ return String(n);
224
+ }
225
+ //# sourceMappingURL=usage.js.map
@@ -0,0 +1,14 @@
1
+ export { invokedAs, parse } from '../args.ts';
2
+ export type { EventLine } from '../eventlog.ts';
3
+ export { paths, readJson, writeJson } from '../home.ts';
4
+ export { envOf, type KeyEntry, type KeyStore, type Provider } from '../keys.ts';
5
+ export { PROVIDER_NAMES, splitRef } from '../modelref.ts';
6
+ export { duration } from '../narrate.ts';
7
+ export { copyTree, MEMORY_RULES, TEMPLATES } from '../scaffold.ts';
8
+ export { bold, CliError, confirm, credentialError, cut, cyan, dim, EXIT, green, invalidError, json, note, pad, plain, red, styled, table, usageError, write, writeAll, yellow, } from '../term.ts';
9
+ export { safe } from '../trace.ts';
10
+ export { formatInline, formatMarkdown } from '../tui/markdown.ts';
11
+ export { resolveTheme, type Theme } from '../tui/theme.ts';
12
+ export { boxWidth } from '../tui/wrap.ts';
13
+ export { META_PROMPT_ENV, META_SESSION_ENV } from '../usage.ts';
14
+ //# sourceMappingURL=host.d.ts.map
@@ -0,0 +1,20 @@
1
+ // ---------------------------------------------------------------------------
2
+ // The meta layer's one door into the rest of the CLI
3
+ //
4
+ // Everything under `src/meta/` imports the runtime through this file and no
5
+ // other, so the layer can leave for a package of its own by turning this into
6
+ // a dependency. `test/meta-boundary.test.ts` holds the line.
7
+ // ---------------------------------------------------------------------------
8
+ export { invokedAs, parse } from "../args.js";
9
+ export { paths, readJson, writeJson } from "../home.js";
10
+ export { envOf } from "../keys.js";
11
+ export { PROVIDER_NAMES, splitRef } from "../modelref.js";
12
+ export { duration } from "../narrate.js";
13
+ export { copyTree, MEMORY_RULES, TEMPLATES } from "../scaffold.js";
14
+ export { bold, CliError, confirm, credentialError, cut, cyan, dim, EXIT, green, invalidError, json, note, pad, plain, red, styled, table, usageError, write, writeAll, yellow, } from "../term.js";
15
+ export { safe } from "../trace.js";
16
+ export { formatInline, formatMarkdown } from "../tui/markdown.js";
17
+ export { resolveTheme } from "../tui/theme.js";
18
+ export { boxWidth } from "../tui/wrap.js";
19
+ export { META_PROMPT_ENV, META_SESSION_ENV } from "../usage.js";
20
+ //# sourceMappingURL=host.js.map
@@ -0,0 +1,11 @@
1
+ export * from './editor.ts';
2
+ export * from './finetune/dataset/index.ts';
3
+ export * from './finetune/index.ts';
4
+ export * from './launch.ts';
5
+ export * from './model.ts';
6
+ export * from './prompts.ts';
7
+ export * from './relay.ts';
8
+ export * from './tokens.ts';
9
+ export * from './window.ts';
10
+ export * from './wire.ts';
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,11 @@
1
+ export * from "./editor.js";
2
+ export * from "./finetune/dataset/index.js";
3
+ export * from "./finetune/index.js";
4
+ export * from "./launch.js";
5
+ export * from "./model.js";
6
+ export * from "./prompts.js";
7
+ export * from "./relay.js";
8
+ export * from "./tokens.js";
9
+ export * from "./window.js";
10
+ export * from "./wire.js";
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,81 @@
1
+ import { spawn as nodeSpawn } from 'node:child_process';
2
+ import { type Sink } from './window.ts';
3
+ export interface Binary {
4
+ command: string;
5
+ args: string[];
6
+ from: 'path' | 'npx';
7
+ }
8
+ /**
9
+ * A `copilot` already installed is used as it is; otherwise `npx` fetches one.
10
+ * Note that finding the name on `PATH` is not the same as finding the CLI —
11
+ * an editor may have put a shim there that installs it on first run — so this
12
+ * only decides how to launch, never whether it will work.
13
+ */
14
+ export declare function locate(has?: (cmd: string) => boolean): Binary;
15
+ interface Event {
16
+ type: string;
17
+ data?: Record<string, unknown>;
18
+ [key: string]: unknown;
19
+ }
20
+ export interface Outcome {
21
+ /** the last thing the agent said that was not a preamble to a tool call */
22
+ answer: string;
23
+ exitCode: number;
24
+ sessionId?: string;
25
+ usage?: Record<string, unknown>;
26
+ events: Event[];
27
+ }
28
+ export interface Log {
29
+ readonly path: string;
30
+ /** Where the answer alone goes, beside the log and openable on its own. */
31
+ readonly answerPath: string;
32
+ line(text: string): void;
33
+ saveAnswer(text: string): void;
34
+ close(): void;
35
+ }
36
+ /**
37
+ * `<project>/.tmp/logs/meta.<when>[-<session>].log`, opened for append. The
38
+ * session keeps runs started in the same second - a tuning loop's - apart.
39
+ */
40
+ export declare function openLog(dir: string, now?: Date, session?: string): Log;
41
+ export declare function recordSession(dir: string, id: string): void;
42
+ export declare function lastSession(dir: string): string | undefined;
43
+ /**
44
+ * Folds one event into the outcome and says what, if anything, to show for it.
45
+ *
46
+ * `assistant.message` carries both the running commentary and the final word;
47
+ * what separates them is `toolRequests`, so the last message that asked for no
48
+ * tool is the answer and the rest are narration.
49
+ */
50
+ export declare function absorb(event: Event, out: Outcome, sink: Sink, width: number): void;
51
+ export interface Launch {
52
+ binary: Binary;
53
+ args: string[];
54
+ env: Record<string, string>;
55
+ cwd: string;
56
+ sink?: Sink;
57
+ log?: Log;
58
+ width?: number;
59
+ /** said after the elapsed time on the status row, e.g. the tokens so far */
60
+ status?: () => string;
61
+ /** every event copilot prints, before it is folded */
62
+ onEvent?: (event: Event) => void;
63
+ /** injected by the tests, which have no copilot and want none */
64
+ spawn?: typeof nodeSpawn;
65
+ }
66
+ export declare function launch(opts: Launch): Promise<Outcome>;
67
+ /** Resumes after copilot's own retries run out; `--retries` overrides. */
68
+ export declare const DEFAULT_RESUMES = 5;
69
+ /** What the resumed session is told, since `-p` has to say something. */
70
+ export declare const RESUME_PROMPT = "The previous model call failed. Continue the task from where you stopped.";
71
+ export declare function resumeDelayMs(attempt: number): number;
72
+ /**
73
+ * Why the run is worth resuming, or undefined when waiting cannot help.
74
+ *
75
+ * Only a refusal that time can cure counts: a rate limit, the provider's own
76
+ * failure, a connection that timed out. A 400 or a bad key fails the same way
77
+ * however often it is sent - except the one below.
78
+ */
79
+ export declare function transient(out: Outcome): string | undefined;
80
+ export {};
81
+ //# sourceMappingURL=launch.d.ts.map
@@ -0,0 +1,286 @@
1
+ import { spawn as nodeSpawn } from 'node:child_process';
2
+ import { createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { createInterface } from 'node:readline';
4
+ import { cut, dim, duration, formatInline, plain } from "./host.js";
5
+ import { FRAME_MS, windowSink } from "./window.js";
6
+ /**
7
+ * A `copilot` already installed is used as it is; otherwise `npx` fetches one.
8
+ * Note that finding the name on `PATH` is not the same as finding the CLI —
9
+ * an editor may have put a shim there that installs it on first run — so this
10
+ * only decides how to launch, never whether it will work.
11
+ */
12
+ export function locate(has = (cmd) => onPath(cmd)) {
13
+ return has('copilot')
14
+ ? { command: 'copilot', args: [], from: 'path' }
15
+ : { command: 'npx', args: ['--yes', '@github/copilot'], from: 'npx' };
16
+ }
17
+ function onPath(cmd) {
18
+ const dirs = (process.env.PATH ?? '').split(':').filter(Boolean);
19
+ return dirs.some((d) => existsSync(`${d}/${cmd}`));
20
+ }
21
+ /**
22
+ * `<project>/.tmp/logs/meta.<when>[-<session>].log`, opened for append. The
23
+ * session keeps runs started in the same second - a tuning loop's - apart.
24
+ */
25
+ export function openLog(dir, now = new Date(), session) {
26
+ const two = (n) => String(n).padStart(2, '0');
27
+ const stamp = `${now.getFullYear()}${two(now.getMonth() + 1)}${two(now.getDate())}` +
28
+ `${two(now.getHours())}${two(now.getMinutes())}${two(now.getSeconds())}` +
29
+ (session ? `-${session.slice(0, 8)}` : '');
30
+ const folder = `${dir}/.tmp/logs`;
31
+ mkdirSync(folder, { recursive: true });
32
+ const path = `${folder}/meta.${stamp}.log`;
33
+ const answerPath = `${folder}/meta.${stamp}.md`;
34
+ const file = createWriteStream(path, { flags: 'a' });
35
+ return {
36
+ path,
37
+ answerPath,
38
+ line: (text) => {
39
+ file.write(`${plain(text)}\n`);
40
+ },
41
+ saveAnswer: (text) => {
42
+ writeFileSync(answerPath, text.endsWith('\n') ? text : `${text}\n`);
43
+ },
44
+ close: () => {
45
+ file.end();
46
+ },
47
+ };
48
+ }
49
+ /** The project's last meta session, so `zen meta resume` needs no id. */
50
+ function sessionFile(dir) {
51
+ return `${dir}/.tmp/logs/meta.session`;
52
+ }
53
+ export function recordSession(dir, id) {
54
+ mkdirSync(`${dir}/.tmp/logs`, { recursive: true });
55
+ writeFileSync(sessionFile(dir), `${id}\n`);
56
+ }
57
+ export function lastSession(dir) {
58
+ try {
59
+ return readFileSync(sessionFile(dir), 'utf8').trim() || undefined;
60
+ }
61
+ catch {
62
+ return undefined;
63
+ }
64
+ }
65
+ /** Everything the sink is told, kept by the log too — and the detail only there. */
66
+ function tee(sink, log) {
67
+ return {
68
+ answer: (text) => sink.answer(text),
69
+ narrate: (line) => {
70
+ log.line(line);
71
+ sink.narrate(line);
72
+ },
73
+ detail: (line) => log.line(line),
74
+ warn: (line) => {
75
+ log.line(line);
76
+ sink.warn(line);
77
+ },
78
+ status: (line) => sink.status?.(line),
79
+ close: () => sink.close?.(),
80
+ };
81
+ }
82
+ /**
83
+ * Folds one event into the outcome and says what, if anything, to show for it.
84
+ *
85
+ * `assistant.message` carries both the running commentary and the final word;
86
+ * what separates them is `toolRequests`, so the last message that asked for no
87
+ * tool is the answer and the rest are narration.
88
+ */
89
+ export function absorb(event, out, sink, width) {
90
+ const data = (event.data ?? {});
91
+ switch (event.type) {
92
+ case 'assistant.message': {
93
+ const content = String(data.content ?? '').trim();
94
+ const requests = Array.isArray(data.toolRequests) ? data.toolRequests : [];
95
+ if (requests.length === 0) {
96
+ out.answer = content;
97
+ }
98
+ else if (content) {
99
+ sink.narrate(cut(formatInline(content.replace(/\s+/g, ' ')), width));
100
+ }
101
+ break;
102
+ }
103
+ case 'tool.execution_start': {
104
+ const args = (data.arguments ?? {});
105
+ const what = String(args.command ?? args.description ?? data.toolName ?? '').trim();
106
+ // Step-by-step belongs in the log: on screen it is a wall of `$`
107
+ // saying less than the one line of commentary above it.
108
+ sink.detail?.(`$ ${what.replace(/\s+/g, ' ')}`);
109
+ break;
110
+ }
111
+ case 'tool.execution_complete': {
112
+ if (data.success === false) {
113
+ sink.detail?.(` ${String(data.toolName ?? 'the tool')} failed`);
114
+ }
115
+ break;
116
+ }
117
+ case 'session.tools_updated': {
118
+ if (data.model) {
119
+ sink.detail?.(`model ${String(data.model)}`);
120
+ }
121
+ break;
122
+ }
123
+ case 'session.error': {
124
+ sink.warn(String(data.message ?? 'the session failed'));
125
+ break;
126
+ }
127
+ case 'model.call_failure': {
128
+ const status = data.statusCode ? ` (${String(data.statusCode)})` : '';
129
+ sink.warn(`${cut(String(data.errorMessage ?? 'the model call failed'), width)}${status}`);
130
+ break;
131
+ }
132
+ case 'result': {
133
+ out.exitCode = Number(event.exitCode ?? 0);
134
+ out.sessionId = event.sessionId;
135
+ out.usage = event.usage;
136
+ break;
137
+ }
138
+ default:
139
+ break;
140
+ }
141
+ out.events.push(event);
142
+ }
143
+ export async function launch(opts) {
144
+ const shown = opts.sink ?? windowSink();
145
+ const sink = opts.log ? tee(shown, opts.log) : shown;
146
+ const width = opts.width ?? Math.max(40, (process.stderr.columns ?? 100) - 4);
147
+ const start = opts.spawn ?? nodeSpawn;
148
+ const out = { answer: '', exitCode: 0, events: [] };
149
+ const child = start(opts.binary.command, [...opts.binary.args, ...opts.args], {
150
+ cwd: opts.cwd,
151
+ env: { ...process.env, ...opts.env },
152
+ stdio: ['ignore', 'pipe', 'pipe'],
153
+ shell: false,
154
+ });
155
+ const stop = (signal) => () => {
156
+ child.kill(signal);
157
+ };
158
+ const onInt = stop('SIGINT');
159
+ const onTerm = stop('SIGTERM');
160
+ process.on('SIGINT', onInt);
161
+ process.on('SIGTERM', onTerm);
162
+ // The first model call says nothing for as long as it takes, and silence
163
+ // after the model line is indistinguishable from a hang.
164
+ const began = Date.now();
165
+ const beat = setInterval(() => {
166
+ const extra = opts.status?.();
167
+ sink.status?.(`working: ${duration(Date.now() - began)}${extra ? ` · ${extra}` : ''}`);
168
+ }, FRAME_MS);
169
+ beat.unref();
170
+ child.stderr?.on('data', (chunk) => {
171
+ const text = chunk.toString().trimEnd();
172
+ if (text) {
173
+ sink.narrate(dim(text));
174
+ }
175
+ });
176
+ try {
177
+ const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
178
+ for await (const line of lines) {
179
+ if (!line.trim()) {
180
+ continue;
181
+ }
182
+ let event;
183
+ try {
184
+ event = JSON.parse(line);
185
+ }
186
+ catch {
187
+ // Not ours to interpret, but losing it is worse than showing it.
188
+ sink.narrate(line);
189
+ continue;
190
+ }
191
+ opts.onEvent?.(event);
192
+ absorb(event, out, sink, width);
193
+ }
194
+ const code = await new Promise((resolve) => {
195
+ child.on('close', (value, signal) => resolve(value ?? (signal ? 1 : 0)));
196
+ });
197
+ if (out.exitCode === 0 && code !== 0) {
198
+ out.exitCode = code;
199
+ }
200
+ }
201
+ finally {
202
+ clearInterval(beat);
203
+ process.off('SIGINT', onInt);
204
+ process.off('SIGTERM', onTerm);
205
+ // The answer is written by the caller, so the window has to be gone first.
206
+ sink.close?.();
207
+ }
208
+ return out;
209
+ }
210
+ // ---------------------------------------------------------------------------
211
+ // Resuming
212
+ //
213
+ // Copilot retries a failed model call itself, five times over about a minute,
214
+ // and has no setting to change that. A run that outlasts it is still a session
215
+ // on disk, so the retry happens one level up: wait, then resume that session.
216
+ // ---------------------------------------------------------------------------
217
+ /** Resumes after copilot's own retries run out; `--retries` overrides. */
218
+ export const DEFAULT_RESUMES = 5;
219
+ /** The first wait; it doubles per resume, up to `RESUME_MAX_MS`. */
220
+ const RESUME_INITIAL_MS = 30_000;
221
+ const RESUME_MAX_MS = 300_000;
222
+ /** What the resumed session is told, since `-p` has to say something. */
223
+ export const RESUME_PROMPT = 'The previous model call failed. Continue the task from where you stopped.';
224
+ export function resumeDelayMs(attempt) {
225
+ return Math.min(RESUME_INITIAL_MS * 2 ** attempt, RESUME_MAX_MS);
226
+ }
227
+ /**
228
+ * Why the run is worth resuming, or undefined when waiting cannot help.
229
+ *
230
+ * Only a refusal that time can cure counts: a rate limit, the provider's own
231
+ * failure, a connection that timed out. A 400 or a bad key fails the same way
232
+ * however often it is sent - except the one below.
233
+ */
234
+ export function transient(out) {
235
+ if (out.exitCode === 0 || !out.sessionId) {
236
+ return undefined;
237
+ }
238
+ const at = out.events.findLastIndex((e) => e.type === 'session.error');
239
+ const data = out.events[at]?.data;
240
+ if (!data) {
241
+ return undefined;
242
+ }
243
+ const status = Number(data.statusCode ?? 0);
244
+ if (data.errorType === 'rate_limit' || status === 429) {
245
+ return 'rate-limited (429)';
246
+ }
247
+ if (status === 408 || status >= 500) {
248
+ return `the provider failed (${status})`;
249
+ }
250
+ if (/timed out|ECONNRESET|socket hang up/i.test(String(data.message ?? ''))) {
251
+ return 'the connection failed';
252
+ }
253
+ if (status === 400 && injected(out.events.slice(0, at))) {
254
+ return 'refused a tool call copilot made itself (400)';
255
+ }
256
+ return undefined;
257
+ }
258
+ /**
259
+ * Whether the turn that failed began with a tool call copilot wrote, not the
260
+ * model. When a background command finishes, copilot adds a `read_bash` of its
261
+ * own; Gemini 3 rejects any tool call in the current turn that lacks its thought
262
+ * signature, so that request is a 400 every time. A new user message - the
263
+ * resume - starts a turn that no longer holds it.
264
+ */
265
+ function injected(before) {
266
+ for (let i = before.length - 1; i >= 0; i--) {
267
+ const event = before[i];
268
+ if (event.type === 'system.notification') {
269
+ return true;
270
+ }
271
+ if (event.type === 'user.message') {
272
+ return false;
273
+ }
274
+ if (event.type === 'assistant.message') {
275
+ if (event.data?.model) {
276
+ return false;
277
+ }
278
+ const requests = event.data?.toolRequests;
279
+ if (Array.isArray(requests) && requests.length > 0) {
280
+ return true;
281
+ }
282
+ }
283
+ }
284
+ return false;
285
+ }
286
+ //# sourceMappingURL=launch.js.map