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