@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.
- package/README.md +65 -0
- package/dist/ask.js +28 -5
- package/dist/banner.js +2 -2
- package/dist/batch.d.ts +6 -0
- package/dist/batch.js +76 -1
- package/dist/catalog.d.ts +31 -1
- package/dist/catalog.js +140 -7
- package/dist/commands/index.js +2 -0
- package/dist/commands/inspect.d.ts +52 -2
- package/dist/commands/inspect.js +220 -31
- package/dist/commands/memory.js +156 -32
- package/dist/commands/meta.js +333 -24
- package/dist/commands/models.js +97 -12
- package/dist/commands/open.js +4 -4
- package/dist/commands/run.js +38 -4
- package/dist/commands/sandbox.js +148 -16
- package/dist/commands/view.d.ts +8 -0
- package/dist/commands/view.js +114 -0
- package/dist/engine.d.ts +8 -1
- package/dist/engine.js +7 -0
- package/dist/eventlog.d.ts +64 -0
- package/dist/eventlog.js +170 -0
- package/dist/liveness.d.ts +7 -0
- package/dist/liveness.js +35 -3
- package/dist/main.js +0 -0
- package/dist/meta/editor.d.ts +9 -0
- package/dist/meta/editor.js +43 -0
- package/dist/meta/finetune/analyze-brief.d.ts +26 -0
- package/dist/meta/finetune/analyze-brief.js +131 -0
- package/dist/meta/finetune/apply-brief.d.ts +23 -0
- package/dist/meta/finetune/apply-brief.js +247 -0
- package/dist/meta/finetune/brief.d.ts +45 -0
- package/dist/meta/finetune/brief.js +158 -0
- package/dist/meta/finetune/children.d.ts +8 -0
- package/dist/meta/finetune/children.js +76 -0
- package/dist/meta/finetune/command.d.ts +30 -0
- package/dist/meta/finetune/command.js +516 -0
- package/dist/meta/finetune/cost.d.ts +46 -0
- package/dist/meta/finetune/cost.js +105 -0
- package/dist/meta/finetune/dataset/anchors.d.ts +41 -0
- package/dist/meta/finetune/dataset/anchors.js +239 -0
- package/dist/meta/finetune/dataset/changes.d.ts +36 -0
- package/dist/meta/finetune/dataset/changes.js +319 -0
- package/dist/meta/finetune/dataset/command.d.ts +17 -0
- package/dist/meta/finetune/dataset/command.js +1011 -0
- package/dist/meta/finetune/dataset/drift.d.ts +33 -0
- package/dist/meta/finetune/dataset/drift.js +71 -0
- package/dist/meta/finetune/dataset/export.d.ts +26 -0
- package/dist/meta/finetune/dataset/export.js +51 -0
- package/dist/meta/finetune/dataset/index.d.ts +2 -0
- package/dist/meta/finetune/dataset/index.js +2 -0
- package/dist/meta/finetune/dataset/sample.d.ts +50 -0
- package/dist/meta/finetune/dataset/sample.js +197 -0
- package/dist/meta/finetune/dataset/schema.d.ts +33 -0
- package/dist/meta/finetune/dataset/schema.js +216 -0
- package/dist/meta/finetune/dataset/store.d.ts +35 -0
- package/dist/meta/finetune/dataset/store.js +177 -0
- package/dist/meta/finetune/dataset/types.d.ts +127 -0
- package/dist/meta/finetune/dataset/types.js +39 -0
- package/dist/meta/finetune/failure.d.ts +15 -0
- package/dist/meta/finetune/failure.js +26 -0
- package/dist/meta/finetune/feedback.d.ts +57 -0
- package/dist/meta/finetune/feedback.js +104 -0
- package/dist/meta/finetune/guard.d.ts +29 -0
- package/dist/meta/finetune/guard.js +104 -0
- package/dist/meta/finetune/improvements.d.ts +98 -0
- package/dist/meta/finetune/improvements.js +501 -0
- package/dist/meta/finetune/index.d.ts +2 -0
- package/dist/meta/finetune/index.js +2 -0
- package/dist/meta/finetune/journal.d.ts +35 -0
- package/dist/meta/finetune/journal.js +135 -0
- package/dist/meta/finetune/live.d.ts +84 -0
- package/dist/meta/finetune/live.js +272 -0
- package/dist/meta/finetune/loop.d.ts +3 -0
- package/dist/meta/finetune/loop.js +163 -0
- package/dist/meta/finetune/memories.d.ts +47 -0
- package/dist/meta/finetune/memories.js +154 -0
- package/dist/meta/finetune/recheck.d.ts +58 -0
- package/dist/meta/finetune/recheck.js +311 -0
- package/dist/meta/finetune/report.d.ts +40 -0
- package/dist/meta/finetune/report.js +1397 -0
- package/dist/meta/finetune/retry.d.ts +24 -0
- package/dist/meta/finetune/retry.js +96 -0
- package/dist/meta/finetune/sampler.d.ts +18 -0
- package/dist/meta/finetune/sampler.js +47 -0
- package/dist/meta/finetune/system.d.ts +27 -0
- package/dist/meta/finetune/system.js +132 -0
- package/dist/meta/finetune/train.d.ts +43 -0
- package/dist/meta/finetune/train.js +425 -0
- package/dist/meta/finetune/tui.d.ts +16 -0
- package/dist/meta/finetune/tui.js +316 -0
- package/dist/meta/finetune/tuning.d.ts +193 -0
- package/dist/meta/finetune/tuning.js +308 -0
- package/dist/meta/finetune/usage.d.ts +66 -0
- package/dist/meta/finetune/usage.js +260 -0
- package/dist/meta/finetune/view.d.ts +9 -0
- package/dist/meta/finetune/view.js +27 -0
- package/dist/meta/host.d.ts +15 -0
- package/dist/meta/host.js +21 -0
- package/dist/meta/index.d.ts +11 -0
- package/dist/meta/index.js +11 -0
- package/dist/meta/launch.d.ts +81 -0
- package/dist/meta/launch.js +286 -0
- package/dist/meta/model.d.ts +44 -0
- package/dist/meta/model.js +119 -0
- package/dist/meta/prompts.d.ts +18 -0
- package/dist/meta/prompts.js +61 -0
- package/dist/meta/relay.d.ts +20 -0
- package/dist/meta/relay.js +123 -0
- package/dist/meta/tokens.d.ts +52 -0
- package/dist/meta/tokens.js +226 -0
- package/dist/meta/window.d.ts +61 -0
- package/dist/meta/window.js +198 -0
- package/dist/meta/wire.d.ts +42 -0
- package/dist/meta/wire.js +129 -0
- package/dist/meta.d.ts +71 -1
- package/dist/meta.js +303 -6
- package/dist/modelref.d.ts +16 -0
- package/dist/modelref.js +41 -0
- package/dist/narrate.js +5 -2
- package/dist/podman.d.ts +26 -0
- package/dist/podman.js +83 -3
- package/dist/projects.d.ts +0 -5
- package/dist/projects.js +2 -15
- package/dist/resources.d.ts +215 -0
- package/dist/resources.js +856 -0
- package/dist/sandbox.d.ts +2 -0
- package/dist/sandbox.js +10 -3
- package/dist/scaffold.d.ts +15 -7
- package/dist/scaffold.js +24 -67
- package/dist/session.js +4 -29
- package/dist/term.d.ts +2 -0
- package/dist/term.js +11 -2
- package/dist/trace.d.ts +5 -0
- package/dist/trace.js +107 -7
- package/dist/tui/app.js +19 -5
- package/dist/tui/links.d.ts +27 -0
- package/dist/tui/links.js +142 -0
- package/dist/tui/models.d.ts +35 -0
- package/dist/tui/models.js +447 -0
- package/dist/tui/sandbox.d.ts +10 -0
- package/dist/tui/sandbox.js +131 -0
- package/dist/tui/wrap.d.ts +5 -0
- package/dist/tui/wrap.js +12 -0
- package/dist/usage.d.ts +78 -0
- package/dist/usage.js +51 -0
- package/package.json +4 -2
- package/templates/editor/.github/copilot-instructions.md +60 -145
- package/templates/editor/.github/prompts/analyze.prompt.md +33 -0
- package/templates/editor/.github/prompts/dataset.prompt.md +108 -0
- package/templates/editor/.github/prompts/finetune-apply.prompt.md +270 -0
- package/templates/editor/.github/prompts/finetune.prompt.md +691 -198
- package/templates/editor/.github/prompts/new-agent.prompt.md +3 -1
- package/templates/editor/.github/prompts/new-skill.prompt.md +2 -0
- package/templates/editor/.github/prompts/project-review.prompt.md +5 -4
- package/templates/editor/.github/skills/zen-analyze-run/SKILL.md +588 -0
- package/templates/editor/.github/skills/zen-cli/SKILL.md +9 -1
- package/templates/editor/.github/skills/zen-cli/references/dataset.md +34 -0
- package/templates/editor/.github/skills/zen-cli/references/finetune.md +137 -0
- package/templates/editor/.github/skills/zen-cli/references/inspect.md +63 -11
- package/templates/editor/.github/skills/zen-cli/references/memory.md +33 -10
- package/templates/editor/.github/skills/zen-cli/references/meta.md +61 -0
- package/templates/editor/.github/skills/zen-cli/references/models.md +5 -1
- package/templates/editor/.github/skills/zen-cli/references/projects.md +4 -6
- package/templates/editor/.github/skills/zen-cli/references/rag.md +24 -6
- package/templates/editor/.github/skills/zen-cli/references/sandbox.md +35 -14
- package/templates/editor/.github/skills/zen-code-python/SKILL.md +123 -0
- package/templates/editor/.github/skills/zen-code-python/references/code_python.md +48 -0
- package/templates/editor/.github/skills/zen-dataset/SKILL.md +253 -0
- package/templates/editor/.github/skills/zen-finetune/SKILL.md +1492 -660
- package/templates/editor/.github/skills/zen-finetune/references/readme-template.md +268 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/batch.mjs +390 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/difficult.mjs +243 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/memory.mjs +280 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/next.mjs +398 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/report.mjs +797 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/select.mjs +354 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/usage.mjs +722 -0
- package/templates/editor/.github/skills/zen-inspect/SKILL.md +74 -26
- package/templates/editor/.github/skills/zen-inspect-ask/SKILL.md +271 -0
- package/templates/editor/.github/skills/zen-instructions/SKILL.md +4 -12
- package/templates/editor/.github/skills/zen-memory/SKILL.md +82 -53
- package/templates/editor/.github/skills/zen-rag-docs/SKILL.md +20 -8
- package/templates/editor/.github/skills/zen-review/SKILL.md +8 -31
- package/templates/editor/.github/skills/zen-review/scripts/check-paths.sh +5 -5
- package/templates/editor/.github/skills/zen-review/scripts/review.sh +3 -12
- package/templates/editor/.github/skills/zen-sandbox/SKILL.md +9 -4
- package/templates/editor/.github/skills/zen-sandbox-capacity/SKILL.md +2 -2
- package/templates/editor/.github/skills/zen-spec-sync/SKILL.md +2 -4
- package/templates/editor/.github/skills/zen-topology/SKILL.md +283 -0
- package/templates/editor/.vscode/settings.json +1 -0
- package/templates/project/agents/files-instructions.md +29 -20
- package/templates/project/agents/fork-instructions.md +78 -28
- package/templates/project/agents/memory-instructions.md +222 -494
- package/templates/project/agents/tools-instructions.md +14 -4
- package/templates/project/gitignore +2 -0
- package/templates/editor/.github/skills/zen-finetune/scripts/collect.sh +0 -432
- package/templates/editor/.github/skills/zen-finetune/scripts/sample.sh +0 -199
- package/templates/editor/.github/skills/zen-memory/scripts/check-instructions.sh +0 -116
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { vertexEndpoint } from '@zenera/neo';
|
|
2
|
+
import { GoogleAuth } from 'google-auth-library';
|
|
3
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
4
|
+
import { createServer } from 'node:http';
|
|
5
|
+
import { Readable } from 'node:stream';
|
|
6
|
+
const VERTEX_VERSION = 'vertex-2023-10-16';
|
|
7
|
+
export async function startVertexRelay(spec) {
|
|
8
|
+
const auth = new GoogleAuth({
|
|
9
|
+
keyFile: spec.keyFile,
|
|
10
|
+
scopes: ['https://www.googleapis.com/auth/cloud-platform'],
|
|
11
|
+
});
|
|
12
|
+
const at = vertexEndpoint({
|
|
13
|
+
project: spec.project,
|
|
14
|
+
location: spec.location,
|
|
15
|
+
token: async () => {
|
|
16
|
+
const token = await auth.getAccessToken();
|
|
17
|
+
if (!token) {
|
|
18
|
+
throw new Error(`${spec.keyFile} minted no access token`);
|
|
19
|
+
}
|
|
20
|
+
return token;
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
return startRelay({ key: spec.key, base: at.base, token: at.token });
|
|
24
|
+
}
|
|
25
|
+
export async function startRelay(spec) {
|
|
26
|
+
const server = createServer((req, res) => {
|
|
27
|
+
relay(spec, req, res).catch((err) => {
|
|
28
|
+
fail(res, 502, 'api_error', `relay to vertex: ${err.message}`);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
await new Promise((done, reject) => {
|
|
32
|
+
server.once('error', reject);
|
|
33
|
+
server.listen(0, '127.0.0.1', done);
|
|
34
|
+
});
|
|
35
|
+
const { port } = server.address();
|
|
36
|
+
return {
|
|
37
|
+
url: `http://127.0.0.1:${port}`,
|
|
38
|
+
close: () => new Promise((done) => {
|
|
39
|
+
server.closeAllConnections();
|
|
40
|
+
server.close(() => done());
|
|
41
|
+
}),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async function relay(spec, req, res) {
|
|
45
|
+
if (!authorized(spec.key, req)) {
|
|
46
|
+
fail(res, 401, 'authentication_error', 'not this session’s relay key');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const path = new URL(req.url ?? '/', 'http://relay').pathname;
|
|
50
|
+
const counting = path === '/v1/messages/count_tokens';
|
|
51
|
+
if (req.method !== 'POST' || (path !== '/v1/messages' && !counting)) {
|
|
52
|
+
fail(res, 404, 'not_found_error', `vertex serves no ${req.method} ${path} for Claude`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = JSON.parse(await readBody(req));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
fail(res, 400, 'invalid_request_error', 'the body is not JSON');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const { model, ...rest } = parsed;
|
|
64
|
+
if (!model) {
|
|
65
|
+
fail(res, 400, 'invalid_request_error', 'no model in the body');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const url = counting
|
|
69
|
+
? `${spec.base}/publishers/anthropic/models/count-tokens:rawPredict`
|
|
70
|
+
: `${spec.base}/publishers/anthropic/models/${encodeURIComponent(model)}:${rest.stream ? 'streamRawPredict' : 'rawPredict'}`;
|
|
71
|
+
const body = counting ? parsed : { ...rest, anthropic_version: VERTEX_VERSION };
|
|
72
|
+
const abort = new AbortController();
|
|
73
|
+
res.on('close', () => {
|
|
74
|
+
if (!res.writableFinished) {
|
|
75
|
+
abort.abort();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
const beta = req.headers['anthropic-beta'];
|
|
79
|
+
const upstream = await fetch(url, {
|
|
80
|
+
method: 'POST',
|
|
81
|
+
headers: {
|
|
82
|
+
authorization: `Bearer ${await spec.token()}`,
|
|
83
|
+
'content-type': 'application/json',
|
|
84
|
+
...(typeof beta === 'string' ? { 'anthropic-beta': beta } : {}),
|
|
85
|
+
},
|
|
86
|
+
body: JSON.stringify(body),
|
|
87
|
+
signal: abort.signal,
|
|
88
|
+
});
|
|
89
|
+
res.writeHead(upstream.status, {
|
|
90
|
+
'content-type': upstream.headers.get('content-type') ?? 'application/json',
|
|
91
|
+
});
|
|
92
|
+
if (!upstream.body) {
|
|
93
|
+
res.end();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
Readable.fromWeb(upstream.body)
|
|
97
|
+
.on('error', () => res.destroy())
|
|
98
|
+
.pipe(res);
|
|
99
|
+
}
|
|
100
|
+
function authorized(key, req) {
|
|
101
|
+
const header = req.headers['x-api-key'] ?? req.headers.authorization?.replace(/^Bearer /i, '');
|
|
102
|
+
const given = Buffer.from(typeof header === 'string' ? header : '');
|
|
103
|
+
const wanted = Buffer.from(key);
|
|
104
|
+
return given.length === wanted.length && timingSafeEqual(given, wanted);
|
|
105
|
+
}
|
|
106
|
+
function readBody(req) {
|
|
107
|
+
return new Promise((done, reject) => {
|
|
108
|
+
const chunks = [];
|
|
109
|
+
req.on('data', (c) => chunks.push(c));
|
|
110
|
+
req.on('end', () => done(Buffer.concat(chunks).toString('utf8')));
|
|
111
|
+
req.on('error', reject);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
/** Anthropic's error shape, so copilot reports it as it would a direct call. */
|
|
115
|
+
function fail(res, code, type, message) {
|
|
116
|
+
if (res.headersSent) {
|
|
117
|
+
res.destroy();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
res.writeHead(code, { 'content-type': 'application/json' });
|
|
121
|
+
res.end(JSON.stringify({ type: 'error', error: { type, message } }));
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=relay.js.map
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { ModelUsage, TokenUsage } from '@zenera/neo';
|
|
2
|
+
export interface MetaCall {
|
|
3
|
+
model: string;
|
|
4
|
+
usage: TokenUsage;
|
|
5
|
+
cacheWriteTokens?: number;
|
|
6
|
+
startedAt: string;
|
|
7
|
+
durationMs: number;
|
|
8
|
+
session?: string;
|
|
9
|
+
traceId?: string;
|
|
10
|
+
spanId?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* One line of the span file, as a model call - anything else is undefined.
|
|
14
|
+
* Only counts are read off it: whatever else a span may carry, none of it
|
|
15
|
+
* reaches the ledger.
|
|
16
|
+
*/
|
|
17
|
+
export declare function readSpan(line: string): MetaCall | undefined;
|
|
18
|
+
/** Whole lines out of a stream read in chunks; the unfinished tail is carried. */
|
|
19
|
+
export declare function splitLines(carry: string, chunk: string): {
|
|
20
|
+
lines: string[];
|
|
21
|
+
carry: string;
|
|
22
|
+
};
|
|
23
|
+
/** Reads a span file as it grows. `stop()` reads what is left, once. */
|
|
24
|
+
export declare function tailSpans(path: string, onCall: (call: MetaCall) => void, everyMs?: number): {
|
|
25
|
+
stop: () => void;
|
|
26
|
+
};
|
|
27
|
+
/** Calls and tokens so far, in the one line the status row and the summary share. */
|
|
28
|
+
export declare class Tally {
|
|
29
|
+
calls: number;
|
|
30
|
+
usage: TokenUsage;
|
|
31
|
+
add(call: MetaCall): void;
|
|
32
|
+
toString(): string;
|
|
33
|
+
}
|
|
34
|
+
/** 950, 12.3k, 1.8M. */
|
|
35
|
+
export declare function compact(n: number): string;
|
|
36
|
+
/**
|
|
37
|
+
* Copilot's own per-model totals from the last `session.shutdown` in a session's
|
|
38
|
+
* events - cumulative over every resume of it. Reasoning folded into output,
|
|
39
|
+
* as everywhere in zen.
|
|
40
|
+
*/
|
|
41
|
+
export declare function shutdownModels(events: string): ModelUsage[] | undefined;
|
|
42
|
+
export declare function sessionTotals(sessionId: string): ModelUsage[] | undefined;
|
|
43
|
+
/** Whether this machine holds the session, which is what `--resume` needs. */
|
|
44
|
+
export declare function sessionKept(sessionId: string): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* The model a session's history was written by: its last answer's, else the one it started on.
|
|
47
|
+
* A failed resume on another model writes no answer, so it never becomes the session's model.
|
|
48
|
+
*/
|
|
49
|
+
export declare function sessionModel(sessionId: string): string | undefined;
|
|
50
|
+
/** `google/gemini-3.8-flash` and `gemini-3.8-flash` are one model to copilot. */
|
|
51
|
+
export declare const sameModel: (a: string, b: string) => boolean;
|
|
52
|
+
//# sourceMappingURL=tokens.d.ts.map
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { closeSync, existsSync, openSync, readFileSync, readSync, statSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
/** An OTel time: `[seconds, nanoseconds]` in this exporter; epoch ms either way. */
|
|
5
|
+
function epochMs(t) {
|
|
6
|
+
if (Array.isArray(t) && typeof t[0] === 'number') {
|
|
7
|
+
return t[0] * 1000 + Number(t[1] ?? 0) / 1e6;
|
|
8
|
+
}
|
|
9
|
+
if (typeof t === 'number') {
|
|
10
|
+
return t;
|
|
11
|
+
}
|
|
12
|
+
return typeof t === 'string' ? Date.parse(t) : undefined;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* One line of the span file, as a model call - anything else is undefined.
|
|
16
|
+
* Only counts are read off it: whatever else a span may carry, none of it
|
|
17
|
+
* reaches the ledger.
|
|
18
|
+
*/
|
|
19
|
+
export function readSpan(line) {
|
|
20
|
+
let span;
|
|
21
|
+
try {
|
|
22
|
+
span = JSON.parse(line);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
const attrs = (span.attributes ?? {});
|
|
28
|
+
if (span.type !== 'span' || attrs['gen_ai.operation.name'] !== 'chat') {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
const num = (key) => Number(attrs[key] ?? 0) || 0;
|
|
32
|
+
const start = epochMs(span.startTime) ?? 0;
|
|
33
|
+
const end = epochMs(span.endTime) ?? start;
|
|
34
|
+
// Copilot counts reasoning beside the output; zen counts it inside.
|
|
35
|
+
const reasoning = num('gen_ai.usage.reasoning.output_tokens');
|
|
36
|
+
const cacheWrite = num('gen_ai.usage.cache_creation.input_tokens') ||
|
|
37
|
+
num('gen_ai.usage.cache_creation_input_tokens');
|
|
38
|
+
return {
|
|
39
|
+
model: String(attrs['gen_ai.response.model'] ?? attrs['gen_ai.request.model'] ?? '?'),
|
|
40
|
+
usage: {
|
|
41
|
+
inputTokens: num('gen_ai.usage.input_tokens'),
|
|
42
|
+
cachedInputTokens: num('gen_ai.usage.cache_read.input_tokens') ||
|
|
43
|
+
num('gen_ai.usage.cache_read_input_tokens'),
|
|
44
|
+
outputTokens: num('gen_ai.usage.output_tokens') + reasoning,
|
|
45
|
+
reasoningTokens: reasoning,
|
|
46
|
+
},
|
|
47
|
+
...(cacheWrite ? { cacheWriteTokens: cacheWrite } : {}),
|
|
48
|
+
startedAt: new Date(start).toISOString(),
|
|
49
|
+
durationMs: Math.max(0, Math.round(end - start)),
|
|
50
|
+
session: typeof attrs['gen_ai.conversation.id'] === 'string'
|
|
51
|
+
? attrs['gen_ai.conversation.id']
|
|
52
|
+
: undefined,
|
|
53
|
+
traceId: typeof span.traceId === 'string' ? span.traceId : undefined,
|
|
54
|
+
spanId: typeof span.spanId === 'string' ? span.spanId : undefined,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** Whole lines out of a stream read in chunks; the unfinished tail is carried. */
|
|
58
|
+
export function splitLines(carry, chunk) {
|
|
59
|
+
const parts = `${carry}${chunk}`.split('\n');
|
|
60
|
+
const rest = parts.pop() ?? '';
|
|
61
|
+
return { lines: parts.filter((l) => l.trim()), carry: rest };
|
|
62
|
+
}
|
|
63
|
+
/** Reads a span file as it grows. `stop()` reads what is left, once. */
|
|
64
|
+
export function tailSpans(path, onCall, everyMs = 1000) {
|
|
65
|
+
let offset = 0;
|
|
66
|
+
let carry = '';
|
|
67
|
+
const poll = () => {
|
|
68
|
+
let fd;
|
|
69
|
+
try {
|
|
70
|
+
const size = statSync(path).size;
|
|
71
|
+
if (size <= offset) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
fd = openSync(path, 'r');
|
|
75
|
+
const buf = Buffer.alloc(size - offset);
|
|
76
|
+
readSync(fd, buf, 0, buf.length, offset);
|
|
77
|
+
offset = size;
|
|
78
|
+
const split = splitLines(carry, buf.toString('utf8'));
|
|
79
|
+
carry = split.carry;
|
|
80
|
+
for (const line of split.lines) {
|
|
81
|
+
const call = readSpan(line);
|
|
82
|
+
if (call) {
|
|
83
|
+
onCall(call);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// Not there yet: copilot creates it at the first span.
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
if (fd !== undefined) {
|
|
92
|
+
closeSync(fd);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
const timer = setInterval(poll, everyMs);
|
|
97
|
+
timer.unref();
|
|
98
|
+
return {
|
|
99
|
+
stop: () => {
|
|
100
|
+
clearInterval(timer);
|
|
101
|
+
poll();
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** Calls and tokens so far, in the one line the status row and the summary share. */
|
|
106
|
+
export class Tally {
|
|
107
|
+
calls = 0;
|
|
108
|
+
usage = {
|
|
109
|
+
inputTokens: 0,
|
|
110
|
+
cachedInputTokens: 0,
|
|
111
|
+
outputTokens: 0,
|
|
112
|
+
reasoningTokens: 0,
|
|
113
|
+
};
|
|
114
|
+
add(call) {
|
|
115
|
+
this.calls += 1;
|
|
116
|
+
this.usage = {
|
|
117
|
+
inputTokens: this.usage.inputTokens + call.usage.inputTokens,
|
|
118
|
+
cachedInputTokens: this.usage.cachedInputTokens + call.usage.cachedInputTokens,
|
|
119
|
+
outputTokens: this.usage.outputTokens + call.usage.outputTokens,
|
|
120
|
+
reasoningTokens: this.usage.reasoningTokens + call.usage.reasoningTokens,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
toString() {
|
|
124
|
+
const u = this.usage;
|
|
125
|
+
const cached = u.cachedInputTokens ? ` (${compact(u.cachedInputTokens)} cached)` : '';
|
|
126
|
+
return (`${this.calls} call${this.calls === 1 ? '' : 's'} · ${compact(u.inputTokens)} in${cached}` +
|
|
127
|
+
` · ${compact(u.outputTokens)} out`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** 950, 12.3k, 1.8M. */
|
|
131
|
+
export function compact(n) {
|
|
132
|
+
if (n < 1000) {
|
|
133
|
+
return String(n);
|
|
134
|
+
}
|
|
135
|
+
if (n < 1e6) {
|
|
136
|
+
return `${(n / 1e3).toFixed(n < 1e4 ? 1 : 0)}k`;
|
|
137
|
+
}
|
|
138
|
+
return `${(n / 1e6).toFixed(n < 1e7 ? 2 : 1)}M`;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Copilot's own per-model totals from the last `session.shutdown` in a session's
|
|
142
|
+
* events - cumulative over every resume of it. Reasoning folded into output,
|
|
143
|
+
* as everywhere in zen.
|
|
144
|
+
*/
|
|
145
|
+
export function shutdownModels(events) {
|
|
146
|
+
const line = events
|
|
147
|
+
.split('\n')
|
|
148
|
+
.reverse()
|
|
149
|
+
.find((l) => l.includes('"session.shutdown"'));
|
|
150
|
+
if (!line) {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
let metrics;
|
|
154
|
+
try {
|
|
155
|
+
metrics = JSON.parse(line).data?.modelMetrics ?? {};
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
return Object.entries(metrics).map(([model, m]) => {
|
|
161
|
+
const u = m.usage ?? {};
|
|
162
|
+
const reasoning = Number(u.reasoningTokens ?? 0);
|
|
163
|
+
return {
|
|
164
|
+
model,
|
|
165
|
+
calls: Number(m.requests?.count ?? 0),
|
|
166
|
+
usage: {
|
|
167
|
+
inputTokens: Number(u.inputTokens ?? 0),
|
|
168
|
+
cachedInputTokens: Number(u.cacheReadTokens ?? 0),
|
|
169
|
+
outputTokens: Number(u.outputTokens ?? 0) + reasoning,
|
|
170
|
+
reasoningTokens: reasoning,
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
export function sessionTotals(sessionId) {
|
|
176
|
+
const home = process.env.COPILOT_HOME ?? join(homedir(), '.copilot');
|
|
177
|
+
try {
|
|
178
|
+
return shutdownModels(readFileSync(join(home, 'session-state', sessionId, 'events.jsonl'), 'utf8'));
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/** Whether this machine holds the session, which is what `--resume` needs. */
|
|
185
|
+
export function sessionKept(sessionId) {
|
|
186
|
+
const home = process.env.COPILOT_HOME ?? join(homedir(), '.copilot');
|
|
187
|
+
return existsSync(join(home, 'session-state', sessionId));
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* The model a session's history was written by: its last answer's, else the one it started on.
|
|
191
|
+
* A failed resume on another model writes no answer, so it never becomes the session's model.
|
|
192
|
+
*/
|
|
193
|
+
export function sessionModel(sessionId) {
|
|
194
|
+
const home = process.env.COPILOT_HOME ?? join(homedir(), '.copilot');
|
|
195
|
+
let text;
|
|
196
|
+
try {
|
|
197
|
+
text = readFileSync(join(home, 'session-state', sessionId, 'events.jsonl'), 'utf8');
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
let started;
|
|
203
|
+
const lines = text.split('\n');
|
|
204
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
205
|
+
const line = lines[i];
|
|
206
|
+
if (!line.includes('"assistant.message"') && !line.includes('"session.start"')) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
const event = JSON.parse(line);
|
|
211
|
+
if (event.type === 'assistant.message' && typeof event.data?.model === 'string') {
|
|
212
|
+
return event.data.model;
|
|
213
|
+
}
|
|
214
|
+
if (event.type === 'session.start' && typeof event.data?.selectedModel === 'string') {
|
|
215
|
+
started = event.data.selectedModel;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
// A line cut by a crash says nothing.
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return started;
|
|
223
|
+
}
|
|
224
|
+
/** `google/gemini-3.8-flash` and `gemini-3.8-flash` are one model to copilot. */
|
|
225
|
+
export const sameModel = (a, b) => a.replace(/^.*\//, '').toLowerCase() === b.replace(/^.*\//, '').toLowerCase();
|
|
226
|
+
//# sourceMappingURL=tokens.js.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export interface Sink {
|
|
2
|
+
answer(text: string): void;
|
|
3
|
+
narrate(line: string): void;
|
|
4
|
+
warn(line: string): void;
|
|
5
|
+
/** Kept by the log, never shown: the step-by-step nobody reads while it runs. */
|
|
6
|
+
detail?(line: string): void;
|
|
7
|
+
/** A transient last row, replaced each time and never kept. '' removes it. */
|
|
8
|
+
status?(line: string): void;
|
|
9
|
+
/** Called once when the run ends, so a repainting sink can clear itself. */
|
|
10
|
+
close?(): void;
|
|
11
|
+
}
|
|
12
|
+
export declare const terminalSink: Sink;
|
|
13
|
+
/** Rows of narration the window keeps on screen. */
|
|
14
|
+
export declare const WINDOW_ROWS = 8;
|
|
15
|
+
/** How often the border is redrawn — the wave's frame, not the clock's. */
|
|
16
|
+
export declare const FRAME_MS = 80;
|
|
17
|
+
/** Cells the crest travels per frame — a lap of a wide window in a few seconds. */
|
|
18
|
+
export declare const WAVE_STEP = 3;
|
|
19
|
+
/**
|
|
20
|
+
* How brightly one cell of the border is lit, given where the crest has got to.
|
|
21
|
+
*
|
|
22
|
+
* `position` counts clockwise from the top left corner, so one index walks the
|
|
23
|
+
* whole ring and the wave rounds the corners without a seam. Everything the
|
|
24
|
+
* tail has passed sits at the dimmest tone, which is the border's resting
|
|
25
|
+
* colour: the ring is always drawn, only the crest moves.
|
|
26
|
+
*/
|
|
27
|
+
export declare function toneAt(position: number, head: number, span: number): number;
|
|
28
|
+
/**
|
|
29
|
+
* The bottom rule as cells, with the status label set into it near the left.
|
|
30
|
+
*
|
|
31
|
+
* Waiting belongs to the frame rather than to the narration: a row of its own
|
|
32
|
+
* costs a step of what the agent is doing every time, and it settles just
|
|
33
|
+
* under the last line, which is where the eye already is. Near the left, where
|
|
34
|
+
* reading starts, and not so near that it displaces the corner.
|
|
35
|
+
*/
|
|
36
|
+
export declare function footerCells(label: string, width: number): string[];
|
|
37
|
+
/**
|
|
38
|
+
* Narration held to a fixed number of rows that rewrite themselves in place.
|
|
39
|
+
*
|
|
40
|
+
* A long run is hundreds of tool calls, and printing each as its own line
|
|
41
|
+
* scrolls the question, the model line and every warning off the top of the
|
|
42
|
+
* screen — by the end the terminal holds a transcript nobody asked for and the
|
|
43
|
+
* answer is somewhere in the middle of it. Only the last few steps say what it
|
|
44
|
+
* is doing now, which is the only thing narration is for; the rest is in the
|
|
45
|
+
* session copilot recorded.
|
|
46
|
+
*
|
|
47
|
+
* Warnings are not narration: they leave the window and stay on the screen.
|
|
48
|
+
*
|
|
49
|
+
* Without a terminal there is nothing to rewrite over, so every line is its
|
|
50
|
+
* own — which is what a CI log wants anyway.
|
|
51
|
+
*/
|
|
52
|
+
export declare function windowSink(rows?: number): Sink;
|
|
53
|
+
/**
|
|
54
|
+
* The answer in the same box `zen run` draws it in: bounded width, rounded
|
|
55
|
+
* rule, formatted with full markdown styling (bold, italics, code, lists, quotes, tables).
|
|
56
|
+
*
|
|
57
|
+
* Redirected output gets the text and nothing else — `zen meta run ... > out.md`
|
|
58
|
+
* is meant to produce a file you can read, not one with a border down its side.
|
|
59
|
+
*/
|
|
60
|
+
export declare function answerBox(text: string, columns?: number): string[];
|
|
61
|
+
//# sourceMappingURL=window.d.ts.map
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { boxWidth, cut, cyan, dim, formatMarkdown, note, pad, red, styled, write } from "./host.js";
|
|
2
|
+
export const terminalSink = {
|
|
3
|
+
answer: (text) => write(text),
|
|
4
|
+
narrate: (line) => note(line),
|
|
5
|
+
warn: (line) => note(red(line)),
|
|
6
|
+
};
|
|
7
|
+
/** Rows of narration the window keeps on screen. */
|
|
8
|
+
export const WINDOW_ROWS = 8;
|
|
9
|
+
const BOX = { tl: '╭', tr: '╮', bl: '╰', br: '╯', h: '─', v: '│' };
|
|
10
|
+
/** How often the border is redrawn — the wave's frame, not the clock's. */
|
|
11
|
+
export const FRAME_MS = 80;
|
|
12
|
+
// Dimmest first, crest last. Written as escapes rather than `styleText` because
|
|
13
|
+
// a named colour has no ramp, which is also why `styled()` has to be asked.
|
|
14
|
+
const WAVE = [
|
|
15
|
+
'\u001b[0;38;5;24m',
|
|
16
|
+
'\u001b[0;38;5;31m',
|
|
17
|
+
'\u001b[0;38;5;38m',
|
|
18
|
+
'\u001b[0;38;5;45m',
|
|
19
|
+
'\u001b[0;38;5;51m',
|
|
20
|
+
'\u001b[1;38;5;159m',
|
|
21
|
+
'\u001b[1;38;5;231m',
|
|
22
|
+
];
|
|
23
|
+
const RESET = '\u001b[0m';
|
|
24
|
+
/** Cells each tone of the tail covers, so the crest reads as a band, not a dot. */
|
|
25
|
+
const WAVE_TAIL = 2;
|
|
26
|
+
/** Cells the crest travels per frame — a lap of a wide window in a few seconds. */
|
|
27
|
+
export const WAVE_STEP = 3;
|
|
28
|
+
/**
|
|
29
|
+
* How brightly one cell of the border is lit, given where the crest has got to.
|
|
30
|
+
*
|
|
31
|
+
* `position` counts clockwise from the top left corner, so one index walks the
|
|
32
|
+
* whole ring and the wave rounds the corners without a seam. Everything the
|
|
33
|
+
* tail has passed sits at the dimmest tone, which is the border's resting
|
|
34
|
+
* colour: the ring is always drawn, only the crest moves.
|
|
35
|
+
*/
|
|
36
|
+
export function toneAt(position, head, span) {
|
|
37
|
+
const behind = (((head - position) % span) + span) % span;
|
|
38
|
+
const step = Math.floor(behind / WAVE_TAIL);
|
|
39
|
+
return step < WAVE.length ? WAVE.length - 1 - step : 0;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A run of border cells, coloured by the wave and emitting an escape only where
|
|
43
|
+
* the tone changes — a frame of the whole ring is a handful of them.
|
|
44
|
+
*
|
|
45
|
+
* `from` is the ring position of the first cell and `step` which way round the
|
|
46
|
+
* ring the cells run, because the bottom rule is printed left to right while
|
|
47
|
+
* the ring travels it right to left.
|
|
48
|
+
*/
|
|
49
|
+
function lit(cells, from, step, head, span) {
|
|
50
|
+
let out = '';
|
|
51
|
+
let tone = -1;
|
|
52
|
+
for (let i = 0; i < cells.length; i++) {
|
|
53
|
+
const next = toneAt(from + i * step, head, span);
|
|
54
|
+
if (next !== tone) {
|
|
55
|
+
out += WAVE[next];
|
|
56
|
+
tone = next;
|
|
57
|
+
}
|
|
58
|
+
out += cells[i];
|
|
59
|
+
}
|
|
60
|
+
return `${out}${RESET}`;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The bottom rule as cells, with the status label set into it near the left.
|
|
64
|
+
*
|
|
65
|
+
* Waiting belongs to the frame rather than to the narration: a row of its own
|
|
66
|
+
* costs a step of what the agent is doing every time, and it settles just
|
|
67
|
+
* under the last line, which is where the eye already is. Near the left, where
|
|
68
|
+
* reading starts, and not so near that it displaces the corner.
|
|
69
|
+
*/
|
|
70
|
+
export function footerCells(label, width) {
|
|
71
|
+
const room = width - 6;
|
|
72
|
+
const text = label && room > 0 ? [...label].slice(0, room) : [];
|
|
73
|
+
const cells = text.length > 0 ? [BOX.bl, BOX.h, ' ', ...text, ' '] : [BOX.bl];
|
|
74
|
+
while (cells.length < width - 1) {
|
|
75
|
+
cells.push(BOX.h);
|
|
76
|
+
}
|
|
77
|
+
return [...cells, BOX.br];
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Narration held to a fixed number of rows that rewrite themselves in place.
|
|
81
|
+
*
|
|
82
|
+
* A long run is hundreds of tool calls, and printing each as its own line
|
|
83
|
+
* scrolls the question, the model line and every warning off the top of the
|
|
84
|
+
* screen — by the end the terminal holds a transcript nobody asked for and the
|
|
85
|
+
* answer is somewhere in the middle of it. Only the last few steps say what it
|
|
86
|
+
* is doing now, which is the only thing narration is for; the rest is in the
|
|
87
|
+
* session copilot recorded.
|
|
88
|
+
*
|
|
89
|
+
* Warnings are not narration: they leave the window and stay on the screen.
|
|
90
|
+
*
|
|
91
|
+
* Without a terminal there is nothing to rewrite over, so every line is its
|
|
92
|
+
* own — which is what a CI log wants anyway.
|
|
93
|
+
*/
|
|
94
|
+
export function windowSink(rows = WINDOW_ROWS) {
|
|
95
|
+
if (!process.stderr.isTTY) {
|
|
96
|
+
return terminalSink;
|
|
97
|
+
}
|
|
98
|
+
const kept = [];
|
|
99
|
+
const colour = styled();
|
|
100
|
+
let label = '';
|
|
101
|
+
let head = 0;
|
|
102
|
+
let painted = 0;
|
|
103
|
+
// The frame must stay under the viewport: a block taller than the screen
|
|
104
|
+
// scrolls its own top away, and then the cursor-up erase falls short and
|
|
105
|
+
// strands a copy of every repaint. The two border rows count towards it.
|
|
106
|
+
const height = () => Math.max(1, Math.min(rows, (process.stderr.rows ?? 24) - 4));
|
|
107
|
+
const columns = () => Math.max(20, (process.stderr.columns ?? 80) - 1);
|
|
108
|
+
const innerOf = () => columns() - 4;
|
|
109
|
+
const erase = () => {
|
|
110
|
+
if (painted > 0) {
|
|
111
|
+
process.stderr.write(`\u001b[${painted}A\u001b[0J`);
|
|
112
|
+
painted = 0;
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
/** The whole frame, border lit by the wave, every row exactly one row. */
|
|
116
|
+
const frameOf = (show, inner) => {
|
|
117
|
+
const width = inner + 4;
|
|
118
|
+
// Clockwise from the top left: across the top, down the right, back
|
|
119
|
+
// along the bottom, up the left. One ring, so one index addresses it.
|
|
120
|
+
const span = 2 * width + 2 * show.length;
|
|
121
|
+
const top = [BOX.tl, ...Array(width - 2).fill(BOX.h), BOX.tr];
|
|
122
|
+
const bottom = footerCells(label, width);
|
|
123
|
+
const edge = (position) => colour ? lit([BOX.v], position, 1, head, span) : cyan(BOX.v);
|
|
124
|
+
return [
|
|
125
|
+
colour ? lit(top, 0, 1, head, span) : cyan(top.join('')),
|
|
126
|
+
...show.map((line, r) => `${edge(span - 1 - r)} ${pad(cut(line, inner), inner)} ${edge(width + r)}`),
|
|
127
|
+
colour
|
|
128
|
+
? lit(bottom, 2 * width + show.length - 1, -1, head, span)
|
|
129
|
+
: cyan(bottom.join('')),
|
|
130
|
+
];
|
|
131
|
+
};
|
|
132
|
+
const paint = () => {
|
|
133
|
+
const show = kept.slice(-height());
|
|
134
|
+
if (show.length === 0 && label === '') {
|
|
135
|
+
erase();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
// The wave wants a frame to run round before there is anything to say.
|
|
139
|
+
const lines = frameOf(show.length > 0 ? show : [''], innerOf());
|
|
140
|
+
if (painted === lines.length) {
|
|
141
|
+
// Erasing before a repaint is what flickers; overwriting in place
|
|
142
|
+
// does not, and at frame rate the whole border has to be redrawn.
|
|
143
|
+
process.stderr.write(`\u001b[${painted}A${lines.map((l) => `\r${l}\u001b[0K\n`).join('')}`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
erase();
|
|
147
|
+
process.stderr.write(lines.map((l) => `${l}\n`).join(''));
|
|
148
|
+
painted = lines.length;
|
|
149
|
+
};
|
|
150
|
+
return {
|
|
151
|
+
answer: (text) => {
|
|
152
|
+
erase();
|
|
153
|
+
write(text);
|
|
154
|
+
},
|
|
155
|
+
narrate: (line) => {
|
|
156
|
+
kept.push(...line.split('\n'));
|
|
157
|
+
if (kept.length > height() * 4) {
|
|
158
|
+
kept.splice(0, kept.length - height());
|
|
159
|
+
}
|
|
160
|
+
paint();
|
|
161
|
+
},
|
|
162
|
+
warn: (line) => {
|
|
163
|
+
erase();
|
|
164
|
+
note(red(line));
|
|
165
|
+
paint();
|
|
166
|
+
},
|
|
167
|
+
status: (line) => {
|
|
168
|
+
label = line;
|
|
169
|
+
head += WAVE_STEP;
|
|
170
|
+
paint();
|
|
171
|
+
},
|
|
172
|
+
close: erase,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* The answer in the same box `zen run` draws it in: bounded width, rounded
|
|
177
|
+
* rule, formatted with full markdown styling (bold, italics, code, lists, quotes, tables).
|
|
178
|
+
*
|
|
179
|
+
* Redirected output gets the text and nothing else — `zen meta run ... > out.md`
|
|
180
|
+
* is meant to produce a file you can read, not one with a border down its side.
|
|
181
|
+
*/
|
|
182
|
+
export function answerBox(text, columns = process.stdout.columns ?? 80) {
|
|
183
|
+
if (!process.stdout.isTTY) {
|
|
184
|
+
return text.split('\n');
|
|
185
|
+
}
|
|
186
|
+
const outer = boxWidth(text, columns);
|
|
187
|
+
const inner = outer - 4;
|
|
188
|
+
const body = formatMarkdown(text, inner);
|
|
189
|
+
const rule = BOX.h.repeat(outer - 2);
|
|
190
|
+
return [
|
|
191
|
+
'',
|
|
192
|
+
dim(`${BOX.tl}${rule}${BOX.tr}`),
|
|
193
|
+
...body.map((l) => `${dim(BOX.v)} ${pad(l, inner)} ${dim(BOX.v)}`),
|
|
194
|
+
dim(`${BOX.bl}${rule}${BOX.br}`),
|
|
195
|
+
'',
|
|
196
|
+
];
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=window.js.map
|