@illuminis/comprism 0.1.3 → 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.
Files changed (131) hide show
  1. package/README.md +61 -2
  2. package/out/agent/command.d.ts +75 -4
  3. package/out/agent/command.js +220 -25
  4. package/out/agent/render.d.ts +17 -2
  5. package/out/agent/render.js +157 -14
  6. package/out/agent/session.d.ts +141 -2
  7. package/out/agent/session.js +735 -149
  8. package/out/commands/agents.d.ts +2 -0
  9. package/out/commands/agents.js +79 -0
  10. package/out/commands/ask.d.ts +1 -1
  11. package/out/commands/ask.js +78 -11
  12. package/out/commands/commands-thin.js +79 -17
  13. package/out/commands/config.d.ts +1 -0
  14. package/out/commands/config.js +138 -0
  15. package/out/commands/cost.d.ts +1 -0
  16. package/out/commands/cost.js +167 -0
  17. package/out/commands/hooks.d.ts +1 -0
  18. package/out/commands/hooks.js +83 -0
  19. package/out/commands/install.d.ts +44 -1
  20. package/out/commands/install.js +198 -4
  21. package/out/commands/instructions.d.ts +1 -0
  22. package/out/commands/instructions.js +113 -0
  23. package/out/commands/integrations.d.ts +3 -0
  24. package/out/commands/integrations.js +215 -0
  25. package/out/commands/jobs.d.ts +5 -0
  26. package/out/commands/jobs.js +157 -0
  27. package/out/commands/login.js +188 -36
  28. package/out/commands/memory.d.ts +3 -0
  29. package/out/commands/memory.js +113 -0
  30. package/out/commands/permissions.d.ts +1 -0
  31. package/out/commands/permissions.js +94 -0
  32. package/out/commands/plugins.d.ts +4 -0
  33. package/out/commands/plugins.js +192 -0
  34. package/out/commands/privacy.d.ts +1 -0
  35. package/out/commands/privacy.js +57 -0
  36. package/out/commands/providerKey.d.ts +32 -0
  37. package/out/commands/providerKey.js +108 -0
  38. package/out/commands/repl.d.ts +8 -1
  39. package/out/commands/repl.js +1207 -118
  40. package/out/commands/report.d.ts +39 -0
  41. package/out/commands/report.js +115 -0
  42. package/out/commands/review.d.ts +5 -0
  43. package/out/commands/review.js +223 -0
  44. package/out/commands/sessions.d.ts +23 -0
  45. package/out/commands/sessions.js +115 -0
  46. package/out/commands/settings.d.ts +3 -1
  47. package/out/commands/settings.js +18 -16
  48. package/out/commands/skills.d.ts +21 -0
  49. package/out/commands/skills.js +207 -0
  50. package/out/commands/unattended.d.ts +7 -0
  51. package/out/commands/unattended.js +351 -0
  52. package/out/commands/update.d.ts +1 -0
  53. package/out/commands/update.js +123 -0
  54. package/out/commands/worktrees.d.ts +5 -0
  55. package/out/commands/worktrees.js +186 -0
  56. package/out/executor/browser.d.ts +14 -0
  57. package/out/executor/browser.js +270 -0
  58. package/out/executor/diagnostics.d.ts +2 -0
  59. package/out/executor/diagnostics.js +181 -0
  60. package/out/executor/files.js +270 -40
  61. package/out/executor/git.js +42 -29
  62. package/out/executor/hooks.d.ts +42 -58
  63. package/out/executor/hooks.js +89 -182
  64. package/out/executor/index.d.ts +21 -5
  65. package/out/executor/index.js +160 -14
  66. package/out/executor/paths.d.ts +6 -1
  67. package/out/executor/paths.js +34 -6
  68. package/out/executor/sandbox.d.ts +40 -0
  69. package/out/executor/sandbox.js +299 -0
  70. package/out/executor/shell.d.ts +49 -4
  71. package/out/executor/shell.js +302 -56
  72. package/out/executor/toolservers.d.ts +20 -0
  73. package/out/executor/toolservers.js +189 -0
  74. package/out/executor/worktree.d.ts +9 -0
  75. package/out/executor/worktree.js +119 -0
  76. package/out/graph/read-python.js +2 -1
  77. package/out/lib/attach.d.ts +56 -12
  78. package/out/lib/attach.js +230 -63
  79. package/out/lib/clipboard.d.ts +23 -0
  80. package/out/lib/clipboard.js +182 -0
  81. package/out/lib/commandlist.d.ts +20 -0
  82. package/out/lib/commandlist.js +58 -0
  83. package/out/lib/decision.d.ts +22 -0
  84. package/out/lib/decision.js +50 -0
  85. package/out/lib/fingerprint.d.ts +25 -0
  86. package/out/lib/fingerprint.js +58 -0
  87. package/out/lib/gateway.d.ts +186 -2
  88. package/out/lib/gateway.js +59 -4
  89. package/out/lib/history.d.ts +24 -0
  90. package/out/lib/history.js +137 -0
  91. package/out/lib/ide.d.ts +19 -0
  92. package/out/lib/ide.js +131 -0
  93. package/out/lib/keyboard.d.ts +95 -0
  94. package/out/lib/keyboard.js +383 -0
  95. package/out/lib/machine.d.ts +21 -0
  96. package/out/lib/machine.js +91 -0
  97. package/out/lib/notify.d.ts +4 -0
  98. package/out/lib/notify.js +52 -0
  99. package/out/lib/output.d.ts +48 -0
  100. package/out/lib/output.js +108 -0
  101. package/out/lib/project-ops.d.ts +19 -0
  102. package/out/lib/project-ops.js +146 -0
  103. package/out/lib/project.d.ts +28 -0
  104. package/out/lib/project.js +114 -0
  105. package/out/lib/prompt.js +15 -2
  106. package/out/lib/queue.d.ts +13 -0
  107. package/out/lib/queue.js +116 -0
  108. package/out/lib/readiness.d.ts +19 -0
  109. package/out/lib/readiness.js +170 -1
  110. package/out/lib/self.d.ts +23 -0
  111. package/out/lib/self.js +124 -0
  112. package/out/lib/sessions.d.ts +19 -0
  113. package/out/lib/sessions.js +221 -0
  114. package/out/lib/stdin.d.ts +32 -0
  115. package/out/lib/stdin.js +117 -0
  116. package/out/lib/store.d.ts +40 -0
  117. package/out/lib/store.js +138 -0
  118. package/out/lib/sync.d.ts +18 -0
  119. package/out/lib/sync.js +81 -0
  120. package/out/lib/ui.d.ts +2 -4
  121. package/out/lib/ui.js +31 -25
  122. package/out/lib/voice.js +24 -0
  123. package/out/postinstall.js +42 -17
  124. package/out/providers/anthropic.d.ts +22 -0
  125. package/out/providers/anthropic.js +80 -0
  126. package/out/providers/index.d.ts +8 -0
  127. package/out/providers/index.js +83 -0
  128. package/out/providers/openai.d.ts +11 -0
  129. package/out/providers/openai.js +57 -0
  130. package/out/thin.js +594 -32
  131. package/package.json +9 -49
@@ -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
- return (`${text.slice(0, MAX_OUTPUT_CHARS)}\n\n[... ${cut.toLocaleString()} more ` +
105
- `characters. Narrow the command, or write the output to a file and read part of it.]`);
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
- const timeoutMs = opts.timeoutMs ?? 300_000;
116
- return new Promise((resolve) => {
117
- let out = "";
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
- // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
132
- const child = (0, child_process_1.spawn)(command, {
133
- cwd, shell: true,
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 timer = setTimeout(() => {
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
- try {
152
- process.kill(-child.pid, "SIGKILL");
153
- }
154
- catch {
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[Stopped after ${Math.round(timeoutMs / 1000)}s. If this command ` +
161
- `waits for input it will never finish; run it in a way that does not.]`,
162
- summary: `timed out after ${Math.round(timeoutMs / 1000)}s`,
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
- child.on("error", (err) => {
166
- if (done)
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
- done = true;
169
- clearTimeout(timer);
170
- resolve({ isError: true, content: `Could not run it: ${err.message}` });
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
- resolve({
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
- // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
196
- const child = (0, child_process_1.spawn)(command, {
197
- cwd, shell: true, env: safeEnvironment(), stdio: ["ignore", "pipe", "pipe"],
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
- entry.buffer += chunk.toString();
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 alive = entry.child.exitCode === null && !entry.child.killed;
420
+ const up = alive(entry);
224
421
  return {
225
422
  content: (fresh.trim() || "(nothing new)") +
226
- `\n\n[${id} is ${alive ? "still running" : `finished, exit ${entry.child.exitCode}`}.]`,
227
- summary: alive ? `${id} running` : `${id} exited`,
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
- catch {
238
- entry.child.kill("SIGTERM");
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 { content: `Stopped ${id}.`, summary: `stopped ${id}` };
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
- /** Everything still running, stopped. Called when a session ends, so a job that
244
- * started a dev server does not leave it holding a port after the person has
245
- * closed the terminal. */
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
- for (const id of Array.from(running.keys()))
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 "python -m pytest -q";
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 "python -m pytest -q";
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, { ...opts, timeoutMs: opts.timeoutMs ?? 600_000 });
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
+ }