@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.
Files changed (142) hide show
  1. package/README.md +61 -2
  2. package/out/agent/command.d.ts +75 -4
  3. package/out/agent/command.js +221 -26
  4. package/out/agent/render.d.ts +56 -8
  5. package/out/agent/render.js +441 -39
  6. package/out/agent/session.d.ts +156 -2
  7. package/out/agent/session.js +795 -154
  8. package/out/commands/agents.d.ts +2 -0
  9. package/out/commands/agents.js +79 -0
  10. package/out/commands/ask.d.ts +1 -1
  11. package/out/commands/ask.js +88 -12
  12. package/out/commands/commands-thin.js +79 -17
  13. package/out/commands/config.d.ts +1 -0
  14. package/out/commands/config.js +138 -0
  15. package/out/commands/cost.d.ts +1 -0
  16. package/out/commands/cost.js +167 -0
  17. package/out/commands/hooks.d.ts +1 -0
  18. package/out/commands/hooks.js +83 -0
  19. package/out/commands/install.d.ts +44 -1
  20. package/out/commands/install.js +198 -4
  21. package/out/commands/instructions.d.ts +1 -0
  22. package/out/commands/instructions.js +113 -0
  23. package/out/commands/integrations.d.ts +3 -0
  24. package/out/commands/integrations.js +215 -0
  25. package/out/commands/jobs.d.ts +5 -0
  26. package/out/commands/jobs.js +157 -0
  27. package/out/commands/login.js +249 -43
  28. package/out/commands/memory.d.ts +3 -0
  29. package/out/commands/memory.js +113 -0
  30. package/out/commands/permissions.d.ts +1 -0
  31. package/out/commands/permissions.js +94 -0
  32. package/out/commands/plugins.d.ts +4 -0
  33. package/out/commands/plugins.js +192 -0
  34. package/out/commands/privacy.d.ts +1 -0
  35. package/out/commands/privacy.js +57 -0
  36. package/out/commands/providerKey.d.ts +32 -0
  37. package/out/commands/providerKey.js +108 -0
  38. package/out/commands/repl.d.ts +12 -1
  39. package/out/commands/repl.js +1649 -122
  40. package/out/commands/report.d.ts +39 -0
  41. package/out/commands/report.js +115 -0
  42. package/out/commands/review.d.ts +5 -0
  43. package/out/commands/review.js +223 -0
  44. package/out/commands/sessions.d.ts +23 -0
  45. package/out/commands/sessions.js +115 -0
  46. package/out/commands/settings.d.ts +3 -1
  47. package/out/commands/settings.js +18 -16
  48. package/out/commands/skills.d.ts +21 -0
  49. package/out/commands/skills.js +207 -0
  50. package/out/commands/unattended.d.ts +7 -0
  51. package/out/commands/unattended.js +351 -0
  52. package/out/commands/update.d.ts +1 -0
  53. package/out/commands/update.js +123 -0
  54. package/out/commands/welcome.d.ts +25 -1
  55. package/out/commands/welcome.js +71 -35
  56. package/out/commands/worktrees.d.ts +5 -0
  57. package/out/commands/worktrees.js +186 -0
  58. package/out/executor/browser.d.ts +14 -0
  59. package/out/executor/browser.js +270 -0
  60. package/out/executor/diagnostics.d.ts +2 -0
  61. package/out/executor/diagnostics.js +181 -0
  62. package/out/executor/files.js +270 -40
  63. package/out/executor/git.js +42 -29
  64. package/out/executor/hooks.d.ts +42 -58
  65. package/out/executor/hooks.js +89 -182
  66. package/out/executor/index.d.ts +21 -5
  67. package/out/executor/index.js +160 -14
  68. package/out/executor/paths.d.ts +6 -1
  69. package/out/executor/paths.js +34 -6
  70. package/out/executor/sandbox.d.ts +40 -0
  71. package/out/executor/sandbox.js +299 -0
  72. package/out/executor/shell.d.ts +49 -4
  73. package/out/executor/shell.js +302 -56
  74. package/out/executor/toolservers.d.ts +20 -0
  75. package/out/executor/toolservers.js +189 -0
  76. package/out/executor/worktree.d.ts +9 -0
  77. package/out/executor/worktree.js +119 -0
  78. package/out/graph/read-python.js +2 -1
  79. package/out/lib/attach.d.ts +56 -12
  80. package/out/lib/attach.js +230 -63
  81. package/out/lib/clipboard.d.ts +23 -0
  82. package/out/lib/clipboard.js +182 -0
  83. package/out/lib/commandlist.d.ts +20 -0
  84. package/out/lib/commandlist.js +58 -0
  85. package/out/lib/decision.d.ts +22 -0
  86. package/out/lib/decision.js +50 -0
  87. package/out/lib/fingerprint.d.ts +25 -0
  88. package/out/lib/fingerprint.js +58 -0
  89. package/out/lib/gateway.d.ts +190 -2
  90. package/out/lib/gateway.js +59 -4
  91. package/out/lib/history.d.ts +24 -0
  92. package/out/lib/history.js +137 -0
  93. package/out/lib/ide.d.ts +19 -0
  94. package/out/lib/ide.js +131 -0
  95. package/out/lib/keyboard.d.ts +98 -0
  96. package/out/lib/keyboard.js +390 -0
  97. package/out/lib/lastlogin.d.ts +14 -0
  98. package/out/lib/lastlogin.js +105 -0
  99. package/out/lib/machine.d.ts +21 -0
  100. package/out/lib/machine.js +91 -0
  101. package/out/lib/notify.d.ts +4 -0
  102. package/out/lib/notify.js +52 -0
  103. package/out/lib/output.d.ts +48 -0
  104. package/out/lib/output.js +108 -0
  105. package/out/lib/picker.d.ts +1 -0
  106. package/out/lib/picker.js +158 -0
  107. package/out/lib/project-ops.d.ts +19 -0
  108. package/out/lib/project-ops.js +146 -0
  109. package/out/lib/project.d.ts +28 -0
  110. package/out/lib/project.js +114 -0
  111. package/out/lib/prompt.d.ts +3 -0
  112. package/out/lib/prompt.js +36 -2
  113. package/out/lib/queue.d.ts +13 -0
  114. package/out/lib/queue.js +116 -0
  115. package/out/lib/readiness.d.ts +19 -0
  116. package/out/lib/readiness.js +170 -1
  117. package/out/lib/self.d.ts +23 -0
  118. package/out/lib/self.js +124 -0
  119. package/out/lib/sessions.d.ts +19 -0
  120. package/out/lib/sessions.js +221 -0
  121. package/out/lib/startcache.d.ts +13 -0
  122. package/out/lib/startcache.js +78 -0
  123. package/out/lib/stdin.d.ts +32 -0
  124. package/out/lib/stdin.js +117 -0
  125. package/out/lib/store.d.ts +40 -0
  126. package/out/lib/store.js +138 -0
  127. package/out/lib/sync.d.ts +18 -0
  128. package/out/lib/sync.js +81 -0
  129. package/out/lib/ui.d.ts +10 -4
  130. package/out/lib/ui.js +65 -25
  131. package/out/lib/voice.d.ts +36 -0
  132. package/out/lib/voice.js +160 -1
  133. package/out/native/dictate +0 -0
  134. package/out/postinstall.js +42 -17
  135. package/out/providers/anthropic.d.ts +22 -0
  136. package/out/providers/anthropic.js +80 -0
  137. package/out/providers/index.d.ts +8 -0
  138. package/out/providers/index.js +83 -0
  139. package/out/providers/openai.d.ts +11 -0
  140. package/out/providers/openai.js +57 -0
  141. package/out/thin.js +595 -33
  142. package/package.json +11 -50
@@ -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
- const supportsColour = process.stdout.isTTY && process.env.NO_COLOR === undefined;
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
- const mark = ok ? (0, exports.green)("ok ") : (0, exports.red)("fail");
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
- return (0, exports.dim)(` step ${index} ${model || "?"} ${money(usd)} ${duration(ms)}${warm}`);
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)("done")
106
- : t.status === "in_progress" ? (0, exports.yellow)("now ") : (0, exports.dim)("todo");
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 ` ${mark} ${text}`;
160
+ return ` ${mark} ${text}`;
109
161
  });
110
- return `\n ${(0, exports.bold)("Plan")}\n${rows.join("\n")}\n`;
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
- const head = `\n ${(0, exports.bold)(name)} ${target}\n`;
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
- return lines.length > 0 ? `${lines.join("\n")}\n` : "";
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
- * Plain text, from a model that writes markdown.
574
+ * An answer, laid out for reading in a terminal (manual 6.17).
231
575
  *
232
- * A terminal has no bold and no bullet glyphs, so `**like this**` arrives on
233
- * screen as literal asterisks and a heading looks like a typing mistake. The
234
- * emphasis is real, so it is kept as terminal bold rather than thrown away;
235
- * everything that only exists to be rendered by a browser is removed.
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
- return text
239
- // Fenced code keeps its content and loses its fence markers.
240
- .replace(/^```[a-zA-Z0-9]*\n?/gm, '')
241
- .replace(/^```$/gm, '')
242
- // Headings become their own words, emphasized.
243
- .replace(/^#{1,6}\s+(.+)$/gm, (_m, t) => (0, exports.bold)(String(t)))
244
- // Bold and italic become the terminal's own emphasis.
245
- .replace(/\*\*([^*]+)\*\*/g, (_m, t) => (0, exports.bold)(String(t)))
246
- .replace(/__([^_]+)__/g, (_m, t) => (0, exports.bold)(String(t)))
247
- .replace(/(^|[^*])\*([^*\n]+)\*/g, (_m, pre, t) => pre + String(t))
248
- // A bullet is a bullet, not an asterisk.
249
- .replace(/^\s*[*-]\s+/gm, ' . ')
250
- // Inline code keeps its text; the backticks were never for a terminal.
251
- .replace(/`([^`]+)`/g, (_m, t) => String(t))
252
- // A link reads as its words, with the address after it.
253
- .replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_m, t, u) => `${t} (${u})`)
254
- .replace(/\n{3,}/g, '\n\n');
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
  }