teapot-coding-agent 0.26.12 → 0.26.14
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/dist/agent/kill.js +70 -0
- package/dist/agent/skills.js +4 -1
- package/dist/agent/tools.js +12 -21
- package/dist/home.js +51 -0
- package/dist/master.js +4 -1
- package/dist/server/api.js +6 -2
- package/package.json +3 -2
- package/public/assets/{index-D2dVnJim.js → index-C1UUe1T-.js} +6 -6
- package/public/assets/index-C1UUe1T-.js.map +1 -0
- package/public/assets/{shiki-Dz0hO2d7.js → shiki-BxZS8Zdw.js} +2 -2
- package/public/assets/{shiki-Dz0hO2d7.js.map → shiki-BxZS8Zdw.js.map} +1 -1
- package/public/index.html +1 -1
- package/public/assets/index-D2dVnJim.js.map +0 -1
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** can this platform kill a whole process group? */
|
|
2
|
+
export function canKillProcessGroup(platform = process.platform) {
|
|
3
|
+
return platform !== "win32";
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Kill a spawned child, and its whole group where the platform allows it.
|
|
7
|
+
*
|
|
8
|
+
* Deliberately does NOT throw: callers are timeouts and abort handlers, where a
|
|
9
|
+
* throw would replace the real message with a secondary failure.
|
|
10
|
+
*/
|
|
11
|
+
export function killTree(child, platform = process.platform) {
|
|
12
|
+
if (!child?.pid)
|
|
13
|
+
return { killed: false, wholeTree: false, reason: "no-pid" };
|
|
14
|
+
if (!canKillProcessGroup(platform)) {
|
|
15
|
+
// Windows: no group kill. Try the direct child so at least THAT stops, and
|
|
16
|
+
// report that the tree is not covered rather than implying it is.
|
|
17
|
+
try {
|
|
18
|
+
process.kill(child.pid, "SIGKILL");
|
|
19
|
+
return { killed: true, wholeTree: false };
|
|
20
|
+
}
|
|
21
|
+
catch (err) {
|
|
22
|
+
const code = err.code;
|
|
23
|
+
return {
|
|
24
|
+
killed: false,
|
|
25
|
+
wholeTree: false,
|
|
26
|
+
reason: code === "ESRCH" ? "already-gone" : "error",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
process.kill(-child.pid, "SIGKILL"); // whole group
|
|
32
|
+
return { killed: true, wholeTree: true };
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
const code = err.code;
|
|
36
|
+
// EPERM on the group can still mean the child is killable directly, and
|
|
37
|
+
// EPERM is what a partially-privileged tree produces.
|
|
38
|
+
if (code === "EPERM") {
|
|
39
|
+
try {
|
|
40
|
+
process.kill(child.pid, "SIGKILL");
|
|
41
|
+
return { killed: true, wholeTree: false };
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return { killed: false, wholeTree: false, reason: "error" };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (code === "ESRCH")
|
|
48
|
+
return { killed: false, wholeTree: false, reason: "already-gone" };
|
|
49
|
+
return { killed: false, wholeTree: false, reason: "error" };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** a sentence for the tool result, so a partial kill is never silent */
|
|
53
|
+
export function describeKill(outcome) {
|
|
54
|
+
if (outcome.killed && outcome.wholeTree)
|
|
55
|
+
return "killed (whole process group)";
|
|
56
|
+
if (outcome.killed) {
|
|
57
|
+
return ("killed the process, but this platform has no process groups — any " +
|
|
58
|
+
"background children it started may still be running");
|
|
59
|
+
}
|
|
60
|
+
switch (outcome.reason) {
|
|
61
|
+
case "no-pid":
|
|
62
|
+
return "no process handle to kill";
|
|
63
|
+
case "already-gone":
|
|
64
|
+
return "already exited";
|
|
65
|
+
case "group-unsupported":
|
|
66
|
+
return "process groups are not supported on this platform";
|
|
67
|
+
default:
|
|
68
|
+
return "could not be killed";
|
|
69
|
+
}
|
|
70
|
+
}
|
package/dist/agent/skills.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { promises as fs } from "node:fs";
|
|
20
20
|
import path from "node:path";
|
|
21
|
+
import { homeDir } from "../home.js";
|
|
21
22
|
export const SKILL_FILE = "SKILL.md";
|
|
22
23
|
/**
|
|
23
24
|
* Cheap change-detector for a set of skill roots. Returns a stable string
|
|
@@ -148,7 +149,9 @@ function unquote(v) {
|
|
|
148
149
|
* directories are skipped silently, and nothing is read unless it exists.
|
|
149
150
|
*/
|
|
150
151
|
export function foreignSkillRoots(workspace) {
|
|
151
|
-
|
|
152
|
+
// #110: the shared helper also covers HOMEDRIVE/HOMEPATH, which is how
|
|
153
|
+
// Windows reports a home when USERPROFILE is absent.
|
|
154
|
+
const home = homeDir();
|
|
152
155
|
const out = [];
|
|
153
156
|
const add = (dir) => {
|
|
154
157
|
if (dir && !out.some((r) => r.dir === dir))
|
package/dist/agent/tools.js
CHANGED
|
@@ -7,6 +7,7 @@ import { existsSync, promises as fs, readlinkSync, realpathSync } from "node:fs"
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { discoverSkills, isValidSkillName, readSkillFile, saveSkill, SKILL_FILE, } from "./skills.js";
|
|
9
9
|
import { isReasoningEffort } from "../model-meta.js";
|
|
10
|
+
import { killTree, describeKill } from "./kill.js";
|
|
10
11
|
const str = (v, fallback = "") => (typeof v === "string" ? v : fallback);
|
|
11
12
|
const num = (v, fallback) => (typeof v === "number" ? v : fallback);
|
|
12
13
|
function clip(s, max) {
|
|
@@ -322,23 +323,18 @@ function runShell(cmd, ctx, timeoutMs) {
|
|
|
322
323
|
};
|
|
323
324
|
child.stdout.on("data", collect);
|
|
324
325
|
child.stderr.on("data", collect);
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
/* already gone */
|
|
332
|
-
}
|
|
333
|
-
};
|
|
326
|
+
// #110: the kill REPORTS whether it worked. It used to swallow every failure
|
|
327
|
+
// as "already gone", and on Windows a group kill throws ALWAYS — so a timeout
|
|
328
|
+
// would report `TIMEOUT after …ms` for a process that was still running, and
|
|
329
|
+
// shutdown would leave it alive. A timeout that claims success is worse than
|
|
330
|
+
// a crash, because nothing afterwards looks wrong.
|
|
331
|
+
const killGroup = () => killTree(child);
|
|
334
332
|
const timer = setTimeout(() => {
|
|
335
|
-
killReason = `TIMEOUT after ${timeoutMs}ms`;
|
|
336
|
-
killGroup();
|
|
333
|
+
killReason = `TIMEOUT after ${timeoutMs}ms — ${describeKill(killGroup())}`;
|
|
337
334
|
}, timeoutMs);
|
|
338
335
|
// harness shutdown must not wait out a long-running command
|
|
339
336
|
const onAbort = () => {
|
|
340
|
-
killReason =
|
|
341
|
-
killGroup();
|
|
337
|
+
killReason = `ABORTED (harness shutdown) — ${describeKill(killGroup())}`;
|
|
342
338
|
};
|
|
343
339
|
if (ctx.signal) {
|
|
344
340
|
if (ctx.signal.aborted)
|
|
@@ -1422,14 +1418,9 @@ export const TOOLS = [
|
|
|
1422
1418
|
`jobs are per-workspace and forgotten on harness restart`,
|
|
1423
1419
|
};
|
|
1424
1420
|
if (str(args.action) === "kill") {
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
process.kill(-sh.child.pid, "SIGKILL");
|
|
1429
|
-
}
|
|
1430
|
-
catch { /* already gone */ }
|
|
1431
|
-
}
|
|
1432
|
-
const note = sh.exited ? "(already exited)" : "killed";
|
|
1421
|
+
// #110: same swallowed-kill bug — this reported "killed" and dropped the
|
|
1422
|
+
// job from the map while the process kept running, now untrackable.
|
|
1423
|
+
const note = sh.exited ? "(already exited)" : describeKill(killTree(sh.child));
|
|
1433
1424
|
map.delete(sh.id);
|
|
1434
1425
|
return { ok: true, result: `job ${sh.id} ${note}. Output so far:\n${clip(sh.out, 4000)}` };
|
|
1435
1426
|
}
|
package/dist/home.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The user's home directory, on every platform.
|
|
3
|
+
*
|
|
4
|
+
* #110: `process.env.HOME` is a POSIX convention and is normally UNSET on
|
|
5
|
+
* Windows. Code that expanded `~` with `process.env.HOME ?? "~"` therefore got
|
|
6
|
+
* the LITERAL string "~" back on Windows — and since the result was then passed
|
|
7
|
+
* to `path.resolve(base, …)` and `mkdirSync`, teapot created a directory named
|
|
8
|
+
* `~`. That is the reported symptom.
|
|
9
|
+
*
|
|
10
|
+
* `os.homedir()` is the answer: on Windows Node resolves it from `USERPROFILE`,
|
|
11
|
+
* on POSIX from `HOME`, so it is never the literal `~`. The env lookups are kept
|
|
12
|
+
* only so a test can drive the Windows shape deterministically — `os.homedir()`
|
|
13
|
+
* on a Linux CI box would report `/root` and hide the very condition under test.
|
|
14
|
+
*
|
|
15
|
+
* Returns "" only when nothing at all is known, which callers treat as "leave the
|
|
16
|
+
* `~` alone" rather than inventing a path.
|
|
17
|
+
*/
|
|
18
|
+
export function homeDir(env = process.env) {
|
|
19
|
+
if (env.HOME)
|
|
20
|
+
return env.HOME;
|
|
21
|
+
if (env.USERPROFILE)
|
|
22
|
+
return env.USERPROFILE;
|
|
23
|
+
// Windows also reports a home as HOMEDRIVE + HOMEPATH. Concatenate
|
|
24
|
+
// defensively: `drive.replace(...) + path` yields the STRING "undefined"
|
|
25
|
+
// rather than undefined when either part is missing, which is exactly the
|
|
26
|
+
// class of bug this helper exists to remove.
|
|
27
|
+
const drive = env.HOMEDRIVE;
|
|
28
|
+
const rest = env.HOMEPATH;
|
|
29
|
+
if (drive && rest)
|
|
30
|
+
return drive.replace(/\\+$/, "") + rest;
|
|
31
|
+
if (drive)
|
|
32
|
+
return drive;
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Expand a leading `~` to the home directory.
|
|
37
|
+
*
|
|
38
|
+
* Kept in one place so the Windows case cannot be got wrong a second time — the
|
|
39
|
+
* bug existed in three separate places before this helper (#110).
|
|
40
|
+
*/
|
|
41
|
+
export function expandTilde(input, env = process.env) {
|
|
42
|
+
// #110: on Windows a path can be `~\foo`, hence the backslash in the lookahead.
|
|
43
|
+
if (!/^~(?=$|[/\\])/.test(input))
|
|
44
|
+
return input;
|
|
45
|
+
const home = homeDir(env);
|
|
46
|
+
// With no home known, returning the input untouched is more honest than
|
|
47
|
+
// substituting a literal that would become a real directory.
|
|
48
|
+
if (!home)
|
|
49
|
+
return input;
|
|
50
|
+
return input.replace(/^~(?=$|[/\\])/, home);
|
|
51
|
+
}
|
package/dist/master.js
CHANGED
|
@@ -7,6 +7,7 @@ import { readFileSync, existsSync, mkdirSync, writeFileSync, readdirSync, statSy
|
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
8
|
import path from "node:path";
|
|
9
9
|
import os from "node:os";
|
|
10
|
+
import { expandTilde } from "./home.js";
|
|
10
11
|
import { fileURLToPath } from "node:url";
|
|
11
12
|
import { spawn } from "node:child_process";
|
|
12
13
|
import { Agent } from "./agent/agent.js";
|
|
@@ -64,7 +65,9 @@ function readAppVersion() {
|
|
|
64
65
|
* `~` is expanded for both forms; absolute paths pass through untouched.
|
|
65
66
|
*/
|
|
66
67
|
export function resolveWorkspace(input, base) {
|
|
67
|
-
|
|
68
|
+
// #110: `~` must not expand to the LITERAL "~" — that is how a directory named
|
|
69
|
+
// `~` got created on Windows, where process.env.HOME is unset. See src/home.ts.
|
|
70
|
+
const expanded = expandTilde(input.trim());
|
|
68
71
|
if (path.isAbsolute(expanded))
|
|
69
72
|
return path.normalize(expanded);
|
|
70
73
|
return path.resolve(base, expanded);
|
package/dist/server/api.js
CHANGED
|
@@ -11,6 +11,7 @@ import { promises as fs } from "node:fs";
|
|
|
11
11
|
import { fileURLToPath } from "node:url";
|
|
12
12
|
import { parseSchedule } from "../scheduler/cron.js";
|
|
13
13
|
import { TermSizeTracker } from "./term-size.js";
|
|
14
|
+
import { expandTilde, homeDir } from "../home.js";
|
|
14
15
|
import { SUB_PERSONAS, resolveWorkspace } from "../master.js";
|
|
15
16
|
import { ConfigPatchSchema, formatZodError } from "../config-schema.js";
|
|
16
17
|
import { providerHeaders } from "../agent/llm.js";
|
|
@@ -440,8 +441,11 @@ export function buildApp(master) {
|
|
|
440
441
|
});
|
|
441
442
|
// ---- filesystem browsing (read-only, for the workspace picker) ----
|
|
442
443
|
app.get("/api/fs", async (c) => {
|
|
443
|
-
|
|
444
|
-
|
|
444
|
+
// #110: read the home directory through the shared helper. `process.env.HOME`
|
|
445
|
+
// is unset on Windows, which made the old fallback yield the literal "~" —
|
|
446
|
+
// and /api/fs would then browse a directory named ~.
|
|
447
|
+
let p = c.req.query("path") || homeDir() || path.parse(process.cwd()).root;
|
|
448
|
+
p = path.resolve(expandTilde(p));
|
|
445
449
|
try {
|
|
446
450
|
const entries = await fs.readdir(p, { withFileTypes: true });
|
|
447
451
|
return c.json({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "teapot-coding-agent",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.14",
|
|
4
4
|
"description": "A lightweight, always-on multi-agent harness for AI coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "AGPL-3.0-or-later",
|
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
"start": "node dist/index.js",
|
|
40
40
|
"dev": "node src/index.ts",
|
|
41
41
|
"dev-web": "vite",
|
|
42
|
-
"test": "node --test test/*.test.ts"
|
|
42
|
+
"test": "node --test test/*.test.ts",
|
|
43
|
+
"test:portable": "node scripts/run-portable-tests.mjs"
|
|
43
44
|
},
|
|
44
45
|
"dependencies": {
|
|
45
46
|
"@hono/node-server": "^2.1.1",
|