@zenera/cli 1.1.25 → 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 (96) hide show
  1. package/README.md +13 -1
  2. package/dist/catalog.d.ts +20 -0
  3. package/dist/catalog.js +60 -0
  4. package/dist/commands/index.js +2 -0
  5. package/dist/commands/inspect.d.ts +5 -1
  6. package/dist/commands/inspect.js +54 -14
  7. package/dist/commands/memory.js +28 -3
  8. package/dist/commands/meta.js +24 -5
  9. package/dist/commands/models.js +2 -1
  10. package/dist/commands/sandbox.js +73 -3
  11. package/dist/commands/view.d.ts +8 -0
  12. package/dist/commands/view.js +114 -0
  13. package/dist/engine.d.ts +3 -1
  14. package/dist/engine.js +6 -0
  15. package/dist/eventlog.d.ts +5 -0
  16. package/dist/eventlog.js +25 -1
  17. package/dist/liveness.js +20 -2
  18. package/dist/main.js +0 -0
  19. package/dist/meta/editor.d.ts +1 -1
  20. package/dist/meta/editor.js +4 -21
  21. package/dist/meta/finetune/analyze-brief.d.ts +26 -0
  22. package/dist/meta/finetune/analyze-brief.js +131 -0
  23. package/dist/meta/finetune/apply-brief.d.ts +23 -0
  24. package/dist/meta/finetune/apply-brief.js +247 -0
  25. package/dist/meta/finetune/brief.d.ts +45 -0
  26. package/dist/meta/finetune/brief.js +158 -0
  27. package/dist/meta/finetune/command.d.ts +1 -1
  28. package/dist/meta/finetune/command.js +224 -46
  29. package/dist/meta/finetune/cost.d.ts +46 -0
  30. package/dist/meta/finetune/cost.js +105 -0
  31. package/dist/meta/finetune/dataset/types.d.ts +3 -1
  32. package/dist/meta/finetune/feedback.d.ts +14 -2
  33. package/dist/meta/finetune/feedback.js +22 -3
  34. package/dist/meta/finetune/guard.d.ts +29 -0
  35. package/dist/meta/finetune/guard.js +104 -0
  36. package/dist/meta/finetune/improvements.d.ts +50 -3
  37. package/dist/meta/finetune/improvements.js +290 -33
  38. package/dist/meta/finetune/live.d.ts +17 -7
  39. package/dist/meta/finetune/live.js +56 -10
  40. package/dist/meta/finetune/loop.js +59 -10
  41. package/dist/meta/finetune/recheck.d.ts +58 -0
  42. package/dist/meta/finetune/recheck.js +311 -0
  43. package/dist/meta/finetune/report.d.ts +9 -2
  44. package/dist/meta/finetune/report.js +551 -438
  45. package/dist/meta/finetune/sampler.js +8 -2
  46. package/dist/meta/finetune/train.d.ts +6 -2
  47. package/dist/meta/finetune/train.js +55 -21
  48. package/dist/meta/finetune/tui.d.ts +3 -3
  49. package/dist/meta/finetune/tui.js +37 -12
  50. package/dist/meta/finetune/tuning.d.ts +27 -1
  51. package/dist/meta/finetune/tuning.js +21 -2
  52. package/dist/meta/finetune/usage.d.ts +4 -0
  53. package/dist/meta/finetune/usage.js +35 -0
  54. package/dist/meta/finetune/view.d.ts +9 -0
  55. package/dist/meta/finetune/view.js +27 -0
  56. package/dist/meta/host.d.ts +2 -1
  57. package/dist/meta/host.js +2 -1
  58. package/dist/meta.d.ts +291 -0
  59. package/dist/meta.js +981 -0
  60. package/dist/resources.d.ts +215 -0
  61. package/dist/resources.js +856 -0
  62. package/dist/scaffold.d.ts +3 -4
  63. package/dist/scaffold.js +21 -9
  64. package/dist/trace.d.ts +5 -0
  65. package/dist/trace.js +54 -5
  66. package/dist/tui/app.js +19 -5
  67. package/dist/tui/links.d.ts +27 -0
  68. package/dist/tui/links.js +142 -0
  69. package/dist/tui/models.d.ts +3 -1
  70. package/dist/tui/models.js +65 -7
  71. package/dist/tui/sandbox.d.ts +10 -0
  72. package/dist/tui/sandbox.js +131 -0
  73. package/package.json +3 -2
  74. package/templates/editor/.github/copilot-instructions.md +29 -43
  75. package/templates/editor/.github/prompts/analyze.prompt.md +3 -1
  76. package/templates/editor/.github/prompts/finetune-apply.prompt.md +221 -42
  77. package/templates/editor/.github/prompts/project-review.prompt.md +1 -1
  78. package/templates/editor/.github/skills/zen-analyze-run/SKILL.md +162 -57
  79. package/templates/editor/.github/skills/zen-cli/references/finetune.md +74 -14
  80. package/templates/editor/.github/skills/zen-cli/references/inspect.md +16 -2
  81. package/templates/editor/.github/skills/zen-cli/references/projects.md +4 -6
  82. package/templates/editor/.github/skills/zen-cli/references/sandbox.md +25 -13
  83. package/templates/editor/.github/skills/zen-finetune/SKILL.md +74 -33
  84. package/templates/editor/.github/skills/zen-inspect/SKILL.md +14 -1
  85. package/templates/editor/.github/skills/zen-inspect-ask/SKILL.md +4 -3
  86. package/templates/editor/.github/skills/zen-instructions/SKILL.md +4 -12
  87. package/templates/editor/.github/skills/zen-memory/SKILL.md +64 -39
  88. package/templates/editor/.github/skills/zen-review/SKILL.md +6 -31
  89. package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +1 -2
  90. package/templates/editor/.github/skills/zen-review/scripts/review.sh +3 -12
  91. package/templates/editor/.github/skills/zen-sandbox/SKILL.md +2 -1
  92. package/templates/editor/.github/skills/zen-spec-sync/SKILL.md +2 -4
  93. package/templates/project/agents/fork-instructions.md +78 -28
  94. package/templates/project/agents/memory-instructions.md +222 -493
  95. package/templates/project/agents/tools-instructions.md +7 -0
  96. package/templates/editor/.github/skills/zen-memory/scripts/check-instructions.sh +0 -116
package/dist/meta.js ADDED
@@ -0,0 +1,981 @@
1
+ import { spawn as nodeSpawn } from 'node:child_process';
2
+ import { appendFileSync, closeSync, createWriteStream, existsSync, mkdirSync, openSync, readFileSync, readSync, statSync, writeFileSync, } from 'node:fs';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { homedir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { createInterface } from 'node:readline';
7
+ import { invokedAs } from "./args.js";
8
+ import { paths, readJson, writeJson } from "./home.js";
9
+ import { envOf } from "./keys.js";
10
+ import { duration } from "./narrate.js";
11
+ import { credentialError, cut, cyan, dim, note, pad, plain, red, styled, usageError, write, yellow, } from "./term.js";
12
+ import { formatInline, formatMarkdown } from "./tui/markdown.js";
13
+ import { boxWidth } from "./tui/wrap.js";
14
+ export const PROMPT_DIR = '.github/prompts';
15
+ const PROMPT_SUFFIX = '.prompt.md';
16
+ // ---------------------------------------------------------------------------
17
+ // The model
18
+ // ---------------------------------------------------------------------------
19
+ /** The variable a project's `.env` names it with — a zen ref, not a wire id. */
20
+ export const MODEL_ENV = 'ZENERA_META_MODEL';
21
+ /** Where the answer is looked for, best first. */
22
+ export const ORDER = [
23
+ 'flag',
24
+ 'shell',
25
+ 'env',
26
+ 'store',
27
+ 'project',
28
+ 'default',
29
+ ];
30
+ export function chooseModel(sources) {
31
+ const at = {
32
+ flag: sources.flag,
33
+ // `.env` only fills gaps, so an answer that survived it came from the shell
34
+ shell: sources.shell,
35
+ env: sources.shell ? undefined : sources.env,
36
+ store: sources.stored,
37
+ project: sources.project,
38
+ default: sources.fallback,
39
+ };
40
+ for (const from of ORDER) {
41
+ const ref = at[from]?.trim();
42
+ if (ref) {
43
+ return { ref, from };
44
+ }
45
+ }
46
+ return undefined;
47
+ }
48
+ export const SOURCE_LABELS = {
49
+ flag: '--model',
50
+ shell: `${MODEL_ENV} (shell)`,
51
+ env: `${MODEL_ENV} (.env)`,
52
+ store: 'zen meta model',
53
+ project: 'agents.yaml model:',
54
+ default: 'recommended',
55
+ };
56
+ // ---------------------------------------------------------------------------
57
+ // What to run when nobody said
58
+ //
59
+ // A first `zen meta` should work on a machine that has a key and nothing else,
60
+ // so the last resort is the best model the keys on hand can buy rather than an
61
+ // error. Deep tier deliberately: this agent is pointed at the project itself,
62
+ // and a cheap model reading a specification is a false economy.
63
+ // ---------------------------------------------------------------------------
64
+ /** First of each row is the default; the rest are what `--pick` offers beside it. */
65
+ export const RECOMMENDED = {
66
+ openai: ['gpt-5.6-sol', 'gpt-5.6-terra'],
67
+ anthropic: ['claude-opus-5', 'claude-sonnet-5'],
68
+ vertex: ['gemini-3.8-flash', 'gemini-3.5-flash-lite'],
69
+ google: ['gemini-3.8-flash', 'gemini-3.5-flash-lite'],
70
+ openrouter: ['anthropic/claude-opus-5', 'openai/gpt-5.6-sol'],
71
+ };
72
+ /** Which provider is tried first when several are held. */
73
+ const PREFERENCE = ['anthropic', 'openai', 'vertex', 'google', 'openrouter'];
74
+ /** The recommended ref for the first provider with a key, or for `only`. */
75
+ export function defaultRef(store, only) {
76
+ for (const provider of only ? [only] : PREFERENCE) {
77
+ const id = RECOMMENDED[provider]?.[0];
78
+ if (id && store.active(provider)) {
79
+ return `${provider}/${id}`;
80
+ }
81
+ }
82
+ return undefined;
83
+ }
84
+ const EMPTY = { version: 1 };
85
+ export const readMeta = () => readJson(paths.meta(), EMPTY);
86
+ export function writeMeta(file) {
87
+ writeJson(paths.meta(), file);
88
+ }
89
+ // ---------------------------------------------------------------------------
90
+ // Refs
91
+ // ---------------------------------------------------------------------------
92
+ const PROVIDER_NAMES = new Set(['openai', 'anthropic', 'google', 'vertex', 'openrouter']);
93
+ /**
94
+ * `vertex/gemini-3.8-flash` splits; `gpt-5.6-sol` does not. Only a known provider
95
+ * counts as a prefix, because `google/gemini-3.8-flash` is also a perfectly good
96
+ * bare id on an endpoint that speaks publisher names.
97
+ *
98
+ * The colon form is the library's own — `openai:gpt-5.6-sol`, and with an api
99
+ * selector `openai/responses:gpt-5.6-sol` — which is what arrives when the ref
100
+ * came from `agents.yaml`.
101
+ */
102
+ export function splitRef(ref) {
103
+ const colon = ref.indexOf(':');
104
+ if (colon > 0) {
105
+ const head = ref.slice(0, colon).split('/')[0];
106
+ if (PROVIDER_NAMES.has(head)) {
107
+ return { provider: head, id: ref.slice(colon + 1) };
108
+ }
109
+ }
110
+ const slash = ref.indexOf('/');
111
+ if (slash < 0) {
112
+ return { id: ref };
113
+ }
114
+ const head = ref.slice(0, slash);
115
+ return PROVIDER_NAMES.has(head)
116
+ ? { provider: head, id: ref.slice(slash + 1) }
117
+ : { id: ref };
118
+ }
119
+ /**
120
+ * The provider a misspelt prefix was reaching for, if that is what it is.
121
+ *
122
+ * `vertes/gemini-3.8-flash` is shaped exactly like the OpenRouter id
123
+ * `meta-llama/llama-4`, so an unknown prefix cannot be an error on its own —
124
+ * only one close enough to a real provider name to be a slip of the hand.
125
+ */
126
+ export function misspelledProvider(ref) {
127
+ const slash = ref.indexOf('/');
128
+ if (slash <= 0) {
129
+ return undefined;
130
+ }
131
+ const head = ref.slice(0, slash).toLowerCase();
132
+ if (PROVIDER_NAMES.has(head)) {
133
+ return undefined;
134
+ }
135
+ for (const name of PROVIDER_NAMES) {
136
+ // Two, so that a transposition — which Levenshtein counts twice — reads
137
+ // as the typo it is. No provider name is short enough for that to be loose.
138
+ if (distance(head, name) <= 2) {
139
+ return name;
140
+ }
141
+ }
142
+ return undefined;
143
+ }
144
+ /** Levenshtein, one row at a time. Both operands are a word long. */
145
+ function distance(a, b) {
146
+ if (Math.abs(a.length - b.length) > 2) {
147
+ return 3;
148
+ }
149
+ let row = Array.from({ length: b.length + 1 }, (_, i) => i);
150
+ for (let i = 1; i <= a.length; i++) {
151
+ const next = [i];
152
+ for (let j = 1; j <= b.length; j++) {
153
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
154
+ next[j] = Math.min(next[j - 1] + 1, row[j] + 1, row[j - 1] + cost);
155
+ }
156
+ row = next;
157
+ }
158
+ return row[b.length];
159
+ }
160
+ // ---------------------------------------------------------------------------
161
+ // Wiring
162
+ // ---------------------------------------------------------------------------
163
+ const TYPES = {
164
+ openai: 'openai',
165
+ anthropic: 'anthropic',
166
+ google: 'openai',
167
+ vertex: 'openai',
168
+ openrouter: 'openai',
169
+ };
170
+ const BASE_URLS = {
171
+ openai: 'https://api.openai.com/v1',
172
+ anthropic: 'https://api.anthropic.com',
173
+ google: 'https://generativelanguage.googleapis.com/v1beta/openai/',
174
+ openrouter: 'https://openrouter.ai/api/v1',
175
+ };
176
+ /**
177
+ * Copilot offers its tools as OpenAI *custom* tools, which the completions API
178
+ * rejects outright — `400 Invalid value: 'custom'`. Only the responses API
179
+ * takes them, and only the reasoning models serve it, so the wire API follows
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.
182
+ */
183
+ export function wireApi(provider, id) {
184
+ return provider === 'openai' && !/^(gpt-4|gpt-3\.5|chatgpt-)/.test(id)
185
+ ? 'responses'
186
+ : undefined;
187
+ }
188
+ /**
189
+ * Turns a zen key entry and a model id into copilot's environment.
190
+ *
191
+ * `COPILOT_PROVIDER_BASE_URL` is what activates BYOK at all, so every provider
192
+ * gets one — including OpenAI, whose url copilot would otherwise have reached
193
+ * through a GitHub account we are deliberately not using.
194
+ */
195
+ export function wire(store, entry, id) {
196
+ const provider = entry.provider;
197
+ const env = {};
198
+ const secret = [];
199
+ const warnings = [];
200
+ env.COPILOT_PROVIDER_TYPE = TYPES[provider];
201
+ env.COPILOT_MODEL = id;
202
+ if (provider === 'vertex') {
203
+ const project = store.projectOf(entry)?.id;
204
+ if (!project) {
205
+ throw credentialError(`no GCP project on ${entry.provider}/${entry.name}`, 'set one: zen key add vertex --project <id>');
206
+ }
207
+ const location = entry.location?.trim() || 'global';
208
+ if (location === 'global') {
209
+ warnings.push('location `global` adds about ten seconds of cold start');
210
+ }
211
+ env.COPILOT_PROVIDER_BASE_URL = `https://aiplatform.googleapis.com/v1/projects/${project}/locations/${location}/endpoints/openapi`;
212
+ // Vertex speaks publisher names on the wire and knows nothing of zen's
213
+ // provider prefix; the bare id stays as the model *id* so copilot can
214
+ // still match its catalogue for token limits and tool support.
215
+ env.COPILOT_PROVIDER_WIRE_MODEL = id.includes('/') ? id : `google/${id}`;
216
+ env.COPILOT_PROVIDER_MODEL_ID = id.includes('/') ? id.slice(id.indexOf('/') + 1) : id;
217
+ }
218
+ else {
219
+ const url = BASE_URLS[provider];
220
+ if (!url) {
221
+ throw credentialError(`no BYOK endpoint known for ${provider}`);
222
+ }
223
+ env.COPILOT_PROVIDER_BASE_URL = url;
224
+ }
225
+ const api = wireApi(provider, id);
226
+ if (api) {
227
+ env.COPILOT_PROVIDER_WIRE_API = api;
228
+ }
229
+ if (entry.holds === 'file') {
230
+ // A Google access token is good for an hour and a session is not, so
231
+ // copilot is told how to mint one rather than handed one that will be
232
+ // stale by the time it matters.
233
+ env.COPILOT_PROVIDER_API_KEY_COMMAND = `${invokedAs('zen')} key token ${entry.provider}`;
234
+ env[envOf(entry)] = store.fileOf(entry);
235
+ }
236
+ else {
237
+ env.COPILOT_PROVIDER_API_KEY = store.reveal(entry);
238
+ secret.push('COPILOT_PROVIDER_API_KEY');
239
+ }
240
+ return { provider, model: env.COPILOT_PROVIDER_WIRE_MODEL ?? id, env, secret, warnings };
241
+ }
242
+ /** Values a `--dry-run` may print. A key is four characters and an apology. */
243
+ export function masked(env, secret) {
244
+ return Object.entries(env)
245
+ .sort(([a], [b]) => a.localeCompare(b))
246
+ .map(([name, value]) => `${name}=${secret.includes(name) ? '••••' : value}`);
247
+ }
248
+ // ---------------------------------------------------------------------------
249
+ // Stored prompts
250
+ //
251
+ // Copilot reads `AGENTS.md`, `.github/skills/` and `.github/agents/`, but it
252
+ // has no notion of `.github/prompts/*.prompt.md` — the files an editor offers
253
+ // as slash commands. Reading one and handing over its body is the whole of
254
+ // `zen meta run`, and it is why that subcommand exists.
255
+ // ---------------------------------------------------------------------------
256
+ /** `/project-review`, `project-review`, `project-review.prompt.md` or a path. */
257
+ export function promptPath(dir, name) {
258
+ const bare = name.replace(/^\//, '');
259
+ if (bare.includes('/') || bare.endsWith('.md')) {
260
+ return `${dir}/${bare}`;
261
+ }
262
+ return `${dir}/${PROMPT_DIR}/${bare}${PROMPT_SUFFIX}`;
263
+ }
264
+ /**
265
+ * Frontmatter is the editor's business — `mode`, `tools`, `description` — and
266
+ * none of it means anything to copilot, so only the body is sent. `description`
267
+ * is kept for the one line of narration that says which prompt is running.
268
+ */
269
+ export function readPrompt(path, name, text) {
270
+ let body = text;
271
+ let description;
272
+ const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
273
+ if (match) {
274
+ body = text.slice(match[0].length);
275
+ const found = /^description:\s*(.+)$/m.exec(match[1]);
276
+ description = found?.[1].trim().replace(/^['"]|['"]$/g, '');
277
+ }
278
+ return { name, path, description, body: body.trim() };
279
+ }
280
+ export async function loadPrompt(dir, name) {
281
+ const path = promptPath(dir, name);
282
+ if (!existsSync(path)) {
283
+ const known = await listPrompts(dir);
284
+ throw usageError(`no prompt named ${name.replace(/^\//, '')}`, known.length > 0
285
+ ? `try: ${known.map((p) => `/${p}`).join(', ')}`
286
+ : `put one in ${PROMPT_DIR}/`);
287
+ }
288
+ return readPrompt(path, name.replace(/^\//, ''), await readFile(path, 'utf8'));
289
+ }
290
+ export async function listPrompts(dir) {
291
+ const { readdir } = await import('node:fs/promises');
292
+ try {
293
+ const names = await readdir(`${dir}/${PROMPT_DIR}`);
294
+ return names
295
+ .filter((n) => n.endsWith(PROMPT_SUFFIX))
296
+ .map((n) => n.slice(0, -PROMPT_SUFFIX.length))
297
+ .sort();
298
+ }
299
+ catch {
300
+ return [];
301
+ }
302
+ }
303
+ /**
304
+ * A `copilot` already installed is used as it is; otherwise `npx` fetches one.
305
+ * Note that finding the name on `PATH` is not the same as finding the CLI —
306
+ * an editor may have put a shim there that installs it on first run — so this
307
+ * only decides how to launch, never whether it will work.
308
+ */
309
+ export function locate(has = (cmd) => onPath(cmd)) {
310
+ return has('copilot')
311
+ ? { command: 'copilot', args: [], from: 'path' }
312
+ : { command: 'npx', args: ['--yes', '@github/copilot'], from: 'npx' };
313
+ }
314
+ function onPath(cmd) {
315
+ const dirs = (process.env.PATH ?? '').split(':').filter(Boolean);
316
+ return dirs.some((d) => existsSync(`${d}/${cmd}`));
317
+ }
318
+ export const terminalSink = {
319
+ answer: (text) => write(text),
320
+ narrate: (line) => note(line),
321
+ warn: (line) => note(red(line)),
322
+ };
323
+ /** Rows of narration the window keeps on screen. */
324
+ export const WINDOW_ROWS = 8;
325
+ const BOX = { tl: '╭', tr: '╮', bl: '╰', br: '╯', h: '─', v: '│' };
326
+ /** How often the border is redrawn — the wave's frame, not the clock's. */
327
+ export const FRAME_MS = 80;
328
+ // Dimmest first, crest last. Written as escapes rather than `styleText` because
329
+ // a named colour has no ramp, which is also why `styled()` has to be asked.
330
+ const WAVE = [
331
+ '\u001b[0;38;5;24m',
332
+ '\u001b[0;38;5;31m',
333
+ '\u001b[0;38;5;38m',
334
+ '\u001b[0;38;5;45m',
335
+ '\u001b[0;38;5;51m',
336
+ '\u001b[1;38;5;159m',
337
+ '\u001b[1;38;5;231m',
338
+ ];
339
+ const RESET = '\u001b[0m';
340
+ /** Cells each tone of the tail covers, so the crest reads as a band, not a dot. */
341
+ const WAVE_TAIL = 2;
342
+ /** Cells the crest travels per frame — a lap of a wide window in a few seconds. */
343
+ export const WAVE_STEP = 3;
344
+ /**
345
+ * How brightly one cell of the border is lit, given where the crest has got to.
346
+ *
347
+ * `position` counts clockwise from the top left corner, so one index walks the
348
+ * whole ring and the wave rounds the corners without a seam. Everything the
349
+ * tail has passed sits at the dimmest tone, which is the border's resting
350
+ * colour: the ring is always drawn, only the crest moves.
351
+ */
352
+ export function toneAt(position, head, span) {
353
+ const behind = (((head - position) % span) + span) % span;
354
+ const step = Math.floor(behind / WAVE_TAIL);
355
+ return step < WAVE.length ? WAVE.length - 1 - step : 0;
356
+ }
357
+ /**
358
+ * A run of border cells, coloured by the wave and emitting an escape only where
359
+ * the tone changes — a frame of the whole ring is a handful of them.
360
+ *
361
+ * `from` is the ring position of the first cell and `step` which way round the
362
+ * ring the cells run, because the bottom rule is printed left to right while
363
+ * the ring travels it right to left.
364
+ */
365
+ function lit(cells, from, step, head, span) {
366
+ let out = '';
367
+ let tone = -1;
368
+ for (let i = 0; i < cells.length; i++) {
369
+ const next = toneAt(from + i * step, head, span);
370
+ if (next !== tone) {
371
+ out += WAVE[next];
372
+ tone = next;
373
+ }
374
+ out += cells[i];
375
+ }
376
+ return `${out}${RESET}`;
377
+ }
378
+ /**
379
+ * The bottom rule as cells, with the status label set into it near the left.
380
+ *
381
+ * Waiting belongs to the frame rather than to the narration: a row of its own
382
+ * costs a step of what the agent is doing every time, and it settles just
383
+ * under the last line, which is where the eye already is. Near the left, where
384
+ * reading starts, and not so near that it displaces the corner.
385
+ */
386
+ export function footerCells(label, width) {
387
+ const room = width - 6;
388
+ const text = label && room > 0 ? [...label].slice(0, room) : [];
389
+ const cells = text.length > 0 ? [BOX.bl, BOX.h, ' ', ...text, ' '] : [BOX.bl];
390
+ while (cells.length < width - 1) {
391
+ cells.push(BOX.h);
392
+ }
393
+ return [...cells, BOX.br];
394
+ }
395
+ /**
396
+ * Narration held to a fixed number of rows that rewrite themselves in place.
397
+ *
398
+ * A long run is hundreds of tool calls, and printing each as its own line
399
+ * scrolls the question, the model line and every warning off the top of the
400
+ * screen — by the end the terminal holds a transcript nobody asked for and the
401
+ * answer is somewhere in the middle of it. Only the last few steps say what it
402
+ * is doing now, which is the only thing narration is for; the rest is in the
403
+ * session copilot recorded.
404
+ *
405
+ * Warnings are not narration: they leave the window and stay on the screen.
406
+ *
407
+ * Without a terminal there is nothing to rewrite over, so every line is its
408
+ * own — which is what a CI log wants anyway.
409
+ */
410
+ export function windowSink(rows = WINDOW_ROWS) {
411
+ if (!process.stderr.isTTY) {
412
+ return terminalSink;
413
+ }
414
+ const kept = [];
415
+ const colour = styled();
416
+ let label = '';
417
+ let head = 0;
418
+ let painted = 0;
419
+ // The frame must stay under the viewport: a block taller than the screen
420
+ // scrolls its own top away, and then the cursor-up erase falls short and
421
+ // strands a copy of every repaint. The two border rows count towards it.
422
+ const height = () => Math.max(1, Math.min(rows, (process.stderr.rows ?? 24) - 4));
423
+ const columns = () => Math.max(20, (process.stderr.columns ?? 80) - 1);
424
+ const innerOf = () => columns() - 4;
425
+ const erase = () => {
426
+ if (painted > 0) {
427
+ process.stderr.write(`\u001b[${painted}A\u001b[0J`);
428
+ painted = 0;
429
+ }
430
+ };
431
+ /** The whole frame, border lit by the wave, every row exactly one row. */
432
+ const frameOf = (show, inner) => {
433
+ const width = inner + 4;
434
+ // Clockwise from the top left: across the top, down the right, back
435
+ // along the bottom, up the left. One ring, so one index addresses it.
436
+ const span = 2 * width + 2 * show.length;
437
+ const top = [BOX.tl, ...Array(width - 2).fill(BOX.h), BOX.tr];
438
+ const bottom = footerCells(label, width);
439
+ const edge = (position) => colour ? lit([BOX.v], position, 1, head, span) : cyan(BOX.v);
440
+ return [
441
+ colour ? lit(top, 0, 1, head, span) : cyan(top.join('')),
442
+ ...show.map((line, r) => `${edge(span - 1 - r)} ${pad(cut(line, inner), inner)} ${edge(width + r)}`),
443
+ colour
444
+ ? lit(bottom, 2 * width + show.length - 1, -1, head, span)
445
+ : cyan(bottom.join('')),
446
+ ];
447
+ };
448
+ const paint = () => {
449
+ const show = kept.slice(-height());
450
+ if (show.length === 0 && label === '') {
451
+ erase();
452
+ return;
453
+ }
454
+ // The wave wants a frame to run round before there is anything to say.
455
+ const lines = frameOf(show.length > 0 ? show : [''], innerOf());
456
+ if (painted === lines.length) {
457
+ // Erasing before a repaint is what flickers; overwriting in place
458
+ // does not, and at frame rate the whole border has to be redrawn.
459
+ process.stderr.write(`\u001b[${painted}A${lines.map((l) => `\r${l}\u001b[0K\n`).join('')}`);
460
+ return;
461
+ }
462
+ erase();
463
+ process.stderr.write(lines.map((l) => `${l}\n`).join(''));
464
+ painted = lines.length;
465
+ };
466
+ return {
467
+ answer: (text) => {
468
+ erase();
469
+ write(text);
470
+ },
471
+ narrate: (line) => {
472
+ kept.push(...line.split('\n'));
473
+ if (kept.length > height() * 4) {
474
+ kept.splice(0, kept.length - height());
475
+ }
476
+ paint();
477
+ },
478
+ warn: (line) => {
479
+ erase();
480
+ note(red(line));
481
+ paint();
482
+ },
483
+ status: (line) => {
484
+ label = line;
485
+ head += WAVE_STEP;
486
+ paint();
487
+ },
488
+ close: erase,
489
+ };
490
+ }
491
+ /**
492
+ * The answer in the same box `zen run` draws it in: bounded width, rounded
493
+ * rule, formatted with full markdown styling (bold, italics, code, lists, quotes, tables).
494
+ *
495
+ * Redirected output gets the text and nothing else — `zen meta run ... > out.md`
496
+ * is meant to produce a file you can read, not one with a border down its side.
497
+ */
498
+ export function answerBox(text, columns = process.stdout.columns ?? 80) {
499
+ if (!process.stdout.isTTY) {
500
+ return text.split('\n');
501
+ }
502
+ const outer = boxWidth(text, columns);
503
+ const inner = outer - 4;
504
+ const body = formatMarkdown(text, inner);
505
+ const rule = BOX.h.repeat(outer - 2);
506
+ return [
507
+ '',
508
+ dim(`${BOX.tl}${rule}${BOX.tr}`),
509
+ ...body.map((l) => `${dim(BOX.v)} ${pad(l, inner)} ${dim(BOX.v)}`),
510
+ dim(`${BOX.bl}${rule}${BOX.br}`),
511
+ '',
512
+ ];
513
+ }
514
+ /** `<project>/.tmp/logs/meta.<when>.log`, opened for append. */
515
+ export function openLog(dir, now = new Date()) {
516
+ const two = (n) => String(n).padStart(2, '0');
517
+ const stamp = `${now.getFullYear()}${two(now.getMonth() + 1)}${two(now.getDate())}` +
518
+ `${two(now.getHours())}${two(now.getMinutes())}${two(now.getSeconds())}`;
519
+ const folder = `${dir}/.tmp/logs`;
520
+ mkdirSync(folder, { recursive: true });
521
+ const path = `${folder}/meta.${stamp}.log`;
522
+ const answerPath = `${folder}/meta.${stamp}.md`;
523
+ const file = createWriteStream(path, { flags: 'a' });
524
+ return {
525
+ path,
526
+ answerPath,
527
+ line: (text) => {
528
+ file.write(`${plain(text)}\n`);
529
+ },
530
+ saveAnswer: (text) => {
531
+ writeFileSync(answerPath, text.endsWith('\n') ? text : `${text}\n`);
532
+ },
533
+ close: () => {
534
+ file.end();
535
+ },
536
+ };
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
+ }
573
+ /** Everything the sink is told, kept by the log too — and the detail only there. */
574
+ function tee(sink, log) {
575
+ return {
576
+ answer: (text) => sink.answer(text),
577
+ narrate: (line) => {
578
+ log.line(line);
579
+ sink.narrate(line);
580
+ },
581
+ detail: (line) => log.line(line),
582
+ warn: (line) => {
583
+ log.line(line);
584
+ sink.warn(line);
585
+ },
586
+ status: (line) => sink.status?.(line),
587
+ close: () => sink.close?.(),
588
+ };
589
+ }
590
+ /**
591
+ * Folds one event into the outcome and says what, if anything, to show for it.
592
+ *
593
+ * `assistant.message` carries both the running commentary and the final word;
594
+ * what separates them is `toolRequests`, so the last message that asked for no
595
+ * tool is the answer and the rest are narration.
596
+ */
597
+ export function absorb(event, out, sink, width) {
598
+ const data = (event.data ?? {});
599
+ switch (event.type) {
600
+ case 'assistant.message': {
601
+ const content = String(data.content ?? '').trim();
602
+ const requests = Array.isArray(data.toolRequests) ? data.toolRequests : [];
603
+ if (requests.length === 0) {
604
+ out.answer = content;
605
+ }
606
+ else if (content) {
607
+ sink.narrate(cut(formatInline(content.replace(/\s+/g, ' ')), width));
608
+ }
609
+ break;
610
+ }
611
+ case 'tool.execution_start': {
612
+ const args = (data.arguments ?? {});
613
+ const what = String(args.command ?? args.description ?? data.toolName ?? '').trim();
614
+ // Step-by-step belongs in the log: on screen it is a wall of `$`
615
+ // saying less than the one line of commentary above it.
616
+ sink.detail?.(`$ ${what.replace(/\s+/g, ' ')}`);
617
+ break;
618
+ }
619
+ case 'tool.execution_complete': {
620
+ if (data.success === false) {
621
+ sink.detail?.(` ${String(data.toolName ?? 'the tool')} failed`);
622
+ }
623
+ break;
624
+ }
625
+ case 'session.tools_updated': {
626
+ if (data.model) {
627
+ sink.detail?.(`model ${String(data.model)}`);
628
+ }
629
+ break;
630
+ }
631
+ case 'session.error': {
632
+ sink.warn(String(data.message ?? 'the session failed'));
633
+ break;
634
+ }
635
+ case 'model.call_failure': {
636
+ const status = data.statusCode ? ` (${String(data.statusCode)})` : '';
637
+ sink.warn(`${cut(String(data.errorMessage ?? 'the model call failed'), width)}${status}`);
638
+ break;
639
+ }
640
+ case 'result': {
641
+ out.exitCode = Number(event.exitCode ?? 0);
642
+ out.sessionId = event.sessionId;
643
+ out.usage = event.usage;
644
+ break;
645
+ }
646
+ default:
647
+ break;
648
+ }
649
+ out.events.push(event);
650
+ }
651
+ export async function launch(opts) {
652
+ const shown = opts.sink ?? windowSink();
653
+ const sink = opts.log ? tee(shown, opts.log) : shown;
654
+ const width = opts.width ?? Math.max(40, (process.stderr.columns ?? 100) - 4);
655
+ const start = opts.spawn ?? nodeSpawn;
656
+ const out = { answer: '', exitCode: 0, events: [] };
657
+ const child = start(opts.binary.command, [...opts.binary.args, ...opts.args], {
658
+ cwd: opts.cwd,
659
+ env: { ...process.env, ...opts.env },
660
+ stdio: ['ignore', 'pipe', 'pipe'],
661
+ shell: false,
662
+ });
663
+ const stop = (signal) => () => {
664
+ child.kill(signal);
665
+ };
666
+ const onInt = stop('SIGINT');
667
+ const onTerm = stop('SIGTERM');
668
+ process.on('SIGINT', onInt);
669
+ process.on('SIGTERM', onTerm);
670
+ // The first model call says nothing for as long as it takes, and silence
671
+ // after the model line is indistinguishable from a hang.
672
+ const began = Date.now();
673
+ const beat = setInterval(() => {
674
+ const extra = opts.status?.();
675
+ sink.status?.(`working: ${duration(Date.now() - began)}${extra ? ` · ${extra}` : ''}`);
676
+ }, FRAME_MS);
677
+ beat.unref();
678
+ child.stderr?.on('data', (chunk) => {
679
+ const text = chunk.toString().trimEnd();
680
+ if (text) {
681
+ sink.narrate(dim(text));
682
+ }
683
+ });
684
+ try {
685
+ const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
686
+ for await (const line of lines) {
687
+ if (!line.trim()) {
688
+ continue;
689
+ }
690
+ let event;
691
+ try {
692
+ event = JSON.parse(line);
693
+ }
694
+ catch {
695
+ // Not ours to interpret, but losing it is worse than showing it.
696
+ sink.narrate(line);
697
+ continue;
698
+ }
699
+ absorb(event, out, sink, width);
700
+ }
701
+ const code = await new Promise((resolve) => {
702
+ child.on('close', (value, signal) => resolve(value ?? (signal ? 1 : 0)));
703
+ });
704
+ if (out.exitCode === 0 && code !== 0) {
705
+ out.exitCode = code;
706
+ }
707
+ }
708
+ finally {
709
+ clearInterval(beat);
710
+ process.off('SIGINT', onInt);
711
+ process.off('SIGTERM', onTerm);
712
+ // The answer is written by the caller, so the window has to be gone first.
713
+ sink.close?.();
714
+ }
715
+ return out;
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
+ }
973
+ /** Non-fatal, said once, because a competing file is a silent override. */
974
+ export function providersWarning() {
975
+ const configured = process.env.COPILOT_PROVIDERS_CONFIG;
976
+ const path = configured ?? `${process.env.HOME ?? ''}/.copilot/providers.json`;
977
+ return existsSync(path)
978
+ ? yellow(`${path} may override the provider zen just wired`)
979
+ : undefined;
980
+ }
981
+ //# sourceMappingURL=meta.js.map