@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/session.js
CHANGED
|
@@ -60,10 +60,32 @@ exports.TerminalSession = void 0;
|
|
|
60
60
|
* front. Both are the same session; the difference is who answers.
|
|
61
61
|
*/
|
|
62
62
|
const gateway_1 = require("../lib/gateway");
|
|
63
|
+
const fs = __importStar(require("fs"));
|
|
64
|
+
const path = __importStar(require("path"));
|
|
63
65
|
const readline = __importStar(require("readline"));
|
|
64
66
|
const executor_1 = require("../executor");
|
|
67
|
+
const project_1 = require("../lib/project");
|
|
65
68
|
const ui = __importStar(require("./render"));
|
|
69
|
+
const jobs_1 = require("../commands/jobs");
|
|
70
|
+
/** The actions that change a file, whose change is shown as it is made. */
|
|
71
|
+
const CHANGES = new Set(["edit_file", "multi_edit", "write_file"]);
|
|
72
|
+
/** Files with changes in Git as the job starts: the person's, not the agent's. */
|
|
73
|
+
function dirtyFiles(root) {
|
|
74
|
+
try {
|
|
75
|
+
const { spawnSync } = require("child_process");
|
|
76
|
+
const r = spawnSync("git", ["status", "--porcelain", "-uall"], { cwd: root, encoding: "utf8", timeout: 5000 });
|
|
77
|
+
if (r.status !== 0)
|
|
78
|
+
return [];
|
|
79
|
+
return String(r.stdout).split("\n").filter(Boolean).map((l) => l.slice(3).split(" -> ").pop() ?? "")
|
|
80
|
+
.filter(Boolean).slice(0, 500);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
66
86
|
class TerminalSession {
|
|
87
|
+
/** The VS Code window this terminal is inside, when there is one (VS Code manual 9.12). */
|
|
88
|
+
editor = null;
|
|
67
89
|
socket = null;
|
|
68
90
|
executor;
|
|
69
91
|
opts;
|
|
@@ -75,17 +97,28 @@ class TerminalSession {
|
|
|
75
97
|
* with nothing between the copies. */
|
|
76
98
|
lastSaid = "";
|
|
77
99
|
canceled = false;
|
|
100
|
+
/** The live "thinking 12s" line while the model works on a step. Drawn in
|
|
101
|
+
* place and erased before anything else prints, so it never lands in the
|
|
102
|
+
* answer. Without it a step is a blank screen for as long as the model takes,
|
|
103
|
+
* and "thinking" and "hung" look identical. */
|
|
104
|
+
working = null;
|
|
105
|
+
/** When the current wait began, kept across the line being redrawn. Cleared
|
|
106
|
+
* when a step finishes, so the next step counts from its own start. */
|
|
107
|
+
workFrom = null;
|
|
108
|
+
write;
|
|
78
109
|
constructor(opts, write = (s) => process.stdout.write(s)) {
|
|
79
110
|
this.opts = opts;
|
|
80
|
-
this.
|
|
111
|
+
this.write = write;
|
|
112
|
+
this.out = (s) => { this.stopWorking(); write(s); };
|
|
81
113
|
this.executor = new executor_1.NativeExecutor({
|
|
82
114
|
root: opts.root,
|
|
115
|
+
added: opts.project?.added ?? [],
|
|
83
116
|
// Streamed straight through while a command runs. A build that takes two
|
|
84
117
|
// minutes must not look like a hang, and the output is the only thing
|
|
85
118
|
// that proves it is not.
|
|
86
119
|
onOutput: (chunk) => {
|
|
87
120
|
if (!opts.headless)
|
|
88
|
-
this.
|
|
121
|
+
this.stream(chunk);
|
|
89
122
|
},
|
|
90
123
|
});
|
|
91
124
|
}
|
|
@@ -126,8 +159,18 @@ class TerminalSession {
|
|
|
126
159
|
// include coding and an allowance that has been spent have different
|
|
127
160
|
// fixes, and a caller told only "refused" looks in the wrong place.
|
|
128
161
|
this.refusal = reply.message || "your workspace could not be reached";
|
|
162
|
+
// The reason word, for the exit number (Appendix B): the service's own
|
|
163
|
+
// when it answered, else what the missing answer means.
|
|
164
|
+
this.refusalReason = reply.reason
|
|
165
|
+
?? (reply.httpStatus === 401 ? "signed_out"
|
|
166
|
+
: reply.ok || reply.httpStatus ? "incomplete" : "unreachable");
|
|
167
|
+
this.refusalResult = reply.result ?? undefined;
|
|
168
|
+
this.refusalTable = reply.exit_status ?? undefined;
|
|
129
169
|
return null;
|
|
130
170
|
}
|
|
171
|
+
this.jobId = reply.job.job_id;
|
|
172
|
+
if (process.env.COMPRISM_JOB_LOG && this.jobId)
|
|
173
|
+
(0, jobs_1.notePointer)(this.jobId, process.env.COMPRISM_JOB_LOG);
|
|
131
174
|
return { ticket: reply.job.ticket, jobId: reply.job.job_id };
|
|
132
175
|
}
|
|
133
176
|
/** Where the stream lives, derived from the workspace URL.
|
|
@@ -154,8 +197,13 @@ class TerminalSession {
|
|
|
154
197
|
project() {
|
|
155
198
|
return this.opts.root.split("/").filter(Boolean).pop() || this.opts.root;
|
|
156
199
|
}
|
|
200
|
+
/** Connections lost during this job, so reconnecting gives up in the end. */
|
|
201
|
+
drops = 0;
|
|
157
202
|
/** Why the one service would not start this job, in its own words. */
|
|
158
203
|
refusal = null;
|
|
204
|
+
refusalReason;
|
|
205
|
+
refusalResult;
|
|
206
|
+
refusalTable;
|
|
159
207
|
/** The workspace does not carry the permission question yet, so fall back. */
|
|
160
208
|
olderWorkspace = false;
|
|
161
209
|
/** Set while one is actually running, so two never overlap. */
|
|
@@ -237,7 +285,7 @@ class TerminalSession {
|
|
|
237
285
|
async run(request) {
|
|
238
286
|
const result = {
|
|
239
287
|
outcome: "failed", steps: 0, actions: 0, totalUsd: 0, savedUsd: null,
|
|
240
|
-
receipt: "", models: [], text: "",
|
|
288
|
+
receipt: "", models: [], text: "", todos: [], lateNotes: [],
|
|
241
289
|
};
|
|
242
290
|
// ── the map of this project, brought up to date first ──────────────────
|
|
243
291
|
//
|
|
@@ -286,6 +334,24 @@ class TerminalSession {
|
|
|
286
334
|
// Permission first, from the one service. The stream opens only once the
|
|
287
335
|
// job has been authorized there, so this socket carries no decision of its
|
|
288
336
|
// own and the workspace credential is no longer the thing that opens it.
|
|
337
|
+
// Joined to VS Code when run in its terminal (VS Code manual 9.12): the
|
|
338
|
+
// window's selection goes with the request, and proposed changes open in
|
|
339
|
+
// its diff view. CompletionPrism's own connection; no other tool's.
|
|
340
|
+
if (!this.opts.headless) {
|
|
341
|
+
const { findEditor, selectionText } = await Promise.resolve().then(() => __importStar(require("../lib/ide")));
|
|
342
|
+
this.editor = findEditor(this.opts.root);
|
|
343
|
+
if (this.editor) {
|
|
344
|
+
this.out(ui.dim(" Connected to VS Code\n"));
|
|
345
|
+
request += selectionText(await this.editor.context());
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// Working from the moment the request leaves, not from the moment the
|
|
349
|
+
// model starts. The pass, the stream and the service's own preparation
|
|
350
|
+
// take several seconds before the first step, and a blank screen for that
|
|
351
|
+
// long reads as nothing happening. The service's words replace this as
|
|
352
|
+
// soon as its first step starts.
|
|
353
|
+
this.workFrom = null;
|
|
354
|
+
this.startWorking("thinking");
|
|
289
355
|
const granted = await this.pass(request);
|
|
290
356
|
if (this.olderWorkspace) {
|
|
291
357
|
// Straight on to the stream with the credential, silently. A person does
|
|
@@ -298,181 +364,664 @@ class TerminalSession {
|
|
|
298
364
|
// service's own wording, because a plan that does not include coding and
|
|
299
365
|
// an allowance that has been spent have different fixes.
|
|
300
366
|
result.detail = this.refusal;
|
|
367
|
+
result.refusalReason = this.refusalReason;
|
|
368
|
+
if (this.refusalResult)
|
|
369
|
+
result.result = this.refusalResult;
|
|
370
|
+
if (this.refusalTable)
|
|
371
|
+
result.exitStatus = this.refusalTable;
|
|
372
|
+
this.out(ui.red(` ${this.refusal}\n`));
|
|
301
373
|
return result;
|
|
302
374
|
}
|
|
375
|
+
// What this machine did for the job, by action id: received, then done
|
|
376
|
+
// with its result. Sent back after a dropped connection so the service can
|
|
377
|
+
// match it, which is how nothing is ever done twice (manual 8.9).
|
|
378
|
+
const journal = new Map();
|
|
379
|
+
let finished = false;
|
|
303
380
|
await new Promise((resolve, reject) => {
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
socket.
|
|
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);
|
|
381
|
+
const open = (ticket, first) => {
|
|
382
|
+
const socket = new WebSocket(this.socketUrl(ticket));
|
|
383
|
+
this.socket = socket;
|
|
384
|
+
let ready = false;
|
|
385
|
+
socket.onopen = () => {
|
|
325
386
|
socket.send(JSON.stringify({
|
|
326
|
-
type:
|
|
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,
|
|
387
|
+
type: "hello",
|
|
333
388
|
client: this.opts.headless ? "cli-headless" : "cli",
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
without_graph: Boolean(this.opts.withoutMap),
|
|
389
|
+
// `ask_user` says a person is here to answer questions (manual
|
|
390
|
+
// 4.3). Unattended, it is left out and the service tells the agent to
|
|
391
|
+
// choose the safest option instead.
|
|
392
|
+
// `reattach`: this tool reconnects after a drop, so the service
|
|
393
|
+
// holds the job for it rather than ending it (manual 8.9).
|
|
394
|
+
capabilities: this.opts.headless
|
|
395
|
+
? [...this.executor.capabilities, "reattach"]
|
|
396
|
+
: [...this.executor.capabilities, "ask_user", "reattach"],
|
|
343
397
|
}));
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
398
|
+
};
|
|
399
|
+
socket.onmessage = async (event) => {
|
|
400
|
+
let frame;
|
|
401
|
+
try {
|
|
402
|
+
frame = JSON.parse(String(event.data));
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (frame.type === "ready" && !ready) {
|
|
408
|
+
ready = true;
|
|
409
|
+
if (!first) {
|
|
410
|
+
// Back after a drop: what this machine did, for the service to match.
|
|
411
|
+
socket.send(JSON.stringify({
|
|
412
|
+
type: "reattach", job_id: this.jobId, journal: [...journal.values()],
|
|
413
|
+
}));
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
this.announce(frame);
|
|
417
|
+
socket.send(JSON.stringify({
|
|
418
|
+
type: this.opts.resumeJobId ? "resume" : "run",
|
|
419
|
+
job_id: this.opts.resumeJobId,
|
|
420
|
+
continues_job: this.opts.continuesJob,
|
|
421
|
+
conversation_id: this.opts.conversationId,
|
|
422
|
+
request,
|
|
423
|
+
workspace: this.opts.root.split("/").pop(),
|
|
424
|
+
workspace_source: "local",
|
|
425
|
+
workspace_root: this.opts.root,
|
|
426
|
+
...(this.opts.project ? (0, project_1.asWire)(this.opts.project) : {}),
|
|
427
|
+
client: process.env.COMPRISM_BACKGROUND === "1" ? "cli-background"
|
|
428
|
+
: this.opts.headless ? "cli-headless" : "cli",
|
|
429
|
+
// Absent when not chosen: the service takes it from the settings.
|
|
430
|
+
permission_mode: this.opts.permissionMode,
|
|
431
|
+
plan_only: Boolean(this.opts.planOnly),
|
|
432
|
+
plan_approved: Boolean(this.opts.planApproved),
|
|
433
|
+
model: this.opts.model,
|
|
434
|
+
pin_model: Boolean(this.opts.pinModel),
|
|
435
|
+
max_spend_usd: this.opts.maxSpendUsd,
|
|
436
|
+
...(this.opts.maxSteps ? { max_steps: this.opts.maxSteps } : {}),
|
|
437
|
+
...(this.opts.maxMinutes ? { max_minutes: this.opts.maxMinutes } : {}),
|
|
438
|
+
...(this.opts.effort ? { effort: this.opts.effort } : {}),
|
|
439
|
+
sandbox: Boolean(this.opts.sandbox),
|
|
440
|
+
// Which sandbox this machine can offer, so the service can turn it
|
|
441
|
+
// on from settings too, or refuse when it cannot (manual 4.12).
|
|
442
|
+
sandbox_system: this.sandboxSystem(),
|
|
443
|
+
command_timeout_s: this.opts.commandTimeoutS,
|
|
444
|
+
...(this.opts.issue ? { issue: this.opts.issue } : {}),
|
|
445
|
+
...(this.opts.team ? { team: this.opts.team } : {}),
|
|
446
|
+
...(this.opts.review ? { review: this.opts.review } : {}),
|
|
447
|
+
approved_commands: this.opts.approvedCommands ?? [],
|
|
448
|
+
attachments: this.opts.attachmentIds ?? [],
|
|
449
|
+
left_out: this.opts.leftOut ?? [],
|
|
450
|
+
without_graph: Boolean(this.opts.withoutMap),
|
|
451
|
+
...(this.opts.answerSchema ? { answer_schema: this.opts.answerSchema } : {}),
|
|
452
|
+
...(this.opts.settingsConfig ? { settings_config: this.opts.settingsConfig } : {}),
|
|
453
|
+
...(this.opts.noPersonal ? { no_personal: true } : {}),
|
|
454
|
+
...(this.opts.queuedJob ? { queued_job: this.opts.queuedJob } : {}),
|
|
455
|
+
// Files already changed before the job, so they are never counted
|
|
456
|
+
// as the agent's (manual 7.1).
|
|
457
|
+
dirty_files: dirtyFiles(this.opts.root),
|
|
458
|
+
// Which system runs the commands, so the agent writes for its shell
|
|
459
|
+
// (manual 1.2): PowerShell on Windows.
|
|
460
|
+
machine_os: process.platform,
|
|
461
|
+
}));
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
if (frame.type === "tool_call") {
|
|
465
|
+
const id = String(frame.id);
|
|
466
|
+
const seen = journal.get(id);
|
|
467
|
+
if (seen?.state === "done") {
|
|
468
|
+
// Sent again after a drop, but already carried out here: the
|
|
469
|
+
// result is sent, the action is not repeated.
|
|
470
|
+
this.socket?.send(JSON.stringify({ type: "tool_result", id, ...seen.result }));
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
if (seen)
|
|
474
|
+
return;
|
|
475
|
+
const input = frame.input ?? {};
|
|
476
|
+
const target = String(input.path ?? input.file_path ?? input.command ?? "");
|
|
477
|
+
// An approved action is checked against what the person approved
|
|
478
|
+
// immediately before it runs: the same action, the same arguments,
|
|
479
|
+
// the same folder (manual 4.8). Anything changed does not run.
|
|
480
|
+
const approved = this.approvedCalls.get(id);
|
|
481
|
+
if (approved !== undefined && approved !== this.fingerprint(String(frame.name), input)) {
|
|
482
|
+
const refused = {
|
|
483
|
+
content: "This action changed after it was approved (a different command, "
|
|
484
|
+
+ "argument or folder), so it did not run. Ask again for the new one.",
|
|
485
|
+
is_error: true, summary: "changed after approval; not run",
|
|
486
|
+
};
|
|
487
|
+
this.socket?.send(JSON.stringify({ type: "tool_result", id, ...refused }));
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
journal.set(id, { id, name: String(frame.name), target, state: "running" });
|
|
491
|
+
// The hooks the service named for this action (manual 10.9).
|
|
492
|
+
const plan = frame.hooks && typeof frame.hooks === "object"
|
|
493
|
+
? frame.hooks : undefined;
|
|
494
|
+
const r = await this.executor.execute(String(frame.name), input, plan);
|
|
495
|
+
// THE MAP IS CAUGHT UP BEFORE THE RESULT GOES BACK.
|
|
496
|
+
//
|
|
497
|
+
// Before, not after, and that ordering is the whole of it. The server
|
|
498
|
+
// runs a step's actions one after another, so an agent that writes a
|
|
499
|
+
// file and then asks the map about it does both inside one step. A
|
|
500
|
+
// refresh started after this result is sent has not finished when the
|
|
501
|
+
// question runs, and the agent is told the thing it just wrote does
|
|
502
|
+
// not exist. Measured on a real job before this line existed.
|
|
503
|
+
if (!r.isError)
|
|
504
|
+
await this.noteChanged(String(frame.name), frame.input);
|
|
505
|
+
const answer = {
|
|
506
|
+
content: r.content, is_error: Boolean(r.isError), summary: r.summary,
|
|
507
|
+
facts: r.facts,
|
|
508
|
+
};
|
|
509
|
+
journal.set(id, { id, name: String(frame.name), target, state: "done", result: answer });
|
|
510
|
+
// On whichever connection is open now. If none is, the journal holds
|
|
511
|
+
// the result until the service asks for it.
|
|
512
|
+
if (this.socket?.readyState === WebSocket.OPEN) {
|
|
513
|
+
this.socket.send(JSON.stringify({ type: "tool_result", id, ...answer }));
|
|
514
|
+
}
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (frame.type === "approval_ask") {
|
|
518
|
+
const callId = String(frame.call_id ?? frame.action_id ?? "");
|
|
519
|
+
this.asked.add(callId);
|
|
520
|
+
const input = frame.input ?? {};
|
|
521
|
+
// Answered in the browser while this terminal waits (web manual 6.15):
|
|
522
|
+
// the service already has the answer, so none is sent from here.
|
|
523
|
+
const remote = new Promise((resolve) => {
|
|
524
|
+
this.answeredElsewhere.set(String(frame.id), () => resolve("remote"));
|
|
525
|
+
});
|
|
526
|
+
if (this.editor && ["write_file", "edit_file", "multi_edit"].includes(String(frame.name))) {
|
|
527
|
+
void this.editor.diff(String(frame.name), input);
|
|
528
|
+
}
|
|
529
|
+
const local = this.ask(String(frame.name), input, String(frame.prompt ?? ""));
|
|
530
|
+
const first = await Promise.race([local, remote]);
|
|
531
|
+
this.answeredElsewhere.delete(String(frame.id));
|
|
532
|
+
if (first === "remote")
|
|
533
|
+
return;
|
|
534
|
+
const answer = first;
|
|
535
|
+
// The words go to the service, which decides what they mean (4.7 to
|
|
536
|
+
// 4.9). `granted` is kept for a service that reads only yes or no.
|
|
537
|
+
const word = (answer ?? "").trim().toLowerCase();
|
|
538
|
+
const granted = word === "y" || word === "yes" || word === "a";
|
|
539
|
+
if (granted)
|
|
540
|
+
this.approvedCalls.set(callId, this.fingerprint(String(frame.name), input));
|
|
541
|
+
this.socket?.send(JSON.stringify(answer === null
|
|
542
|
+
? { type: "approval_reply", id: frame.id, granted: false, cancelled: true } // american-english-allowed: the service's field name
|
|
543
|
+
: { type: "approval_reply", id: frame.id, granted, answer,
|
|
544
|
+
// Nobody at the keyboard: the service records it as an approval
|
|
545
|
+
// nobody could give (manual 10.1, exit 4).
|
|
546
|
+
...(this.opts.headless ? { unattended: true } : {}) }));
|
|
547
|
+
if (answer === null)
|
|
548
|
+
this.cancel();
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
if (frame.type === "question") {
|
|
552
|
+
const answer = await this.question(frame);
|
|
553
|
+
this.socket?.send(JSON.stringify(answer === null
|
|
554
|
+
? { type: "question_reply", id: frame.id, cancelled: true } // american-english-allowed: the service's field name
|
|
555
|
+
: { type: "question_reply", id: frame.id, answer }));
|
|
556
|
+
if (answer === null)
|
|
557
|
+
this.cancel();
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
if (frame.type === "event" && frame.kind === "approval_answered") {
|
|
561
|
+
const done = this.answeredElsewhere.get(String(frame.id));
|
|
562
|
+
if (done) {
|
|
563
|
+
this.out(ui.dim(`\n ${String(frame.line ?? "answered in the browser")}\n`));
|
|
564
|
+
done();
|
|
565
|
+
}
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
if (frame.type === "event" && frame.kind === "sandbox") {
|
|
569
|
+
// The service turned the sandbox on for this job: every command from
|
|
570
|
+
// here runs inside it, with its allowed addresses (manual 4.12).
|
|
571
|
+
const { Sandbox } = await Promise.resolve().then(() => __importStar(require("../executor/sandbox")));
|
|
572
|
+
const system = this.sandboxSystem();
|
|
573
|
+
if (system) {
|
|
574
|
+
const box = new Sandbox(system, this.opts.root, this.opts.project?.added ?? []);
|
|
575
|
+
box.setAllowed(Array.isArray(frame.allow) ? frame.allow : []);
|
|
576
|
+
await box.start();
|
|
577
|
+
this.executor.sandbox = box;
|
|
578
|
+
}
|
|
579
|
+
this.out(ui.yellow(` ${String(frame.line ?? "")}`) + "\n");
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
if (frame.type === "event") {
|
|
583
|
+
// The published stream lines the service attached (manual 10.4).
|
|
584
|
+
if (this.opts.onStream && Array.isArray(frame.stream) && frame.stream.length) {
|
|
585
|
+
this.opts.onStream(frame.stream);
|
|
586
|
+
}
|
|
587
|
+
// The session's name, pushed by the service as soon as it exists, is
|
|
588
|
+
// shown as the terminal window's title, where it never interrupts
|
|
589
|
+
// the answer being printed.
|
|
590
|
+
if (frame.kind === "session_named" && typeof frame.title === "string"
|
|
591
|
+
&& frame.title && process.stdout.isTTY) {
|
|
592
|
+
process.stdout.write(`\x1b]0;${frame.title.replace(/[\x00-\x1f\x7f]/g, "")}\x07`);
|
|
593
|
+
}
|
|
594
|
+
if (frame.kind === "job_opened" && frame.job_id) {
|
|
595
|
+
this.jobId = String(frame.job_id);
|
|
596
|
+
// A background job notes where its log is, for `jobs attach` (5.14).
|
|
597
|
+
if (process.env.COMPRISM_JOB_LOG)
|
|
598
|
+
(0, jobs_1.notePointer)(this.jobId, process.env.COMPRISM_JOB_LOG);
|
|
599
|
+
}
|
|
600
|
+
// The service recorded this action, so it is settled either way.
|
|
601
|
+
if (frame.kind === "action_finished" && frame.id) {
|
|
602
|
+
const j = journal.get(String(frame.id));
|
|
603
|
+
if (j)
|
|
604
|
+
j.confirmed = true;
|
|
605
|
+
}
|
|
606
|
+
if (frame.kind === "reconciled") {
|
|
607
|
+
// The service's own report of what was matched (manual 8.9).
|
|
608
|
+
for (const line of frame.lines ?? []) {
|
|
609
|
+
this.out(` ${line}\n`);
|
|
610
|
+
}
|
|
611
|
+
this.out("\n");
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
this.narrate(frame);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (frame.type === "done") {
|
|
618
|
+
finished = true;
|
|
619
|
+
if (this.opts.onStream && Array.isArray(frame.stream)) {
|
|
620
|
+
this.opts.onStream(frame.stream);
|
|
621
|
+
}
|
|
622
|
+
result.outcome = String(frame.outcome ?? "failed");
|
|
623
|
+
result.detail = frame.detail ?? null;
|
|
624
|
+
result.jobId = frame.job_id;
|
|
625
|
+
result.steps = Number(frame.steps ?? 0);
|
|
626
|
+
result.actions = Number(frame.actions ?? 0);
|
|
627
|
+
result.totalUsd = Number(frame.total_usd ?? 0);
|
|
628
|
+
// The saving is the server's arithmetic, taken as it is sent. A
|
|
629
|
+
// client that subtracted its own would disagree with the stored
|
|
630
|
+
// receipt within a release, and the stored one is the record.
|
|
631
|
+
{
|
|
632
|
+
const economics = frame.economics;
|
|
633
|
+
const saved = economics?.saved_usd;
|
|
634
|
+
result.savedUsd = typeof saved === "number" ? saved : null;
|
|
635
|
+
const rows = economics?.models;
|
|
636
|
+
result.models = Array.isArray(rows) ? rows : [];
|
|
637
|
+
}
|
|
638
|
+
// The sentence the server wrote. Everything a reader is told about
|
|
639
|
+
// cost and saving comes from here.
|
|
640
|
+
result.receipt = String(frame.receipt ?? "");
|
|
641
|
+
result.card = frame.card ?? null;
|
|
642
|
+
result.reusedPct = ui.reusePct(0, frame.cached_tokens, Math.max(0, Number(frame.context_tokens ?? 0) - Number(frame.cached_tokens ?? 0)));
|
|
643
|
+
result.text = String(frame.text ?? "");
|
|
644
|
+
result.why = this.lastWhy;
|
|
645
|
+
result.todos = Array.isArray(frame.todos)
|
|
646
|
+
? frame.todos : [];
|
|
647
|
+
if (frame.result && typeof frame.result === "object") {
|
|
648
|
+
result.result = frame.result;
|
|
649
|
+
}
|
|
650
|
+
if (frame.exit_status && typeof frame.exit_status === "object") {
|
|
651
|
+
result.exitStatus = frame.exit_status;
|
|
652
|
+
}
|
|
653
|
+
result.lateNotes = Array.isArray(frame.late_notes)
|
|
654
|
+
? frame.late_notes.map(String) : [];
|
|
655
|
+
{
|
|
656
|
+
const ending = frame.ending;
|
|
657
|
+
if (ending && typeof ending === "object") {
|
|
658
|
+
result.ending = {
|
|
659
|
+
line: typeof ending.line === "string" ? ending.line : null,
|
|
660
|
+
proof: Array.isArray(ending.proof) ? ending.proof.map(String) : [],
|
|
661
|
+
exit: typeof ending.exit === "number" ? ending.exit : null,
|
|
662
|
+
...(typeof ending.reason === "string" ? { reason: ending.reason } : {}),
|
|
663
|
+
};
|
|
664
|
+
result.testsPassed = typeof ending.tests_passed === "boolean"
|
|
665
|
+
? ending.tests_passed : undefined;
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
result.testsPassed = this.executor.lastTestsPassed;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
// What the agent itself changed goes back into the map before the
|
|
672
|
+
// next job asks anything. Without this the second job of a session
|
|
673
|
+
// works from a picture of the code as it was before the first job
|
|
674
|
+
// edited it, which is the exact failure the freshness rule exists to
|
|
675
|
+
// prevent, and the most likely one: the agent that made the map stale
|
|
676
|
+
// is the agent about to trust it.
|
|
677
|
+
void this.remap();
|
|
678
|
+
socket.close();
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
if (frame.type === "error") {
|
|
682
|
+
this.out(ui.red(`\n ${String(frame.message ?? "Something went wrong.")}\n`));
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
socket.onerror = () => {
|
|
686
|
+
// Say what actually failed.
|
|
349
687
|
//
|
|
350
|
-
//
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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 : [];
|
|
688
|
+
// This used to read "Check you are signed in", on a screen that had
|
|
689
|
+
// just printed the person's own email, their company and their plan,
|
|
690
|
+
// all of which came from the very server it claimed it could not reach.
|
|
691
|
+
// Being told to do the one thing you have visibly already done sends
|
|
692
|
+
// somebody to sign in again, which fixes nothing and costs an evening.
|
|
693
|
+
//
|
|
694
|
+
// By the time this fires the credential has already been accepted: the
|
|
695
|
+
// job was authorized and the server issued a pass for it. What failed
|
|
696
|
+
// is the live connection that carries the work, and the wording names
|
|
697
|
+
// that instead.
|
|
698
|
+
if (!ready && first) {
|
|
699
|
+
reject(new Error("The live connection to your workspace could not be opened. "
|
|
700
|
+
+ "You are signed in; the work stream is what failed."));
|
|
389
701
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
702
|
+
};
|
|
703
|
+
socket.onclose = () => {
|
|
704
|
+
if (socket !== this.socket)
|
|
705
|
+
return;
|
|
706
|
+
this.stopWorking();
|
|
707
|
+
if (finished || this.canceled || !this.jobId) {
|
|
708
|
+
// One job on its own is its session, so what it started ends with it
|
|
709
|
+
// and is listed (manual 5.13). Inside a session they carry on.
|
|
710
|
+
if (!this.opts.keepProcesses)
|
|
711
|
+
this.out(ui.stoppedLines((0, executor_1.stopEverything)()));
|
|
712
|
+
// The job's browser closes with it (manual 10.16).
|
|
713
|
+
this.executor.close();
|
|
714
|
+
// Tool servers on this machine end with the job (manual 9.10).
|
|
715
|
+
this.executor.toolServers.stopAll();
|
|
716
|
+
resolve();
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
// Dropped mid job. Nothing running here is stopped: an action already
|
|
720
|
+
// under way finishes and its result waits in the journal.
|
|
721
|
+
this.drops += 1;
|
|
722
|
+
if (first || ready)
|
|
723
|
+
this.out(ui.yellow("\n The connection dropped. Reconnecting...\n"));
|
|
724
|
+
void (this.drops > 6 ? Promise.resolve(null) : this.reconnect()).then((ticket) => {
|
|
725
|
+
if (ticket !== null) {
|
|
726
|
+
open(ticket, false);
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
// The service could not be reached again. What this machine knows,
|
|
730
|
+
// said plainly; nothing is repeated (manual 8.9, exit 7).
|
|
731
|
+
this.out(" The connection dropped and could not be restored.\n");
|
|
732
|
+
for (const j of [...journal.values()].filter((e) => !e.confirmed)) {
|
|
733
|
+
this.out(j.state === "done"
|
|
734
|
+
? ` done ${j.name} ${j.target} (done here; the service has not confirmed it)\n`
|
|
735
|
+
: ` unknown ${j.name} ${j.target} (it may or may not have finished)\n`);
|
|
736
|
+
}
|
|
737
|
+
this.out(" Not repeated. Check the result, then tell the agent what happened.\n");
|
|
738
|
+
result.outcome = "unknown";
|
|
739
|
+
result.jobId = this.jobId;
|
|
740
|
+
// Nobody can receive a result now, so a command still running here
|
|
741
|
+
// is stopped rather than left to finish unseen.
|
|
742
|
+
(0, executor_1.stopForeground)();
|
|
743
|
+
(0, executor_1.stopEverything)();
|
|
744
|
+
this.executor.toolServers.stopAll();
|
|
745
|
+
resolve();
|
|
746
|
+
});
|
|
747
|
+
};
|
|
431
748
|
};
|
|
749
|
+
open(granted?.ticket, true);
|
|
432
750
|
});
|
|
433
751
|
this.report(result);
|
|
434
752
|
return result;
|
|
435
753
|
}
|
|
436
|
-
/**
|
|
754
|
+
/**
|
|
755
|
+
* A pass to rejoin this job after a dropped connection, trying for about
|
|
756
|
+
* half a minute, inside the service's window (manual 8.9). Null when the
|
|
757
|
+
* service could not be reached again, or refused.
|
|
758
|
+
*/
|
|
759
|
+
async reconnect() {
|
|
760
|
+
for (const wait of [1000, 2000, 3000, 5000, 8000, 10000]) {
|
|
761
|
+
await new Promise((r) => setTimeout(r, wait));
|
|
762
|
+
if (this.canceled)
|
|
763
|
+
return null;
|
|
764
|
+
const reply = await (0, gateway_1.ask)({ intent: "job_start", prompt: "", resumeJobId: this.jobId });
|
|
765
|
+
if (reply.served === false)
|
|
766
|
+
return undefined;
|
|
767
|
+
if (reply.ok && reply.job?.ticket)
|
|
768
|
+
return reply.job.ticket;
|
|
769
|
+
if (reply.ok && reply.granted === false)
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
772
|
+
return null;
|
|
773
|
+
}
|
|
774
|
+
/** Stop the running job. Takes effect mid action, not at the end of the run:
|
|
775
|
+
* a command already running on this machine is stopped too (manual 6.7). */
|
|
437
776
|
cancel() {
|
|
438
777
|
if (this.canceled)
|
|
439
778
|
return;
|
|
440
779
|
this.canceled = true;
|
|
780
|
+
this.paused = false;
|
|
441
781
|
this.out(ui.yellow("\n stopping...\n"));
|
|
442
782
|
this.socket?.send(JSON.stringify({ type: "cancel" }));
|
|
783
|
+
(0, executor_1.stopForeground)();
|
|
443
784
|
}
|
|
444
|
-
/** Something typed while it works. Joins the conversation at the next step
|
|
785
|
+
/** Something typed while it works. Joins the conversation at the next step;
|
|
786
|
+
* the service says so in the `note` event it sends back (6.8). */
|
|
445
787
|
steer(text) {
|
|
446
788
|
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
789
|
}
|
|
790
|
+
/** Esc: finish the current action, then wait (manual 6.7). */
|
|
791
|
+
pause() {
|
|
792
|
+
if (this.canceled || this.paused)
|
|
793
|
+
return;
|
|
794
|
+
this.paused = true;
|
|
795
|
+
this.socket?.send(JSON.stringify({ type: "pause" }));
|
|
796
|
+
}
|
|
797
|
+
/** Carry on after Esc, with a correction or without one. */
|
|
798
|
+
carryOn(text) {
|
|
799
|
+
this.paused = false;
|
|
800
|
+
this.socket?.send(JSON.stringify({ type: "carry_on", text }));
|
|
801
|
+
}
|
|
802
|
+
/** This job's id, once the service has issued it (`transcript`, 6.9). */
|
|
803
|
+
jobId;
|
|
804
|
+
/** Set from Esc until the job carries on or stops. */
|
|
805
|
+
paused = false;
|
|
806
|
+
/** Helpers running now, from the service's own events (manual 5.25). */
|
|
807
|
+
helpers = new Map();
|
|
808
|
+
/** `stop h2` (5.25): the service stops that one helper. */
|
|
809
|
+
stopHelper(id) {
|
|
810
|
+
this.socket?.send(JSON.stringify({ type: "stop_helper", helper: id }));
|
|
811
|
+
}
|
|
812
|
+
/** `queue` and `queue clear` (6.8): the service answers with the notes. */
|
|
813
|
+
queue(clear) {
|
|
814
|
+
this.socket?.send(JSON.stringify({ type: clear ? "queue_clear" : "queue" }));
|
|
815
|
+
}
|
|
816
|
+
/** Ctrl+O: each action's full input and output, or the short line (6.9). */
|
|
817
|
+
full = false;
|
|
449
818
|
announce(frame) {
|
|
450
819
|
const allowance = frame.allowance;
|
|
451
|
-
|
|
452
|
-
|
|
820
|
+
// The project, company and mode are the service's first line, printed when
|
|
821
|
+
// it arrives (manual 2.2). The allowance follows it.
|
|
453
822
|
if (allowance && allowance.included && !allowance.unlimited) {
|
|
454
|
-
this.
|
|
823
|
+
this.allowanceLine = ui.dim(` coding allowance left this month: ${ui.money(Number(allowance.remaining_usd))}\n`);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
/** The allowance and plan notes, printed under the first line so it stays first. */
|
|
827
|
+
allowanceLine = "";
|
|
828
|
+
/** A running command's output on screen (manual 5.12): the first lines as
|
|
829
|
+
* they arrive, then held, and at the end either the rest or the last lines
|
|
830
|
+
* with a count of what was left out. How many is the service's number. */
|
|
831
|
+
screen = { limit: 0, edge: 0, shown: 0, held: [], partial: "" };
|
|
832
|
+
stream(chunk) {
|
|
833
|
+
const sc = this.screen;
|
|
834
|
+
if (!sc.limit) {
|
|
835
|
+
this.out(ui.gray(chunk));
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
const parts = (sc.partial + chunk).split("\n");
|
|
839
|
+
sc.partial = parts.pop() ?? "";
|
|
840
|
+
for (const line of parts) {
|
|
841
|
+
if (sc.shown < sc.edge) {
|
|
842
|
+
this.out(ui.gray(`${line}\n`));
|
|
843
|
+
sc.shown += 1;
|
|
844
|
+
}
|
|
845
|
+
else
|
|
846
|
+
sc.held.push(line);
|
|
455
847
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
848
|
+
}
|
|
849
|
+
flushScreen() {
|
|
850
|
+
const sc = this.screen;
|
|
851
|
+
if (!sc.limit)
|
|
852
|
+
return;
|
|
853
|
+
if (sc.partial) {
|
|
854
|
+
sc.held.push(sc.partial);
|
|
855
|
+
sc.partial = "";
|
|
460
856
|
}
|
|
857
|
+
const room = sc.limit - sc.edge;
|
|
858
|
+
const show = sc.held.length <= room ? sc.held : sc.held.slice(-sc.edge);
|
|
859
|
+
const left = sc.held.length - show.length;
|
|
860
|
+
if (left > 0)
|
|
861
|
+
this.out(ui.dim(` ... ${left.toLocaleString()} lines not shown ...\n`));
|
|
862
|
+
for (const line of show)
|
|
863
|
+
this.out(ui.gray(`${line}\n`));
|
|
864
|
+
this.screen = { limit: 0, edge: 0, shown: 0, held: [], partial: "" };
|
|
865
|
+
}
|
|
866
|
+
/** Changes already put in front of the person to approve, so they are not
|
|
867
|
+
* shown twice. Every other change is shown as it is made (5.6 to 5.8). */
|
|
868
|
+
asked = new Set();
|
|
869
|
+
/** The sandbox this machine offers, worked out once (manual 4.12). */
|
|
870
|
+
sandboxChecked;
|
|
871
|
+
sandboxSystem() {
|
|
872
|
+
if (this.sandboxChecked === undefined) {
|
|
873
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
874
|
+
const { availability } = require("../executor/sandbox");
|
|
875
|
+
this.sandboxChecked = availability().system;
|
|
876
|
+
}
|
|
877
|
+
return (this.sandboxChecked ?? null);
|
|
878
|
+
}
|
|
879
|
+
/** The full reason for the last step, as the service wrote it (4.16). */
|
|
880
|
+
lastWhy = [];
|
|
881
|
+
/** An unattended run refused an action that needed approval (Appendix B, 4). */
|
|
882
|
+
refusedUnattended = false;
|
|
883
|
+
/** Approved actions by call, and what exactly was approved (manual 4.8). */
|
|
884
|
+
approvedCalls = new Map();
|
|
885
|
+
/** Approvals waiting here that the browser may answer first (web manual 6.15). */
|
|
886
|
+
answeredElsewhere = new Map();
|
|
887
|
+
/** An action as approved: its name, its arguments and the real folder it
|
|
888
|
+
* runs in, so a changed argument or a moved folder is caught. */
|
|
889
|
+
fingerprint(name, input) {
|
|
890
|
+
const pathMod = path;
|
|
891
|
+
const fsMod = fs;
|
|
892
|
+
let folder = this.opts.root;
|
|
893
|
+
if (typeof input.cwd === "string" && input.cwd)
|
|
894
|
+
folder = pathMod.resolve(this.opts.root, input.cwd);
|
|
895
|
+
try {
|
|
896
|
+
folder = fsMod.realpathSync(folder);
|
|
897
|
+
}
|
|
898
|
+
catch { /* the folder is gone: still compared */ }
|
|
899
|
+
const sorted = (v) => (v && typeof v === "object" && !Array.isArray(v)
|
|
900
|
+
? Object.fromEntries(Object.keys(v).sort().map((k) => [k, sorted(v[k])]))
|
|
901
|
+
: v);
|
|
902
|
+
return JSON.stringify([name, sorted(input), folder]);
|
|
903
|
+
}
|
|
904
|
+
/** Starts the working line with the service's own words. Only on a real
|
|
905
|
+
* terminal: a piped or unattended run gets the answer and nothing else. */
|
|
906
|
+
startWorking(line) {
|
|
907
|
+
this.stopWorking();
|
|
908
|
+
if (!line || this.opts.headless || !process.stdout.isTTY)
|
|
909
|
+
return;
|
|
910
|
+
// One count for one wait: the service's words taking over from the
|
|
911
|
+
// request's own do not start the clock again.
|
|
912
|
+
const started = this.workFrom ??= Date.now();
|
|
913
|
+
const draw = () => this.write(`\r\x1b[2K ${ui.dim(`${line} ${ui.duration(Date.now() - started)}`)}`);
|
|
914
|
+
draw();
|
|
915
|
+
const timer = setInterval(draw, 1000);
|
|
916
|
+
timer.unref?.();
|
|
917
|
+
this.working = { timer };
|
|
918
|
+
}
|
|
919
|
+
stopWorking() {
|
|
920
|
+
if (!this.working)
|
|
921
|
+
return;
|
|
922
|
+
clearInterval(this.working.timer);
|
|
923
|
+
this.working = null;
|
|
924
|
+
this.write("\r\x1b[2K");
|
|
461
925
|
}
|
|
462
926
|
narrate(frame) {
|
|
463
927
|
switch (frame.kind) {
|
|
928
|
+
// The model is working on a step. Shown at once, so the wait is visibly
|
|
929
|
+
// work (the web app draws the same moment as its "thinking" row).
|
|
930
|
+
case "step_started":
|
|
931
|
+
this.startWorking(String(frame.line ?? ""));
|
|
932
|
+
break;
|
|
933
|
+
// Helpers, in the service's words (manual 5.23, 5.25).
|
|
934
|
+
case "helper_started":
|
|
935
|
+
this.helpers.set(String(frame.id), {
|
|
936
|
+
id: String(frame.id), question: String(frame.question ?? ""),
|
|
937
|
+
agent: frame.agent ?? null,
|
|
938
|
+
});
|
|
939
|
+
this.out(` ${ui.bold(String(frame.id))} ${String(frame.line ?? "")}\n`);
|
|
940
|
+
break;
|
|
941
|
+
case "helper_finished":
|
|
942
|
+
this.helpers.delete(String(frame.id));
|
|
943
|
+
this.out(ui.dim(` ${String(frame.line ?? "")}\n`));
|
|
944
|
+
if (frame.used_line)
|
|
945
|
+
this.out(ui.dim(` ${String(frame.used_line)}\n`));
|
|
946
|
+
break;
|
|
947
|
+
// A skill the job follows, in the service's words (manual 9.2, 9.5).
|
|
948
|
+
case "skill_used":
|
|
949
|
+
case "skill_file":
|
|
950
|
+
if (frame.line)
|
|
951
|
+
this.out((frame.ok === false ? ui.red : ui.dim)(` ${String(frame.line)}\n`));
|
|
952
|
+
break;
|
|
953
|
+
case "team":
|
|
954
|
+
this.out((frame.ok === false ? ui.red : ui.bold)(` ${String(frame.line ?? "")}`) + "\n");
|
|
955
|
+
break;
|
|
956
|
+
case "helper_conflict":
|
|
957
|
+
this.out(ui.red(` ${String(frame.line ?? "")}\n`));
|
|
958
|
+
if (frame.diff)
|
|
959
|
+
this.out(ui.dim(String(frame.diff).split("\n").map((l) => ` ${l}`).join("\n") + "\n"));
|
|
960
|
+
break;
|
|
961
|
+
case "helper_stopping":
|
|
962
|
+
this.out(ui.dim(` ${String(frame.line ?? "")}\n`));
|
|
963
|
+
break;
|
|
964
|
+
case "action_started":
|
|
965
|
+
this.flushScreen();
|
|
966
|
+
if (typeof frame.screen_lines === "number" && typeof frame.screen_edge === "number") {
|
|
967
|
+
this.screen = { limit: frame.screen_lines, edge: frame.screen_edge, shown: 0, held: [], partial: "" };
|
|
968
|
+
}
|
|
969
|
+
break;
|
|
970
|
+
// The first line of the job, exactly as the service wrote it.
|
|
971
|
+
case "project_line":
|
|
972
|
+
this.out(`${ui.bold(String(frame.line ?? ""))}\n`);
|
|
973
|
+
if (this.allowanceLine)
|
|
974
|
+
this.out(this.allowanceLine);
|
|
975
|
+
this.allowanceLine = "";
|
|
976
|
+
// Still working: the first step has not started yet.
|
|
977
|
+
this.startWorking("thinking");
|
|
978
|
+
break;
|
|
979
|
+
// Planning and plan approval, in the service's words (manual 4.1, 4.2).
|
|
980
|
+
case "planning":
|
|
981
|
+
this.out(ui.yellow(` ${String(frame.line ?? "")}`) + "\n");
|
|
982
|
+
break;
|
|
983
|
+
// A warning the service wants on screen, in its words (manual 4.13).
|
|
984
|
+
case "notice":
|
|
985
|
+
this.out(ui.yellow(` ${String(frame.line ?? "")}`) + "\n");
|
|
986
|
+
break;
|
|
987
|
+
case "plan_approved":
|
|
988
|
+
this.out(ui.dim(` ${String(frame.reason ?? "")}\n`));
|
|
989
|
+
break;
|
|
464
990
|
case "step_finished": {
|
|
465
|
-
this.
|
|
991
|
+
this.workFrom = null;
|
|
992
|
+
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), String(frame.reason ?? "")) + "\n");
|
|
993
|
+
if (Array.isArray(frame.why_lines))
|
|
994
|
+
this.lastWhy = frame.why_lines;
|
|
466
995
|
const said = String(frame.text ?? "").trim();
|
|
467
996
|
// Markdown is for a browser. A terminal shows the asterisks.
|
|
468
997
|
if (said) {
|
|
469
|
-
|
|
998
|
+
// The plan is titled so it reads as the plan (manual 4.1).
|
|
999
|
+
const plan = ui.plain(said.replace(/^\s*(#+\s*)?\**plan\**:?\s*\n/i, ""));
|
|
1000
|
+
this.out(frame.plan ? ` ${ui.bold("Plan")}\n${plan}\n` : `\n${ui.plain(said)}\n`);
|
|
470
1001
|
this.lastSaid = said;
|
|
471
1002
|
}
|
|
472
1003
|
break;
|
|
473
1004
|
}
|
|
474
1005
|
case "action_finished":
|
|
1006
|
+
this.flushScreen();
|
|
475
1007
|
this.out(ui.action(String(frame.name ?? ""), this.targetOf(frame), frame.ok !== false, frame.latency_ms) + "\n");
|
|
1008
|
+
// The change itself, as it is made, unless it was just shown for
|
|
1009
|
+
// approval (manual 5.6 to 5.8). A change that changed nothing has
|
|
1010
|
+
// nothing to show.
|
|
1011
|
+
if (frame.ok !== false && !this.full && CHANGES.has(String(frame.name))
|
|
1012
|
+
&& !this.asked.has(String(frame.id)) && !/ no change$/.test(String(frame.target ?? ""))) {
|
|
1013
|
+
this.out(ui.diff(String(frame.name), frame.input ?? {}, false));
|
|
1014
|
+
}
|
|
1015
|
+
// The full view: exactly what went in and what came back (6.9).
|
|
1016
|
+
if (this.full) {
|
|
1017
|
+
this.out(ui.dim(` input ${JSON.stringify(frame.input ?? {})}\n`));
|
|
1018
|
+
const got = String(frame.output ?? frame.summary ?? "");
|
|
1019
|
+
this.out(ui.dim(` output\n${got.split("\n").map((l) => ` ${l}`).join("\n")}\n`));
|
|
1020
|
+
}
|
|
1021
|
+
// A refused or failed action says why, with the path (manual 2.4).
|
|
1022
|
+
if (frame.ok === false && frame.summary) {
|
|
1023
|
+
this.out(ui.dim(` ${String(frame.summary).split("\n")[0]}\n`));
|
|
1024
|
+
}
|
|
476
1025
|
break;
|
|
477
1026
|
case "action_refused":
|
|
478
1027
|
this.out(ui.red(` refused ${String(frame.name ?? "")}`)
|
|
@@ -494,22 +1043,49 @@ class TerminalSession {
|
|
|
494
1043
|
case "document_made": {
|
|
495
1044
|
const saved = frame.saved_to ? String(frame.saved_to) : "";
|
|
496
1045
|
const bytes = Number(frame.bytes ?? 0).toLocaleString();
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
1046
|
+
const name = String(frame.filename ?? "a file");
|
|
1047
|
+
if (saved) {
|
|
1048
|
+
this.out(ui.savedFile(saved, path.resolve(this.opts.root, saved), bytes));
|
|
1049
|
+
}
|
|
1050
|
+
else {
|
|
1051
|
+
// Not written here, so the stored copy is the only way to it.
|
|
1052
|
+
const url = frame.url ? String(frame.url) : "";
|
|
1053
|
+
this.out(ui.yellow(` made `) + (url ? ui.link(name, url) : name)
|
|
1054
|
+
+ ui.dim(` ${bytes} bytes, not saved here${url ? ", click to download" : ""}\n`));
|
|
1055
|
+
}
|
|
501
1056
|
break;
|
|
502
1057
|
}
|
|
503
1058
|
case "compacting":
|
|
504
1059
|
this.out(ui.dim(" summarizing the conversation so far to keep it affordable\n"));
|
|
505
1060
|
break;
|
|
1061
|
+
// Notes, pausing and the queue, in the service's words (6.7, 6.8).
|
|
506
1062
|
case "steered":
|
|
1063
|
+
case "note":
|
|
1064
|
+
case "pausing":
|
|
1065
|
+
case "paused":
|
|
1066
|
+
case "resumed":
|
|
1067
|
+
case "queue":
|
|
1068
|
+
case "queue_clear":
|
|
1069
|
+
if (frame.kind === "paused")
|
|
1070
|
+
this.paused = true;
|
|
1071
|
+
if (frame.kind === "resumed")
|
|
1072
|
+
this.paused = false;
|
|
1073
|
+
if (frame.line)
|
|
1074
|
+
this.out(ui.dim(` ${String(frame.line)}\n`));
|
|
1075
|
+
if (frame.kind === "queue" && Array.isArray(frame.notes)) {
|
|
1076
|
+
for (const n of frame.notes)
|
|
1077
|
+
this.out(ui.dim(` ${String(n.text ?? "")}\n`));
|
|
1078
|
+
}
|
|
507
1079
|
break;
|
|
508
1080
|
default:
|
|
509
1081
|
break;
|
|
510
1082
|
}
|
|
511
1083
|
}
|
|
512
1084
|
targetOf(frame) {
|
|
1085
|
+
// The service's words when it sent them (manual 5.2 to 5.4); an older
|
|
1086
|
+
// service sends none and the path or pattern is shown as before.
|
|
1087
|
+
if (typeof frame.target === "string")
|
|
1088
|
+
return frame.target;
|
|
513
1089
|
const input = frame.input ?? {};
|
|
514
1090
|
return String(input.path ?? input.pattern ?? input.command ?? input.to ?? "");
|
|
515
1091
|
}
|
|
@@ -524,7 +1100,7 @@ class TerminalSession {
|
|
|
524
1100
|
* nobody ever saw, which is the opposite of what a permission mode is for:
|
|
525
1101
|
* a script that needs to change files says so by choosing a mode that allows
|
|
526
1102
|
* it, up front. */
|
|
527
|
-
async ask(name, input) {
|
|
1103
|
+
async ask(name, input, prompt = "") {
|
|
528
1104
|
if (this.opts.headless) {
|
|
529
1105
|
// Name what was refused. A refusal that does not say WHICH command it
|
|
530
1106
|
// turned down cannot be acted on: the operator's only remedy is to
|
|
@@ -535,22 +1111,73 @@ class TerminalSession {
|
|
|
535
1111
|
this.out(ui.yellow(` ${name} needs approval and nobody is watching, so it was refused. `)
|
|
536
1112
|
+ (what ? ui.dim(`Refused: ${what}\n `) : "")
|
|
537
1113
|
+ ui.dim("Re-run with --allow \"<that exact command>\", or choose a permission mode that allows it.\n"));
|
|
538
|
-
|
|
1114
|
+
this.refusedUnattended = true;
|
|
1115
|
+
return "n";
|
|
539
1116
|
}
|
|
540
1117
|
this.out(ui.diff(name, input));
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
1118
|
+
// A command shows the folder it will run in (manual 4.7).
|
|
1119
|
+
if (typeof input.command === "string") {
|
|
1120
|
+
const pathMod = path;
|
|
1121
|
+
const folder = typeof input.cwd === "string" && input.cwd
|
|
1122
|
+
? pathMod.resolve(this.opts.root, input.cwd) : this.opts.root;
|
|
1123
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
1124
|
+
this.out(ui.dim(` in ${home && folder.startsWith(home) ? `~${folder.slice(home.length)}` : folder}\n`));
|
|
545
1125
|
}
|
|
1126
|
+
const question = prompt ? ui.bold(prompt) : `${ui.bold("Apply this?")} ${ui.dim("[y/N]")}`;
|
|
1127
|
+
let answer;
|
|
1128
|
+
if (this.opts.prompt) {
|
|
1129
|
+
answer = await this.opts.prompt(question);
|
|
1130
|
+
}
|
|
1131
|
+
else if (this.opts.confirm) {
|
|
1132
|
+
answer = (await this.opts.confirm(question)) ? "y" : "n";
|
|
1133
|
+
}
|
|
1134
|
+
else {
|
|
1135
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1136
|
+
try {
|
|
1137
|
+
answer = await new Promise((resolve) => {
|
|
1138
|
+
rl.on("SIGINT", () => { this.out("\n"); resolve(null); });
|
|
1139
|
+
rl.question(` ${question} `, (a) => resolve(a));
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
finally {
|
|
1143
|
+
rl.close();
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
const word = (answer ?? "").trim().toLowerCase();
|
|
1147
|
+
if (answer === null)
|
|
1148
|
+
return null;
|
|
1149
|
+
if (word === "y" || word === "yes")
|
|
1150
|
+
this.out(ui.green(" applied\n"));
|
|
1151
|
+
else if (word === "a")
|
|
1152
|
+
this.out(ui.green(" applied, and yes to exactly this for the rest of the session\n"));
|
|
1153
|
+
else if (word === "" || word === "n" || word === "no")
|
|
1154
|
+
this.out(ui.dim(" skipped\n"));
|
|
1155
|
+
else
|
|
1156
|
+
this.out(ui.dim(" not applied; your words go to the agent\n"));
|
|
1157
|
+
return answer;
|
|
1158
|
+
}
|
|
1159
|
+
/** The plan question or one of the agent's questions (manual 4.2, 4.3).
|
|
1160
|
+
* Returns what the person typed, or null for Ctrl+C. The meaning of the
|
|
1161
|
+
* answer (y, n, a number, words) is decided by the service. */
|
|
1162
|
+
async question(frame) {
|
|
1163
|
+
const kind = String(frame.kind ?? "question");
|
|
1164
|
+
const options = Array.isArray(frame.options) ? frame.options : [];
|
|
1165
|
+
// The trust question lists the project's allow rules as they are (4.10).
|
|
1166
|
+
if (kind === "trust")
|
|
1167
|
+
this.out(`\n ${String(frame.question ?? "").split("\n").join("\n ")}\n`);
|
|
1168
|
+
else if (kind !== "plan")
|
|
1169
|
+
this.out(ui.question(String(frame.question ?? ""), options));
|
|
1170
|
+
const label = String(frame.prompt ?? "");
|
|
1171
|
+
const shown = kind === "plan" ? `${ui.bold(label)}` : label;
|
|
1172
|
+
if (this.opts.prompt)
|
|
1173
|
+
return this.opts.prompt(shown);
|
|
546
1174
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
547
1175
|
try {
|
|
548
|
-
|
|
549
|
-
|
|
1176
|
+
return await new Promise((resolve) => {
|
|
1177
|
+
// Ctrl+C at a question stops the job, as it does anywhere else.
|
|
1178
|
+
rl.on("SIGINT", () => { this.out("\n"); resolve(null); });
|
|
1179
|
+
rl.question(` ${shown} `, (a) => resolve(a));
|
|
550
1180
|
});
|
|
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
1181
|
}
|
|
555
1182
|
finally {
|
|
556
1183
|
rl.close();
|
|
@@ -561,8 +1188,22 @@ class TerminalSession {
|
|
|
561
1188
|
// IS the step's text, and printing it again read as a stutter.
|
|
562
1189
|
if (r.text && r.text.trim() !== this.lastSaid)
|
|
563
1190
|
this.out(`\n${ui.plain(r.text)}\n`);
|
|
564
|
-
|
|
565
|
-
|
|
1191
|
+
// An item the agent could not finish stays on screen, unticked (4.4).
|
|
1192
|
+
if (r.todos.some((t) => t.status !== "completed"))
|
|
1193
|
+
this.out(ui.todos(r.todos));
|
|
1194
|
+
if (r.ending?.line) {
|
|
1195
|
+
// The service's own words, decided from what ran (manual 5.15).
|
|
1196
|
+
this.out(ui.ending(r.ending.line, r.detail));
|
|
1197
|
+
for (const line of r.ending.proof)
|
|
1198
|
+
this.out(ui.dim(` ${line}\n`));
|
|
1199
|
+
this.out(ui.receipt(r.receipt, r.models, undefined, r.card));
|
|
1200
|
+
}
|
|
1201
|
+
else {
|
|
1202
|
+
this.out(ui.outcome(r.outcome, r.detail));
|
|
1203
|
+
for (const line of r.ending?.proof ?? [])
|
|
1204
|
+
this.out(ui.dim(` ${line}\n`));
|
|
1205
|
+
this.out(ui.receipt(r.receipt, r.models, r.ending ? undefined : r.testsPassed, r.card));
|
|
1206
|
+
}
|
|
566
1207
|
if (r.jobId) {
|
|
567
1208
|
// The job's own reference, so a person can quote it. The link to the
|
|
568
1209
|
// account sits in the receipt above, where the server put it.
|