@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/executor/shell.js
CHANGED
|
@@ -33,11 +33,17 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.DEFAULT_TIMEOUT_MS = void 0;
|
|
36
37
|
exports.safeEnvironment = safeEnvironment;
|
|
38
|
+
exports.keptOutput = keptOutput;
|
|
39
|
+
exports.shellFor = shellFor;
|
|
37
40
|
exports.runCommand = runCommand;
|
|
41
|
+
exports.runProgram = runProgram;
|
|
42
|
+
exports.stopForeground = stopForeground;
|
|
38
43
|
exports.startBackground = startBackground;
|
|
39
44
|
exports.readOutput = readOutput;
|
|
40
45
|
exports.stopProcess = stopProcess;
|
|
46
|
+
exports.listProcesses = listProcesses;
|
|
41
47
|
exports.stopEverything = stopEverything;
|
|
42
48
|
exports.guessTestCommand = guessTestCommand;
|
|
43
49
|
exports.runTests = runTests;
|
|
@@ -97,12 +103,52 @@ function safeEnvironment(from = process.env) {
|
|
|
97
103
|
out.ILLUMINIS_AGENT = "1";
|
|
98
104
|
return out;
|
|
99
105
|
}
|
|
106
|
+
/** Long output keeps its beginning and its end, where a build's first error
|
|
107
|
+
* and a test run's summary are, and says how much of the middle was cut
|
|
108
|
+
* (manual 5.12). */
|
|
100
109
|
function clip(text) {
|
|
101
110
|
if (text.length <= MAX_OUTPUT_CHARS)
|
|
102
111
|
return text;
|
|
103
112
|
const cut = text.length - MAX_OUTPUT_CHARS;
|
|
104
|
-
|
|
105
|
-
|
|
113
|
+
const head = text.slice(0, MAX_OUTPUT_CHARS / 2);
|
|
114
|
+
const tail = text.slice(text.length - MAX_OUTPUT_CHARS / 2);
|
|
115
|
+
return (`${head}\n\n[... ${cut.toLocaleString()} characters cut from the middle. The first and ` +
|
|
116
|
+
`last parts are shown. Narrow the command, or write the output to a file and read part of it.]\n\n${tail}`);
|
|
117
|
+
}
|
|
118
|
+
/** The default time limit on a command (manual 5.12). */
|
|
119
|
+
exports.DEFAULT_TIMEOUT_MS = 120_000;
|
|
120
|
+
/** How long a command may sit silent after printing what looks like a question
|
|
121
|
+
* before it is taken to be waiting for someone to type (manual 5.12). */
|
|
122
|
+
const WAITING_MS = 8_000;
|
|
123
|
+
const ASK_WORDS = /(password|passphrase|y\/n|yes\/no|continue|proceed|press (enter|any key)|enter |select|choose|overwrite|confirm)/i;
|
|
124
|
+
/** A last line that reads as a question to whoever is at the keyboard. */
|
|
125
|
+
function asksForInput(line) {
|
|
126
|
+
const last = line.trim();
|
|
127
|
+
if (!last)
|
|
128
|
+
return false;
|
|
129
|
+
if (/\?$/.test(last))
|
|
130
|
+
return true;
|
|
131
|
+
return /[:>\])]$/.test(last) && ASK_WORDS.test(last);
|
|
132
|
+
}
|
|
133
|
+
/** Every command's whole output, by its number, for `output N` in a session.
|
|
134
|
+
* Kept in memory for this process only and never sent anywhere. */
|
|
135
|
+
const outputs = new Map();
|
|
136
|
+
let outputCounter = 0;
|
|
137
|
+
const MAX_KEPT = 50;
|
|
138
|
+
function keptOutput(n) {
|
|
139
|
+
return outputs.get(n);
|
|
140
|
+
}
|
|
141
|
+
function keep(command, text) {
|
|
142
|
+
outputCounter += 1;
|
|
143
|
+
outputs.set(outputCounter, { command, text });
|
|
144
|
+
if (outputs.size > MAX_KEPT)
|
|
145
|
+
outputs.delete(Math.min(...outputs.keys()));
|
|
146
|
+
return outputCounter;
|
|
147
|
+
}
|
|
148
|
+
function lineCount(text) {
|
|
149
|
+
if (!text)
|
|
150
|
+
return 0;
|
|
151
|
+
return text.split("\n").length - (text.endsWith("\n") ? 1 : 0);
|
|
106
152
|
}
|
|
107
153
|
/** Run a command and wait for it.
|
|
108
154
|
*
|
|
@@ -110,12 +156,39 @@ function clip(text) {
|
|
|
110
156
|
* RESULT: it is usually the most useful thing that can happen, because it is
|
|
111
157
|
* how the agent learns what is wrong.
|
|
112
158
|
*/
|
|
159
|
+
/**
|
|
160
|
+
* The shell a command runs in: the system shell on macOS and Linux, and
|
|
161
|
+
* PowerShell on Windows (manual 1.2), which is what a Windows developer types
|
|
162
|
+
* into and what the agent is told it is writing for.
|
|
163
|
+
*/
|
|
164
|
+
function shellFor(command) {
|
|
165
|
+
if (process.platform === "win32" && process.env.COMPRISM_WINDOWS_SHELL !== "cmd") {
|
|
166
|
+
// PowerShell 7 where it is installed, because it takes `a && b` as people
|
|
167
|
+
// write it; Windows PowerShell otherwise, which every Windows machine has.
|
|
168
|
+
return [windowsPowerShell(), ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass",
|
|
169
|
+
"-Command", command], false];
|
|
170
|
+
}
|
|
171
|
+
return [command, [], true];
|
|
172
|
+
}
|
|
173
|
+
let pwshFound;
|
|
174
|
+
function windowsPowerShell() {
|
|
175
|
+
if (pwshFound === undefined) {
|
|
176
|
+
pwshFound = null;
|
|
177
|
+
for (const dir of (process.env.PATH || "").split(path.delimiter)) {
|
|
178
|
+
const candidate = path.join(dir, "pwsh.exe");
|
|
179
|
+
if (dir && fs.existsSync(candidate)) {
|
|
180
|
+
pwshFound = candidate;
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return pwshFound ?? "powershell.exe";
|
|
186
|
+
}
|
|
113
187
|
function runCommand(command, opts) {
|
|
114
188
|
const cwd = opts.cwd ? (0, paths_1.resolveInside)(opts.root, opts.cwd, true) : fs.realpathSync(opts.root);
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
let done = false;
|
|
189
|
+
if (opts.sandbox)
|
|
190
|
+
return sandboxed(command, cwd, opts);
|
|
191
|
+
return collect({ ...opts, label: opts.label ?? command }, () => {
|
|
119
192
|
// Through a shell on purpose: pipes, redirection and `&&` are how people
|
|
120
193
|
// actually write commands, and an agent told it cannot use them writes three
|
|
121
194
|
// commands where a person would write one.
|
|
@@ -128,77 +201,197 @@ function runCommand(command, opts) {
|
|
|
128
201
|
// would move the same commands somewhere with fewer checks in front of them.
|
|
129
202
|
// The marker has to be the LAST line before the call, not the first line of
|
|
130
203
|
// the explanation: semgrep only reads the line immediately above.
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
204
|
+
const [file, argv, useShell] = shellFor(command);
|
|
205
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process -- runs the command the person approved, which is what this tool is for
|
|
206
|
+
const child = (0, child_process_1.spawn)(file, argv, {
|
|
207
|
+
cwd, shell: useShell,
|
|
134
208
|
env: { ...safeEnvironment(), ...(opts.extraEnv ?? {}) },
|
|
135
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
209
|
+
stdio: [opts.input === undefined ? "ignore" : "pipe", "pipe", "pipe"],
|
|
210
|
+
// Its own process group, so stopping it stops what the shell started as
|
|
211
|
+
// well (manual 6.7). Not on Windows, where it would open a new window.
|
|
212
|
+
detached: process.platform !== "win32",
|
|
136
213
|
});
|
|
214
|
+
if (opts.input !== undefined && child.stdin) {
|
|
215
|
+
// A command that never reads its input must not fail on it.
|
|
216
|
+
child.stdin.on("error", () => undefined);
|
|
217
|
+
child.stdin.end(opts.input);
|
|
218
|
+
}
|
|
219
|
+
return child;
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
/** The same command inside the sandbox, and anything it blocked said as
|
|
223
|
+
* blocked by the sandbox (manual 4.12). */
|
|
224
|
+
async function sandboxed(command, cwd, opts) {
|
|
225
|
+
const box = opts.sandbox;
|
|
226
|
+
box.takeBlocked();
|
|
227
|
+
const w = box.wrap(command, cwd);
|
|
228
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process -- the approved command, wrapped by the sandbox
|
|
229
|
+
const result = await collect({ ...opts, label: opts.label ?? command }, () => (0, child_process_1.spawn)(w.file, w.args, {
|
|
230
|
+
cwd, env: { ...safeEnvironment(), ...(opts.extraEnv ?? {}), ...w.env },
|
|
231
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
232
|
+
detached: process.platform !== "win32",
|
|
233
|
+
}));
|
|
234
|
+
const why = box.explain(result.content, box.takeBlocked());
|
|
235
|
+
return why ? { ...result, isError: true, summary: why, content: `${why}\n\n${result.content}` } : result;
|
|
236
|
+
}
|
|
237
|
+
/** Run one program with its arguments as a list, and no shell in between.
|
|
238
|
+
*
|
|
239
|
+
* For commands this tool builds itself, such as git. A shell would have to be
|
|
240
|
+
* told how to quote each argument, and the quoting differs by shell: the POSIX
|
|
241
|
+
* single quotes that kept a commit message whole on macOS and Linux reached
|
|
242
|
+
* git on Windows as literal characters, so a commit read `'fix` and a branch
|
|
243
|
+
* name no longer matched. With no shell there is nothing to quote, on any
|
|
244
|
+
* system, and a model written message cannot become a second command. */
|
|
245
|
+
function runProgram(file, args, opts) {
|
|
246
|
+
const cwd = opts.cwd ? (0, paths_1.resolveInside)(opts.root, opts.cwd, true) : fs.realpathSync(opts.root);
|
|
247
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process -- a fixed program with its arguments as a list, no shell
|
|
248
|
+
return collect(opts, () => (0, child_process_1.spawn)(file, args, {
|
|
249
|
+
cwd, env: { ...safeEnvironment(), ...(opts.extraEnv ?? {}) },
|
|
250
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
251
|
+
}));
|
|
252
|
+
}
|
|
253
|
+
/** Commands running in the foreground right now, so Ctrl+C can stop them
|
|
254
|
+
* (manual 6.7). Each is marked stopped before it is killed, so its result
|
|
255
|
+
* says so rather than reading as an ordinary exit. */
|
|
256
|
+
const foreground = new Map();
|
|
257
|
+
/** Stop every foreground command and everything it started. */
|
|
258
|
+
function stopForeground() {
|
|
259
|
+
for (const [child, mark] of foreground) {
|
|
260
|
+
mark.stopped = true;
|
|
261
|
+
killTree(child);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function killTree(child) {
|
|
265
|
+
if (process.platform === "win32" && child.pid) {
|
|
266
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
267
|
+
(0, child_process_1.spawn)("taskkill", ["/pid", String(child.pid), "/T", "/F"], { stdio: "ignore" });
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
try {
|
|
271
|
+
process.kill(-child.pid, "SIGKILL");
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
child.kill("SIGKILL");
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function collect(opts, start) {
|
|
278
|
+
const timeoutMs = opts.timeoutMs ?? exports.DEFAULT_TIMEOUT_MS;
|
|
279
|
+
const began = Date.now();
|
|
280
|
+
return new Promise((resolve) => {
|
|
281
|
+
let out = "";
|
|
282
|
+
let done = false;
|
|
283
|
+
const child = start();
|
|
284
|
+
const mark = { stopped: false };
|
|
285
|
+
foreground.set(child, mark);
|
|
286
|
+
let lastOutput = Date.now();
|
|
137
287
|
const collect = (chunk) => {
|
|
138
288
|
const text = chunk.toString();
|
|
139
289
|
out += text;
|
|
290
|
+
lastOutput = Date.now();
|
|
140
291
|
opts.onOutput?.(text);
|
|
141
292
|
};
|
|
142
293
|
child.stdout?.on("data", collect);
|
|
143
294
|
child.stderr?.on("data", collect);
|
|
144
|
-
const
|
|
295
|
+
const finish = (r) => {
|
|
145
296
|
if (done)
|
|
146
297
|
return;
|
|
147
298
|
done = true;
|
|
299
|
+
clearTimeout(timer);
|
|
300
|
+
clearInterval(watch);
|
|
301
|
+
foreground.delete(child);
|
|
302
|
+
const id = keep(opts.label ?? "", out);
|
|
303
|
+
resolve({ ...r, facts: { ...(r.facts ?? {}), output_id: id, lines: lineCount(out),
|
|
304
|
+
seconds: Math.round((Date.now() - began) / 100) / 10 } });
|
|
305
|
+
};
|
|
306
|
+
const timer = setTimeout(() => {
|
|
148
307
|
// The whole process group, not just the shell. Killing the shell leaves
|
|
149
308
|
// whatever it started running, which is how a stuck job leaves a server
|
|
150
|
-
// holding a port after everybody has gone home.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
child.kill("SIGKILL");
|
|
156
|
-
}
|
|
157
|
-
resolve({
|
|
309
|
+
// holding a port after everybody has gone home. On Windows the tree is
|
|
310
|
+
// stopped with taskkill, since there are no process groups.
|
|
311
|
+
killTree(child);
|
|
312
|
+
const secs = Math.round(timeoutMs / 1000);
|
|
313
|
+
finish({
|
|
158
314
|
isError: true,
|
|
159
315
|
content: clip(out) +
|
|
160
|
-
`\n\n[
|
|
161
|
-
|
|
162
|
-
|
|
316
|
+
`\n\n[Timed out: stopped after ${secs}s, the time limit. If this command ` +
|
|
317
|
+
"waits for input it will never finish; run it in a way that does not. " +
|
|
318
|
+
"A longer limit is --timeout on the job.]",
|
|
319
|
+
summary: `timed out after ${secs}s`,
|
|
320
|
+
facts: { timed_out: 1, limit_s: secs },
|
|
163
321
|
});
|
|
164
322
|
}, timeoutMs);
|
|
165
|
-
|
|
166
|
-
|
|
323
|
+
// A command waiting for somebody to type never finishes: stdin is closed,
|
|
324
|
+
// but a program that reads the terminal directly (a password prompt) sits
|
|
325
|
+
// there until the limit. Silence after a question is taken as that.
|
|
326
|
+
const watch = setInterval(() => {
|
|
327
|
+
if (done || Date.now() - lastOutput < WAITING_MS)
|
|
167
328
|
return;
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
329
|
+
const last = out.trimEnd().split("\n").pop() ?? "";
|
|
330
|
+
if (!asksForInput(last))
|
|
331
|
+
return;
|
|
332
|
+
killTree(child);
|
|
333
|
+
finish({
|
|
334
|
+
isError: true,
|
|
335
|
+
content: clip(out) +
|
|
336
|
+
"\n\n[Stopped: it was waiting for keyboard input, and nobody can type into a " +
|
|
337
|
+
"command the agent runs. Pass the answer as an option, or run it yourself.]",
|
|
338
|
+
summary: "stopped: waiting for keyboard input",
|
|
339
|
+
facts: { waiting_input: 1 },
|
|
340
|
+
});
|
|
341
|
+
}, 1_000);
|
|
342
|
+
child.on("error", (err) => {
|
|
343
|
+
finish({ isError: true, content: `Could not run it: ${err.message}` });
|
|
171
344
|
});
|
|
172
345
|
child.on("close", (code) => {
|
|
173
|
-
if (done)
|
|
174
|
-
return;
|
|
175
|
-
done = true;
|
|
176
|
-
clearTimeout(timer);
|
|
177
346
|
const body = clip(out).trim() || "(no output)";
|
|
178
|
-
|
|
347
|
+
if (mark.stopped) {
|
|
348
|
+
finish({
|
|
349
|
+
isError: true,
|
|
350
|
+
content: `${body}\n\n[Stopped by the person. The output above is everything it printed before it was stopped.]`,
|
|
351
|
+
summary: "stopped",
|
|
352
|
+
facts: { stopped: 1 },
|
|
353
|
+
});
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
finish({
|
|
179
357
|
// Non-zero is not an error in the sense that matters here. It is
|
|
180
358
|
// information, and it is usually the most useful information available.
|
|
181
359
|
isError: false,
|
|
182
360
|
content: `exit ${code}\n\n${body}`,
|
|
183
361
|
summary: code === 0 ? "exit 0" : `exit ${code}`,
|
|
362
|
+
facts: { exit: code ?? -1 },
|
|
184
363
|
});
|
|
185
364
|
});
|
|
186
365
|
});
|
|
187
366
|
}
|
|
188
367
|
const running = new Map();
|
|
189
368
|
let counter = 0;
|
|
369
|
+
/** A port a server announces in its output: "port 8765", "localhost:5173". */
|
|
370
|
+
const PORT = /(?:\bport\s+|localhost:|127\.0\.0\.1:|0\.0\.0\.0:|\[::\]:|\/\/[\w.-]+:)(\d{2,5})\b/i;
|
|
190
371
|
function startBackground(command, opts) {
|
|
191
372
|
const cwd = opts.cwd ? (0, paths_1.resolveInside)(opts.root, opts.cwd, true) : fs.realpathSync(opts.root);
|
|
192
373
|
const id = `p${++counter}`;
|
|
193
374
|
// The background half of the same shell tool, with the same approval list and
|
|
194
|
-
// the same workspace root in front of it. See `runCommand` above.
|
|
195
|
-
//
|
|
196
|
-
const
|
|
197
|
-
|
|
375
|
+
// the same workspace root in front of it. See `runCommand` above. Its own
|
|
376
|
+
// process group, so stopping it stops everything it started (manual 5.13).
|
|
377
|
+
const [file, argv, useShell] = shellFor(command);
|
|
378
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process -- runs the command the person approved, which is what this tool is for
|
|
379
|
+
const child = (0, child_process_1.spawn)(file, argv, {
|
|
380
|
+
// Unbuffered, so a Python server's "port 8765" line arrives when it is
|
|
381
|
+
// printed rather than when a buffer fills (read_output, processes).
|
|
382
|
+
cwd, shell: useShell, env: { ...safeEnvironment(), PYTHONUNBUFFERED: "1" },
|
|
383
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
384
|
+
detached: process.platform !== "win32",
|
|
198
385
|
});
|
|
199
|
-
const entry = { child, buffer: "", read: 0, command };
|
|
386
|
+
const entry = { child, buffer: "", read: 0, command, started: Date.now() };
|
|
200
387
|
const collect = (chunk) => {
|
|
201
|
-
|
|
388
|
+
const text = chunk.toString();
|
|
389
|
+
entry.buffer += text;
|
|
390
|
+
if (!entry.port) {
|
|
391
|
+
const m = PORT.exec(text);
|
|
392
|
+
if (m)
|
|
393
|
+
entry.port = Number(m[1]);
|
|
394
|
+
}
|
|
202
395
|
// Bounded. A watcher left running for an hour would otherwise hold its whole
|
|
203
396
|
// output in memory, and nobody is going to read the first hour of it.
|
|
204
397
|
if (entry.buffer.length > MAX_OUTPUT_CHARS * 4) {
|
|
@@ -212,40 +405,77 @@ function startBackground(command, opts) {
|
|
|
212
405
|
return {
|
|
213
406
|
content: `Started as ${id}: ${command}\nUse read_output with ${id} to see what it prints, and stop_process to end it.`,
|
|
214
407
|
summary: `started ${id}`,
|
|
408
|
+
facts: { process: counter },
|
|
215
409
|
};
|
|
216
410
|
}
|
|
411
|
+
function alive(entry) {
|
|
412
|
+
return entry.child.exitCode === null && entry.child.signalCode === null;
|
|
413
|
+
}
|
|
217
414
|
function readOutput(id) {
|
|
218
415
|
const entry = running.get(id);
|
|
219
416
|
if (!entry)
|
|
220
|
-
return { isError: true, content: `No process called ${id}.` };
|
|
417
|
+
return { isError: true, content: `No process called ${id}. Only processes this session started can be read.` };
|
|
221
418
|
const fresh = entry.buffer.slice(entry.read);
|
|
222
419
|
entry.read = entry.buffer.length;
|
|
223
|
-
const
|
|
420
|
+
const up = alive(entry);
|
|
224
421
|
return {
|
|
225
422
|
content: (fresh.trim() || "(nothing new)") +
|
|
226
|
-
`\n\n[${id} is ${
|
|
227
|
-
summary:
|
|
423
|
+
`\n\n[${id} is ${up ? "still running" : `finished, exit ${entry.child.exitCode}`}.]`,
|
|
424
|
+
summary: up ? `${id} running` : `${id} exited`,
|
|
228
425
|
};
|
|
229
426
|
}
|
|
427
|
+
/** Stop one process this tool started, and everything it started, by the
|
|
428
|
+
* number it was given. Never by name or by port (manual 5.13). */
|
|
230
429
|
function stopProcess(id) {
|
|
231
430
|
const entry = running.get(id);
|
|
232
|
-
if (!entry)
|
|
233
|
-
return { isError: true, content: `No process called ${id}.` };
|
|
234
|
-
try {
|
|
235
|
-
process.kill(-entry.child.pid, "SIGTERM");
|
|
431
|
+
if (!entry) {
|
|
432
|
+
return { isError: true, content: `No process called ${id}. The agent can stop only processes it started, by their number.` };
|
|
236
433
|
}
|
|
237
|
-
|
|
238
|
-
|
|
434
|
+
const wasUp = alive(entry);
|
|
435
|
+
if (wasUp) {
|
|
436
|
+
if (process.platform === "win32" && entry.child.pid) {
|
|
437
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
|
|
438
|
+
(0, child_process_1.spawn)("taskkill", ["/pid", String(entry.child.pid), "/T", "/F"], { stdio: "ignore" });
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
try {
|
|
442
|
+
process.kill(-entry.child.pid, "SIGTERM");
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
entry.child.kill("SIGTERM");
|
|
446
|
+
}
|
|
447
|
+
// Whatever ignores the polite request is stopped a moment later.
|
|
448
|
+
const pid = entry.child.pid;
|
|
449
|
+
setTimeout(() => { try {
|
|
450
|
+
process.kill(-pid, "SIGKILL");
|
|
451
|
+
}
|
|
452
|
+
catch { /* already gone */ } }, 1500).unref();
|
|
453
|
+
}
|
|
239
454
|
}
|
|
240
455
|
running.delete(id);
|
|
241
|
-
return {
|
|
456
|
+
return {
|
|
457
|
+
content: wasUp ? `Stopped ${id} (${entry.command}) and everything it started.` : `${id} had already finished.`,
|
|
458
|
+
summary: `stopped ${id}`,
|
|
459
|
+
};
|
|
242
460
|
}
|
|
243
|
-
/**
|
|
244
|
-
|
|
245
|
-
|
|
461
|
+
/** What this session started and is still tracking, for `processes`. */
|
|
462
|
+
function listProcesses() {
|
|
463
|
+
return [...running.entries()].map(([id, e]) => ({
|
|
464
|
+
id, command: e.command, running: alive(e), seconds: Math.round((Date.now() - e.started) / 1000),
|
|
465
|
+
...(e.port ? { port: e.port } : {}),
|
|
466
|
+
}));
|
|
467
|
+
}
|
|
468
|
+
/** Everything still running, stopped, and what was stopped. Called when a
|
|
469
|
+
* session ends, so a job that started a dev server does not leave it holding
|
|
470
|
+
* a port after the person has closed the terminal (manual 5.13). */
|
|
246
471
|
function stopEverything() {
|
|
247
|
-
|
|
472
|
+
const stopped = [];
|
|
473
|
+
for (const [id, e] of Array.from(running.entries())) {
|
|
474
|
+
if (alive(e))
|
|
475
|
+
stopped.push(`${id} ${e.command}`);
|
|
248
476
|
stopProcess(id);
|
|
477
|
+
}
|
|
478
|
+
return stopped;
|
|
249
479
|
}
|
|
250
480
|
// ── the tests ───────────────────────────────────────────────────────────────
|
|
251
481
|
/** How this project runs its tests, worked out from what is in it.
|
|
@@ -271,6 +501,22 @@ function looksLikePython(root) {
|
|
|
271
501
|
return false;
|
|
272
502
|
}
|
|
273
503
|
}
|
|
504
|
+
/** The Python this machine runs: `python3` where `python` is not installed,
|
|
505
|
+
* which is every recent Mac and most Linux. Guessing `python` there ran
|
|
506
|
+
* nothing and cost the agent a step (exit 127). */
|
|
507
|
+
function python() {
|
|
508
|
+
const dirs = (process.env.PATH ?? "").split(path.delimiter);
|
|
509
|
+
const exts = process.platform === "win32" ? [".exe", ".cmd", ".bat", ""] : [""];
|
|
510
|
+
const has = (name) => dirs.some((d) => exts.some((e) => {
|
|
511
|
+
try {
|
|
512
|
+
return fs.statSync(path.join(d, name + e)).isFile();
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
}));
|
|
518
|
+
return has("python") || !has("python3") ? "python" : "python3";
|
|
519
|
+
}
|
|
274
520
|
function guessTestCommand(root) {
|
|
275
521
|
const has = (f) => fs.existsSync(path.join(root, f));
|
|
276
522
|
const pkgPath = path.join(root, "package.json");
|
|
@@ -285,7 +531,7 @@ function guessTestCommand(root) {
|
|
|
285
531
|
catch { /* a malformed package.json is not our problem to report here */ }
|
|
286
532
|
}
|
|
287
533
|
if (has("pytest.ini") || has("pyproject.toml") || has("tests") || has("setup.cfg")) {
|
|
288
|
-
return
|
|
534
|
+
return `${python()} -m pytest -q`;
|
|
289
535
|
}
|
|
290
536
|
// A project identified by its TEST FILES rather than by a configuration file.
|
|
291
537
|
// A plain folder of `test_*.py` with no `pyproject.toml` is an entirely normal
|
|
@@ -296,7 +542,7 @@ function guessTestCommand(root) {
|
|
|
296
542
|
//
|
|
297
543
|
// One shallow directory read, and only when nothing above matched.
|
|
298
544
|
if (looksLikePython(root))
|
|
299
|
-
return
|
|
545
|
+
return `${python()} -m pytest -q`;
|
|
300
546
|
if (has("Cargo.toml"))
|
|
301
547
|
return "cargo test";
|
|
302
548
|
if (has("go.mod"))
|
|
@@ -321,7 +567,7 @@ async function runTests(opts) {
|
|
|
321
567
|
}
|
|
322
568
|
if (opts.path)
|
|
323
569
|
command = `${command} ${(0, paths_1.resolveInside)(root, opts.path, false)}`;
|
|
324
|
-
const result = await runCommand(command,
|
|
570
|
+
const result = await runCommand(command, opts);
|
|
325
571
|
const passed = /^exit 0\b/.test(result.content);
|
|
326
572
|
return {
|
|
327
573
|
...result,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** The tool servers one job started, by name. */
|
|
2
|
+
export declare class ToolServers {
|
|
3
|
+
private readonly cwd;
|
|
4
|
+
private running;
|
|
5
|
+
constructor(cwd: string);
|
|
6
|
+
private server;
|
|
7
|
+
/** The server's tools, as it described them, as JSON text. */
|
|
8
|
+
list(name: string, command: string): Promise<{
|
|
9
|
+
isError: boolean;
|
|
10
|
+
content: string;
|
|
11
|
+
}>;
|
|
12
|
+
/** One call, and the text the server returned. */
|
|
13
|
+
call(name: string, command: string, tool: string, args: Record<string, unknown>): Promise<{
|
|
14
|
+
isError: boolean;
|
|
15
|
+
content: string;
|
|
16
|
+
}>;
|
|
17
|
+
/** Every server this job started, stopped. Called when the job ends. */
|
|
18
|
+
stopAll(): void;
|
|
19
|
+
get count(): number;
|
|
20
|
+
}
|