@illuminis/comprism 0.1.0

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 (80) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +281 -0
  3. package/out/agent/command.d.ts +86 -0
  4. package/out/agent/command.js +259 -0
  5. package/out/agent/render.d.ts +97 -0
  6. package/out/agent/render.js +255 -0
  7. package/out/agent/session.d.ts +175 -0
  8. package/out/agent/session.js +573 -0
  9. package/out/commands/ask.d.ts +1 -0
  10. package/out/commands/ask.js +146 -0
  11. package/out/commands/codemap.d.ts +2 -0
  12. package/out/commands/codemap.js +151 -0
  13. package/out/commands/commands-thin.d.ts +39 -0
  14. package/out/commands/commands-thin.js +182 -0
  15. package/out/commands/install.d.ts +163 -0
  16. package/out/commands/install.js +543 -0
  17. package/out/commands/keys.d.ts +55 -0
  18. package/out/commands/keys.js +344 -0
  19. package/out/commands/login.d.ts +9 -0
  20. package/out/commands/login.js +384 -0
  21. package/out/commands/repl.d.ts +1 -0
  22. package/out/commands/repl.js +752 -0
  23. package/out/commands/settings.d.ts +21 -0
  24. package/out/commands/settings.js +244 -0
  25. package/out/commands/welcome.d.ts +1 -0
  26. package/out/commands/welcome.js +196 -0
  27. package/out/executor/documents.d.ts +40 -0
  28. package/out/executor/documents.js +170 -0
  29. package/out/executor/files.d.ts +2 -0
  30. package/out/executor/files.js +360 -0
  31. package/out/executor/git.d.ts +48 -0
  32. package/out/executor/git.js +132 -0
  33. package/out/executor/hooks.d.ts +67 -0
  34. package/out/executor/hooks.js +247 -0
  35. package/out/executor/index.d.ts +29 -0
  36. package/out/executor/index.js +221 -0
  37. package/out/executor/notebook.d.ts +2 -0
  38. package/out/executor/notebook.js +147 -0
  39. package/out/executor/paths.d.ts +15 -0
  40. package/out/executor/paths.js +126 -0
  41. package/out/executor/shell.d.ts +41 -0
  42. package/out/executor/shell.js +336 -0
  43. package/out/graph/build.d.ts +45 -0
  44. package/out/graph/build.js +91 -0
  45. package/out/graph/facts.d.ts +47 -0
  46. package/out/graph/facts.js +12 -0
  47. package/out/graph/files.d.ts +45 -0
  48. package/out/graph/files.js +207 -0
  49. package/out/graph/read-locales.d.ts +29 -0
  50. package/out/graph/read-locales.js +246 -0
  51. package/out/graph/read-python.d.ts +11 -0
  52. package/out/graph/read-python.js +115 -0
  53. package/out/graph/read-typescript.d.ts +16 -0
  54. package/out/graph/read-typescript.js +292 -0
  55. package/out/graph/sync.d.ts +66 -0
  56. package/out/graph/sync.js +242 -0
  57. package/out/lib/attach.d.ts +62 -0
  58. package/out/lib/attach.js +228 -0
  59. package/out/lib/config.d.ts +93 -0
  60. package/out/lib/config.js +198 -0
  61. package/out/lib/connection.d.ts +73 -0
  62. package/out/lib/connection.js +188 -0
  63. package/out/lib/gateway.d.ts +239 -0
  64. package/out/lib/gateway.js +171 -0
  65. package/out/lib/prompt.d.ts +34 -0
  66. package/out/lib/prompt.js +108 -0
  67. package/out/lib/types.d.ts +417 -0
  68. package/out/lib/types.js +21 -0
  69. package/out/lib/ui.d.ts +114 -0
  70. package/out/lib/ui.js +265 -0
  71. package/out/lib/version.d.ts +24 -0
  72. package/out/lib/version.js +27 -0
  73. package/out/lib/voice.d.ts +50 -0
  74. package/out/lib/voice.js +218 -0
  75. package/out/postinstall.d.ts +2 -0
  76. package/out/postinstall.js +92 -0
  77. package/out/thin.d.ts +2 -0
  78. package/out/thin.js +259 -0
  79. package/package.json +101 -0
  80. package/scripts/read_python.py +270 -0
@@ -0,0 +1,573 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.TerminalSession = void 0;
37
+ /**
38
+ * A coding job, run from a terminal.
39
+ *
40
+ * Specification: docs/modules/CODING_AGENT_BUILD_SPECIFICATION.md, register
41
+ * items A6 (interactive), A7 (scriptable), A10 and H6 (one job, any client),
42
+ * C14 (resume) and D40 (a terminal the human can watch).
43
+ *
44
+ * ## What this file is, and what it is not
45
+ *
46
+ * It is the wire between a terminal and the loop, plus the rendering. **The loop
47
+ * is not here.** Which model takes each step, what an action costs, whether an
48
+ * action is allowed and what gets written down are all decided on the server,
49
+ * exactly as they are for the browser. This connects, performs what it is asked,
50
+ * prints what happened, and asks the person when the server says to.
51
+ *
52
+ * That is deliberate and it is the whole reason a terminal agent was cheap to
53
+ * build at all: the second client reuses everything except the drawing.
54
+ *
55
+ * ## Two ways to run
56
+ *
57
+ * Interactive, with a person at the keyboard who can approve, interrupt and
58
+ * steer. And headless, driven by a script with nobody present, where an
59
+ * approval has no one to ask and the permission mode decides everything up
60
+ * front. Both are the same session; the difference is who answers.
61
+ */
62
+ const gateway_1 = require("../lib/gateway");
63
+ const readline = __importStar(require("readline"));
64
+ const executor_1 = require("../executor");
65
+ const ui = __importStar(require("./render"));
66
+ class TerminalSession {
67
+ socket = null;
68
+ executor;
69
+ opts;
70
+ out;
71
+ lastTodos = "";
72
+ /** The last thing it said on screen, so the ending does not say it twice.
73
+ * Every step prints what the model said, and the closing frame carries the
74
+ * final text again, so the answer to a one step job appeared twice in a row
75
+ * with nothing between the copies. */
76
+ lastSaid = "";
77
+ canceled = false;
78
+ constructor(opts, write = (s) => process.stdout.write(s)) {
79
+ this.opts = opts;
80
+ this.out = write;
81
+ this.executor = new executor_1.NativeExecutor({
82
+ root: opts.root,
83
+ // Streamed straight through while a command runs. A build that takes two
84
+ // minutes must not look like a hang, and the output is the only thing
85
+ // that proves it is not.
86
+ onOutput: (chunk) => {
87
+ if (!opts.headless)
88
+ this.out(ui.gray(chunk));
89
+ },
90
+ });
91
+ }
92
+ /** Where the socket lives, derived from the workspace URL.
93
+ *
94
+ * Derived rather than configured: a second setting for the socket address is
95
+ * a second thing that can point at the wrong server, and the failure would be
96
+ * a job running against somebody else's workspace. */
97
+ /**
98
+ * A pass for this job's stream, from the one service.
99
+ *
100
+ * The job is authorized THERE, where everything else in the product is
101
+ * authorized, and this returns only permission to open the stream. Null when
102
+ * the service refused or could not be reached, and the caller then reports
103
+ * the service's own words rather than guessing at them.
104
+ */
105
+ async pass(request) {
106
+ const reply = await (0, gateway_1.ask)({
107
+ intent: "job_start",
108
+ prompt: request,
109
+ ...(this.opts.resumeJobId ? { resumeJobId: this.opts.resumeJobId } : {}),
110
+ });
111
+ // An OLDER WORKSPACE that has never heard of this question is not a
112
+ // refusal. Its own credential path is alive and works, so the job falls
113
+ // back to it rather than stopping. This is the one place in the design
114
+ // where a second path is permitted, it is temporary, and it is deleted in
115
+ // the release after every workspace is known to be current.
116
+ if (reply.served === false) {
117
+ // Not a refusal. The workspace's own credential path is alive and works,
118
+ // so the job proceeds on it rather than stopping. This is the one place
119
+ // in the design where a second path is permitted, it is temporary, and it
120
+ // goes in the release after every workspace is known to be current.
121
+ this.olderWorkspace = true;
122
+ return null;
123
+ }
124
+ if (!reply.ok || !reply.job?.ticket) {
125
+ // A real refusal, in the service's own words: a plan that does not
126
+ // include coding and an allowance that has been spent have different
127
+ // fixes, and a caller told only "refused" looks in the wrong place.
128
+ this.refusal = reply.message || "your workspace could not be reached";
129
+ return null;
130
+ }
131
+ return { ticket: reply.job.ticket, jobId: reply.job.job_id };
132
+ }
133
+ /** Where the stream lives, derived from the workspace URL.
134
+ *
135
+ * Derived rather than configured: a second setting for the stream address is
136
+ * a second thing that can point at the wrong server, and the failure would be
137
+ * a job running against somebody else's workspace.
138
+ *
139
+ * The credential still travels because the pass is redeemed against the
140
+ * tenant it was issued in, and the pass is what actually authorizes the job.
141
+ * Without a pass this falls back to the old behavior, which is what a
142
+ * workspace running an older build still understands. */
143
+ socketUrl(ticket) {
144
+ const url = new URL(this.opts.baseUrl);
145
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
146
+ url.pathname = "/api/v1/agent/ws";
147
+ url.search = `?token=${encodeURIComponent(this.opts.credential)}`
148
+ + (ticket ? `&ticket=${encodeURIComponent(ticket)}` : "");
149
+ return url.toString();
150
+ }
151
+ /** The name the map is held under: the folder's own, which is exactly what
152
+ * the run frame already sends as the workspace. One expression, so the map
153
+ * and the job record can never be about two different projects. */
154
+ project() {
155
+ return this.opts.root.split("/").filter(Boolean).pop() || this.opts.root;
156
+ }
157
+ /** Why the one service would not start this job, in its own words. */
158
+ refusal = null;
159
+ /** The workspace does not carry the permission question yet, so fall back. */
160
+ olderWorkspace = false;
161
+ /** Set while one is actually running, so two never overlap. */
162
+ remapping = false;
163
+ /**
164
+ * The agent just changed something on disk. Bring the map back in step.
165
+ *
166
+ * Only for actions that can change a file. A read or a search leaves the
167
+ * project exactly as it was, and re-reading it would be a round trip bought
168
+ * for nothing on the most common actions there are.
169
+ *
170
+ * The refresh itself is the ordinary one: it compares the files against what
171
+ * the service holds and sends back only what moved, which after one edit is
172
+ * one file. Debounced by a short wait, because an agent that writes four
173
+ * files in one step should pay for one update, not four.
174
+ */
175
+ async noteChanged(tool, input) {
176
+ if (this.opts.withoutMap)
177
+ return;
178
+ // A named file changed, so exactly that file is put back into the map and
179
+ // the result waits for it. One round trip, and the agent can ask about what
180
+ // it just wrote in the very same step.
181
+ const NAMES_ITS_FILE = new Set([
182
+ "write_file", "edit_file", "multi_edit", "delete_file", "move_file",
183
+ "write_bytes", "create_dir",
184
+ ]);
185
+ if (NAMES_ITS_FILE.has(tool)) {
186
+ const a = (input ?? {});
187
+ const paths = [a.path, a.file_path, a.from, a.to, a.destination]
188
+ .filter((p) => typeof p === "string" && p.length > 0);
189
+ const edits = Array.isArray(a.edits) ? a.edits : [];
190
+ for (const e of edits) {
191
+ const one = e?.path;
192
+ if (typeof one === "string")
193
+ paths.push(one);
194
+ }
195
+ if (paths.length) {
196
+ const { pushFile } = await Promise.resolve().then(() => __importStar(require("../graph/sync")));
197
+ await pushFile(this.opts.root, this.project(), [...new Set(paths)]);
198
+ return;
199
+ }
200
+ }
201
+ // A command, a patch or an install can change anything: a formatter, a code
202
+ // generator, a package manager writing a lockfile. Nothing here knows what
203
+ // it touched, so the whole project is re-checked rather than one file. That
204
+ // is the comparison that costs milliseconds, and only the files that
205
+ // actually moved are read.
206
+ const CHANGES_SOMETHING = new Set([
207
+ "apply_patch", "run_command", "run_background", "install_dependency",
208
+ "run_tests", "git_branch", "git_commit",
209
+ ]);
210
+ if (CHANGES_SOMETHING.has(tool))
211
+ await this.remap();
212
+ }
213
+ /**
214
+ * Put the files this job changed back into the map.
215
+ *
216
+ * Runs after the job, without being waited on: the person has their answer
217
+ * and a re-read of three files must not hold up their prompt. A failure here
218
+ * costs nothing, because the next job checks the files anyway and would read
219
+ * them then.
220
+ */
221
+ async remap() {
222
+ if (this.opts.withoutMap || this.remapping)
223
+ return;
224
+ this.remapping = true;
225
+ try {
226
+ const { refresh } = await Promise.resolve().then(() => __importStar(require("../graph/sync")));
227
+ await refresh(this.opts.root, this.project());
228
+ }
229
+ catch {
230
+ /* the next check picks the files up anyway */
231
+ }
232
+ finally {
233
+ this.remapping = false;
234
+ }
235
+ }
236
+ /** Run one request to its end. Resolves with how it went. */
237
+ async run(request) {
238
+ const result = {
239
+ outcome: "failed", steps: 0, actions: 0, totalUsd: 0, savedUsd: null,
240
+ receipt: "", models: [], text: "",
241
+ };
242
+ // ── the map of this project, brought up to date first ──────────────────
243
+ //
244
+ // Before the job, not after it, because the whole saving is the agent
245
+ // knowing the shape of the codebase instead of reading forty files to work
246
+ // it out. A map read afterwards would have cost the money it exists to
247
+ // save.
248
+ //
249
+ // Only what MOVED is read. An unchanged project costs about ten
250
+ // milliseconds here, which is the check itself; a few changed files cost a
251
+ // fraction of a second. Nothing about this can stop the job: a service that
252
+ // cannot be reached, a company that has switched the map off, and a machine
253
+ // with no parser all end the same way, with the agent reading files the
254
+ // way every other coding tool does all of the time.
255
+ if (!this.opts.withoutMap) {
256
+ try {
257
+ const { refresh, status } = await Promise.resolve().then(() => __importStar(require("../graph/sync")));
258
+ const now = await status(this.opts.root, this.project());
259
+ if (now.enabled && !now.hasMap) {
260
+ // FIRST TIME IN THIS PROJECT: read it, but do not make anybody wait.
261
+ //
262
+ // A first read of a large project is minutes, because every file has
263
+ // to be parsed and its structure sent. Waiting for that before the
264
+ // job starts would make somebody's first experience of the product a
265
+ // long pause for a feature they did not ask for, to save money on a
266
+ // job they have not started. So the job runs now, reading files the
267
+ // ordinary way, and the second job in this project has the map.
268
+ this.out(ui.dim(" reading a map of this project in the background;"
269
+ + " your next job here will use it\n"));
270
+ void refresh(this.opts.root, this.project()).catch(() => undefined);
271
+ }
272
+ else if (now.enabled && !now.fresh) {
273
+ // A map already exists and some files moved. Only those are read,
274
+ // which is a fraction of a second, and it is worth waiting for:
275
+ // answering this job from a map that is behind the files is the one
276
+ // failure this whole feature has to avoid.
277
+ const done = await refresh(this.opts.root, this.project());
278
+ if (done.filesRead)
279
+ this.out(ui.dim(` code map: ${done.message}\n`));
280
+ }
281
+ }
282
+ catch {
283
+ /* the map is an optimization; never a reason a job does not start */
284
+ }
285
+ }
286
+ // Permission first, from the one service. The stream opens only once the
287
+ // job has been authorized there, so this socket carries no decision of its
288
+ // own and the workspace credential is no longer the thing that opens it.
289
+ const granted = await this.pass(request);
290
+ if (this.olderWorkspace) {
291
+ // Straight on to the stream with the credential, silently. A person does
292
+ // not need to be told their workspace is a release behind when the thing
293
+ // they asked for is about to happen anyway.
294
+ this.refusal = null;
295
+ }
296
+ else if (!granted && this.refusal) {
297
+ // A refusal is an ANSWER, not a failure to connect. It carries the
298
+ // service's own wording, because a plan that does not include coding and
299
+ // an allowance that has been spent have different fixes.
300
+ result.detail = this.refusal;
301
+ return result;
302
+ }
303
+ await new Promise((resolve, reject) => {
304
+ const socket = new WebSocket(this.socketUrl(granted?.ticket));
305
+ this.socket = socket;
306
+ let ready = false;
307
+ socket.onopen = () => {
308
+ socket.send(JSON.stringify({
309
+ type: "hello",
310
+ client: this.opts.headless ? "cli-headless" : "cli",
311
+ capabilities: this.executor.capabilities,
312
+ }));
313
+ };
314
+ socket.onmessage = async (event) => {
315
+ let frame;
316
+ try {
317
+ frame = JSON.parse(String(event.data));
318
+ }
319
+ catch {
320
+ return;
321
+ }
322
+ if (frame.type === "ready" && !ready) {
323
+ ready = true;
324
+ this.announce(frame);
325
+ socket.send(JSON.stringify({
326
+ type: this.opts.resumeJobId ? "resume" : "run",
327
+ job_id: this.opts.resumeJobId,
328
+ continues_job: this.opts.continuesJob,
329
+ request,
330
+ workspace: this.opts.root.split("/").pop(),
331
+ workspace_source: "local",
332
+ workspace_root: this.opts.root,
333
+ client: this.opts.headless ? "cli-headless" : "cli",
334
+ permission_mode: this.opts.permissionMode ?? "approve_writes",
335
+ plan_only: Boolean(this.opts.planOnly),
336
+ plan_approved: Boolean(this.opts.planApproved),
337
+ model: this.opts.model,
338
+ pin_model: Boolean(this.opts.pinModel),
339
+ max_spend_usd: this.opts.maxSpendUsd,
340
+ approved_commands: this.opts.approvedCommands ?? [],
341
+ attachments: this.opts.attachmentIds ?? [],
342
+ without_graph: Boolean(this.opts.withoutMap),
343
+ }));
344
+ return;
345
+ }
346
+ if (frame.type === "tool_call") {
347
+ const r = await this.executor.execute(String(frame.name), frame.input ?? {});
348
+ // THE MAP IS CAUGHT UP BEFORE THE RESULT GOES BACK.
349
+ //
350
+ // Before, not after, and that ordering is the whole of it. The server
351
+ // runs a step's actions one after another, so an agent that writes a
352
+ // file and then asks the map about it does both inside one step. A
353
+ // refresh started after this result is sent has not finished when the
354
+ // question runs, and the agent is told the thing it just wrote does
355
+ // not exist. Measured on a real job before this line existed.
356
+ if (!r.isError)
357
+ await this.noteChanged(String(frame.name), frame.input);
358
+ socket.send(JSON.stringify({
359
+ type: "tool_result", id: frame.id,
360
+ content: r.content, is_error: Boolean(r.isError), summary: r.summary,
361
+ }));
362
+ return;
363
+ }
364
+ if (frame.type === "approval_ask") {
365
+ const granted = await this.ask(String(frame.name), frame.input ?? {});
366
+ socket.send(JSON.stringify({ type: "approval_reply", id: frame.id, granted }));
367
+ return;
368
+ }
369
+ if (frame.type === "event") {
370
+ this.narrate(frame);
371
+ return;
372
+ }
373
+ if (frame.type === "done") {
374
+ result.outcome = String(frame.outcome ?? "failed");
375
+ result.detail = frame.detail ?? null;
376
+ result.jobId = frame.job_id;
377
+ result.steps = Number(frame.steps ?? 0);
378
+ result.actions = Number(frame.actions ?? 0);
379
+ result.totalUsd = Number(frame.total_usd ?? 0);
380
+ // The saving is the server's arithmetic, taken as it is sent. A
381
+ // client that subtracted its own would disagree with the stored
382
+ // receipt within a release, and the stored one is the record.
383
+ {
384
+ const economics = frame.economics;
385
+ const saved = economics?.saved_usd;
386
+ result.savedUsd = typeof saved === "number" ? saved : null;
387
+ const rows = economics?.models;
388
+ result.models = Array.isArray(rows) ? rows : [];
389
+ }
390
+ // The sentence the server wrote. Everything a reader is told about
391
+ // cost and saving comes from here.
392
+ result.receipt = String(frame.receipt ?? "");
393
+ result.reusedPct = ui.reusePct(0, frame.cached_tokens, Math.max(0, Number(frame.context_tokens ?? 0) - Number(frame.cached_tokens ?? 0)));
394
+ result.text = String(frame.text ?? "");
395
+ result.testsPassed = this.executor.lastTestsPassed;
396
+ // What the agent itself changed goes back into the map before the
397
+ // next job asks anything. Without this the second job of a session
398
+ // works from a picture of the code as it was before the first job
399
+ // edited it, which is the exact failure the freshness rule exists to
400
+ // prevent, and the most likely one: the agent that made the map stale
401
+ // is the agent about to trust it.
402
+ void this.remap();
403
+ socket.close();
404
+ return;
405
+ }
406
+ if (frame.type === "error") {
407
+ this.out(ui.red(`\n ${String(frame.message ?? "Something went wrong.")}\n`));
408
+ }
409
+ };
410
+ socket.onerror = () => {
411
+ // Say what actually failed.
412
+ //
413
+ // This used to read "Check you are signed in", on a screen that had
414
+ // just printed the person's own email, their company and their plan,
415
+ // all of which came from the very server it claimed it could not reach.
416
+ // Being told to do the one thing you have visibly already done sends
417
+ // somebody to sign in again, which fixes nothing and costs an evening.
418
+ //
419
+ // By the time this fires the credential has already been accepted: the
420
+ // job was authorized and the server issued a pass for it. What failed
421
+ // is the live connection that carries the work, and the wording names
422
+ // that instead.
423
+ if (!ready) {
424
+ reject(new Error("The live connection to your workspace could not be opened. "
425
+ + "You are signed in; the work stream is what failed."));
426
+ }
427
+ };
428
+ socket.onclose = () => {
429
+ (0, executor_1.stopEverything)();
430
+ resolve();
431
+ };
432
+ });
433
+ this.report(result);
434
+ return result;
435
+ }
436
+ /** Stop the running job. Takes effect mid action, not at the end of the run. */
437
+ cancel() {
438
+ if (this.canceled)
439
+ return;
440
+ this.canceled = true;
441
+ this.out(ui.yellow("\n stopping...\n"));
442
+ this.socket?.send(JSON.stringify({ type: "cancel" }));
443
+ }
444
+ /** Something typed while it works. Joins the conversation at the next step. */
445
+ steer(text) {
446
+ this.socket?.send(JSON.stringify({ type: "steer", text }));
447
+ this.out(ui.dim(" noted, it will pick that up on the next step\n"));
448
+ }
449
+ announce(frame) {
450
+ const allowance = frame.allowance;
451
+ const tools = frame.tools ?? [];
452
+ this.out(ui.dim(` ${this.opts.root} ${tools.length} actions available\n`));
453
+ if (allowance && allowance.included && !allowance.unlimited) {
454
+ this.out(ui.dim(` coding allowance left this month: ${ui.money(Number(allowance.remaining_usd))}\n`));
455
+ }
456
+ if (this.opts.planOnly)
457
+ this.out(ui.yellow(" plan only: nothing will be changed\n"));
458
+ if (this.opts.planApproved) {
459
+ this.out(ui.dim(" plan approved: file changes will not be asked about one at a time\n"));
460
+ }
461
+ }
462
+ narrate(frame) {
463
+ switch (frame.kind) {
464
+ case "step_finished": {
465
+ this.out(ui.step(Number(frame.index ?? 0), String(frame.model ?? ""), frame.usd, frame.latency_ms, ui.reusePct(frame.tokens_in, frame.cache_read_tokens, frame.cache_write_tokens)) + "\n");
466
+ const said = String(frame.text ?? "").trim();
467
+ // Markdown is for a browser. A terminal shows the asterisks.
468
+ if (said) {
469
+ this.out(`\n${ui.plain(said)}\n`);
470
+ this.lastSaid = said;
471
+ }
472
+ break;
473
+ }
474
+ case "action_finished":
475
+ this.out(ui.action(String(frame.name ?? ""), this.targetOf(frame), frame.ok !== false, frame.latency_ms) + "\n");
476
+ break;
477
+ case "action_refused":
478
+ this.out(ui.red(` refused ${String(frame.name ?? "")}`)
479
+ + ui.dim(` ${String(frame.reason ?? "")}\n`));
480
+ break;
481
+ case "todos": {
482
+ const rendered = ui.todos(frame.todos ?? []);
483
+ // Only when it has actually changed. A plan reprinted identically after
484
+ // every step is noise that buries the run.
485
+ if (rendered !== this.lastTodos) {
486
+ this.out(rendered);
487
+ this.lastTodos = rendered;
488
+ }
489
+ break;
490
+ }
491
+ // A file the agent produced, and where it went. Said here rather than
492
+ // left to the model's sentence: a terminal has no timeline to look at, and
493
+ // "ready to download" in a terminal is the same as producing nothing.
494
+ case "document_made": {
495
+ const saved = frame.saved_to ? String(frame.saved_to) : "";
496
+ const bytes = Number(frame.bytes ?? 0).toLocaleString();
497
+ this.out(saved
498
+ ? ui.green(` saved ${saved}`) + ui.dim(` ${bytes} bytes\n`)
499
+ : ui.yellow(` made ${String(frame.filename ?? "a file")}`)
500
+ + ui.dim(` ${bytes} bytes, not saved here\n`));
501
+ break;
502
+ }
503
+ case "compacting":
504
+ this.out(ui.dim(" summarizing the conversation so far to keep it affordable\n"));
505
+ break;
506
+ case "steered":
507
+ break;
508
+ default:
509
+ break;
510
+ }
511
+ }
512
+ targetOf(frame) {
513
+ const input = frame.input ?? {};
514
+ return String(input.path ?? input.pattern ?? input.command ?? input.to ?? "");
515
+ }
516
+ /** Put a change in front of the person and wait.
517
+ *
518
+ * Three cases, in order. A caller that owns the keyboard lent us one, so the
519
+ * question goes there. Nobody lent one and somebody is watching, so we open
520
+ * our own reader. Nobody is watching, so it is refused.
521
+ *
522
+ * In headless mode there is nobody to ask, so it is refused rather than
523
+ * assumed. Treating silence as consent would let a script approve changes
524
+ * nobody ever saw, which is the opposite of what a permission mode is for:
525
+ * a script that needs to change files says so by choosing a mode that allows
526
+ * it, up front. */
527
+ async ask(name, input) {
528
+ if (this.opts.headless) {
529
+ // Name what was refused. A refusal that does not say WHICH command it
530
+ // turned down cannot be acted on: the operator's only remedy is to
531
+ // pre-approve it with --allow, and they cannot approve a string they were
532
+ // never shown. Found while running the same job unattended five ways and
533
+ // watching every arm stall on a command none of them would name.
534
+ const what = this.targetOf({ input });
535
+ this.out(ui.yellow(` ${name} needs approval and nobody is watching, so it was refused. `)
536
+ + (what ? ui.dim(`Refused: ${what}\n `) : "")
537
+ + ui.dim("Re-run with --allow \"<that exact command>\", or choose a permission mode that allows it.\n"));
538
+ return false;
539
+ }
540
+ this.out(ui.diff(name, input));
541
+ if (this.opts.confirm) {
542
+ const granted = await this.opts.confirm(`${ui.bold("Apply this?")} ${ui.dim("[y/N]")}`);
543
+ this.out(granted ? ui.green(" applied\n") : ui.dim(" skipped\n"));
544
+ return granted;
545
+ }
546
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
547
+ try {
548
+ const answer = await new Promise((resolve) => {
549
+ rl.question(` ${ui.bold("Apply this?")} ${ui.dim("[y/N]")} `, resolve);
550
+ });
551
+ const granted = /^y(es)?$/i.test(answer.trim());
552
+ this.out(granted ? ui.green(" applied\n") : ui.dim(" skipped\n"));
553
+ return granted;
554
+ }
555
+ finally {
556
+ rl.close();
557
+ }
558
+ }
559
+ report(r) {
560
+ // Only if it has not already been said. On a one step job the closing text
561
+ // IS the step's text, and printing it again read as a stutter.
562
+ if (r.text && r.text.trim() !== this.lastSaid)
563
+ this.out(`\n${ui.plain(r.text)}\n`);
564
+ this.out(ui.outcome(r.outcome, r.detail));
565
+ this.out(ui.receipt(r.receipt, r.models, r.testsPassed));
566
+ if (r.jobId) {
567
+ // The job's own reference, so a person can quote it. The link to the
568
+ // account sits in the receipt above, where the server put it.
569
+ this.out(ui.dim(` job ${r.jobId}\n`));
570
+ }
571
+ }
572
+ }
573
+ exports.TerminalSession = TerminalSession;
@@ -0,0 +1 @@
1
+ export declare function cmdAsk(args: string[]): Promise<void>;