quadwork 1.19.3 → 2.0.1
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 +19 -35
- package/bin/quadwork.js +48 -1118
- package/out/404.html +1 -1
- package/out/__next.__PAGE__.txt +3 -3
- package/out/__next._full.txt +14 -14
- package/out/__next._head.txt +4 -4
- package/out/__next._index.txt +8 -8
- package/out/__next._tree.txt +2 -2
- package/out/_next/static/chunks/{030cjkhts487t.js → 079wdniva~de1.js} +1 -1
- package/out/_next/static/chunks/{0n~dq4kpx9xxx.js → 07lhk_q6pmm3r.js} +1 -1
- package/out/_next/static/chunks/0_79hkefw1mo2.js +1 -0
- package/out/_next/static/chunks/{08tog0xc~.es_.js → 0jllnzexn48._.js} +1 -1
- package/out/_next/static/chunks/0oxv9vrvc17to.js +2 -0
- package/out/_next/static/chunks/0py7102i226n5.js +1 -0
- package/out/_next/static/chunks/{13fv-yi7.v52g.js → 0q4bm04c1jl_3.js} +1 -1
- package/out/_next/static/chunks/{0_idxioyl0p7h.js → 0sjhy6oe3mbon.js} +1 -1
- package/out/_next/static/chunks/{11khe5i7gu158.js → 0z.9wnba-t6z8.js} +1 -1
- package/out/_next/static/chunks/13xk0vgfbrcld.css +2 -0
- package/out/_next/static/chunks/163_ddkdca5q4.js +25 -0
- package/out/_next/static/chunks/{turbopack-0qm-e3ifrz~2u.js → turbopack-0y2u-q0l2m67w.js} +1 -1
- package/out/_not-found/__next._full.txt +13 -13
- package/out/_not-found/__next._head.txt +4 -4
- package/out/_not-found/__next._index.txt +8 -8
- package/out/_not-found/__next._not-found.__PAGE__.txt +2 -2
- package/out/_not-found/__next._not-found.txt +3 -3
- package/out/_not-found/__next._tree.txt +2 -2
- package/out/_not-found.html +1 -1
- package/out/_not-found.txt +13 -13
- package/out/app-shell/__next._full.txt +13 -13
- package/out/app-shell/__next._head.txt +4 -4
- package/out/app-shell/__next._index.txt +8 -8
- package/out/app-shell/__next._tree.txt +2 -2
- package/out/app-shell/__next.app-shell.__PAGE__.txt +2 -2
- package/out/app-shell/__next.app-shell.txt +3 -3
- package/out/app-shell.html +1 -1
- package/out/app-shell.txt +13 -13
- package/out/index.html +1 -1
- package/out/index.txt +14 -14
- package/out/project/_/__next._full.txt +14 -14
- package/out/project/_/__next._head.txt +4 -4
- package/out/project/_/__next._index.txt +8 -8
- package/out/project/_/__next._tree.txt +2 -2
- package/out/project/_/__next.project.$d$id.__PAGE__.txt +3 -3
- package/out/project/_/__next.project.$d$id.txt +3 -3
- package/out/project/_/__next.project.txt +3 -3
- package/out/project/_/queue/__next._full.txt +14 -14
- package/out/project/_/queue/__next._head.txt +4 -4
- package/out/project/_/queue/__next._index.txt +8 -8
- package/out/project/_/queue/__next._tree.txt +2 -2
- package/out/project/_/queue/__next.project.$d$id.queue.__PAGE__.txt +3 -3
- package/out/project/_/queue/__next.project.$d$id.queue.txt +3 -3
- package/out/project/_/queue/__next.project.$d$id.txt +3 -3
- package/out/project/_/queue/__next.project.txt +3 -3
- package/out/project/_/queue.html +1 -1
- package/out/project/_/queue.txt +14 -14
- package/out/project/_.html +1 -1
- package/out/project/_.txt +14 -14
- package/out/settings/__next._full.txt +14 -14
- package/out/settings/__next._head.txt +4 -4
- package/out/settings/__next._index.txt +8 -8
- package/out/settings/__next._tree.txt +2 -2
- package/out/settings/__next.settings.__PAGE__.txt +3 -3
- package/out/settings/__next.settings.txt +3 -3
- package/out/settings.html +1 -1
- package/out/settings.txt +14 -14
- package/out/setup/__next._full.txt +14 -14
- package/out/setup/__next._head.txt +4 -4
- package/out/setup/__next._index.txt +8 -8
- package/out/setup/__next._tree.txt +2 -2
- package/out/setup/__next.setup.__PAGE__.txt +3 -3
- package/out/setup/__next.setup.txt +3 -3
- package/out/setup.html +1 -1
- package/out/setup.txt +14 -14
- package/package.json +4 -2
- package/server/ac-restore.js +128 -0
- package/server/bridges/discord.js +244 -0
- package/server/bridges/telegram.js +258 -0
- package/server/config.js +4 -60
- package/server/file-chat.js +318 -0
- package/server/index.js +129 -1294
- package/server/install-agentchattr.js +3 -284
- package/server/mcp-chat-shim.js +171 -0
- package/server/migrate-ac.js +158 -0
- package/server/pty-dispatcher.js +188 -0
- package/server/routes.js +155 -1398
- package/templates/CLAUDE.md +2 -2
- package/templates/OVERNIGHT-QUEUE.md +1 -1
- package/templates/seeds/butler.CLAUDE.md +30 -62
- package/templates/seeds/dev.AGENTS.md +10 -1
- package/templates/seeds/head.AGENTS.md +12 -8
- package/templates/seeds/re1.AGENTS.md +3 -3
- package/templates/seeds/re2.AGENTS.md +3 -3
- package/bridges/discord/__pycache__/discord_bridge.cpython-314.pyc +0 -0
- package/bridges/discord/discord_bridge.py +0 -666
- package/bridges/discord/requirements.txt +0 -2
- package/out/_next/static/chunks/08kw.2kplxa.6.css +0 -2
- package/out/_next/static/chunks/0_nm7se0m3twm.js +0 -25
- package/out/_next/static/chunks/0uz5svjlo9dwl.js +0 -1
- package/out/_next/static/chunks/0zahstmgdrpy5.js +0 -1
- package/out/_next/static/chunks/0zfotsowwll1x.js +0 -2
- package/server/__tests__/bridge-auto-stop-guard.test.js +0 -134
- package/server/__tests__/rate-limit-handling.test.js +0 -168
- package/server/__tests__/scrub-secrets.test.js +0 -235
- package/server/__tests__/v1110-security-qa.test.js +0 -312
- package/server/agentchattr-registry.js +0 -188
- package/server/install-agentchattr.patchCrashTimeout.test.js +0 -71
- package/server/queue-watcher.js +0 -171
- package/server/queue-watcher.test.js +0 -64
- package/server/routes.batchProgress.test.js +0 -94
- package/server/routes.chatWsSend.test.js +0 -161
- package/server/routes.discordBridge.test.js +0 -80
- package/server/routes.parseActiveBatch.test.js +0 -88
- package/server/routes.telegramBridge.test.js +0 -241
- package/templates/config.toml +0 -72
- package/templates/wrapper.py +0 -70
- /package/out/_next/static/{D66Um4H226QD5y4w5xTKq → MmPC1Rj12BOy4-HvMJjEX}/_buildManifest.js +0 -0
- /package/out/_next/static/{D66Um4H226QD5y4w5xTKq → MmPC1Rj12BOy4-HvMJjEX}/_clientMiddlewareManifest.js +0 -0
- /package/out/_next/static/{D66Um4H226QD5y4w5xTKq → MmPC1Rj12BOy4-HvMJjEX}/_ssgManifest.js +0 -0
|
@@ -1,64 +1,11 @@
|
|
|
1
|
-
//
|
|
2
|
-
// (bin/quadwork.js) and the web setup route (server/routes.js).
|
|
3
|
-
//
|
|
4
|
-
// Extracted as part of #185 (Phase 2D of master #181) so the web UI
|
|
5
|
-
// can clone AgentChattr per-project without duplicating the locking,
|
|
6
|
-
// idempotency, and cleanup-safety logic that #183 + #187 added.
|
|
7
|
-
//
|
|
8
|
-
// Public API:
|
|
9
|
-
// findAgentChattr(dir) → string|null
|
|
10
|
-
// installAgentChattr(dir) → string|null (.lastError on failure)
|
|
11
|
-
// chattrSpawnArgs(dir, extraArgs) → { command, spawnArgs, cwd } | null
|
|
12
|
-
// AGENTCHATTR_REPO → upstream URL constant
|
|
13
|
-
//
|
|
14
|
-
// Self-contained — depends only on Node built-ins so it's safe to require
|
|
15
|
-
// from anywhere in the project (CLI bin, server routes, future tests).
|
|
1
|
+
// Retained for `npx quadwork cleanup --legacy` — locates AC directories on disk.
|
|
16
2
|
|
|
17
|
-
const { execFileSync } = require("child_process");
|
|
18
3
|
const fs = require("fs");
|
|
19
4
|
const path = require("path");
|
|
20
5
|
|
|
21
|
-
const AGENTCHATTR_REPO = "https://github.com/bcurts/agentchattr.git";
|
|
22
|
-
|
|
23
|
-
// Stale-lock thresholds for installAgentChattr().
|
|
24
|
-
// Lock files older than this OR whose owning pid is no longer alive are
|
|
25
|
-
// treated as crashed and reclaimed. Tuned to comfortably exceed the longest
|
|
26
|
-
// step (pip install of agentchattr requirements, ~120s timeout).
|
|
27
|
-
const INSTALL_LOCK_STALE_MS = 10 * 60 * 1000; // 10 min
|
|
28
|
-
const INSTALL_LOCK_WAIT_TOTAL_MS = 30 * 1000; // wait up to 30s for a peer
|
|
29
|
-
const INSTALL_LOCK_POLL_MS = 500;
|
|
30
|
-
|
|
31
|
-
function _run(cmd, args = [], opts = {}) {
|
|
32
|
-
try { return execFileSync(cmd, args, { encoding: "utf-8", stdio: "pipe", ...opts }).trim(); }
|
|
33
|
-
catch { return null; }
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function _isPidAlive(pid) {
|
|
37
|
-
if (!pid || !Number.isFinite(pid)) return false;
|
|
38
|
-
try { process.kill(pid, 0); return true; }
|
|
39
|
-
catch (e) { return e.code === "EPERM"; }
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function _readLock(lockFile) {
|
|
43
|
-
try {
|
|
44
|
-
const raw = fs.readFileSync(lockFile, "utf-8").trim();
|
|
45
|
-
const [pidStr, tsStr] = raw.split(":");
|
|
46
|
-
return { pid: parseInt(pidStr, 10), ts: parseInt(tsStr, 10) || 0 };
|
|
47
|
-
} catch { return null; }
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function _isLockStale(lockFile) {
|
|
51
|
-
const info = _readLock(lockFile);
|
|
52
|
-
if (!info) return true;
|
|
53
|
-
if (Date.now() - info.ts > INSTALL_LOCK_STALE_MS) return true;
|
|
54
|
-
if (!_isPidAlive(info.pid)) return true;
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
6
|
/**
|
|
59
|
-
* Check if AgentChattr is
|
|
7
|
+
* Check if AgentChattr is installed (cloned + venv ready) at `dir`.
|
|
60
8
|
* Returns the directory path if both run.py and .venv/bin/python exist, or null.
|
|
61
|
-
* Caller must pass an explicit `dir` — there is no default.
|
|
62
9
|
*/
|
|
63
10
|
function findAgentChattr(dir) {
|
|
64
11
|
if (!dir) return null;
|
|
@@ -66,232 +13,4 @@ function findAgentChattr(dir) {
|
|
|
66
13
|
return null;
|
|
67
14
|
}
|
|
68
15
|
|
|
69
|
-
|
|
70
|
-
* Clone AgentChattr and set up its venv at `dir`. Idempotent — safe to
|
|
71
|
-
* re-run on the same path, and safe to call repeatedly with different
|
|
72
|
-
* paths in the same process. Designed to support per-project clones (#181).
|
|
73
|
-
*
|
|
74
|
-
* Behavior on re-run:
|
|
75
|
-
* - Fully-installed path → no-op (skips clone, skips venv create, skips pip)
|
|
76
|
-
* - Missing run.py → clones (only after refusing to overwrite
|
|
77
|
-
* unrelated content; see safety rules below)
|
|
78
|
-
* - Missing venv → creates venv and reinstalls requirements
|
|
79
|
-
*
|
|
80
|
-
* Safety rules — never accidentally clean up unrelated directories:
|
|
81
|
-
* - Empty dir → safe to remove
|
|
82
|
-
* - Git repo whose origin contains "agentchattr" → safe to remove
|
|
83
|
-
* - Anything else → refuse, return null
|
|
84
|
-
*
|
|
85
|
-
* Concurrency: a per-target lock at `${dir}.install.lock` serializes
|
|
86
|
-
* concurrent installs to the same path. Stale locks (dead pid OR older
|
|
87
|
-
* than 10 min) are reclaimed atomically via rename → unlink. Live
|
|
88
|
-
* peers are polled for up to 30s; after that, returns null with a
|
|
89
|
-
* clear lastError.
|
|
90
|
-
*
|
|
91
|
-
* On failure, returns null and stores a human-readable reason on
|
|
92
|
-
* `installAgentChattr.lastError` so callers can surface it without
|
|
93
|
-
* changing the return shape.
|
|
94
|
-
*/
|
|
95
|
-
function installAgentChattr(dir) {
|
|
96
|
-
if (!dir) {
|
|
97
|
-
installAgentChattr.lastError = "installAgentChattr: dir is required";
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
installAgentChattr.lastError = null;
|
|
101
|
-
const setError = (msg) => { installAgentChattr.lastError = msg; return null; };
|
|
102
|
-
|
|
103
|
-
// --- Per-target lock ---
|
|
104
|
-
const lockFile = `${dir}.install.lock`;
|
|
105
|
-
try { fs.mkdirSync(path.dirname(lockFile), { recursive: true, mode: 0o700 }); }
|
|
106
|
-
catch (e) { return setError(`Cannot create parent of ${dir}: ${e.message}`); }
|
|
107
|
-
|
|
108
|
-
let acquired = false;
|
|
109
|
-
const deadline = Date.now() + INSTALL_LOCK_WAIT_TOTAL_MS;
|
|
110
|
-
while (!acquired) {
|
|
111
|
-
try {
|
|
112
|
-
fs.writeFileSync(lockFile, `${process.pid}:${Date.now()}`, { mode: 0o600, flag: "wx" });
|
|
113
|
-
try { fs.chmodSync(lockFile, 0o600); } catch {}
|
|
114
|
-
acquired = true;
|
|
115
|
-
} catch (e) {
|
|
116
|
-
if (e.code !== "EEXIST") return setError(`Cannot create install lock ${lockFile}: ${e.message}`);
|
|
117
|
-
if (_isLockStale(lockFile)) {
|
|
118
|
-
const sideline = `${lockFile}.stale.${process.pid}.${Date.now()}`;
|
|
119
|
-
try {
|
|
120
|
-
fs.renameSync(lockFile, sideline);
|
|
121
|
-
try { fs.unlinkSync(sideline); } catch {}
|
|
122
|
-
} catch (renameErr) {
|
|
123
|
-
if (renameErr.code !== "ENOENT") {
|
|
124
|
-
return setError(`Cannot reclaim stale lock ${lockFile}: ${renameErr.message}`);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
continue;
|
|
128
|
-
}
|
|
129
|
-
if (Date.now() >= deadline) {
|
|
130
|
-
const info = _readLock(lockFile) || { pid: "?", ts: 0 };
|
|
131
|
-
return setError(`Another install is in progress at ${dir} (pid ${info.pid}); timed out after ${INSTALL_LOCK_WAIT_TOTAL_MS}ms. Re-run after it finishes, or remove ${lockFile} if stale.`);
|
|
132
|
-
}
|
|
133
|
-
try { execFileSync("sleep", [String(INSTALL_LOCK_POLL_MS / 1000)], { stdio: "pipe" }); }
|
|
134
|
-
catch { /* sleep interrupted; loop will recheck */ }
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
try {
|
|
139
|
-
return _installAgentChattrLocked(dir, setError);
|
|
140
|
-
} finally {
|
|
141
|
-
try { fs.unlinkSync(lockFile); } catch {}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
installAgentChattr.lastError = null;
|
|
145
|
-
|
|
146
|
-
function _installAgentChattrLocked(dir, setError) {
|
|
147
|
-
const runPy = path.join(dir, "run.py");
|
|
148
|
-
const venvPython = path.join(dir, ".venv", "bin", "python");
|
|
149
|
-
let venvJustCreated = false;
|
|
150
|
-
|
|
151
|
-
// 1. Clone if run.py is missing.
|
|
152
|
-
if (!fs.existsSync(runPy)) {
|
|
153
|
-
if (fs.existsSync(dir)) {
|
|
154
|
-
let entries;
|
|
155
|
-
try { entries = fs.readdirSync(dir); }
|
|
156
|
-
catch (e) { return setError(`Cannot read ${dir}: ${e.message}`); }
|
|
157
|
-
const isEmpty = entries.length === 0;
|
|
158
|
-
if (isEmpty) {
|
|
159
|
-
try { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
160
|
-
catch (e) { return setError(`Cannot remove empty dir ${dir}: ${e.message}`); }
|
|
161
|
-
} else if (fs.existsSync(path.join(dir, ".git"))) {
|
|
162
|
-
const remote = _run("git", ["-C", dir, "remote", "get-url", "origin"]);
|
|
163
|
-
if (remote && remote.includes("agentchattr")) {
|
|
164
|
-
try { fs.rmSync(dir, { recursive: true, force: true }); }
|
|
165
|
-
catch (e) { return setError(`Cannot remove failed clone at ${dir}: ${e.message}`); }
|
|
166
|
-
} else {
|
|
167
|
-
return setError(`Refusing to overwrite ${dir}: contains a non-AgentChattr git repo`);
|
|
168
|
-
}
|
|
169
|
-
} else {
|
|
170
|
-
return setError(`Refusing to overwrite ${dir}: directory exists with unrelated content`);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
try { fs.mkdirSync(path.dirname(dir), { recursive: true, mode: 0o700 }); }
|
|
174
|
-
catch (e) { return setError(`Cannot create parent of ${dir}: ${e.message}`); }
|
|
175
|
-
const cloneResult = _run("git", ["clone", AGENTCHATTR_REPO, dir], { timeout: 60000 });
|
|
176
|
-
if (cloneResult === null) return setError(`git clone of ${AGENTCHATTR_REPO} into ${dir} failed`);
|
|
177
|
-
if (!fs.existsSync(runPy)) return setError(`Clone completed but run.py missing at ${dir}`);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// 2. Create venv if missing.
|
|
181
|
-
if (!fs.existsSync(venvPython)) {
|
|
182
|
-
const venvResult = _run("python3", ["-m", "venv", path.join(dir, ".venv")], { timeout: 60000 });
|
|
183
|
-
if (venvResult === null) return setError(`python3 -m venv failed at ${dir}/.venv (is python3 installed?)`);
|
|
184
|
-
if (!fs.existsSync(venvPython)) return setError(`venv created but ${venvPython} missing`);
|
|
185
|
-
venvJustCreated = true;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// 3. Install requirements only when the venv was just (re)created.
|
|
189
|
-
if (venvJustCreated) {
|
|
190
|
-
const reqFile = path.join(dir, "requirements.txt");
|
|
191
|
-
if (fs.existsSync(reqFile)) {
|
|
192
|
-
const pipResult = _run(venvPython, ["-m", "pip", "install", "-r", reqFile], { timeout: 120000 });
|
|
193
|
-
if (pipResult === null) return setError(`pip install -r ${reqFile} failed`);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
// #388: patch sender-column overflow CSS after clone/install
|
|
197
|
-
patchAgentchattrCss(dir);
|
|
198
|
-
// #629: patch crash timeout before AC's first import
|
|
199
|
-
patchCrashTimeout(dir);
|
|
200
|
-
return dir;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Get spawn args for launching AgentChattr from its cloned directory.
|
|
205
|
-
* Returns { command, spawnArgs, cwd } or null if not fully installed.
|
|
206
|
-
* Requires .venv/bin/python — never falls back to bare python3.
|
|
207
|
-
*/
|
|
208
|
-
function chattrSpawnArgs(dir, extraArgs) {
|
|
209
|
-
if (!dir) return null;
|
|
210
|
-
const venvPython = path.join(dir, ".venv", "bin", "python");
|
|
211
|
-
if (!fs.existsSync(path.join(dir, "run.py")) || !fs.existsSync(venvPython)) return null;
|
|
212
|
-
return { command: venvPython, spawnArgs: ["run.py", ...(extraArgs || [])], cwd: dir };
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* #388: Patch AgentChattr's static files for sender-column overflow.
|
|
217
|
-
* Idempotent — skips if the marker is already present.
|
|
218
|
-
* Called after install and after update (git pull overwrites static/).
|
|
219
|
-
*
|
|
220
|
-
* CSS: cap .msg-sender width with ellipsis truncation.
|
|
221
|
-
* JS: add title attribute to .msg-sender spans for hover tooltip.
|
|
222
|
-
*/
|
|
223
|
-
function patchAgentchattrCss(dir) {
|
|
224
|
-
if (!dir) return;
|
|
225
|
-
// --- CSS patch ---
|
|
226
|
-
const cssPath = path.join(dir, "static", "style.css");
|
|
227
|
-
if (fs.existsSync(cssPath)) {
|
|
228
|
-
try {
|
|
229
|
-
const content = fs.readFileSync(cssPath, "utf-8");
|
|
230
|
-
if (!content.includes("/* quadwork#388 sender-overflow fix */")) {
|
|
231
|
-
const patch = `
|
|
232
|
-
/* quadwork#388 sender-overflow fix */
|
|
233
|
-
.msg-sender {
|
|
234
|
-
max-width: 80px;
|
|
235
|
-
overflow: hidden;
|
|
236
|
-
text-overflow: ellipsis;
|
|
237
|
-
white-space: nowrap;
|
|
238
|
-
display: inline-block;
|
|
239
|
-
vertical-align: middle;
|
|
240
|
-
}
|
|
241
|
-
`;
|
|
242
|
-
fs.writeFileSync(cssPath, content + patch);
|
|
243
|
-
}
|
|
244
|
-
} catch {}
|
|
245
|
-
}
|
|
246
|
-
// --- JS patch: add title attribute to .msg-sender for hover tooltip ---
|
|
247
|
-
const jsPath = path.join(dir, "static", "chat.js");
|
|
248
|
-
if (fs.existsSync(jsPath)) {
|
|
249
|
-
try {
|
|
250
|
-
const content = fs.readFileSync(jsPath, "utf-8");
|
|
251
|
-
if (!content.includes("quadwork#388")) {
|
|
252
|
-
// Add title= to the msg-sender span so truncated names show full on hover
|
|
253
|
-
const patched = content.replace(
|
|
254
|
-
/(<span class="msg-sender" style="color: \$\{senderColor\}">)/g,
|
|
255
|
-
`<span class="msg-sender" title="\${escapeHtml(msg.sender)}" style="color: \${senderColor}">`,
|
|
256
|
-
);
|
|
257
|
-
if (patched !== content) {
|
|
258
|
-
fs.writeFileSync(jsPath, patched + "\n// quadwork#388\n");
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
} catch {}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* #629: Patch AC's crash timeout from 15s to 120s.
|
|
267
|
-
* Must run at clone time (before any `python run.py`) so the first
|
|
268
|
-
* AC process imports the patched value. Idempotent.
|
|
269
|
-
*/
|
|
270
|
-
function patchCrashTimeout(dir) {
|
|
271
|
-
if (!dir) return;
|
|
272
|
-
const appPath = path.join(dir, "app.py");
|
|
273
|
-
if (!fs.existsSync(appPath)) return;
|
|
274
|
-
try {
|
|
275
|
-
let app = fs.readFileSync(appPath, "utf-8");
|
|
276
|
-
if (app.includes("_CRASH_TIMEOUT = 15")) {
|
|
277
|
-
app = app.replace("_CRASH_TIMEOUT = 15", "_CRASH_TIMEOUT = 120");
|
|
278
|
-
app = app.replace(
|
|
279
|
-
"# Crash timeout: if a wrapper hasn't heartbeated for 60s,\n",
|
|
280
|
-
"# Crash timeout: if a wrapper hasn't heartbeated for 120s,\n",
|
|
281
|
-
);
|
|
282
|
-
fs.writeFileSync(appPath, app);
|
|
283
|
-
console.log(`[idle-fix] patched crash timeout to 120s at clone time (#629): ${dir}`);
|
|
284
|
-
}
|
|
285
|
-
} catch (err) {
|
|
286
|
-
console.warn(`[idle-fix] failed to patch crash timeout in ${appPath}: ${err.message}`);
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
module.exports = {
|
|
291
|
-
AGENTCHATTR_REPO,
|
|
292
|
-
findAgentChattr,
|
|
293
|
-
installAgentChattr,
|
|
294
|
-
chattrSpawnArgs,
|
|
295
|
-
patchAgentchattrCss,
|
|
296
|
-
patchCrashTimeout,
|
|
297
|
-
};
|
|
16
|
+
module.exports = { findAgentChattr };
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const http = require("http");
|
|
5
|
+
const readline = require("readline");
|
|
6
|
+
|
|
7
|
+
const args = process.argv.slice(2);
|
|
8
|
+
function flag(name) {
|
|
9
|
+
const i = args.indexOf(`--${name}`);
|
|
10
|
+
return i >= 0 && i + 1 < args.length ? args[i + 1] : null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const PROJECT = flag("project");
|
|
14
|
+
const AGENT = flag("agent");
|
|
15
|
+
const PORT = flag("port");
|
|
16
|
+
const TOKEN = flag("token");
|
|
17
|
+
|
|
18
|
+
if (!PROJECT || !AGENT || !PORT) {
|
|
19
|
+
process.stderr.write("Usage: node mcp-chat-shim.js --project <id> --agent <id> --port <port> [--token <token>]\n");
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const BASE = `http://127.0.0.1:${PORT}`;
|
|
24
|
+
|
|
25
|
+
const TOOLS = [
|
|
26
|
+
{
|
|
27
|
+
name: "chat_send",
|
|
28
|
+
description: "Send a message to the project chat",
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: "object",
|
|
31
|
+
properties: {
|
|
32
|
+
channel: { type: "string", default: "general" },
|
|
33
|
+
message: { type: "string" },
|
|
34
|
+
},
|
|
35
|
+
required: ["message"],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "chat_read",
|
|
40
|
+
description: "Read recent messages from project chat",
|
|
41
|
+
inputSchema: {
|
|
42
|
+
type: "object",
|
|
43
|
+
properties: {
|
|
44
|
+
channel: { type: "string", default: "general" },
|
|
45
|
+
limit: { type: "number", default: 50 },
|
|
46
|
+
since_id: { type: "number" },
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
function jsonRpc(id, result) {
|
|
53
|
+
return JSON.stringify({ jsonrpc: "2.0", id, result });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function jsonRpcError(id, code, message) {
|
|
57
|
+
return JSON.stringify({ jsonrpc: "2.0", id, error: { code, message } });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function httpRequest(method, urlPath, body, extraHeaders) {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const url = new URL(urlPath, BASE);
|
|
63
|
+
const opts = {
|
|
64
|
+
hostname: url.hostname,
|
|
65
|
+
port: url.port,
|
|
66
|
+
path: url.pathname + url.search,
|
|
67
|
+
method,
|
|
68
|
+
headers: { "Content-Type": "application/json", ...extraHeaders },
|
|
69
|
+
};
|
|
70
|
+
const req = http.request(opts, (res) => {
|
|
71
|
+
let data = "";
|
|
72
|
+
res.on("data", (c) => (data += c));
|
|
73
|
+
res.on("end", () => {
|
|
74
|
+
try {
|
|
75
|
+
resolve({ status: res.statusCode, body: JSON.parse(data) });
|
|
76
|
+
} catch {
|
|
77
|
+
resolve({ status: res.statusCode, body: data });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
req.on("error", reject);
|
|
82
|
+
if (body) req.write(JSON.stringify(body));
|
|
83
|
+
req.end();
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function handleToolCall(id, name, params) {
|
|
88
|
+
try {
|
|
89
|
+
if (name === "chat_send") {
|
|
90
|
+
const res = await httpRequest("POST", `/api/chat?project=${encodeURIComponent(PROJECT)}`, {
|
|
91
|
+
text: params.message || "",
|
|
92
|
+
channel: params.channel || "general",
|
|
93
|
+
}, { "X-Chat-Sender": AGENT, ...(TOKEN ? { "X-Chat-Token": TOKEN } : {}) });
|
|
94
|
+
if (res.status >= 400) {
|
|
95
|
+
return jsonRpcError(id, -32000, `API error ${res.status}: ${JSON.stringify(res.body)}`);
|
|
96
|
+
}
|
|
97
|
+
return jsonRpc(id, { content: [{ type: "text", text: JSON.stringify(res.body) }] });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (name === "chat_read") {
|
|
101
|
+
const qs = new URLSearchParams({ project: PROJECT });
|
|
102
|
+
if (params.channel) qs.set("channel", params.channel);
|
|
103
|
+
if (params.limit) qs.set("limit", String(params.limit));
|
|
104
|
+
if (params.since_id) qs.set("since_id", String(params.since_id));
|
|
105
|
+
const res = await httpRequest("GET", `/api/chat?${qs.toString()}`);
|
|
106
|
+
if (res.status >= 400) {
|
|
107
|
+
return jsonRpcError(id, -32000, `API error ${res.status}: ${JSON.stringify(res.body)}`);
|
|
108
|
+
}
|
|
109
|
+
return jsonRpc(id, { content: [{ type: "text", text: JSON.stringify(res.body) }] });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return jsonRpcError(id, -32601, `Unknown tool: ${name}`);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
return jsonRpcError(id, -32000, err.message);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// --- MCP stdio protocol ---
|
|
119
|
+
|
|
120
|
+
async function handleMessage(msg) {
|
|
121
|
+
const { id, method, params } = msg;
|
|
122
|
+
|
|
123
|
+
if (method === "initialize") {
|
|
124
|
+
return jsonRpc(id, {
|
|
125
|
+
protocolVersion: "2024-11-05",
|
|
126
|
+
capabilities: { tools: {} },
|
|
127
|
+
serverInfo: { name: "quadwork-chat", version: "1.0.0" },
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (method === "initialized") {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (method === "tools/list") {
|
|
136
|
+
return jsonRpc(id, { tools: TOOLS });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (method === "tools/call") {
|
|
140
|
+
return handleToolCall(id, params?.name, params?.arguments || {});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (method === "ping") {
|
|
144
|
+
return jsonRpc(id, {});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (id != null) {
|
|
148
|
+
return jsonRpcError(id, -32601, `Method not found: ${method}`);
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const rl = readline.createInterface({ input: process.stdin, terminal: false });
|
|
154
|
+
|
|
155
|
+
rl.on("line", async (line) => {
|
|
156
|
+
let msg;
|
|
157
|
+
try {
|
|
158
|
+
msg = JSON.parse(line);
|
|
159
|
+
} catch {
|
|
160
|
+
process.stdout.write(jsonRpcError(null, -32700, "Parse error") + "\n");
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const response = await handleMessage(msg);
|
|
164
|
+
if (response) {
|
|
165
|
+
process.stdout.write(response + "\n");
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
rl.on("close", () => {
|
|
170
|
+
process.exit(0);
|
|
171
|
+
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const os = require("os");
|
|
4
|
+
const { ensureSecureDir, writeSecureFile } = require("./config");
|
|
5
|
+
const { parseMentions } = require("./file-chat");
|
|
6
|
+
|
|
7
|
+
const KNOWN_FIELDS = new Set([
|
|
8
|
+
"id", "timestamp", "ts", "sender", "channel", "type", "text", "message", "mentions",
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
function convertAcRecord(record, nextId) {
|
|
12
|
+
let ts;
|
|
13
|
+
if (record.timestamp) {
|
|
14
|
+
const raw = Number(record.timestamp);
|
|
15
|
+
ts = !isNaN(raw) && raw > 0
|
|
16
|
+
? new Date(raw * 1000).toISOString()
|
|
17
|
+
: String(record.timestamp);
|
|
18
|
+
} else if (record.ts) {
|
|
19
|
+
const raw = Number(record.ts);
|
|
20
|
+
ts = !isNaN(raw) && raw > 0
|
|
21
|
+
? new Date(raw * 1000).toISOString()
|
|
22
|
+
: String(record.ts);
|
|
23
|
+
} else {
|
|
24
|
+
ts = new Date().toISOString();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const text = record.text || record.message || "";
|
|
28
|
+
const known = {
|
|
29
|
+
id: nextId,
|
|
30
|
+
seq: nextId,
|
|
31
|
+
ts,
|
|
32
|
+
sender: record.sender || "unknown",
|
|
33
|
+
channel: record.channel || "general",
|
|
34
|
+
type: record.type === "system" ? "system" : "message",
|
|
35
|
+
text,
|
|
36
|
+
mentions: parseMentions(text),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const legacy = {};
|
|
40
|
+
for (const [key, val] of Object.entries(record)) {
|
|
41
|
+
if (!KNOWN_FIELDS.has(key)) {
|
|
42
|
+
legacy[key] = val;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (record.type && record.type !== known.type) {
|
|
46
|
+
legacy.type = record.type;
|
|
47
|
+
}
|
|
48
|
+
if (Object.keys(legacy).length > 0) known._legacy = legacy;
|
|
49
|
+
return known;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function migrateProject(projectId) {
|
|
53
|
+
const chatDir = path.join(os.homedir(), ".quadwork", projectId, "chat");
|
|
54
|
+
const migratedPath = path.join(chatDir, ".migrated");
|
|
55
|
+
const targetPath = path.join(chatDir, "general.jsonl");
|
|
56
|
+
|
|
57
|
+
if (fs.existsSync(migratedPath)) return null;
|
|
58
|
+
|
|
59
|
+
if (fs.existsSync(targetPath)) {
|
|
60
|
+
console.log(`[migration] ${projectId}: general.jsonl already exists without .migrated — skipping (Phase 1 test project)`);
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const acLogPath = path.join(
|
|
65
|
+
os.homedir(), ".quadwork", projectId, "agentchattr", "data", "agentchattr_log.jsonl"
|
|
66
|
+
);
|
|
67
|
+
if (!fs.existsSync(acLogPath)) return null;
|
|
68
|
+
|
|
69
|
+
const content = fs.readFileSync(acLogPath, "utf-8");
|
|
70
|
+
const lines = content.split("\n");
|
|
71
|
+
const converted = [];
|
|
72
|
+
let nextId = 0;
|
|
73
|
+
let skipped = 0;
|
|
74
|
+
|
|
75
|
+
for (const line of lines) {
|
|
76
|
+
if (!line.trim()) continue;
|
|
77
|
+
try {
|
|
78
|
+
const record = JSON.parse(line);
|
|
79
|
+
converted.push(convertAcRecord(record, nextId));
|
|
80
|
+
nextId++;
|
|
81
|
+
} catch {
|
|
82
|
+
skipped++;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (converted.length === 0 && skipped === 0) return null;
|
|
87
|
+
|
|
88
|
+
if (converted.length === 0 && skipped > 0) {
|
|
89
|
+
console.log(`[migration] ${projectId}: AC log has ${skipped} lines but none are valid JSON — skipping`);
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
ensureSecureDir(chatDir);
|
|
94
|
+
|
|
95
|
+
const tmpPath = targetPath + ".tmp";
|
|
96
|
+
const output = converted.map((r) => JSON.stringify(r)).join("\n") + "\n";
|
|
97
|
+
writeSecureFile(tmpPath, output);
|
|
98
|
+
|
|
99
|
+
const verifyContent = fs.readFileSync(tmpPath, "utf-8");
|
|
100
|
+
const verifyLines = verifyContent.trim().split("\n");
|
|
101
|
+
let verified = 0;
|
|
102
|
+
for (const vl of verifyLines) {
|
|
103
|
+
try {
|
|
104
|
+
JSON.parse(vl);
|
|
105
|
+
verified++;
|
|
106
|
+
} catch {
|
|
107
|
+
fs.unlinkSync(tmpPath);
|
|
108
|
+
throw new Error(`Migration validation failed for ${projectId}: corrupt line in tmp file`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (verified !== converted.length) {
|
|
112
|
+
fs.unlinkSync(tmpPath);
|
|
113
|
+
throw new Error(`Migration validation failed for ${projectId}: expected ${converted.length} records, got ${verified}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
fs.renameSync(tmpPath, targetPath);
|
|
117
|
+
try { fs.chmodSync(targetPath, 0o600); } catch {}
|
|
118
|
+
|
|
119
|
+
const systemMsg = {
|
|
120
|
+
id: nextId,
|
|
121
|
+
seq: nextId,
|
|
122
|
+
ts: new Date().toISOString(),
|
|
123
|
+
sender: "system",
|
|
124
|
+
channel: "general",
|
|
125
|
+
type: "system",
|
|
126
|
+
text: `Chat history migrated from AC (${converted.length} messages)`,
|
|
127
|
+
mentions: [],
|
|
128
|
+
};
|
|
129
|
+
fs.appendFileSync(targetPath, JSON.stringify(systemMsg) + "\n", { mode: 0o600 });
|
|
130
|
+
|
|
131
|
+
const manifest = {
|
|
132
|
+
migrated_at: new Date().toISOString(),
|
|
133
|
+
source: acLogPath,
|
|
134
|
+
messages: converted.length,
|
|
135
|
+
skipped,
|
|
136
|
+
};
|
|
137
|
+
writeSecureFile(migratedPath, JSON.stringify(manifest, null, 2) + "\n");
|
|
138
|
+
|
|
139
|
+
console.log(`[migration] ${projectId}: migrated ${converted.length} messages, ${skipped} skipped (invalid JSON)`);
|
|
140
|
+
return { messages: converted.length, skipped };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function runAcMigration(config) {
|
|
144
|
+
const failed = [];
|
|
145
|
+
const projects = config.projects || [];
|
|
146
|
+
for (const project of projects) {
|
|
147
|
+
if (!project || !project.id) continue;
|
|
148
|
+
try {
|
|
149
|
+
migrateProject(project.id);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
console.error(`[migration] ${project.id}: failed — ${err.message}`);
|
|
152
|
+
failed.push(project.id);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return failed;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
module.exports = { runAcMigration, migrateProject, convertAcRecord };
|