@zenera/cli 1.1.24 → 1.1.26

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 (199) hide show
  1. package/README.md +65 -0
  2. package/dist/ask.js +28 -5
  3. package/dist/banner.js +2 -2
  4. package/dist/batch.d.ts +6 -0
  5. package/dist/batch.js +76 -1
  6. package/dist/catalog.d.ts +31 -1
  7. package/dist/catalog.js +140 -7
  8. package/dist/commands/index.js +2 -0
  9. package/dist/commands/inspect.d.ts +52 -2
  10. package/dist/commands/inspect.js +220 -31
  11. package/dist/commands/memory.js +156 -32
  12. package/dist/commands/meta.js +333 -24
  13. package/dist/commands/models.js +97 -12
  14. package/dist/commands/open.js +4 -4
  15. package/dist/commands/run.js +38 -4
  16. package/dist/commands/sandbox.js +148 -16
  17. package/dist/commands/view.d.ts +8 -0
  18. package/dist/commands/view.js +114 -0
  19. package/dist/engine.d.ts +8 -1
  20. package/dist/engine.js +7 -0
  21. package/dist/eventlog.d.ts +64 -0
  22. package/dist/eventlog.js +170 -0
  23. package/dist/liveness.d.ts +7 -0
  24. package/dist/liveness.js +35 -3
  25. package/dist/main.js +0 -0
  26. package/dist/meta/editor.d.ts +9 -0
  27. package/dist/meta/editor.js +43 -0
  28. package/dist/meta/finetune/analyze-brief.d.ts +26 -0
  29. package/dist/meta/finetune/analyze-brief.js +131 -0
  30. package/dist/meta/finetune/apply-brief.d.ts +23 -0
  31. package/dist/meta/finetune/apply-brief.js +247 -0
  32. package/dist/meta/finetune/brief.d.ts +45 -0
  33. package/dist/meta/finetune/brief.js +158 -0
  34. package/dist/meta/finetune/children.d.ts +8 -0
  35. package/dist/meta/finetune/children.js +76 -0
  36. package/dist/meta/finetune/command.d.ts +30 -0
  37. package/dist/meta/finetune/command.js +516 -0
  38. package/dist/meta/finetune/cost.d.ts +46 -0
  39. package/dist/meta/finetune/cost.js +105 -0
  40. package/dist/meta/finetune/dataset/anchors.d.ts +41 -0
  41. package/dist/meta/finetune/dataset/anchors.js +239 -0
  42. package/dist/meta/finetune/dataset/changes.d.ts +36 -0
  43. package/dist/meta/finetune/dataset/changes.js +319 -0
  44. package/dist/meta/finetune/dataset/command.d.ts +17 -0
  45. package/dist/meta/finetune/dataset/command.js +1011 -0
  46. package/dist/meta/finetune/dataset/drift.d.ts +33 -0
  47. package/dist/meta/finetune/dataset/drift.js +71 -0
  48. package/dist/meta/finetune/dataset/export.d.ts +26 -0
  49. package/dist/meta/finetune/dataset/export.js +51 -0
  50. package/dist/meta/finetune/dataset/index.d.ts +2 -0
  51. package/dist/meta/finetune/dataset/index.js +2 -0
  52. package/dist/meta/finetune/dataset/sample.d.ts +50 -0
  53. package/dist/meta/finetune/dataset/sample.js +197 -0
  54. package/dist/meta/finetune/dataset/schema.d.ts +33 -0
  55. package/dist/meta/finetune/dataset/schema.js +216 -0
  56. package/dist/meta/finetune/dataset/store.d.ts +35 -0
  57. package/dist/meta/finetune/dataset/store.js +177 -0
  58. package/dist/meta/finetune/dataset/types.d.ts +127 -0
  59. package/dist/meta/finetune/dataset/types.js +39 -0
  60. package/dist/meta/finetune/failure.d.ts +15 -0
  61. package/dist/meta/finetune/failure.js +26 -0
  62. package/dist/meta/finetune/feedback.d.ts +57 -0
  63. package/dist/meta/finetune/feedback.js +104 -0
  64. package/dist/meta/finetune/guard.d.ts +29 -0
  65. package/dist/meta/finetune/guard.js +104 -0
  66. package/dist/meta/finetune/improvements.d.ts +98 -0
  67. package/dist/meta/finetune/improvements.js +501 -0
  68. package/dist/meta/finetune/index.d.ts +2 -0
  69. package/dist/meta/finetune/index.js +2 -0
  70. package/dist/meta/finetune/journal.d.ts +35 -0
  71. package/dist/meta/finetune/journal.js +135 -0
  72. package/dist/meta/finetune/live.d.ts +84 -0
  73. package/dist/meta/finetune/live.js +272 -0
  74. package/dist/meta/finetune/loop.d.ts +3 -0
  75. package/dist/meta/finetune/loop.js +163 -0
  76. package/dist/meta/finetune/memories.d.ts +47 -0
  77. package/dist/meta/finetune/memories.js +154 -0
  78. package/dist/meta/finetune/recheck.d.ts +58 -0
  79. package/dist/meta/finetune/recheck.js +311 -0
  80. package/dist/meta/finetune/report.d.ts +40 -0
  81. package/dist/meta/finetune/report.js +1397 -0
  82. package/dist/meta/finetune/retry.d.ts +24 -0
  83. package/dist/meta/finetune/retry.js +96 -0
  84. package/dist/meta/finetune/sampler.d.ts +18 -0
  85. package/dist/meta/finetune/sampler.js +47 -0
  86. package/dist/meta/finetune/system.d.ts +27 -0
  87. package/dist/meta/finetune/system.js +132 -0
  88. package/dist/meta/finetune/train.d.ts +43 -0
  89. package/dist/meta/finetune/train.js +425 -0
  90. package/dist/meta/finetune/tui.d.ts +16 -0
  91. package/dist/meta/finetune/tui.js +316 -0
  92. package/dist/meta/finetune/tuning.d.ts +193 -0
  93. package/dist/meta/finetune/tuning.js +308 -0
  94. package/dist/meta/finetune/usage.d.ts +66 -0
  95. package/dist/meta/finetune/usage.js +260 -0
  96. package/dist/meta/finetune/view.d.ts +9 -0
  97. package/dist/meta/finetune/view.js +27 -0
  98. package/dist/meta/host.d.ts +15 -0
  99. package/dist/meta/host.js +21 -0
  100. package/dist/meta/index.d.ts +11 -0
  101. package/dist/meta/index.js +11 -0
  102. package/dist/meta/launch.d.ts +81 -0
  103. package/dist/meta/launch.js +286 -0
  104. package/dist/meta/model.d.ts +44 -0
  105. package/dist/meta/model.js +119 -0
  106. package/dist/meta/prompts.d.ts +18 -0
  107. package/dist/meta/prompts.js +61 -0
  108. package/dist/meta/relay.d.ts +20 -0
  109. package/dist/meta/relay.js +123 -0
  110. package/dist/meta/tokens.d.ts +52 -0
  111. package/dist/meta/tokens.js +226 -0
  112. package/dist/meta/window.d.ts +61 -0
  113. package/dist/meta/window.js +198 -0
  114. package/dist/meta/wire.d.ts +42 -0
  115. package/dist/meta/wire.js +129 -0
  116. package/dist/meta.d.ts +71 -1
  117. package/dist/meta.js +303 -6
  118. package/dist/modelref.d.ts +16 -0
  119. package/dist/modelref.js +41 -0
  120. package/dist/narrate.js +5 -2
  121. package/dist/podman.d.ts +26 -0
  122. package/dist/podman.js +83 -3
  123. package/dist/projects.d.ts +0 -5
  124. package/dist/projects.js +2 -15
  125. package/dist/resources.d.ts +215 -0
  126. package/dist/resources.js +856 -0
  127. package/dist/sandbox.d.ts +2 -0
  128. package/dist/sandbox.js +10 -3
  129. package/dist/scaffold.d.ts +15 -7
  130. package/dist/scaffold.js +24 -67
  131. package/dist/session.js +4 -29
  132. package/dist/term.d.ts +2 -0
  133. package/dist/term.js +11 -2
  134. package/dist/trace.d.ts +5 -0
  135. package/dist/trace.js +107 -7
  136. package/dist/tui/app.js +19 -5
  137. package/dist/tui/links.d.ts +27 -0
  138. package/dist/tui/links.js +142 -0
  139. package/dist/tui/models.d.ts +35 -0
  140. package/dist/tui/models.js +447 -0
  141. package/dist/tui/sandbox.d.ts +10 -0
  142. package/dist/tui/sandbox.js +131 -0
  143. package/dist/tui/wrap.d.ts +5 -0
  144. package/dist/tui/wrap.js +12 -0
  145. package/dist/usage.d.ts +78 -0
  146. package/dist/usage.js +51 -0
  147. package/package.json +4 -2
  148. package/templates/editor/.github/copilot-instructions.md +60 -145
  149. package/templates/editor/.github/prompts/analyze.prompt.md +33 -0
  150. package/templates/editor/.github/prompts/dataset.prompt.md +108 -0
  151. package/templates/editor/.github/prompts/finetune-apply.prompt.md +270 -0
  152. package/templates/editor/.github/prompts/finetune.prompt.md +691 -198
  153. package/templates/editor/.github/prompts/new-agent.prompt.md +3 -1
  154. package/templates/editor/.github/prompts/new-skill.prompt.md +2 -0
  155. package/templates/editor/.github/prompts/project-review.prompt.md +5 -4
  156. package/templates/editor/.github/skills/zen-analyze-run/SKILL.md +588 -0
  157. package/templates/editor/.github/skills/zen-cli/SKILL.md +9 -1
  158. package/templates/editor/.github/skills/zen-cli/references/dataset.md +34 -0
  159. package/templates/editor/.github/skills/zen-cli/references/finetune.md +137 -0
  160. package/templates/editor/.github/skills/zen-cli/references/inspect.md +63 -11
  161. package/templates/editor/.github/skills/zen-cli/references/memory.md +33 -10
  162. package/templates/editor/.github/skills/zen-cli/references/meta.md +61 -0
  163. package/templates/editor/.github/skills/zen-cli/references/models.md +5 -1
  164. package/templates/editor/.github/skills/zen-cli/references/projects.md +4 -6
  165. package/templates/editor/.github/skills/zen-cli/references/rag.md +24 -6
  166. package/templates/editor/.github/skills/zen-cli/references/sandbox.md +35 -14
  167. package/templates/editor/.github/skills/zen-code-python/SKILL.md +123 -0
  168. package/templates/editor/.github/skills/zen-code-python/references/code_python.md +48 -0
  169. package/templates/editor/.github/skills/zen-dataset/SKILL.md +253 -0
  170. package/templates/editor/.github/skills/zen-finetune/SKILL.md +1492 -660
  171. package/templates/editor/.github/skills/zen-finetune/references/readme-template.md +268 -0
  172. package/templates/editor/.github/skills/zen-finetune/scripts/batch.mjs +390 -0
  173. package/templates/editor/.github/skills/zen-finetune/scripts/difficult.mjs +243 -0
  174. package/templates/editor/.github/skills/zen-finetune/scripts/memory.mjs +280 -0
  175. package/templates/editor/.github/skills/zen-finetune/scripts/next.mjs +398 -0
  176. package/templates/editor/.github/skills/zen-finetune/scripts/report.mjs +797 -0
  177. package/templates/editor/.github/skills/zen-finetune/scripts/select.mjs +354 -0
  178. package/templates/editor/.github/skills/zen-finetune/scripts/usage.mjs +722 -0
  179. package/templates/editor/.github/skills/zen-inspect/SKILL.md +74 -26
  180. package/templates/editor/.github/skills/zen-inspect-ask/SKILL.md +271 -0
  181. package/templates/editor/.github/skills/zen-instructions/SKILL.md +4 -12
  182. package/templates/editor/.github/skills/zen-memory/SKILL.md +82 -53
  183. package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +20 -8
  184. package/templates/editor/.github/skills/zen-review/SKILL.md +8 -31
  185. package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +5 -5
  186. package/templates/editor/.github/skills/zen-review/scripts/review.sh +3 -12
  187. package/templates/editor/.github/skills/zen-sandbox/SKILL.md +9 -4
  188. package/templates/editor/.github/skills/zen-sandbox-capacity/SKILL.md +2 -2
  189. package/templates/editor/.github/skills/zen-spec-sync/SKILL.md +2 -4
  190. package/templates/editor/.github/skills/zen-topology/SKILL.md +283 -0
  191. package/templates/editor/.vscode/settings.json +1 -0
  192. package/templates/project/agents/files-instructions.md +29 -20
  193. package/templates/project/agents/fork-instructions.md +78 -28
  194. package/templates/project/agents/memory-instructions.md +222 -494
  195. package/templates/project/agents/tools-instructions.md +14 -4
  196. package/templates/project/gitignore +2 -0
  197. package/templates/editor/.github/skills/zen-finetune/scripts/collect.sh +0 -432
  198. package/templates/editor/.github/skills/zen-finetune/scripts/sample.sh +0 -199
  199. package/templates/editor/.github/skills/zen-memory/scripts/check-instructions.sh +0 -116
@@ -0,0 +1,42 @@
1
+ import { type KeyEntry, type KeyStore, type Provider } from './host.ts';
2
+ /**
3
+ * Copilot offers its tools as OpenAI *custom* tools, which the completions API
4
+ * rejects outright — `400 Invalid value: 'custom'`. Only the responses API
5
+ * takes them, and only the reasoning models serve it, so the wire API follows
6
+ * the model rather than being a flag nobody would know to set. Older models are
7
+ * listed rather than newer ones, so each new generation works without a release.
8
+ */
9
+ export declare function wireApi(provider: Provider, id: string): string | undefined;
10
+ export interface Wiring {
11
+ provider: Provider;
12
+ /** what the model is called on the wire */
13
+ model: string;
14
+ /** every `COPILOT_*` name and value, ready to hand to the child */
15
+ env: Record<string, string>;
16
+ /** names whose values are secret, for `--secret-env-vars` and for masking */
17
+ secret: string[];
18
+ /** anything true but unwelcome: a location that will be slow, a stale default */
19
+ warnings: string[];
20
+ /** Claude on Vertex: a loopback relay to start, whose url becomes the base url */
21
+ relay?: {
22
+ key: string;
23
+ project: string;
24
+ location: string;
25
+ keyFile: string;
26
+ };
27
+ }
28
+ /** Where the relay's url goes once it is listening. */
29
+ export declare const RELAY_URL = "http://127.0.0.1:<relay>";
30
+ /**
31
+ * Turns a zen key entry and a model id into copilot's environment.
32
+ *
33
+ * `COPILOT_PROVIDER_BASE_URL` is what activates BYOK at all, so every provider
34
+ * gets one — including OpenAI, whose url copilot would otherwise have reached
35
+ * through a GitHub account we are deliberately not using.
36
+ */
37
+ export declare function wire(store: KeyStore, entry: KeyEntry, id: string): Wiring;
38
+ /** Values a `--dry-run` may print. A key is four characters and an apology. */
39
+ export declare function masked(env: Record<string, string>, secret: readonly string[]): string[];
40
+ /** Non-fatal, said once, because a competing file is a silent override. */
41
+ export declare function providersWarning(): string | undefined;
42
+ //# sourceMappingURL=wire.d.ts.map
@@ -0,0 +1,129 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { existsSync } from 'node:fs';
3
+ import { credentialError, envOf, invokedAs, yellow, } from "./host.js";
4
+ const TYPES = {
5
+ openai: 'openai',
6
+ anthropic: 'anthropic',
7
+ google: 'openai',
8
+ vertex: 'openai',
9
+ openrouter: 'openai',
10
+ };
11
+ const BASE_URLS = {
12
+ openai: 'https://api.openai.com/v1',
13
+ anthropic: 'https://api.anthropic.com',
14
+ google: 'https://generativelanguage.googleapis.com/v1beta/openai/',
15
+ openrouter: 'https://openrouter.ai/api/v1',
16
+ };
17
+ /**
18
+ * Copilot offers its tools as OpenAI *custom* tools, which the completions API
19
+ * rejects outright — `400 Invalid value: 'custom'`. Only the responses API
20
+ * takes them, and only the reasoning models serve it, so the wire API follows
21
+ * the model rather than being a flag nobody would know to set. Older models are
22
+ * listed rather than newer ones, so each new generation works without a release.
23
+ */
24
+ export function wireApi(provider, id) {
25
+ return provider === 'openai' && !/^(gpt-4|gpt-3\.5|chatgpt-)/.test(id)
26
+ ? 'responses'
27
+ : undefined;
28
+ }
29
+ /** Where the relay's url goes once it is listening. */
30
+ export const RELAY_URL = 'http://127.0.0.1:<relay>';
31
+ const vertexClaude = (id) => /^(anthropic\/)?claude-/.test(id);
32
+ /**
33
+ * Turns a zen key entry and a model id into copilot's environment.
34
+ *
35
+ * `COPILOT_PROVIDER_BASE_URL` is what activates BYOK at all, so every provider
36
+ * gets one — including OpenAI, whose url copilot would otherwise have reached
37
+ * through a GitHub account we are deliberately not using.
38
+ */
39
+ export function wire(store, entry, id) {
40
+ const provider = entry.provider;
41
+ const env = {};
42
+ const secret = [];
43
+ const warnings = [];
44
+ env.COPILOT_PROVIDER_TYPE = TYPES[provider];
45
+ env.COPILOT_MODEL = id;
46
+ if (provider === 'vertex' && vertexClaude(id)) {
47
+ const project = store.projectOf(entry)?.id;
48
+ if (entry.holds !== 'file' || !project) {
49
+ throw credentialError(`Claude on vertex needs a service account and a GCP project, not ${entry.provider}/${entry.name}`, 'add one: zen key add vertex --project <id>');
50
+ }
51
+ const model = id.replace(/^anthropic\//, '');
52
+ const key = randomBytes(24).toString('hex');
53
+ env.COPILOT_PROVIDER_TYPE = 'anthropic';
54
+ env.COPILOT_MODEL = model;
55
+ env.COPILOT_PROVIDER_BASE_URL = RELAY_URL;
56
+ env.COPILOT_PROVIDER_API_KEY = key;
57
+ return {
58
+ provider,
59
+ model,
60
+ env,
61
+ secret: ['COPILOT_PROVIDER_API_KEY'],
62
+ warnings,
63
+ relay: {
64
+ key,
65
+ project,
66
+ location: entry.location?.trim() || 'global',
67
+ keyFile: store.fileOf(entry),
68
+ },
69
+ };
70
+ }
71
+ if (provider === 'vertex') {
72
+ const project = store.projectOf(entry)?.id;
73
+ if (!project) {
74
+ throw credentialError(`no GCP project on ${entry.provider}/${entry.name}`, 'set one: zen key add vertex --project <id>');
75
+ }
76
+ const named = entry.location?.trim() || 'global';
77
+ const partner = id.includes('/') && !id.startsWith('google/');
78
+ // Gemini serves the `us`/`eu` multi-regions; partner models answer them with a 404 or 429.
79
+ const location = partner && (named === 'us' || named === 'eu') ? 'global' : named;
80
+ if (location === 'global' && !partner) {
81
+ warnings.push('location `global` adds about ten seconds of cold start');
82
+ }
83
+ env.COPILOT_PROVIDER_BASE_URL = `https://aiplatform.googleapis.com/v1/projects/${project}/locations/${location}/endpoints/openapi`;
84
+ // Vertex speaks publisher names on the wire and knows nothing of zen's
85
+ // provider prefix; the bare id stays as the model *id* so copilot can
86
+ // still match its catalogue for token limits and tool support.
87
+ env.COPILOT_PROVIDER_WIRE_MODEL = id.includes('/') ? id : `google/${id}`;
88
+ // Only Google's own ids are bare: `gpt-oss-…` would get GPT's custom `apply_patch`, which this endpoint rejects.
89
+ env.COPILOT_PROVIDER_MODEL_ID = id.startsWith('google/') ? id.slice('google/'.length) : id;
90
+ }
91
+ else {
92
+ const url = BASE_URLS[provider];
93
+ if (!url) {
94
+ throw credentialError(`no BYOK endpoint known for ${provider}`);
95
+ }
96
+ env.COPILOT_PROVIDER_BASE_URL = url;
97
+ }
98
+ const api = wireApi(provider, id);
99
+ if (api) {
100
+ env.COPILOT_PROVIDER_WIRE_API = api;
101
+ }
102
+ if (entry.holds === 'file') {
103
+ // A Google access token is good for an hour and a session is not, so
104
+ // copilot is told how to mint one rather than handed one that will be
105
+ // stale by the time it matters.
106
+ env.COPILOT_PROVIDER_API_KEY_COMMAND = `${invokedAs('zen')} key token ${entry.provider}`;
107
+ env[envOf(entry)] = store.fileOf(entry);
108
+ }
109
+ else {
110
+ env.COPILOT_PROVIDER_API_KEY = store.reveal(entry);
111
+ secret.push('COPILOT_PROVIDER_API_KEY');
112
+ }
113
+ return { provider, model: env.COPILOT_PROVIDER_WIRE_MODEL ?? id, env, secret, warnings };
114
+ }
115
+ /** Values a `--dry-run` may print. A key is four characters and an apology. */
116
+ export function masked(env, secret) {
117
+ return Object.entries(env)
118
+ .sort(([a], [b]) => a.localeCompare(b))
119
+ .map(([name, value]) => `${name}=${secret.includes(name) ? '••••' : value}`);
120
+ }
121
+ /** Non-fatal, said once, because a competing file is a silent override. */
122
+ export function providersWarning() {
123
+ const configured = process.env.COPILOT_PROVIDERS_CONFIG;
124
+ const path = configured ?? `${process.env.HOME ?? ''}/.copilot/providers.json`;
125
+ return existsSync(path)
126
+ ? yellow(`${path} may override the provider zen just wired`)
127
+ : undefined;
128
+ }
129
+ //# sourceMappingURL=wire.js.map
package/dist/meta.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { ModelUsage, TokenUsage } from '@zenera/neo';
1
2
  import { spawn as nodeSpawn } from 'node:child_process';
2
3
  import { type KeyEntry, type KeyStore, type Provider } from './keys.ts';
3
4
  export declare const PROMPT_DIR = ".github/prompts";
@@ -60,7 +61,8 @@ export declare function misspelledProvider(ref: string): Provider | undefined;
60
61
  * Copilot offers its tools as OpenAI *custom* tools, which the completions API
61
62
  * rejects outright — `400 Invalid value: 'custom'`. Only the responses API
62
63
  * takes them, and only the reasoning models serve it, so the wire API follows
63
- * the model rather than being a flag nobody would know to set.
64
+ * the model rather than being a flag nobody would know to set. Older models are
65
+ * listed rather than newer ones, so each new generation works without a release.
64
66
  */
65
67
  export declare function wireApi(provider: Provider, id: string): string | undefined;
66
68
  export interface Wiring {
@@ -195,6 +197,18 @@ export interface Log {
195
197
  }
196
198
  /** `<project>/.tmp/logs/meta.<when>.log`, opened for append. */
197
199
  export declare function openLog(dir: string, now?: Date): Log;
200
+ /**
201
+ * Every meta session the project ran and how it ended, so `zen meta resume`
202
+ * needs no id. `running` is never overwritten by a run that was killed, so it
203
+ * reads the same as `failed`: not finished.
204
+ */
205
+ export type SessionState = 'running' | 'done' | 'failed';
206
+ export declare function recordSession(dir: string, id: string, state: SessionState): void;
207
+ /**
208
+ * The newest session that did not finish, else the newest of all. A quick
209
+ * question asked after a long run stopped must not stand in for it.
210
+ */
211
+ export declare function lastSession(dir: string): string | undefined;
198
212
  /**
199
213
  * Folds one event into the outcome and says what, if anything, to show for it.
200
214
  *
@@ -211,10 +225,66 @@ export interface Launch {
211
225
  sink?: Sink;
212
226
  log?: Log;
213
227
  width?: number;
228
+ /** said after the elapsed time on the status row, e.g. the tokens so far */
229
+ status?: () => string;
214
230
  /** injected by the tests, which have no copilot and want none */
215
231
  spawn?: typeof nodeSpawn;
216
232
  }
217
233
  export declare function launch(opts: Launch): Promise<Outcome>;
234
+ /** Resumes after copilot's own retries run out; `--retries` overrides. */
235
+ export declare const DEFAULT_RESUMES = 5;
236
+ /** What the resumed session is told, since `-p` has to say something. */
237
+ export declare const RESUME_PROMPT = "The previous model call failed. Continue the task from where you stopped.";
238
+ export declare function resumeDelayMs(attempt: number): number;
239
+ /**
240
+ * Why the run is worth resuming, or undefined when waiting cannot help.
241
+ *
242
+ * Only a refusal that time can cure counts: a rate limit, the provider's own
243
+ * failure, a connection that timed out. A 400 or a bad key fails the same way
244
+ * however often it is sent - except the one below.
245
+ */
246
+ export declare function transient(out: Outcome): string | undefined;
247
+ export interface MetaCall {
248
+ model: string;
249
+ usage: TokenUsage;
250
+ cacheWriteTokens?: number;
251
+ startedAt: string;
252
+ durationMs: number;
253
+ session?: string;
254
+ traceId?: string;
255
+ spanId?: string;
256
+ }
257
+ /**
258
+ * One line of the span file, as a model call - anything else is undefined.
259
+ * Only counts are read off it: whatever else a span may carry, none of it
260
+ * reaches the ledger.
261
+ */
262
+ export declare function readSpan(line: string): MetaCall | undefined;
263
+ /** Whole lines out of a stream read in chunks; the unfinished tail is carried. */
264
+ export declare function splitLines(carry: string, chunk: string): {
265
+ lines: string[];
266
+ carry: string;
267
+ };
268
+ /** Reads a span file as it grows. `stop()` reads what is left, once. */
269
+ export declare function tailSpans(path: string, onCall: (call: MetaCall) => void, everyMs?: number): {
270
+ stop: () => void;
271
+ };
272
+ /** Calls and tokens so far, in the one line the status row and the summary share. */
273
+ export declare class Tally {
274
+ calls: number;
275
+ usage: TokenUsage;
276
+ add(call: MetaCall): void;
277
+ toString(): string;
278
+ }
279
+ /** 950, 12.3k, 1.8M. */
280
+ export declare function compact(n: number): string;
281
+ /**
282
+ * Copilot's own per-model totals from the last `session.shutdown` in a session's
283
+ * events - cumulative over every resume of it. Reasoning folded into output,
284
+ * as everywhere in zen.
285
+ */
286
+ export declare function shutdownModels(events: string): ModelUsage[] | undefined;
287
+ export declare function sessionTotals(sessionId: string): ModelUsage[] | undefined;
218
288
  /** Non-fatal, said once, because a competing file is a silent override. */
219
289
  export declare function providersWarning(): string | undefined;
220
290
  export {};
package/dist/meta.js CHANGED
@@ -1,10 +1,13 @@
1
1
  import { spawn as nodeSpawn } from 'node:child_process';
2
- import { createWriteStream, existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
+ import { appendFileSync, closeSync, createWriteStream, existsSync, mkdirSync, openSync, readFileSync, readSync, statSync, writeFileSync, } from 'node:fs';
3
3
  import { readFile } from 'node:fs/promises';
4
+ import { homedir } from 'node:os';
5
+ import { join } from 'node:path';
4
6
  import { createInterface } from 'node:readline';
5
7
  import { invokedAs } from "./args.js";
6
8
  import { paths, readJson, writeJson } from "./home.js";
7
9
  import { envOf } from "./keys.js";
10
+ import { duration } from "./narrate.js";
8
11
  import { credentialError, cut, cyan, dim, note, pad, plain, red, styled, usageError, write, yellow, } from "./term.js";
9
12
  import { formatInline, formatMarkdown } from "./tui/markdown.js";
10
13
  import { boxWidth } from "./tui/wrap.js";
@@ -174,10 +177,13 @@ const BASE_URLS = {
174
177
  * Copilot offers its tools as OpenAI *custom* tools, which the completions API
175
178
  * rejects outright — `400 Invalid value: 'custom'`. Only the responses API
176
179
  * takes them, and only the reasoning models serve it, so the wire API follows
177
- * the model rather than being a flag nobody would know to set.
180
+ * the model rather than being a flag nobody would know to set. Older models are
181
+ * listed rather than newer ones, so each new generation works without a release.
178
182
  */
179
183
  export function wireApi(provider, id) {
180
- return provider === 'openai' && /^(gpt-5|o[34])/.test(id) ? 'responses' : undefined;
184
+ return provider === 'openai' && !/^(gpt-4|gpt-3\.5|chatgpt-)/.test(id)
185
+ ? 'responses'
186
+ : undefined;
181
187
  }
182
188
  /**
183
189
  * Turns a zen key entry and a model id into copilot's environment.
@@ -529,6 +535,41 @@ export function openLog(dir, now = new Date()) {
529
535
  },
530
536
  };
531
537
  }
538
+ const sessionLog = (dir) => `${dir}/.tmp/logs/meta.sessions`;
539
+ /** The single-id file older versions wrote. */
540
+ const legacySession = (dir) => `${dir}/.tmp/logs/meta.session`;
541
+ export function recordSession(dir, id, state) {
542
+ mkdirSync(`${dir}/.tmp/logs`, { recursive: true });
543
+ appendFileSync(sessionLog(dir), `${new Date().toISOString()}\t${id}\t${state}\n`);
544
+ }
545
+ /**
546
+ * The newest session that did not finish, else the newest of all. A quick
547
+ * question asked after a long run stopped must not stand in for it.
548
+ */
549
+ export function lastSession(dir) {
550
+ let text;
551
+ try {
552
+ text = readFileSync(sessionLog(dir), 'utf8');
553
+ }
554
+ catch {
555
+ try {
556
+ return readFileSync(legacySession(dir), 'utf8').trim() || undefined;
557
+ }
558
+ catch {
559
+ return undefined;
560
+ }
561
+ }
562
+ const latest = new Map();
563
+ for (const line of text.split('\n')) {
564
+ const [, id, state] = line.split('\t');
565
+ if (id && state) {
566
+ latest.delete(id);
567
+ latest.set(id, state);
568
+ }
569
+ }
570
+ const newestFirst = [...latest].reverse();
571
+ return (newestFirst.find(([, state]) => state !== 'done') ?? newestFirst[0])?.[0];
572
+ }
532
573
  /** Everything the sink is told, kept by the log too — and the detail only there. */
533
574
  function tee(sink, log) {
534
575
  return {
@@ -630,8 +671,8 @@ export async function launch(opts) {
630
671
  // after the model line is indistinguishable from a hang.
631
672
  const began = Date.now();
632
673
  const beat = setInterval(() => {
633
- const seconds = Math.round((Date.now() - began) / 1000);
634
- sink.status?.(`working: ${seconds}s`);
674
+ const extra = opts.status?.();
675
+ sink.status?.(`working: ${duration(Date.now() - began)}${extra ? ` · ${extra}` : ''}`);
635
676
  }, FRAME_MS);
636
677
  beat.unref();
637
678
  child.stderr?.on('data', (chunk) => {
@@ -658,7 +699,7 @@ export async function launch(opts) {
658
699
  absorb(event, out, sink, width);
659
700
  }
660
701
  const code = await new Promise((resolve) => {
661
- child.on('close', (value) => resolve(value ?? 0));
702
+ child.on('close', (value, signal) => resolve(value ?? (signal ? 1 : 0)));
662
703
  });
663
704
  if (out.exitCode === 0 && code !== 0) {
664
705
  out.exitCode = code;
@@ -673,6 +714,262 @@ export async function launch(opts) {
673
714
  }
674
715
  return out;
675
716
  }
717
+ // ---------------------------------------------------------------------------
718
+ // Resuming
719
+ //
720
+ // Copilot retries a failed model call itself, five times over about a minute,
721
+ // and has no setting to change that. A run that outlasts it is still a session
722
+ // on disk, so the retry happens one level up: wait, then resume that session.
723
+ // ---------------------------------------------------------------------------
724
+ /** Resumes after copilot's own retries run out; `--retries` overrides. */
725
+ export const DEFAULT_RESUMES = 5;
726
+ /** The first wait; it doubles per resume, up to `RESUME_MAX_MS`. */
727
+ const RESUME_INITIAL_MS = 30_000;
728
+ const RESUME_MAX_MS = 300_000;
729
+ /** What the resumed session is told, since `-p` has to say something. */
730
+ export const RESUME_PROMPT = 'The previous model call failed. Continue the task from where you stopped.';
731
+ export function resumeDelayMs(attempt) {
732
+ return Math.min(RESUME_INITIAL_MS * 2 ** attempt, RESUME_MAX_MS);
733
+ }
734
+ /**
735
+ * Why the run is worth resuming, or undefined when waiting cannot help.
736
+ *
737
+ * Only a refusal that time can cure counts: a rate limit, the provider's own
738
+ * failure, a connection that timed out. A 400 or a bad key fails the same way
739
+ * however often it is sent - except the one below.
740
+ */
741
+ export function transient(out) {
742
+ if (out.exitCode === 0 || !out.sessionId) {
743
+ return undefined;
744
+ }
745
+ const at = out.events.findLastIndex((e) => e.type === 'session.error');
746
+ const data = out.events[at]?.data;
747
+ if (!data) {
748
+ return undefined;
749
+ }
750
+ const status = Number(data.statusCode ?? 0);
751
+ if (data.errorType === 'rate_limit' || status === 429) {
752
+ return 'rate-limited (429)';
753
+ }
754
+ if (status === 408 || status >= 500) {
755
+ return `the provider failed (${status})`;
756
+ }
757
+ if (/timed out|connection error|ECONNRESET|ETIMEDOUT|EAI_AGAIN|socket hang up/i.test(String(data.message ?? ''))) {
758
+ return 'the connection failed';
759
+ }
760
+ if (status === 400 && injected(out.events.slice(0, at))) {
761
+ return 'refused a tool call copilot made itself (400)';
762
+ }
763
+ return undefined;
764
+ }
765
+ /**
766
+ * Whether the turn that failed began with a tool call copilot wrote, not the
767
+ * model. When a background command finishes, copilot adds a `read_bash` of its
768
+ * own; Gemini 3 rejects any tool call in the current turn that lacks its thought
769
+ * signature, so that request is a 400 every time. A new user message - the
770
+ * resume - starts a turn that no longer holds it.
771
+ */
772
+ function injected(before) {
773
+ for (let i = before.length - 1; i >= 0; i--) {
774
+ const event = before[i];
775
+ if (event.type === 'system.notification') {
776
+ return true;
777
+ }
778
+ if (event.type === 'user.message') {
779
+ return false;
780
+ }
781
+ if (event.type === 'assistant.message') {
782
+ if (event.data?.model) {
783
+ return false;
784
+ }
785
+ const requests = event.data?.toolRequests;
786
+ if (Array.isArray(requests) && requests.length > 0) {
787
+ return true;
788
+ }
789
+ }
790
+ }
791
+ return false;
792
+ }
793
+ /** An OTel time: `[seconds, nanoseconds]` in this exporter; epoch ms either way. */
794
+ function epochMs(t) {
795
+ if (Array.isArray(t) && typeof t[0] === 'number') {
796
+ return t[0] * 1000 + Number(t[1] ?? 0) / 1e6;
797
+ }
798
+ if (typeof t === 'number') {
799
+ return t;
800
+ }
801
+ return typeof t === 'string' ? Date.parse(t) : undefined;
802
+ }
803
+ /**
804
+ * One line of the span file, as a model call - anything else is undefined.
805
+ * Only counts are read off it: whatever else a span may carry, none of it
806
+ * reaches the ledger.
807
+ */
808
+ export function readSpan(line) {
809
+ let span;
810
+ try {
811
+ span = JSON.parse(line);
812
+ }
813
+ catch {
814
+ return undefined;
815
+ }
816
+ const attrs = (span.attributes ?? {});
817
+ if (span.type !== 'span' || attrs['gen_ai.operation.name'] !== 'chat') {
818
+ return undefined;
819
+ }
820
+ const num = (key) => Number(attrs[key] ?? 0) || 0;
821
+ const start = epochMs(span.startTime) ?? 0;
822
+ const end = epochMs(span.endTime) ?? start;
823
+ // Copilot counts reasoning beside the output; zen counts it inside.
824
+ const reasoning = num('gen_ai.usage.reasoning.output_tokens');
825
+ const cacheWrite = num('gen_ai.usage.cache_creation.input_tokens') ||
826
+ num('gen_ai.usage.cache_creation_input_tokens');
827
+ return {
828
+ model: String(attrs['gen_ai.response.model'] ?? attrs['gen_ai.request.model'] ?? '?'),
829
+ usage: {
830
+ inputTokens: num('gen_ai.usage.input_tokens'),
831
+ cachedInputTokens: num('gen_ai.usage.cache_read.input_tokens') ||
832
+ num('gen_ai.usage.cache_read_input_tokens'),
833
+ outputTokens: num('gen_ai.usage.output_tokens') + reasoning,
834
+ reasoningTokens: reasoning,
835
+ },
836
+ ...(cacheWrite ? { cacheWriteTokens: cacheWrite } : {}),
837
+ startedAt: new Date(start).toISOString(),
838
+ durationMs: Math.max(0, Math.round(end - start)),
839
+ session: typeof attrs['gen_ai.conversation.id'] === 'string'
840
+ ? attrs['gen_ai.conversation.id']
841
+ : undefined,
842
+ traceId: typeof span.traceId === 'string' ? span.traceId : undefined,
843
+ spanId: typeof span.spanId === 'string' ? span.spanId : undefined,
844
+ };
845
+ }
846
+ /** Whole lines out of a stream read in chunks; the unfinished tail is carried. */
847
+ export function splitLines(carry, chunk) {
848
+ const parts = `${carry}${chunk}`.split('\n');
849
+ const rest = parts.pop() ?? '';
850
+ return { lines: parts.filter((l) => l.trim()), carry: rest };
851
+ }
852
+ /** Reads a span file as it grows. `stop()` reads what is left, once. */
853
+ export function tailSpans(path, onCall, everyMs = 1000) {
854
+ let offset = 0;
855
+ let carry = '';
856
+ const poll = () => {
857
+ let fd;
858
+ try {
859
+ const size = statSync(path).size;
860
+ if (size <= offset) {
861
+ return;
862
+ }
863
+ fd = openSync(path, 'r');
864
+ const buf = Buffer.alloc(size - offset);
865
+ readSync(fd, buf, 0, buf.length, offset);
866
+ offset = size;
867
+ const split = splitLines(carry, buf.toString('utf8'));
868
+ carry = split.carry;
869
+ for (const line of split.lines) {
870
+ const call = readSpan(line);
871
+ if (call) {
872
+ onCall(call);
873
+ }
874
+ }
875
+ }
876
+ catch {
877
+ // Not there yet: copilot creates it at the first span.
878
+ }
879
+ finally {
880
+ if (fd !== undefined) {
881
+ closeSync(fd);
882
+ }
883
+ }
884
+ };
885
+ const timer = setInterval(poll, everyMs);
886
+ timer.unref();
887
+ return {
888
+ stop: () => {
889
+ clearInterval(timer);
890
+ poll();
891
+ },
892
+ };
893
+ }
894
+ /** Calls and tokens so far, in the one line the status row and the summary share. */
895
+ export class Tally {
896
+ calls = 0;
897
+ usage = {
898
+ inputTokens: 0,
899
+ cachedInputTokens: 0,
900
+ outputTokens: 0,
901
+ reasoningTokens: 0,
902
+ };
903
+ add(call) {
904
+ this.calls += 1;
905
+ this.usage = {
906
+ inputTokens: this.usage.inputTokens + call.usage.inputTokens,
907
+ cachedInputTokens: this.usage.cachedInputTokens + call.usage.cachedInputTokens,
908
+ outputTokens: this.usage.outputTokens + call.usage.outputTokens,
909
+ reasoningTokens: this.usage.reasoningTokens + call.usage.reasoningTokens,
910
+ };
911
+ }
912
+ toString() {
913
+ const u = this.usage;
914
+ const cached = u.cachedInputTokens ? ` (${compact(u.cachedInputTokens)} cached)` : '';
915
+ return (`${this.calls} call${this.calls === 1 ? '' : 's'} · ${compact(u.inputTokens)} in${cached}` +
916
+ ` · ${compact(u.outputTokens)} out`);
917
+ }
918
+ }
919
+ /** 950, 12.3k, 1.8M. */
920
+ export function compact(n) {
921
+ if (n < 1000) {
922
+ return String(n);
923
+ }
924
+ if (n < 1e6) {
925
+ return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
926
+ }
927
+ return `${(n / 1e6).toFixed(n < 1e7 ? 2 : 1)}M`;
928
+ }
929
+ /**
930
+ * Copilot's own per-model totals from the last `session.shutdown` in a session's
931
+ * events - cumulative over every resume of it. Reasoning folded into output,
932
+ * as everywhere in zen.
933
+ */
934
+ export function shutdownModels(events) {
935
+ const line = events
936
+ .split('\n')
937
+ .reverse()
938
+ .find((l) => l.includes('"session.shutdown"'));
939
+ if (!line) {
940
+ return undefined;
941
+ }
942
+ let metrics;
943
+ try {
944
+ metrics = JSON.parse(line).data?.modelMetrics ?? {};
945
+ }
946
+ catch {
947
+ return undefined;
948
+ }
949
+ return Object.entries(metrics).map(([model, m]) => {
950
+ const u = m.usage ?? {};
951
+ const reasoning = Number(u.reasoningTokens ?? 0);
952
+ return {
953
+ model,
954
+ calls: Number(m.requests?.count ?? 0),
955
+ usage: {
956
+ inputTokens: Number(u.inputTokens ?? 0),
957
+ cachedInputTokens: Number(u.cacheReadTokens ?? 0),
958
+ outputTokens: Number(u.outputTokens ?? 0) + reasoning,
959
+ reasoningTokens: reasoning,
960
+ },
961
+ };
962
+ });
963
+ }
964
+ export function sessionTotals(sessionId) {
965
+ const home = process.env.COPILOT_HOME ?? join(homedir(), '.copilot');
966
+ try {
967
+ return shutdownModels(readFileSync(join(home, 'session-state', sessionId, 'events.jsonl'), 'utf8'));
968
+ }
969
+ catch {
970
+ return undefined;
971
+ }
972
+ }
676
973
  /** Non-fatal, said once, because a competing file is a silent override. */
677
974
  export function providersWarning() {
678
975
  const configured = process.env.COPILOT_PROVIDERS_CONFIG;
@@ -0,0 +1,16 @@
1
+ import type { Provider } from './keys.ts';
2
+ export declare const PROVIDER_NAMES: ReadonlySet<string>;
3
+ /**
4
+ * `vertex/gemini-3.8-flash` splits; `gpt-5.6-sol` does not. Only a known provider
5
+ * counts as a prefix, because `google/gemini-3.8-flash` is also a perfectly good
6
+ * bare id on an endpoint that speaks publisher names.
7
+ *
8
+ * The colon form is the library's own — `openai:gpt-5.6-sol`, and with an api
9
+ * selector `openai/responses:gpt-5.6-sol` — which is what arrives when the ref
10
+ * came from `agents.yaml`.
11
+ */
12
+ export declare function splitRef(ref: string): {
13
+ provider?: Provider;
14
+ id: string;
15
+ };
16
+ //# sourceMappingURL=modelref.d.ts.map
@@ -0,0 +1,41 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Model refs as people type them
3
+ //
4
+ // `vertex/gemini-3.8-flash` from a person, `openai:gpt-5.6-sol` from
5
+ // `agents.yaml`. Read by `zen inspect ask` and by `zen meta`, so it lives with
6
+ // neither.
7
+ // ---------------------------------------------------------------------------
8
+ export const PROVIDER_NAMES = new Set([
9
+ 'openai',
10
+ 'anthropic',
11
+ 'google',
12
+ 'vertex',
13
+ 'openrouter',
14
+ ]);
15
+ /**
16
+ * `vertex/gemini-3.8-flash` splits; `gpt-5.6-sol` does not. Only a known provider
17
+ * counts as a prefix, because `google/gemini-3.8-flash` is also a perfectly good
18
+ * bare id on an endpoint that speaks publisher names.
19
+ *
20
+ * The colon form is the library's own — `openai:gpt-5.6-sol`, and with an api
21
+ * selector `openai/responses:gpt-5.6-sol` — which is what arrives when the ref
22
+ * came from `agents.yaml`.
23
+ */
24
+ export function splitRef(ref) {
25
+ const colon = ref.indexOf(':');
26
+ if (colon > 0) {
27
+ const head = ref.slice(0, colon).split('/')[0];
28
+ if (PROVIDER_NAMES.has(head)) {
29
+ return { provider: head, id: ref.slice(colon + 1) };
30
+ }
31
+ }
32
+ const slash = ref.indexOf('/');
33
+ if (slash < 0) {
34
+ return { id: ref };
35
+ }
36
+ const head = ref.slice(0, slash);
37
+ return PROVIDER_NAMES.has(head)
38
+ ? { provider: head, id: ref.slice(slash + 1) }
39
+ : { id: ref };
40
+ }
41
+ //# sourceMappingURL=modelref.js.map