quadwork 1.2.1 → 1.2.3
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/out/404.html +1 -1
- package/out/__next.__PAGE__.txt +3 -3
- package/out/__next._full.txt +12 -12
- package/out/__next._head.txt +4 -4
- package/out/__next._index.txt +6 -6
- package/out/__next._tree.txt +2 -2
- package/out/_next/static/chunks/0-yus965h3bk_.js +24 -0
- package/out/_next/static/chunks/0d.f~y5jeh785.css +2 -0
- package/out/_next/static/chunks/{064engxz5n7u9.js → 0e~ue9ca5zrep.js} +1 -1
- package/out/_next/static/chunks/0md7hgvwnovzq.js +1 -0
- package/out/_next/static/chunks/0whtwwbpg72ar.js +1 -0
- package/out/_next/static/chunks/{0o97ax9om2kj1.js → 16ell.n1p8o7d.js} +1 -1
- package/out/_not-found/__next._full.txt +11 -11
- package/out/_not-found/__next._head.txt +4 -4
- package/out/_not-found/__next._index.txt +6 -6
- 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 +11 -11
- package/out/app-shell/__next._full.txt +11 -11
- package/out/app-shell/__next._head.txt +4 -4
- package/out/app-shell/__next._index.txt +6 -6
- 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 +11 -11
- package/out/index.html +1 -1
- package/out/index.txt +12 -12
- package/out/project/_/__next._full.txt +12 -12
- package/out/project/_/__next._head.txt +4 -4
- package/out/project/_/__next._index.txt +6 -6
- 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/_/memory/__next._full.txt +12 -12
- package/out/project/_/memory/__next._head.txt +4 -4
- package/out/project/_/memory/__next._index.txt +6 -6
- package/out/project/_/memory/__next._tree.txt +2 -2
- package/out/project/_/memory/__next.project.$d$id.memory.__PAGE__.txt +3 -3
- package/out/project/_/memory/__next.project.$d$id.memory.txt +3 -3
- package/out/project/_/memory/__next.project.$d$id.txt +3 -3
- package/out/project/_/memory/__next.project.txt +3 -3
- package/out/project/_/memory.html +1 -1
- package/out/project/_/memory.txt +12 -12
- package/out/project/_/queue/__next._full.txt +12 -12
- package/out/project/_/queue/__next._head.txt +4 -4
- package/out/project/_/queue/__next._index.txt +6 -6
- 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 +12 -12
- package/out/project/_.html +1 -1
- package/out/project/_.txt +12 -12
- package/out/settings/__next._full.txt +12 -12
- package/out/settings/__next._head.txt +4 -4
- package/out/settings/__next._index.txt +6 -6
- 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 +12 -12
- package/out/setup/__next._full.txt +12 -12
- package/out/setup/__next._head.txt +4 -4
- package/out/setup/__next._index.txt +6 -6
- 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 +12 -12
- package/package.json +1 -1
- package/server/agentchattr-registry.js +105 -0
- package/server/index.js +125 -21
- package/server/routes.js +99 -10
- package/out/_next/static/chunks/03v5eoc-wic6o.js +0 -1
- package/out/_next/static/chunks/0738cfu-x.0ul.js +0 -24
- package/out/_next/static/chunks/0r-00ph4jahrl.css +0 -2
- /package/out/_next/static/{R3KHD-zZk76pfWNOR4boQ → 6W2vNw7Pp8z2_l_OJ2hqC}/_buildManifest.js +0 -0
- /package/out/_next/static/{R3KHD-zZk76pfWNOR4boQ → 6W2vNw7Pp8z2_l_OJ2hqC}/_clientMiddlewareManifest.js +0 -0
- /package/out/_next/static/{R3KHD-zZk76pfWNOR4boQ → 6W2vNw7Pp8z2_l_OJ2hqC}/_ssgManifest.js +0 -0
package/server/index.js
CHANGED
|
@@ -8,6 +8,7 @@ const pty = require("node-pty");
|
|
|
8
8
|
const { spawn } = require("child_process");
|
|
9
9
|
const { readConfig, resolveAgentCwd, resolveAgentCommand, resolveProjectChattr, resolveChattrSpawn, syncChattrToken, CONFIG_PATH } = require("./config");
|
|
10
10
|
const routes = require("./routes");
|
|
11
|
+
const { waitForAgentChattrReady, registerAgent, deregisterAgent } = require("./agentchattr-registry");
|
|
11
12
|
|
|
12
13
|
const net = require("net");
|
|
13
14
|
const config = readConfig();
|
|
@@ -207,6 +208,39 @@ const PERMISSION_FLAGS = {
|
|
|
207
208
|
* Generate a per-agent MCP config file for Claude (--mcp-config).
|
|
208
209
|
* Returns the absolute path to the written JSON file.
|
|
209
210
|
*/
|
|
211
|
+
/**
|
|
212
|
+
* Per-agent registration tokens persisted across QuadWork restarts so
|
|
213
|
+
* #242 stale-slot reclaim works after a crash. Without this the
|
|
214
|
+
* in-memory _tokenCache is empty on startup and the family-name
|
|
215
|
+
* deregister returns 403 (app.py:2123-2135).
|
|
216
|
+
*/
|
|
217
|
+
function _agentTokenPath(projectId, agentId) {
|
|
218
|
+
const configDir = path.join(os.homedir(), ".quadwork", projectId);
|
|
219
|
+
return path.join(configDir, `agent-token-${agentId}.txt`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function readPersistedAgentToken(projectId, agentId) {
|
|
223
|
+
try {
|
|
224
|
+
return fs.readFileSync(_agentTokenPath(projectId, agentId), "utf8").trim() || null;
|
|
225
|
+
} catch {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function writePersistedAgentToken(projectId, agentId, token) {
|
|
231
|
+
try {
|
|
232
|
+
const configDir = path.join(os.homedir(), ".quadwork", projectId);
|
|
233
|
+
if (!fs.existsSync(configDir)) fs.mkdirSync(configDir, { recursive: true });
|
|
234
|
+
fs.writeFileSync(_agentTokenPath(projectId, agentId), token, { mode: 0o600 });
|
|
235
|
+
} catch {
|
|
236
|
+
// non-fatal — stale-slot reclaim will degrade but registration still works
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function clearPersistedAgentToken(projectId, agentId) {
|
|
241
|
+
try { fs.unlinkSync(_agentTokenPath(projectId, agentId)); } catch {}
|
|
242
|
+
}
|
|
243
|
+
|
|
210
244
|
function writeMcpConfigFile(projectId, agentId, mcpHttpPort, token) {
|
|
211
245
|
const os = require("os");
|
|
212
246
|
const configDir = path.join(os.homedir(), ".quadwork", projectId);
|
|
@@ -233,12 +267,14 @@ function writeMcpConfigFile(projectId, agentId, mcpHttpPort, token) {
|
|
|
233
267
|
async function buildAgentArgs(projectId, agentId) {
|
|
234
268
|
const cfg = readConfig();
|
|
235
269
|
const project = cfg.projects?.find((p) => p.id === projectId);
|
|
236
|
-
if (!project) return [];
|
|
270
|
+
if (!project) return { args: [], acRegistrationName: null, acServerPort: null };
|
|
237
271
|
|
|
238
272
|
const agentCfg = project.agents?.[agentId] || {};
|
|
239
273
|
const command = agentCfg.command || "claude";
|
|
240
274
|
const cliBase = command.split("/").pop().split(" ")[0]; // extract base CLI name
|
|
241
275
|
const args = [];
|
|
276
|
+
let acRegistrationName = null;
|
|
277
|
+
let acServerPort = null;
|
|
242
278
|
|
|
243
279
|
// Permission bypass flags
|
|
244
280
|
if (agentCfg.auto_approve !== false) {
|
|
@@ -252,21 +288,63 @@ async function buildAgentArgs(projectId, agentId) {
|
|
|
252
288
|
if (mcpHttpPort) {
|
|
253
289
|
const injectMode = agentCfg.mcp_inject || (cliBase === "codex" ? "proxy_flag" : cliBase === "gemini" ? "env" : "flag");
|
|
254
290
|
if (injectMode === "flag") {
|
|
255
|
-
// Claude/Kimi:
|
|
256
|
-
|
|
291
|
+
// Claude/Kimi: register with AgentChattr to obtain a per-agent
|
|
292
|
+
// token (#239 — session_token is browser auth, not MCP auth) and
|
|
293
|
+
// write that into the per-agent MCP config file.
|
|
294
|
+
const chattrInfo = resolveProjectChattr(projectId);
|
|
295
|
+
acServerPort = Number(new URL(chattrInfo.url).port) || 8300;
|
|
296
|
+
await waitForAgentChattrReady(acServerPort);
|
|
297
|
+
// #242: best-effort deregister any stale registration of the
|
|
298
|
+
// canonical name (left over by a crashed previous QuadWork
|
|
299
|
+
// session) so the fresh register lands at slot 1 instead of
|
|
300
|
+
// head-2 / reviewer2-2. We need the previous agent's bearer
|
|
301
|
+
// token because app.py:2123 requires authenticated agent
|
|
302
|
+
// session for family names — load it from disk (persisted
|
|
303
|
+
// across restarts). Failures are non-fatal.
|
|
304
|
+
const stalePersistedToken = readPersistedAgentToken(projectId, agentId);
|
|
305
|
+
if (stalePersistedToken) {
|
|
306
|
+
await deregisterAgent(acServerPort, agentId, stalePersistedToken).catch(() => {});
|
|
307
|
+
clearPersistedAgentToken(projectId, agentId);
|
|
308
|
+
}
|
|
309
|
+
const registration = await registerAgent(acServerPort, agentId, agentCfg.display_name || null);
|
|
310
|
+
if (!registration) {
|
|
311
|
+
throw new Error(`Failed to register ${agentId}: ${registerAgent.lastError}`);
|
|
312
|
+
}
|
|
313
|
+
acRegistrationName = registration.name;
|
|
314
|
+
writePersistedAgentToken(projectId, agentId, registration.token);
|
|
315
|
+
const mcpConfigPath = writeMcpConfigFile(projectId, agentId, mcpHttpPort, registration.token);
|
|
257
316
|
const flag = agentCfg.mcp_flag || "--mcp-config";
|
|
258
317
|
args.push(flag, mcpConfigPath);
|
|
259
318
|
} else if (injectMode === "proxy_flag") {
|
|
260
|
-
// Codex:
|
|
319
|
+
// Codex: register with AgentChattr first (#240) so the proxy
|
|
320
|
+
// injects a real per-agent token, not the global session token.
|
|
321
|
+
// Resolve via resolveProjectChattr so legacy/global-config
|
|
322
|
+
// projects without a per-project agentchattr_url still work.
|
|
323
|
+
const chattrInfo = resolveProjectChattr(projectId);
|
|
324
|
+
acServerPort = Number(new URL(chattrInfo.url).port) || 8300;
|
|
325
|
+
await waitForAgentChattrReady(acServerPort);
|
|
326
|
+
// #242: best-effort deregister stale canonical name first using
|
|
327
|
+
// the persisted bearer token from a previous session.
|
|
328
|
+
const stalePersistedToken = readPersistedAgentToken(projectId, agentId);
|
|
329
|
+
if (stalePersistedToken) {
|
|
330
|
+
await deregisterAgent(acServerPort, agentId, stalePersistedToken).catch(() => {});
|
|
331
|
+
clearPersistedAgentToken(projectId, agentId);
|
|
332
|
+
}
|
|
333
|
+
const registration = await registerAgent(acServerPort, agentId, agentCfg.display_name || null);
|
|
334
|
+
if (!registration) {
|
|
335
|
+
throw new Error(`Failed to register ${agentId}: ${registerAgent.lastError}`);
|
|
336
|
+
}
|
|
337
|
+
acRegistrationName = registration.name;
|
|
338
|
+
writePersistedAgentToken(projectId, agentId, registration.token);
|
|
261
339
|
const upstreamUrl = `http://127.0.0.1:${mcpHttpPort}`;
|
|
262
|
-
const proxyUrl = await startMcpProxy(projectId, agentId, upstreamUrl, token);
|
|
340
|
+
const proxyUrl = await startMcpProxy(projectId, agentId, upstreamUrl, registration.token);
|
|
263
341
|
if (proxyUrl) {
|
|
264
342
|
args.push("-c", `mcp_servers.agentchattr.url="${proxyUrl}"`);
|
|
265
343
|
}
|
|
266
344
|
}
|
|
267
345
|
}
|
|
268
346
|
|
|
269
|
-
return args;
|
|
347
|
+
return { args, acRegistrationName, acServerPort };
|
|
270
348
|
}
|
|
271
349
|
|
|
272
350
|
/**
|
|
@@ -313,7 +391,8 @@ async function spawnAgentPty(project, agent) {
|
|
|
313
391
|
if (!cwd) return { ok: false, error: `Unknown agent: ${key}` };
|
|
314
392
|
|
|
315
393
|
const command = resolveAgentCommand(project, agent) || (process.env.SHELL || "/bin/zsh");
|
|
316
|
-
const
|
|
394
|
+
const built = await buildAgentArgs(project, agent);
|
|
395
|
+
const args = built.args;
|
|
317
396
|
const extraEnv = buildAgentEnv(project, agent);
|
|
318
397
|
|
|
319
398
|
try {
|
|
@@ -325,7 +404,16 @@ async function spawnAgentPty(project, agent) {
|
|
|
325
404
|
env: { ...process.env, ...extraEnv },
|
|
326
405
|
});
|
|
327
406
|
|
|
328
|
-
const session = {
|
|
407
|
+
const session = {
|
|
408
|
+
projectId: project,
|
|
409
|
+
agentId: agent,
|
|
410
|
+
term,
|
|
411
|
+
ws: null,
|
|
412
|
+
state: "running",
|
|
413
|
+
error: null,
|
|
414
|
+
acRegistrationName: built.acRegistrationName,
|
|
415
|
+
acServerPort: built.acServerPort,
|
|
416
|
+
};
|
|
329
417
|
agentSessions.set(key, session);
|
|
330
418
|
|
|
331
419
|
term.onExit(({ exitCode }) => {
|
|
@@ -349,8 +437,11 @@ async function spawnAgentPty(project, agent) {
|
|
|
349
437
|
}
|
|
350
438
|
}
|
|
351
439
|
|
|
352
|
-
// Helper: stop an agent session — kill PTY, close WS
|
|
353
|
-
|
|
440
|
+
// Helper: stop an agent session — kill PTY, close WS, deregister.
|
|
441
|
+
// Async because deregister must complete before a restart re-registers,
|
|
442
|
+
// otherwise the old slot stays occupied and a fresh register lands at
|
|
443
|
+
// head-2 instead of slot 1 (#241).
|
|
444
|
+
async function stopAgentSession(key) {
|
|
354
445
|
const session = agentSessions.get(key);
|
|
355
446
|
if (!session) {
|
|
356
447
|
agentSessions.set(key, { projectId: null, agentId: null, term: null, ws: null, state: "stopped", error: null });
|
|
@@ -366,6 +457,19 @@ function stopAgentSession(key) {
|
|
|
366
457
|
session.ws = null;
|
|
367
458
|
session.state = "stopped";
|
|
368
459
|
session.error = null;
|
|
460
|
+
// Best-effort deregister from AgentChattr (#241) so the slot frees
|
|
461
|
+
// and the next register lands at slot 1 instead of head-2.
|
|
462
|
+
if (session.acRegistrationName && session.acServerPort) {
|
|
463
|
+
try {
|
|
464
|
+
await deregisterAgent(session.acServerPort, session.acRegistrationName);
|
|
465
|
+
} catch {
|
|
466
|
+
// best-effort — failures are non-fatal
|
|
467
|
+
}
|
|
468
|
+
if (session.projectId && session.agentId) {
|
|
469
|
+
clearPersistedAgentToken(session.projectId, session.agentId);
|
|
470
|
+
}
|
|
471
|
+
session.acRegistrationName = null;
|
|
472
|
+
}
|
|
369
473
|
// Clean up MCP auth proxy if running
|
|
370
474
|
const [projectId, agentId] = key.split("/");
|
|
371
475
|
if (projectId && agentId) stopMcpProxy(projectId, agentId);
|
|
@@ -630,10 +734,10 @@ app.post("/api/agents/:project/:agent/start", async (req, res) => {
|
|
|
630
734
|
|
|
631
735
|
// --- Lifecycle: stop kills PTY + closes WS ---
|
|
632
736
|
|
|
633
|
-
app.post("/api/agents/:project/:agent/stop", (req, res) => {
|
|
737
|
+
app.post("/api/agents/:project/:agent/stop", async (req, res) => {
|
|
634
738
|
const { project, agent } = req.params;
|
|
635
739
|
const key = `${project}/${agent}`;
|
|
636
|
-
stopAgentSession(key);
|
|
740
|
+
await stopAgentSession(key);
|
|
637
741
|
res.json({ ok: true, state: "stopped" });
|
|
638
742
|
});
|
|
639
743
|
|
|
@@ -643,16 +747,16 @@ app.post("/api/agents/:project/:agent/restart", async (req, res) => {
|
|
|
643
747
|
const { project, agent } = req.params;
|
|
644
748
|
const key = `${project}/${agent}`;
|
|
645
749
|
|
|
646
|
-
|
|
750
|
+
// #241: must await deregister before respawn so the slot frees and
|
|
751
|
+
// the fresh register lands at slot 1 instead of head-2.
|
|
752
|
+
await stopAgentSession(key);
|
|
647
753
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
}
|
|
655
|
-
}, 500);
|
|
754
|
+
const result = await spawnAgentPty(project, agent);
|
|
755
|
+
if (result.ok) {
|
|
756
|
+
res.json({ ok: true, state: "running", pid: result.pid });
|
|
757
|
+
} else {
|
|
758
|
+
res.status(500).json({ ok: false, state: "error", error: result.error });
|
|
759
|
+
}
|
|
656
760
|
});
|
|
657
761
|
|
|
658
762
|
// --- Sessions tracking (for /api/projects dashboard) ---
|
package/server/routes.js
CHANGED
|
@@ -122,19 +122,108 @@ router.get("/api/chat", async (req, res) => {
|
|
|
122
122
|
}
|
|
123
123
|
});
|
|
124
124
|
|
|
125
|
+
// #225 sub-E: send chat messages from the dashboard via the
|
|
126
|
+
// AgentChattr WebSocket, not via /api/send.
|
|
127
|
+
//
|
|
128
|
+
// /api/send requires `Authorization: Bearer <registration_token>` and
|
|
129
|
+
// the token must resolve to a registered instance via
|
|
130
|
+
// `registry.resolve_token()`. The session_token we store on the
|
|
131
|
+
// project entry only authorizes browser/middleware traffic — it is
|
|
132
|
+
// NOT a registration token, so /api/send always 401s with
|
|
133
|
+
// "missing Authorization: Bearer <token>". The dashboard browser
|
|
134
|
+
// already sends through the WebSocket on `/ws?token=<session_token>`
|
|
135
|
+
// and the server accepts that path, so we mirror that exact flow
|
|
136
|
+
// from the express server: open a one-shot ws, push the message,
|
|
137
|
+
// wait briefly for ack, close.
|
|
138
|
+
const { WebSocket: NodeWebSocket } = require("ws");
|
|
139
|
+
const { syncChattrToken } = require("./config");
|
|
140
|
+
|
|
141
|
+
function sendViaWebSocket(baseUrl, sessionToken, message) {
|
|
142
|
+
return new Promise((resolve, reject) => {
|
|
143
|
+
const wsUrl = `${baseUrl.replace(/^http/, "ws")}/ws?token=${encodeURIComponent(sessionToken || "")}`;
|
|
144
|
+
const ws = new NodeWebSocket(wsUrl);
|
|
145
|
+
let settled = false;
|
|
146
|
+
const finish = (err, value) => {
|
|
147
|
+
if (settled) return;
|
|
148
|
+
settled = true;
|
|
149
|
+
try { ws.close(); } catch {}
|
|
150
|
+
if (err) reject(err); else resolve(value);
|
|
151
|
+
};
|
|
152
|
+
const giveUp = setTimeout(() => finish(new Error("websocket send timeout")), 4000);
|
|
153
|
+
ws.on("open", () => {
|
|
154
|
+
try {
|
|
155
|
+
ws.send(JSON.stringify({ type: "message", ...message }));
|
|
156
|
+
// Server acks via broadcast, but the dashboard's POST /api/chat
|
|
157
|
+
// contract only needs to know the message was accepted. Wait
|
|
158
|
+
// ~250ms for the server to enqueue + close cleanly.
|
|
159
|
+
setTimeout(() => { clearTimeout(giveUp); finish(null, { ok: true }); }, 250);
|
|
160
|
+
} catch (err) { clearTimeout(giveUp); finish(err); }
|
|
161
|
+
});
|
|
162
|
+
ws.on("error", (err) => { clearTimeout(giveUp); finish(err); });
|
|
163
|
+
ws.on("close", (code, reason) => {
|
|
164
|
+
// Code 4003 = bad token (see app.py /ws handler). Surface as
|
|
165
|
+
// 401 so the dashboard's chat error banner shows the right thing.
|
|
166
|
+
if (!settled && code === 4003) {
|
|
167
|
+
clearTimeout(giveUp);
|
|
168
|
+
const msg = (reason && reason.toString()) || "forbidden: invalid session token";
|
|
169
|
+
const e = new Error(msg);
|
|
170
|
+
e.code = "EAGENTCHATTR_401";
|
|
171
|
+
finish(e);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
125
177
|
router.post("/api/chat", async (req, res) => {
|
|
126
|
-
const
|
|
127
|
-
const
|
|
178
|
+
const projectId = req.query.project || req.body.project;
|
|
179
|
+
const { url: base, token: sessionToken } = getChattrConfig(projectId);
|
|
180
|
+
if (!base) return res.status(400).json({ error: "Missing project" });
|
|
181
|
+
|
|
182
|
+
// #230: ignore any client-supplied sender. /api/chat is the
|
|
183
|
+
// dashboard's send path, so the message must always be attributed
|
|
184
|
+
// to "user". Forwarding `req.body.sender` would let any caller
|
|
185
|
+
// hitting QuadWork's /api/chat impersonate an agent identity (t1,
|
|
186
|
+
// t3, …) over the AgentChattr ws path, which the old /api/send
|
|
187
|
+
// flow could not do.
|
|
188
|
+
const message = {
|
|
189
|
+
text: typeof req.body?.text === "string" ? req.body.text : "",
|
|
190
|
+
channel: req.body?.channel || "general",
|
|
191
|
+
sender: "user",
|
|
192
|
+
attachments: Array.isArray(req.body?.attachments) ? req.body.attachments : [],
|
|
193
|
+
};
|
|
194
|
+
if (!message.text && message.attachments.length === 0) {
|
|
195
|
+
return res.status(400).json({ error: "text or attachments required" });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const attemptSend = () => sendViaWebSocket(base, sessionToken, message);
|
|
199
|
+
|
|
128
200
|
try {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
headers: { "Content-Type": "application/json", ...chatAuthHeaders(token) },
|
|
132
|
-
body: JSON.stringify(req.body),
|
|
133
|
-
});
|
|
134
|
-
if (!r.ok) return res.status(r.status).json({ error: `AgentChattr returned ${r.status}` });
|
|
135
|
-
res.json(await r.json());
|
|
201
|
+
await attemptSend();
|
|
202
|
+
return res.json({ ok: true });
|
|
136
203
|
} catch (err) {
|
|
137
|
-
|
|
204
|
+
// If the cached session_token is stale (AgentChattr regenerates
|
|
205
|
+
// one on every restart) the ws closes with code 4003 — re-sync
|
|
206
|
+
// the token from AgentChattr's HTML and retry once before giving
|
|
207
|
+
// up. This is the actual fix for the "401 after restart" report
|
|
208
|
+
// in #230 (the cache was stuck on an old token).
|
|
209
|
+
if (err && err.code === "EAGENTCHATTR_401") {
|
|
210
|
+
console.warn(`[chat] ws auth failed for project ${projectId}, re-syncing session token and retrying...`);
|
|
211
|
+
try { await syncChattrToken(projectId); }
|
|
212
|
+
catch (syncErr) { console.warn(`[chat] syncChattrToken failed: ${syncErr.message}`); }
|
|
213
|
+
const { token: refreshed } = getChattrConfig(projectId);
|
|
214
|
+
if (refreshed && refreshed !== sessionToken) {
|
|
215
|
+
try {
|
|
216
|
+
await sendViaWebSocket(base, refreshed, message);
|
|
217
|
+
return res.json({ ok: true, resynced: true });
|
|
218
|
+
} catch (retryErr) {
|
|
219
|
+
console.warn(`[chat] retry after token resync failed: ${retryErr.message}`);
|
|
220
|
+
return res.status(401).json({ error: "AgentChattr auth failed (token resync did not help)", detail: retryErr.message });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return res.status(401).json({ error: "AgentChattr auth failed", detail: err.message });
|
|
224
|
+
}
|
|
225
|
+
console.warn(`[chat] send failed for project ${projectId}: ${err && err.message}`);
|
|
226
|
+
return res.status(502).json({ error: "AgentChattr unreachable", detail: err && err.message });
|
|
138
227
|
}
|
|
139
228
|
});
|
|
140
229
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
(globalThis.TURBOPACK||(globalThis.TURBOPACK=[])).push(["object"==typeof document?document.currentScript:void 0,16348,e=>{"use strict";var t=e.i(85899),s=e.i(4232),r=e.i(2270);e.s(["default",0,function(){let[e,a]=(0,s.useState)([]),[c,i]=(0,s.useState)([]);return(0,s.useEffect)(()=>{fetch("/api/projects").then(e=>{if(!e.ok)throw Error(`${e.status}`);return e.json()}).then(e=>{e.projects&&Array.isArray(e.projects)&&a(e.projects.filter(e=>!e.archived)),e.recentEvents&&Array.isArray(e.recentEvents)&&i(e.recentEvents)}).catch(()=>{})},[]),(0,t.jsxs)("div",{className:"h-full overflow-y-auto p-6",children:[(0,t.jsxs)("div",{className:"mb-6",children:[(0,t.jsx)("h1",{className:"text-lg font-semibold text-text tracking-tight",children:"Projects"}),(0,t.jsxs)("p",{className:"text-xs text-text-muted mt-1",children:[e.length," configured project",1!==e.length?"s":""]})]}),(0,t.jsxs)("div",{className:"grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3 mb-8",children:[e.map(e=>(0,t.jsxs)(r.default,{href:`/project/${e.id}`,className:"block border border-border bg-bg-surface p-4 hover:bg-[#1a1a1a] transition-colors group",children:[(0,t.jsxs)("div",{className:"flex items-center justify-between mb-3",children:[(0,t.jsxs)("div",{className:"flex items-center gap-2",children:[(0,t.jsx)("span",{className:`w-1.5 h-1.5 rounded-full ${"active"===e.state?"bg-accent":"bg-text-muted"}`}),(0,t.jsx)("span",{className:"text-sm font-semibold text-text",children:e.name}),(0,t.jsx)("span",{className:"text-[10px] text-text-muted",children:e.state})]}),(0,t.jsx)("span",{className:"text-[10px] text-text-muted opacity-0 group-hover:opacity-100 transition-opacity",children:"open →"})]}),(0,t.jsxs)("div",{className:"flex gap-4 text-[11px] mb-2",children:[(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{className:"text-text-muted",children:"agents"}),(0,t.jsx)("span",{className:"ml-1.5 text-text",children:e.agentCount})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{className:"text-text-muted",children:"PRs"}),(0,t.jsx)("span",{className:"ml-1.5 text-text",children:e.openPrs})]}),(0,t.jsxs)("div",{children:[(0,t.jsx)("span",{className:"text-text-muted",children:"repo"}),(0,t.jsx)("span",{className:"ml-1.5 text-text",children:e.repo})]})]}),e.lastActivity&&(0,t.jsxs)("div",{className:"text-[10px] text-text-muted",children:["last activity: ",function(e){let t=Math.floor((Date.now()-new Date(e).getTime())/6e4);if(t<1)return"just now";if(t<60)return`${t}m ago`;let s=Math.floor(t/60);if(s<24)return`${s}h ago`;let r=Math.floor(s/24);return`${r}d ago`}(e.lastActivity)]})]},e.id)),(0,t.jsx)(r.default,{href:"/setup",className:"border border-dashed border-border p-4 flex items-center justify-center text-text-muted hover:text-text hover:border-text-muted transition-colors min-h-[88px]",children:(0,t.jsx)("span",{className:"text-sm",children:"+ New Project"})})]}),(0,t.jsxs)("div",{className:"mb-6",children:[(0,t.jsx)("h2",{className:"text-xs text-text-muted uppercase tracking-wider mb-3",children:"Recent Activity"}),(0,t.jsxs)("div",{className:"border border-border bg-bg-surface",children:[0===c.length&&(0,t.jsx)("div",{className:"px-3 py-3 text-[11px] text-text-muted",children:"No recent activity"}),c.map((e,s)=>(0,t.jsxs)("div",{className:"flex gap-3 px-3 py-1.5 border-b border-border/50 last:border-b-0 text-[11px]",children:[(0,t.jsx)("span",{className:"text-text-muted shrink-0 w-10 text-right tabular-nums",children:e.time?.slice(0,5)||""}),(0,t.jsx)("span",{className:"text-accent shrink-0 font-semibold w-12",children:e.projectName}),(0,t.jsx)("span",{className:"text-[#ffcc00] shrink-0 font-semibold w-6",children:e.actor}),(0,t.jsx)("span",{className:"text-text truncate min-w-0",children:e.text})]},`${e.time}-${s}`))]})]})]})}])}]);
|