@illuminis/comprism 0.1.3 → 0.1.5
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 +61 -2
- package/out/agent/command.d.ts +75 -4
- package/out/agent/command.js +221 -26
- package/out/agent/render.d.ts +56 -8
- package/out/agent/render.js +441 -39
- package/out/agent/session.d.ts +156 -2
- package/out/agent/session.js +795 -154
- package/out/commands/agents.d.ts +2 -0
- package/out/commands/agents.js +79 -0
- package/out/commands/ask.d.ts +1 -1
- package/out/commands/ask.js +88 -12
- package/out/commands/commands-thin.js +79 -17
- package/out/commands/config.d.ts +1 -0
- package/out/commands/config.js +138 -0
- package/out/commands/cost.d.ts +1 -0
- package/out/commands/cost.js +167 -0
- package/out/commands/hooks.d.ts +1 -0
- package/out/commands/hooks.js +83 -0
- package/out/commands/install.d.ts +44 -1
- package/out/commands/install.js +198 -4
- package/out/commands/instructions.d.ts +1 -0
- package/out/commands/instructions.js +113 -0
- package/out/commands/integrations.d.ts +3 -0
- package/out/commands/integrations.js +215 -0
- package/out/commands/jobs.d.ts +5 -0
- package/out/commands/jobs.js +157 -0
- package/out/commands/login.js +249 -43
- package/out/commands/memory.d.ts +3 -0
- package/out/commands/memory.js +113 -0
- package/out/commands/permissions.d.ts +1 -0
- package/out/commands/permissions.js +94 -0
- package/out/commands/plugins.d.ts +4 -0
- package/out/commands/plugins.js +192 -0
- package/out/commands/privacy.d.ts +1 -0
- package/out/commands/privacy.js +57 -0
- package/out/commands/providerKey.d.ts +32 -0
- package/out/commands/providerKey.js +108 -0
- package/out/commands/repl.d.ts +12 -1
- package/out/commands/repl.js +1649 -122
- package/out/commands/report.d.ts +39 -0
- package/out/commands/report.js +115 -0
- package/out/commands/review.d.ts +5 -0
- package/out/commands/review.js +223 -0
- package/out/commands/sessions.d.ts +23 -0
- package/out/commands/sessions.js +115 -0
- package/out/commands/settings.d.ts +3 -1
- package/out/commands/settings.js +18 -16
- package/out/commands/skills.d.ts +21 -0
- package/out/commands/skills.js +207 -0
- package/out/commands/unattended.d.ts +7 -0
- package/out/commands/unattended.js +351 -0
- package/out/commands/update.d.ts +1 -0
- package/out/commands/update.js +123 -0
- package/out/commands/welcome.d.ts +25 -1
- package/out/commands/welcome.js +71 -35
- package/out/commands/worktrees.d.ts +5 -0
- package/out/commands/worktrees.js +186 -0
- package/out/executor/browser.d.ts +14 -0
- package/out/executor/browser.js +270 -0
- package/out/executor/diagnostics.d.ts +2 -0
- package/out/executor/diagnostics.js +181 -0
- package/out/executor/files.js +270 -40
- package/out/executor/git.js +42 -29
- package/out/executor/hooks.d.ts +42 -58
- package/out/executor/hooks.js +89 -182
- package/out/executor/index.d.ts +21 -5
- package/out/executor/index.js +160 -14
- package/out/executor/paths.d.ts +6 -1
- package/out/executor/paths.js +34 -6
- package/out/executor/sandbox.d.ts +40 -0
- package/out/executor/sandbox.js +299 -0
- package/out/executor/shell.d.ts +49 -4
- package/out/executor/shell.js +302 -56
- package/out/executor/toolservers.d.ts +20 -0
- package/out/executor/toolservers.js +189 -0
- package/out/executor/worktree.d.ts +9 -0
- package/out/executor/worktree.js +119 -0
- package/out/graph/read-python.js +2 -1
- package/out/lib/attach.d.ts +56 -12
- package/out/lib/attach.js +230 -63
- package/out/lib/clipboard.d.ts +23 -0
- package/out/lib/clipboard.js +182 -0
- package/out/lib/commandlist.d.ts +20 -0
- package/out/lib/commandlist.js +58 -0
- package/out/lib/decision.d.ts +22 -0
- package/out/lib/decision.js +50 -0
- package/out/lib/fingerprint.d.ts +25 -0
- package/out/lib/fingerprint.js +58 -0
- package/out/lib/gateway.d.ts +190 -2
- package/out/lib/gateway.js +59 -4
- package/out/lib/history.d.ts +24 -0
- package/out/lib/history.js +137 -0
- package/out/lib/ide.d.ts +19 -0
- package/out/lib/ide.js +131 -0
- package/out/lib/keyboard.d.ts +98 -0
- package/out/lib/keyboard.js +390 -0
- package/out/lib/lastlogin.d.ts +14 -0
- package/out/lib/lastlogin.js +105 -0
- package/out/lib/machine.d.ts +21 -0
- package/out/lib/machine.js +91 -0
- package/out/lib/notify.d.ts +4 -0
- package/out/lib/notify.js +52 -0
- package/out/lib/output.d.ts +48 -0
- package/out/lib/output.js +108 -0
- package/out/lib/picker.d.ts +1 -0
- package/out/lib/picker.js +158 -0
- package/out/lib/project-ops.d.ts +19 -0
- package/out/lib/project-ops.js +146 -0
- package/out/lib/project.d.ts +28 -0
- package/out/lib/project.js +114 -0
- package/out/lib/prompt.d.ts +3 -0
- package/out/lib/prompt.js +36 -2
- package/out/lib/queue.d.ts +13 -0
- package/out/lib/queue.js +116 -0
- package/out/lib/readiness.d.ts +19 -0
- package/out/lib/readiness.js +170 -1
- package/out/lib/self.d.ts +23 -0
- package/out/lib/self.js +124 -0
- package/out/lib/sessions.d.ts +19 -0
- package/out/lib/sessions.js +221 -0
- package/out/lib/startcache.d.ts +13 -0
- package/out/lib/startcache.js +78 -0
- package/out/lib/stdin.d.ts +32 -0
- package/out/lib/stdin.js +117 -0
- package/out/lib/store.d.ts +40 -0
- package/out/lib/store.js +138 -0
- package/out/lib/sync.d.ts +18 -0
- package/out/lib/sync.js +81 -0
- package/out/lib/ui.d.ts +10 -4
- package/out/lib/ui.js +65 -25
- package/out/lib/voice.d.ts +36 -0
- package/out/lib/voice.js +160 -1
- package/out/native/dictate +0 -0
- package/out/postinstall.js +42 -17
- package/out/providers/anthropic.d.ts +22 -0
- package/out/providers/anthropic.js +80 -0
- package/out/providers/index.d.ts +8 -0
- package/out/providers/index.js +83 -0
- package/out/providers/openai.d.ts +11 -0
- package/out/providers/openai.js +57 -0
- package/out/thin.js +595 -33
- package/package.json +11 -50
package/out/agent/render.js
CHANGED
|
@@ -22,23 +22,32 @@
|
|
|
22
22
|
* symbol as well as a color.
|
|
23
23
|
*/
|
|
24
24
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
25
|
-
exports.gray = exports.yellow = exports.green = exports.red = exports.bold = exports.dim = void 0;
|
|
25
|
+
exports.violet = exports.gray = exports.yellow = exports.green = exports.red = exports.bold = exports.dim = void 0;
|
|
26
|
+
exports.modelName = modelName;
|
|
26
27
|
exports.money = money;
|
|
27
28
|
exports.duration = duration;
|
|
28
29
|
exports.action = action;
|
|
30
|
+
exports.actionName = actionName;
|
|
29
31
|
exports.reusePct = reusePct;
|
|
30
32
|
exports.step = step;
|
|
31
33
|
exports.todos = todos;
|
|
34
|
+
exports.question = question;
|
|
32
35
|
exports.diff = diff;
|
|
36
|
+
exports.stoppedLines = stoppedLines;
|
|
37
|
+
exports.ending = ending;
|
|
33
38
|
exports.outcome = outcome;
|
|
39
|
+
exports.link = link;
|
|
40
|
+
exports.savedFile = savedFile;
|
|
34
41
|
exports.receipt = receipt;
|
|
35
42
|
exports.plain = plain;
|
|
36
43
|
/** Built from a character code rather than written as a literal escape, so this
|
|
37
44
|
* file contains no control characters and stays safe to grep, diff and paste. */
|
|
45
|
+
const url_1 = require("url");
|
|
38
46
|
const CSI = String.fromCharCode(27) + "[";
|
|
39
|
-
|
|
47
|
+
/** Decided when drawn, so plain mode set at start up applies (manual 6.12). */
|
|
48
|
+
const supportsColour = () => process.stdout.isTTY && !process.env.NO_COLOR;
|
|
40
49
|
function paint(code, text) {
|
|
41
|
-
return supportsColour ? CSI + code + "m" + text + CSI + "0m" : text;
|
|
50
|
+
return supportsColour() ? CSI + code + "m" + text + CSI + "0m" : text;
|
|
42
51
|
}
|
|
43
52
|
const dim = (t) => paint("2", t);
|
|
44
53
|
exports.dim = dim;
|
|
@@ -52,6 +61,34 @@ const yellow = (t) => paint("33", t);
|
|
|
52
61
|
exports.yellow = yellow;
|
|
53
62
|
const gray = (t) => paint("90", t);
|
|
54
63
|
exports.gray = gray;
|
|
64
|
+
/** The brand's violet, for model names and the receipt's label. */
|
|
65
|
+
const violet = (t) => paint("38;5;141", t);
|
|
66
|
+
exports.violet = violet;
|
|
67
|
+
/** The rail down the left of a job's steps, as the website draws it. */
|
|
68
|
+
const rail = () => (0, exports.dim)("\u2502");
|
|
69
|
+
const TICK = "\u2713";
|
|
70
|
+
const CROSS = "\u2717";
|
|
71
|
+
/** What a person sees, with color and link escapes taken out, so lines can be
|
|
72
|
+
* measured and padded. */
|
|
73
|
+
function visible(text) {
|
|
74
|
+
const esc = String.fromCharCode(27);
|
|
75
|
+
return text
|
|
76
|
+
.replace(new RegExp(`${esc}\\][^${String.fromCharCode(7)}${esc}]*(?:${String.fromCharCode(7)}|${esc}\\\\)`, "g"), "")
|
|
77
|
+
.replace(new RegExp(`${esc}\\[[0-9;]*m`, "g"), "");
|
|
78
|
+
}
|
|
79
|
+
function width() {
|
|
80
|
+
return Math.max(60, Math.min(process.stdout.columns || 100, 110));
|
|
81
|
+
}
|
|
82
|
+
/** `claude-haiku-4-5` as a person says it, `Claude Haiku 4.5`. Other vendors'
|
|
83
|
+
* ids are shown as they arrive: a wrong guess at a name is worse than the id. */
|
|
84
|
+
function modelName(id) {
|
|
85
|
+
const m = /^claude-([a-z]+)-(\d+)(?:-(\d+))?$/.exec(id);
|
|
86
|
+
if (!m)
|
|
87
|
+
return id;
|
|
88
|
+
const word = m[1] ?? "";
|
|
89
|
+
const family = word.charAt(0).toUpperCase() + word.slice(1);
|
|
90
|
+
return `Claude ${family} ${m[2]}${m[3] ? `.${m[3]}` : ""}`;
|
|
91
|
+
}
|
|
55
92
|
/** Money, at the precision a person can act on.
|
|
56
93
|
*
|
|
57
94
|
* Four decimal places below a cent, because a coding step often costs less
|
|
@@ -70,9 +107,16 @@ function duration(ms) {
|
|
|
70
107
|
/** One action, as it happens. A word as well as a color, so this still reads
|
|
71
108
|
* when piped to a file. */
|
|
72
109
|
function action(name, target, ok, ms) {
|
|
73
|
-
|
|
110
|
+
// A tick or a cross on the rail, as the website draws a job. A failure also
|
|
111
|
+
// says so in words, so it still reads in a log with no color.
|
|
112
|
+
const mark = ok ? (0, exports.green)(TICK) : (0, exports.red)(`${CROSS} failed`);
|
|
74
113
|
const tail = ms ? (0, exports.dim)(` ${duration(ms)}`) : "";
|
|
75
|
-
return ` ${mark} ${(0, exports.bold)(name)} ${(0, exports.dim)(target)}${tail}`;
|
|
114
|
+
return ` ${rail()} ${mark} ${(0, exports.bold)(actionName(name))} ${(0, exports.dim)(target)}${tail}`;
|
|
115
|
+
}
|
|
116
|
+
/** A connected tool as people write it, `server.tool` (manual 9.10). */
|
|
117
|
+
function actionName(name) {
|
|
118
|
+
const m = /^connected__(.+?)__(.+)$/.exec(name);
|
|
119
|
+
return m ? `${m[1]}.${m[2]}` : name;
|
|
76
120
|
}
|
|
77
121
|
/** The share of a step's context the provider served from its cache, or null
|
|
78
122
|
* when it reported no counts. Anthropic counts cached tokens OUTSIDE the input
|
|
@@ -89,9 +133,16 @@ function reusePct(tokensIn, cacheRead, cacheWrite) {
|
|
|
89
133
|
* provider had already seen. The last figure is the one study 005 found
|
|
90
134
|
* missing from our receipt: without it nobody could tell a warm step from a
|
|
91
135
|
* cold one, and every step was cold. */
|
|
92
|
-
function step(index, model, usd, ms, reused = null) {
|
|
136
|
+
function step(index, model, usd, ms, reused = null, reason = "") {
|
|
93
137
|
const warm = reused === null ? "" : ` cached ${reused}%`;
|
|
94
|
-
|
|
138
|
+
// The model that ran and why, in the service's words (manual 4.16).
|
|
139
|
+
const why = reason ? ` chosen: ${reason}` : "";
|
|
140
|
+
// The step and why its model was chosen on the left, the model on the right,
|
|
141
|
+
// as the website draws a job; the cost and timing follow, quieter.
|
|
142
|
+
const left = ` ${rail()} ${(0, exports.dim)(`step ${index}${why}`)}`;
|
|
143
|
+
const right = `${(0, exports.violet)(modelName(model || "?"))}${(0, exports.dim)(` ${money(usd)} ${duration(ms)}${warm}`)}`;
|
|
144
|
+
const gap = Math.max(2, width() - visible(left).length - visible(right).length - 2);
|
|
145
|
+
return `${left}${" ".repeat(gap)}${right}`;
|
|
95
146
|
}
|
|
96
147
|
/** The task list, reprinted whenever it changes.
|
|
97
148
|
*
|
|
@@ -99,24 +150,33 @@ function step(index, model, usd, ms, reused = null) {
|
|
|
99
150
|
* is worth keeping, and it is often the fastest way to see where a job went off
|
|
100
151
|
* course. */
|
|
101
152
|
function todos(items) {
|
|
153
|
+
// Drawn as manual 4.4 shows it: [x] done, [>] in progress, [ ] not yet.
|
|
102
154
|
if (!items.length)
|
|
103
155
|
return "";
|
|
104
156
|
const rows = items.map((t) => {
|
|
105
|
-
const mark = t.status === "completed" ? (0, exports.green)("
|
|
106
|
-
: t.status === "in_progress" ? (0, exports.yellow)("
|
|
157
|
+
const mark = t.status === "completed" ? (0, exports.green)("[x]")
|
|
158
|
+
: t.status === "in_progress" ? (0, exports.yellow)("[>]") : (0, exports.dim)("[ ]");
|
|
107
159
|
const text = t.status === "completed" ? (0, exports.dim)(t.content) : t.content;
|
|
108
|
-
return `
|
|
160
|
+
return ` ${mark} ${text}`;
|
|
109
161
|
});
|
|
110
|
-
return
|
|
162
|
+
return `${rows.join("\n")}\n`;
|
|
163
|
+
}
|
|
164
|
+
/** A question from the agent, or the plan question (manual 4.2, 4.3). The
|
|
165
|
+
* wording is the service's; this only lays it out. */
|
|
166
|
+
function question(q, options) {
|
|
167
|
+
const lines = [` ${(0, exports.bold)("Question:")} ${q}`];
|
|
168
|
+
options.forEach((o, i) => lines.push(` ${i + 1} ${o}`));
|
|
169
|
+
return `\n${lines.join("\n")}\n`;
|
|
111
170
|
}
|
|
112
171
|
/** A change, before it is made.
|
|
113
172
|
*
|
|
114
173
|
* Full text for a small edit and a summary for a large one. Pasting four
|
|
115
174
|
* hundred lines into a terminal is not review, it is a wall somebody presses
|
|
116
175
|
* through, and an approval people press through protects nobody. */
|
|
117
|
-
function diff(name, input) {
|
|
176
|
+
function diff(name, input, withHead = true) {
|
|
118
177
|
const target = String(input.path ?? input.to ?? "");
|
|
119
|
-
|
|
178
|
+
// Without the heading under an action line that already names the file.
|
|
179
|
+
const head = withHead ? `\n ${(0, exports.bold)(name)} ${target}\n` : "";
|
|
120
180
|
const show = (from, to) => {
|
|
121
181
|
const before = from.split("\n"), after = to.split("\n");
|
|
122
182
|
if (before.length + after.length > 60) {
|
|
@@ -142,6 +202,12 @@ function diff(name, input) {
|
|
|
142
202
|
}
|
|
143
203
|
return head + ` ${(0, exports.dim)(`${lines.length} lines, ${bodyText.length.toLocaleString()} characters`)}\n`;
|
|
144
204
|
}
|
|
205
|
+
if (name === "git_commit") {
|
|
206
|
+
// What will be committed, shown before it is (manual 5.17).
|
|
207
|
+
const files = input.paths ?? [];
|
|
208
|
+
return `\n ${(0, exports.bold)("git_commit")}\n files ${files.join(", ") || (0, exports.dim)("none")}\n`
|
|
209
|
+
+ ` message ${String(input.message ?? "").split("\n")[0]}\n`;
|
|
210
|
+
}
|
|
145
211
|
if (name === "run_command" || name === "run_background") {
|
|
146
212
|
return `\n ${(0, exports.bold)("run")} ${String(input.command ?? "")}\n`;
|
|
147
213
|
}
|
|
@@ -154,6 +220,22 @@ function diff(name, input) {
|
|
|
154
220
|
}
|
|
155
221
|
return head;
|
|
156
222
|
}
|
|
223
|
+
/** What was stopped when a session ended, one line each (manual 5.13). */
|
|
224
|
+
function stoppedLines(stopped) {
|
|
225
|
+
if (!stopped.length)
|
|
226
|
+
return "";
|
|
227
|
+
return (0, exports.dim)(` stopped the background process${stopped.length === 1 ? "" : "es"} this session started:\n`)
|
|
228
|
+
+ stopped.map((l) => (0, exports.dim)(` ${l}\n`)).join("");
|
|
229
|
+
}
|
|
230
|
+
/** The first line of a finished job, as the service wrote it (manual 5.15).
|
|
231
|
+
* Colored by its first word only; the words are the service's. */
|
|
232
|
+
function ending(line, detail) {
|
|
233
|
+
const paint = /^Done$/.test(line) ? exports.green : /^Done,/.test(line) ? exports.yellow
|
|
234
|
+
: /^(Stopped|Incomplete)/.test(line) ? exports.red : (t) => t;
|
|
235
|
+
// The service may put the same words in both; they are said once.
|
|
236
|
+
const extra = detail && detail !== line ? (0, exports.dim)(` ${detail}`) : "";
|
|
237
|
+
return `\n${paint(line)}${extra}\n`;
|
|
238
|
+
}
|
|
157
239
|
/** How the job ended, in the words a person reads. */
|
|
158
240
|
function outcome(o, detail) {
|
|
159
241
|
const word = {
|
|
@@ -163,6 +245,8 @@ function outcome(o, detail) {
|
|
|
163
245
|
spend_limit: (0, exports.yellow)("Stopped at the spending limit"),
|
|
164
246
|
failed: (0, exports.red)("Could not finish"),
|
|
165
247
|
refused: (0, exports.red)("Refused"),
|
|
248
|
+
busy: (0, exports.yellow)("Busy"),
|
|
249
|
+
unknown: (0, exports.yellow)("Outcome unknown"),
|
|
166
250
|
};
|
|
167
251
|
return `\n${word[o] ?? o}${detail ? (0, exports.dim)(` ${detail}`) : ""}\n`;
|
|
168
252
|
}
|
|
@@ -179,6 +263,32 @@ function link(text, url) {
|
|
|
179
263
|
return `${text} (${url})`;
|
|
180
264
|
return `]8;;${url}${text}]8;;`;
|
|
181
265
|
}
|
|
266
|
+
/**
|
|
267
|
+
* A file the agent made, as a link that opens it, with where it lives beneath.
|
|
268
|
+
*
|
|
269
|
+
* The name alone is not enough: somebody who asked from their home folder for a
|
|
270
|
+
* diagram cannot tell which folder "saved diagram.png" means. So the full path
|
|
271
|
+
* is written out underneath in gray, where it can be copied into Finder or a
|
|
272
|
+
* terminal, and the name above it opens the file on a click.
|
|
273
|
+
*/
|
|
274
|
+
function savedFile(name, fullPath, bytes) {
|
|
275
|
+
const href = (0, url_1.pathToFileURL)(fullPath).href;
|
|
276
|
+
return `${(0, exports.green)(" saved ")}${link(name, href)}${(0, exports.dim)(` ${bytes} bytes`)}\n`
|
|
277
|
+
+ `${(0, exports.gray)(` ${fullPath}`)}\n`;
|
|
278
|
+
}
|
|
279
|
+
/** The card's two lines, as the approved look draws them. */
|
|
280
|
+
function cardRows(card) {
|
|
281
|
+
const rows = [];
|
|
282
|
+
const head = [card.title ? (0, exports.bold)(card.title) : "", card.badge ? (0, exports.violet)(card.badge) : "",
|
|
283
|
+
card.vs ? (0, exports.dim)(card.vs) : ""].filter(Boolean).join(" ");
|
|
284
|
+
if (head)
|
|
285
|
+
rows.push(head);
|
|
286
|
+
if (card.amount)
|
|
287
|
+
rows.push(card.saved ? (0, exports.bold)((0, exports.green)(card.amount)) : (0, exports.dim)(card.amount));
|
|
288
|
+
if (card.warning)
|
|
289
|
+
rows.push((0, exports.dim)(card.warning));
|
|
290
|
+
return rows;
|
|
291
|
+
}
|
|
182
292
|
/**
|
|
183
293
|
* The receipt under a finished job.
|
|
184
294
|
*
|
|
@@ -195,9 +305,9 @@ function link(text, url) {
|
|
|
195
305
|
* it, quieten it, and turn the address in it into something clickable. That is
|
|
196
306
|
* formatting, which belongs to the client, and it is the only part that does.
|
|
197
307
|
*/
|
|
198
|
-
function receipt(text, models = [], testsPassed) {
|
|
199
|
-
const lines = [];
|
|
200
|
-
for (const line of String(text || "").split("\n")) {
|
|
308
|
+
function receipt(text, models = [], testsPassed, card) {
|
|
309
|
+
const lines = card ? cardRows(card).map((r) => ` ${r}`) : [];
|
|
310
|
+
for (const line of card ? [] : String(text || "").split("\n")) {
|
|
201
311
|
if (!line.trim())
|
|
202
312
|
continue;
|
|
203
313
|
// The address our server put in the sentence becomes a real link, and the
|
|
@@ -224,32 +334,324 @@ function receipt(text, models = [], testsPassed) {
|
|
|
224
334
|
if (testsPassed !== undefined) {
|
|
225
335
|
lines.push(testsPassed ? (0, exports.green)(" tests passed") : (0, exports.red)(" tests failed"));
|
|
226
336
|
}
|
|
227
|
-
|
|
337
|
+
if (!lines.length)
|
|
338
|
+
return "";
|
|
339
|
+
// On a screen, the receipt is a highlighted box, as on the website. Piped to
|
|
340
|
+
// a file or another program it stays plain lines, which is what a log wants.
|
|
341
|
+
if (card) {
|
|
342
|
+
// The link the server put in its sentence still goes last.
|
|
343
|
+
const found = /\[([^\]]+)\]\(([^)]+)\)/.exec(String(text || ""));
|
|
344
|
+
if (found)
|
|
345
|
+
lines.push((0, exports.dim)(` ${link(String(card.details || found[1]), String(found[2]))}`));
|
|
346
|
+
}
|
|
347
|
+
return process.stdout.isTTY ? boxed(text, models, testsPassed, card) : `${lines.join("\n")}\n`;
|
|
348
|
+
}
|
|
349
|
+
/** Words wrapped to a width, never breaking a word. */
|
|
350
|
+
function wrap(text, room) {
|
|
351
|
+
const out = [];
|
|
352
|
+
let line = "";
|
|
353
|
+
for (const word of text.split(/\s+/).filter(Boolean)) {
|
|
354
|
+
if (line && line.length + 1 + word.length > room) {
|
|
355
|
+
out.push(line);
|
|
356
|
+
line = word;
|
|
357
|
+
}
|
|
358
|
+
else
|
|
359
|
+
line = line ? `${line} ${word}` : word;
|
|
360
|
+
}
|
|
361
|
+
if (line)
|
|
362
|
+
out.push(line);
|
|
363
|
+
return out;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* The receipt in a rounded box: `receipt` as its label, the saving in green,
|
|
367
|
+
* everything else quiet. The server's words are unchanged; each clause it
|
|
368
|
+
* separated with `|` gets its own line, and its link goes last, clickable.
|
|
369
|
+
*/
|
|
370
|
+
function boxed(text, models, testsPassed, card) {
|
|
371
|
+
// Room for the border, the padding and the `receipt` label on the first line.
|
|
372
|
+
const inner = width() - 16;
|
|
373
|
+
const rows = card ? cardRows(card) : [];
|
|
374
|
+
const links = [];
|
|
375
|
+
for (const raw of String(text || "").split("\n")) {
|
|
376
|
+
if (card) {
|
|
377
|
+
// The card carries the words; only the sentence's link is kept.
|
|
378
|
+
raw.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, label, url) => {
|
|
379
|
+
links.push({ label: String(card.details || label), url: String(url) });
|
|
380
|
+
return "";
|
|
381
|
+
});
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
const withoutLinks = raw.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, label, url) => {
|
|
385
|
+
links.push({ label: String(label), url: String(url) });
|
|
386
|
+
return "";
|
|
387
|
+
});
|
|
388
|
+
for (const clause of withoutLinks.split("|").map((c) => c.trim()).filter(Boolean)) {
|
|
389
|
+
const paintIt = /\bsaved\b/i.test(clause) ? exports.green : exports.dim;
|
|
390
|
+
for (const piece of wrap(clause, inner))
|
|
391
|
+
rows.push(paintIt(piece));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
const priced = models.filter((m) => m.model);
|
|
395
|
+
if (priced.length > 1) {
|
|
396
|
+
const order = [...priced].sort((a, b) => (a.usd ?? 0) - (b.usd ?? 0));
|
|
397
|
+
const w = Math.max(...order.map((m) => modelName(String(m.model)).length));
|
|
398
|
+
for (const m of order) {
|
|
399
|
+
const steps = `${m.steps} step${m.steps === 1 ? "" : "s"}`;
|
|
400
|
+
const cost = m.usd === null || m.usd === undefined ? "not priced" : money(m.usd);
|
|
401
|
+
rows.push(`${(0, exports.violet)(modelName(String(m.model)).padEnd(w))}${(0, exports.dim)(` ${steps.padEnd(9)} ${cost}`)}`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (testsPassed !== undefined)
|
|
405
|
+
rows.push(testsPassed ? (0, exports.green)("tests passed") : (0, exports.red)("tests failed"));
|
|
406
|
+
for (const l of links)
|
|
407
|
+
rows.push((0, exports.dim)(link(l.label, l.url)));
|
|
408
|
+
if (rows.length)
|
|
409
|
+
rows[0] = `${(0, exports.violet)("receipt")} ${rows[0]}`;
|
|
410
|
+
const widest = Math.max(...rows.map((r) => visible(r).length));
|
|
411
|
+
const bar = "\u2500".repeat(widest + 2);
|
|
412
|
+
const body = rows.map((r) => ` ${(0, exports.violet)("\u2502")} ${r}${" ".repeat(widest - visible(r).length)} ${(0, exports.violet)("\u2502")}`);
|
|
413
|
+
return [` ${(0, exports.violet)(`\u256d${bar}\u256e`)}`, ...body, ` ${(0, exports.violet)(`\u2570${bar}\u256f`)}`].join("\n") + "\n";
|
|
414
|
+
}
|
|
415
|
+
/** A markdown table's cells, with emphasis and code marks taken off and a
|
|
416
|
+
* `<br>` kept as a line break inside the cell. */
|
|
417
|
+
function cells(row) {
|
|
418
|
+
return row.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim()
|
|
419
|
+
.replace(/\*\*([^*]+)\*\*/g, "$1").replace(/__([^_]+)__/g, "$1").replace(/`([^`]+)`/g, "$1")
|
|
420
|
+
.replace(/<br\s*\/?>/gi, "\n"));
|
|
421
|
+
}
|
|
422
|
+
/** Widths that fit the terminal: each column its natural width when they all
|
|
423
|
+
* fit, otherwise the narrow ones keep theirs and the wide ones share the rest. */
|
|
424
|
+
function fitWidths(natural, room) {
|
|
425
|
+
const out = natural.map(() => 0);
|
|
426
|
+
let left = room;
|
|
427
|
+
let open = natural.map((_, i) => i).sort((a, b) => natural[a] - natural[b]);
|
|
428
|
+
while (open.length) {
|
|
429
|
+
const share = Math.floor(left / open.length);
|
|
430
|
+
const i = open[0];
|
|
431
|
+
if (natural[i] <= share) {
|
|
432
|
+
out[i] = natural[i];
|
|
433
|
+
left -= natural[i];
|
|
434
|
+
open = open.slice(1);
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
for (const j of open)
|
|
438
|
+
out[j] = Math.max(6, share);
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
return out;
|
|
442
|
+
}
|
|
443
|
+
/** One cell's text as lines no wider than its column. */
|
|
444
|
+
function cellLines(text, w) {
|
|
445
|
+
const lines = [];
|
|
446
|
+
for (const part of text.split("\n")) {
|
|
447
|
+
const words = wrap(part, w);
|
|
448
|
+
for (const l of words.length ? words : [""]) {
|
|
449
|
+
// A single word longer than the column is cut, never allowed to push the border.
|
|
450
|
+
for (let k = 0; k < Math.max(1, l.length); k += w)
|
|
451
|
+
lines.push(l.slice(k, k + w));
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return lines;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Markdown tables drawn as terminal tables.
|
|
458
|
+
*
|
|
459
|
+
* A model asked for "a summary table" writes pipes and dashes, which a browser
|
|
460
|
+
* draws and a terminal shows as raw markup. Each block of `|` rows with a
|
|
461
|
+
* `|---|` line under its header becomes a boxed table, with long cells wrapped
|
|
462
|
+
* inside their column so the table always fits the window.
|
|
463
|
+
*/
|
|
464
|
+
function tables(text) {
|
|
465
|
+
const lines = text.split("\n");
|
|
466
|
+
const out = [];
|
|
467
|
+
const isRow = (l) => l !== undefined && /^\s*\|.*\|\s*$/.test(l);
|
|
468
|
+
const isRule = (l) => l !== undefined && /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)*\|?\s*$/.test(l);
|
|
469
|
+
for (let i = 0; i < lines.length; i++) {
|
|
470
|
+
if (!(isRow(lines[i]) && isRule(lines[i + 1]))) {
|
|
471
|
+
out.push(lines[i]);
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
const head = cells(lines[i]);
|
|
475
|
+
const body = [];
|
|
476
|
+
i += 2;
|
|
477
|
+
while (isRow(lines[i]))
|
|
478
|
+
body.push(cells(lines[i++]));
|
|
479
|
+
i--;
|
|
480
|
+
const cols = head.length;
|
|
481
|
+
const rows = [head, ...body].map((r) => Array.from({ length: cols }, (_, k) => r[k] ?? ""));
|
|
482
|
+
const natural = Array.from({ length: cols }, (_, k) => Math.max(3, ...rows.map((r) => Math.max(...r[k].split("\n").map((s) => s.length)))));
|
|
483
|
+
const w = fitWidths(natural, width() - 4 - (3 * cols + 1));
|
|
484
|
+
const bar = (l, m, r) => (0, exports.dim)(` ${l}${w.map((n) => "─".repeat(n + 2)).join(m)}${r}`);
|
|
485
|
+
const draw = (r, strong) => {
|
|
486
|
+
const wrapped = r.map((c, k) => cellLines(c, w[k]));
|
|
487
|
+
const height = Math.max(...wrapped.map((c) => c.length));
|
|
488
|
+
for (let h = 0; h < height; h++) {
|
|
489
|
+
out.push(` ${(0, exports.dim)("│")}${wrapped.map((c, k) => {
|
|
490
|
+
const s = (c[h] ?? "").padEnd(w[k]);
|
|
491
|
+
return ` ${strong ? (0, exports.bold)(s) : s} `;
|
|
492
|
+
}).join((0, exports.dim)("│"))}${(0, exports.dim)("│")}`);
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
out.push(bar("┌", "┬", "┐"));
|
|
496
|
+
draw(rows[0], true);
|
|
497
|
+
out.push(bar("├", "┼", "┤"));
|
|
498
|
+
rows.slice(1).forEach((r, n) => {
|
|
499
|
+
draw(r, false);
|
|
500
|
+
if (n < rows.length - 2)
|
|
501
|
+
out.push(bar("├", "┼", "┤"));
|
|
502
|
+
});
|
|
503
|
+
out.push(bar("└", "┴", "┘"));
|
|
504
|
+
}
|
|
505
|
+
return out.join("\n");
|
|
506
|
+
}
|
|
507
|
+
/** The answer's two accents, one pair per terminal background (manual 6.10):
|
|
508
|
+
* the brand violet and amber on a dark terminal, the brand purple and deep
|
|
509
|
+
* blue on a light one, each above 4.5 to 1 against its background. */
|
|
510
|
+
const onLight = () => process.env.COMPRISM_THEME === "light";
|
|
511
|
+
const headingPaint = (t) => paint(onLight() ? "1;38;2;107;91;167" : "1;38;5;141", t);
|
|
512
|
+
const codePaint = (t) => paint(onLight() ? "38;2;31;77;120" : "38;2;217;119;6", t);
|
|
513
|
+
/** Characters of text per line, the width Glow and Claude Code read at. */
|
|
514
|
+
const READING_WIDTH = 80;
|
|
515
|
+
const same = (t) => t;
|
|
516
|
+
/** A line of markdown as words, each piece carrying its own emphasis, so a
|
|
517
|
+
* wrapped line never starts inside a color it cannot see the start of. */
|
|
518
|
+
function words(text) {
|
|
519
|
+
const linked = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, t, u) => `${t} (${u})`);
|
|
520
|
+
const parts = [];
|
|
521
|
+
const re = /(\*\*[^*]+\*\*|__[^_]+__|`[^`]+`|\*[^*\s][^*]*\*)/g;
|
|
522
|
+
let last = 0;
|
|
523
|
+
for (let m = re.exec(linked); m; m = re.exec(linked)) {
|
|
524
|
+
parts.push({ t: linked.slice(last, m.index), p: same });
|
|
525
|
+
const s = m[0];
|
|
526
|
+
parts.push(s.startsWith("`") ? { t: s.slice(1, -1), p: codePaint }
|
|
527
|
+
: s.startsWith("**") || s.startsWith("__") ? { t: s.slice(2, -2), p: exports.bold }
|
|
528
|
+
: { t: s.slice(1, -1), p: (t) => paint("3", t) });
|
|
529
|
+
last = m.index + s.length;
|
|
530
|
+
}
|
|
531
|
+
parts.push({ t: linked.slice(last), p: same });
|
|
532
|
+
const out = [];
|
|
533
|
+
let cur = [];
|
|
534
|
+
for (const part of parts) {
|
|
535
|
+
for (const piece of part.t.split(/(\s+)/)) {
|
|
536
|
+
if (!piece)
|
|
537
|
+
continue;
|
|
538
|
+
if (/^\s+$/.test(piece)) {
|
|
539
|
+
if (cur.length) {
|
|
540
|
+
out.push(cur);
|
|
541
|
+
cur = [];
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
else
|
|
545
|
+
cur.push({ t: piece, p: part.p });
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
if (cur.length)
|
|
549
|
+
out.push(cur);
|
|
550
|
+
return out;
|
|
551
|
+
}
|
|
552
|
+
const show = (w) => w.map((x) => x.p(x.t)).join("");
|
|
553
|
+
const size = (w) => w.reduce((n, x) => n + x.t.length, 0);
|
|
554
|
+
/** Words filled into lines no wider than `room`, a word never split, every
|
|
555
|
+
* line after the first starting under the first line's text. */
|
|
556
|
+
function fill(text, lead, hang, room) {
|
|
557
|
+
const lines = [];
|
|
558
|
+
let line = lead, used = visible(lead).length, empty = true;
|
|
559
|
+
for (const w of words(text)) {
|
|
560
|
+
if (!empty && used + 1 + size(w) > room) {
|
|
561
|
+
lines.push(line);
|
|
562
|
+
line = hang;
|
|
563
|
+
used = visible(hang).length;
|
|
564
|
+
empty = true;
|
|
565
|
+
}
|
|
566
|
+
line += (empty ? "" : " ") + show(w);
|
|
567
|
+
used += (empty ? 0 : 1) + size(w);
|
|
568
|
+
empty = false;
|
|
569
|
+
}
|
|
570
|
+
lines.push(line);
|
|
571
|
+
return lines;
|
|
228
572
|
}
|
|
229
573
|
/**
|
|
230
|
-
*
|
|
574
|
+
* An answer, laid out for reading in a terminal (manual 6.17).
|
|
231
575
|
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
576
|
+
* The approved design (docs/product/design/cli-answer, option B): the whole
|
|
577
|
+
* answer on one left margin behind a violet rail, so where the question ends
|
|
578
|
+
* and the answer begins is never in doubt; words wrapped at a reading width
|
|
579
|
+
* and never split by the window edge; a blank line between every block;
|
|
580
|
+
* headings in the brand violet; code in its own color; wrapped list items
|
|
581
|
+
* continuing under their own text. Piped to a file or another program, the
|
|
582
|
+
* same answer comes out without the rail or the color, as a log wants it.
|
|
236
583
|
*/
|
|
237
584
|
function plain(text) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
.
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
585
|
+
const screen = supportsColour();
|
|
586
|
+
const lead = screen ? ` ${paint(onLight() ? "38;2;107;91;167" : "38;5;141", "│")} ` : " ";
|
|
587
|
+
const railOnly = screen ? lead.trimEnd() : "";
|
|
588
|
+
const room = Math.min(width() - 1, visible(lead).length + READING_WIDTH);
|
|
589
|
+
const src = text.replace(/\r\n/g, "\n").split("\n");
|
|
590
|
+
const out = [];
|
|
591
|
+
const gap = () => { if (out.length && out[out.length - 1] !== railOnly)
|
|
592
|
+
out.push(railOnly); };
|
|
593
|
+
const isTableRow = (l) => l !== undefined && /^\s*\|.*\|\s*$/.test(l);
|
|
594
|
+
const startsBlock = (l) => /^\s*([-*+•]\s|\d+[.)]\s|#{1,6}\s|```|>|\|)/.test(l) || /^\s*([-*_])\s*\1\s*\1[\s\-*_]*$/.test(l);
|
|
595
|
+
for (let i = 0; i < src.length; i++) {
|
|
596
|
+
const l = src[i];
|
|
597
|
+
if (!l.trim())
|
|
598
|
+
continue;
|
|
599
|
+
if (/^\s*```/.test(l)) {
|
|
600
|
+
gap();
|
|
601
|
+
for (i++; i < src.length && !/^\s*```/.test(src[i]); i++)
|
|
602
|
+
out.push(`${lead} ${codePaint(src[i])}`);
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
const h = /^\s*#{1,6}\s+(.+?)\s*#*\s*$/.exec(l);
|
|
606
|
+
if (h) {
|
|
607
|
+
gap();
|
|
608
|
+
out.push(lead + headingPaint(h[1].replace(/\*\*/g, "")));
|
|
609
|
+
gap();
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
if (/^\s*([-*_])\s*\1\s*\1[\s\-*_]*$/.test(l)) {
|
|
613
|
+
gap();
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
if (isTableRow(l)) {
|
|
617
|
+
const block = [];
|
|
618
|
+
while (isTableRow(src[i]))
|
|
619
|
+
block.push(src[i++]);
|
|
620
|
+
i--;
|
|
621
|
+
gap();
|
|
622
|
+
// The table's own two space indent gives way to the rail, whether or
|
|
623
|
+
// not a color code comes before it.
|
|
624
|
+
const indent = new RegExp(`^((?:${String.fromCharCode(27)}\\[[0-9;]*m)*) {2}`);
|
|
625
|
+
for (const row of tables(block.join("\n")).split("\n"))
|
|
626
|
+
out.push(lead + row.replace(indent, "$1"));
|
|
627
|
+
gap();
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
const bullet = /^(\s*)([-*+•]|\d+[.)])\s+(.*)$/.exec(l);
|
|
631
|
+
if (bullet) {
|
|
632
|
+
if (out.length && !/^\s*([-*+•]|\d+[.)])\s/.test(src[i - 1] ?? ""))
|
|
633
|
+
gap();
|
|
634
|
+
const depth = Math.floor(bullet[1].replace(/\t/g, " ").length / 2);
|
|
635
|
+
const mark = /\d/.test(bullet[2]) ? `${bullet[2].replace(")", ".")} ` : "• ";
|
|
636
|
+
let body = bullet[3];
|
|
637
|
+
// A list item that runs onto an indented next line is one item.
|
|
638
|
+
while (i + 1 < src.length && src[i + 1].trim() && /^\s{2,}\S/.test(src[i + 1]) && !startsBlock(src[i + 1]))
|
|
639
|
+
body += " " + src[++i].trim();
|
|
640
|
+
const indent = " ".repeat(depth);
|
|
641
|
+
out.push(...fill(body, lead + indent + (0, exports.dim)(mark), lead + indent + " ".repeat(mark.length), room));
|
|
642
|
+
if (i + 1 < src.length && !/^\s*([-*+•]|\d+[.)])\s/.test(src[i + 1]))
|
|
643
|
+
gap();
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
// A paragraph is every line up to the next blank line or block.
|
|
647
|
+
let para = l.trim().replace(/^>\s?/, "");
|
|
648
|
+
while (i + 1 < src.length && src[i + 1].trim() && !startsBlock(src[i + 1]))
|
|
649
|
+
para += " " + src[++i].trim();
|
|
650
|
+
gap();
|
|
651
|
+
out.push(...fill(para, lead, lead, room));
|
|
652
|
+
gap();
|
|
653
|
+
}
|
|
654
|
+
while (out.length && out[out.length - 1] === railOnly)
|
|
655
|
+
out.pop();
|
|
656
|
+
return out.join("\n");
|
|
255
657
|
}
|