agent-board-runner 0.1.2 → 0.1.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/dist/cli.js +159 -61
- package/dist/index.js +164 -66
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -188,7 +188,7 @@ import { execFile as execFile2 } from "node:child_process";
|
|
|
188
188
|
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
189
189
|
import { existsSync as existsSync4, lstatSync, mkdirSync as mkdirSync4, readdirSync, readFileSync as readFileSync4, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "node:fs";
|
|
190
190
|
import { homedir as homedir4 } from "node:os";
|
|
191
|
-
import { join as
|
|
191
|
+
import { join as join9, resolve as resolve2, sep } from "node:path";
|
|
192
192
|
import { parse } from "node:url";
|
|
193
193
|
import { promisify as promisify2 } from "node:util";
|
|
194
194
|
|
|
@@ -412,7 +412,10 @@ async function savePastedImage(sessionId, extCode, bytes) {
|
|
|
412
412
|
return path;
|
|
413
413
|
}
|
|
414
414
|
function registerPtySession(sessionId, pty, ticket) {
|
|
415
|
-
const handle = { pty, ticket, outputBuffer: [], sockets: /* @__PURE__ */ new Set(), readOnly: false };
|
|
415
|
+
const handle = { pty, ticket, outputBuffer: [], sockets: /* @__PURE__ */ new Set(), readOnly: false, exited: false };
|
|
416
|
+
pty.onExit(() => {
|
|
417
|
+
handle.exited = true;
|
|
418
|
+
});
|
|
416
419
|
pty.onData((data) => {
|
|
417
420
|
handle.outputBuffer.push(data);
|
|
418
421
|
if (handle.outputBuffer.length > MAX_BUFFERED_CHUNKS) handle.outputBuffer.shift();
|
|
@@ -431,7 +434,7 @@ function setPtySessionReadOnly(sessionId, readOnly) {
|
|
|
431
434
|
}
|
|
432
435
|
function resizePtySession(sessionId, cols, rows) {
|
|
433
436
|
const handle = registry.get(sessionId);
|
|
434
|
-
if (!handle) return;
|
|
437
|
+
if (!handle || handle.exited) return;
|
|
435
438
|
handle.pty.resize(cols, rows);
|
|
436
439
|
}
|
|
437
440
|
function stopPtySession(sessionId) {
|
|
@@ -735,10 +738,12 @@ async function logActivity(supabase, params) {
|
|
|
735
738
|
}
|
|
736
739
|
|
|
737
740
|
// src/sessions.ts
|
|
738
|
-
async function
|
|
739
|
-
const {
|
|
741
|
+
async function getRunningSessionForProject(supabase, projectId, ownerUserId) {
|
|
742
|
+
const { data, error } = await supabase.from("sessions").select("id, card_id, agent_state, cards!card_id!inner(project_id, assigned_user_id)").eq("cards.project_id", projectId).eq("cards.assigned_user_id", ownerUserId).eq("status", "running").limit(1).maybeSingle();
|
|
740
743
|
if (error) throw error;
|
|
741
|
-
|
|
744
|
+
if (!data) return null;
|
|
745
|
+
const row = data;
|
|
746
|
+
return { id: row.id, cardId: row.card_id, agentState: row.agent_state };
|
|
742
747
|
}
|
|
743
748
|
async function listQueuedSessionsForRunner(supabase, ownerUserId) {
|
|
744
749
|
const { data, error } = await supabase.from("sessions").select(
|
|
@@ -812,7 +817,8 @@ async function updateCard(supabase, cardId, changes) {
|
|
|
812
817
|
...changes.deployError !== void 0 ? { deploy_error: changes.deployError } : {},
|
|
813
818
|
...changes.customActionStatus !== void 0 ? { custom_action_status: changes.customActionStatus } : {},
|
|
814
819
|
...changes.customActionOutput !== void 0 ? { custom_action_output: changes.customActionOutput } : {},
|
|
815
|
-
...changes.requeueRequestedAt !== void 0 ? { requeue_requested_at: changes.requeueRequestedAt } : {}
|
|
820
|
+
...changes.requeueRequestedAt !== void 0 ? { requeue_requested_at: changes.requeueRequestedAt } : {},
|
|
821
|
+
...changes.restartRequestedAt !== void 0 ? { restart_requested_at: changes.restartRequestedAt } : {}
|
|
816
822
|
}).eq("id", cardId);
|
|
817
823
|
if (error) throw error;
|
|
818
824
|
}
|
|
@@ -828,7 +834,7 @@ async function getProjectRepos(supabase, projectId) {
|
|
|
828
834
|
}
|
|
829
835
|
async function listCardsInColumn(supabase, projectId, column) {
|
|
830
836
|
const { data, error } = await supabase.from("cards").select(
|
|
831
|
-
"id, project_id, title, instructions, column_name, assigned_user_id, deploy_error, file_references, preview_urls, custom_action_status, requeue_requested_at"
|
|
837
|
+
"id, project_id, title, instructions, column_name, assigned_user_id, deploy_error, file_references, preview_urls, custom_action_status, requeue_requested_at, restart_requested_at"
|
|
832
838
|
).eq("project_id", projectId).eq("column_name", column);
|
|
833
839
|
if (error) throw error;
|
|
834
840
|
return data;
|
|
@@ -911,9 +917,9 @@ async function upsertClaudeUsage(supabase, userId, changes) {
|
|
|
911
917
|
|
|
912
918
|
// src/usageMonitor.ts
|
|
913
919
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
914
|
-
import {
|
|
915
|
-
import {
|
|
916
|
-
import { join as
|
|
920
|
+
import { mkdirSync as mkdirSync3 } from "node:fs";
|
|
921
|
+
import { tmpdir as tmpdir5 } from "node:os";
|
|
922
|
+
import { join as join8 } from "node:path";
|
|
917
923
|
import { spawn as spawn2 } from "node-pty";
|
|
918
924
|
|
|
919
925
|
// src/agentMomentHookScript.ts
|
|
@@ -1263,38 +1269,79 @@ function claudeCliSettingsArgs() {
|
|
|
1263
1269
|
];
|
|
1264
1270
|
}
|
|
1265
1271
|
|
|
1266
|
-
// src/
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
var IDLE_MS = 2e3;
|
|
1272
|
-
var HARD_TIMEOUT_MS = 6e4;
|
|
1273
|
-
function usageMonitorSessionId() {
|
|
1274
|
-
return SESSION_ID;
|
|
1275
|
-
}
|
|
1276
|
-
function usageMonitorTicket() {
|
|
1277
|
-
return TICKET;
|
|
1278
|
-
}
|
|
1279
|
-
function ensureScratchDirTrusted() {
|
|
1272
|
+
// src/claudeTrust.ts
|
|
1273
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync6 } from "node:fs";
|
|
1274
|
+
import { homedir as homedir3 } from "node:os";
|
|
1275
|
+
import { join as join7 } from "node:path";
|
|
1276
|
+
function ensureDirTrusted(dirPath) {
|
|
1280
1277
|
const claudeJsonPath = join7(homedir3(), ".claude.json");
|
|
1281
1278
|
if (!existsSync3(claudeJsonPath)) return;
|
|
1282
1279
|
try {
|
|
1283
1280
|
const raw = readFileSync3(claudeJsonPath, "utf8");
|
|
1284
1281
|
const data = JSON.parse(raw);
|
|
1285
1282
|
data.projects ??= {};
|
|
1286
|
-
const keys = [
|
|
1283
|
+
const keys = [dirPath, dirPath.replace(/\\/g, "/")];
|
|
1287
1284
|
const alreadyTrusted = keys.every((key) => data.projects[key]?.hasTrustDialogAccepted === true);
|
|
1288
1285
|
if (alreadyTrusted) return;
|
|
1289
1286
|
for (const key of keys) {
|
|
1290
1287
|
data.projects[key] = { ...data.projects[key], hasTrustDialogAccepted: true };
|
|
1291
1288
|
}
|
|
1292
1289
|
writeFileSync6(claudeJsonPath, JSON.stringify(data, null, 2));
|
|
1293
|
-
logger.runner.info("
|
|
1290
|
+
logger.runner.info("TRUST", `Pre-trusted ${dirPath} in ~/.claude.json.`);
|
|
1294
1291
|
} catch (err) {
|
|
1295
|
-
logger.runner.error("
|
|
1292
|
+
logger.runner.error("TRUST", `Failed to pre-trust ${dirPath}:`, err);
|
|
1296
1293
|
}
|
|
1297
1294
|
}
|
|
1295
|
+
function ensureMcpServersTrusted(dirPath) {
|
|
1296
|
+
const mcpJsonPath = join7(dirPath, ".mcp.json");
|
|
1297
|
+
if (!existsSync3(mcpJsonPath)) return;
|
|
1298
|
+
let serverNames;
|
|
1299
|
+
try {
|
|
1300
|
+
const mcpConfig = JSON.parse(readFileSync3(mcpJsonPath, "utf8"));
|
|
1301
|
+
serverNames = Object.keys(mcpConfig.mcpServers ?? {});
|
|
1302
|
+
} catch (err) {
|
|
1303
|
+
logger.runner.error("TRUST", `Failed to read ${mcpJsonPath}:`, err);
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
if (serverNames.length === 0) return;
|
|
1307
|
+
const claudeJsonPath = join7(homedir3(), ".claude.json");
|
|
1308
|
+
if (!existsSync3(claudeJsonPath)) return;
|
|
1309
|
+
try {
|
|
1310
|
+
const raw = readFileSync3(claudeJsonPath, "utf8");
|
|
1311
|
+
const data = JSON.parse(raw);
|
|
1312
|
+
data.projects ??= {};
|
|
1313
|
+
const keys = [dirPath, dirPath.replace(/\\/g, "/")];
|
|
1314
|
+
const alreadyApproved = keys.every(
|
|
1315
|
+
(key) => serverNames.every((name) => data.projects[key]?.enabledMcpjsonServers?.includes(name))
|
|
1316
|
+
);
|
|
1317
|
+
if (alreadyApproved) return;
|
|
1318
|
+
for (const key of keys) {
|
|
1319
|
+
const project = data.projects[key] ??= {};
|
|
1320
|
+
project.enabledMcpjsonServers = [.../* @__PURE__ */ new Set([...project.enabledMcpjsonServers ?? [], ...serverNames])];
|
|
1321
|
+
project.disabledMcpjsonServers = (project.disabledMcpjsonServers ?? []).filter(
|
|
1322
|
+
(name) => !serverNames.includes(name)
|
|
1323
|
+
);
|
|
1324
|
+
}
|
|
1325
|
+
writeFileSync6(claudeJsonPath, JSON.stringify(data, null, 2));
|
|
1326
|
+
logger.runner.info("TRUST", `Pre-approved MCP servers [${serverNames.join(", ")}] for ${dirPath}.`);
|
|
1327
|
+
} catch (err) {
|
|
1328
|
+
logger.runner.error("TRUST", `Failed to pre-approve MCP servers for ${dirPath}:`, err);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// src/usageMonitor.ts
|
|
1333
|
+
var SESSION_ID = "usage-monitor";
|
|
1334
|
+
var TICKET = randomUUID4();
|
|
1335
|
+
var SCRATCH_DIR = join8(tmpdir5(), "agent-board-usage-monitor");
|
|
1336
|
+
var POLL_INTERVAL_MS = Number(process.env.AGENT_BOARD_USAGE_POLL_INTERVAL_MS ?? 15 * 6e4);
|
|
1337
|
+
var IDLE_MS = 2e3;
|
|
1338
|
+
var HARD_TIMEOUT_MS = 6e4;
|
|
1339
|
+
function usageMonitorSessionId() {
|
|
1340
|
+
return SESSION_ID;
|
|
1341
|
+
}
|
|
1342
|
+
function usageMonitorTicket() {
|
|
1343
|
+
return TICKET;
|
|
1344
|
+
}
|
|
1298
1345
|
function pollUsageOnce() {
|
|
1299
1346
|
let pty;
|
|
1300
1347
|
try {
|
|
@@ -1341,7 +1388,7 @@ function pollUsageOnce() {
|
|
|
1341
1388
|
}
|
|
1342
1389
|
function startUsageMonitor() {
|
|
1343
1390
|
mkdirSync3(SCRATCH_DIR, { recursive: true });
|
|
1344
|
-
|
|
1391
|
+
ensureDirTrusted(SCRATCH_DIR);
|
|
1345
1392
|
pollUsageOnce();
|
|
1346
1393
|
setInterval(pollUsageOnce, POLL_INTERVAL_MS);
|
|
1347
1394
|
}
|
|
@@ -1357,7 +1404,7 @@ var IGNORED_ENTRY_NAMES = /* @__PURE__ */ new Set([".git", ".claude", "node_modu
|
|
|
1357
1404
|
var AGENT_SAFE_SUBDIR_NAMES = /* @__PURE__ */ new Set(["skills", "commands", "agents", "rules"]);
|
|
1358
1405
|
function ensureSkillsDirExists(claudeDir) {
|
|
1359
1406
|
try {
|
|
1360
|
-
mkdirSync4(
|
|
1407
|
+
mkdirSync4(join9(claudeDir, "skills"), { recursive: true });
|
|
1361
1408
|
} catch (err) {
|
|
1362
1409
|
logger.runner.warn("FILES", "Could not create .claude/skills:", err);
|
|
1363
1410
|
}
|
|
@@ -1484,7 +1531,7 @@ function listSafeAgentDirectory(baseDir, relPath, atClaudeRoot) {
|
|
|
1484
1531
|
}
|
|
1485
1532
|
const isRepoRootLevel = segments.length === 0 || segments.length === 1 && segments[0] !== ".claude";
|
|
1486
1533
|
if (isRepoRootLevel) {
|
|
1487
|
-
ensureSkillsDirExists(
|
|
1534
|
+
ensureSkillsDirExists(join9(baseDir, relPath, ".claude"));
|
|
1488
1535
|
const entries2 = listDirectory(baseDir, relPath, { includeDotClaude: true });
|
|
1489
1536
|
return entries2.filter((entry) => entry.type === "file" && entry.name === "CLAUDE.md" || entry.type === "dir" && entry.name === ".claude");
|
|
1490
1537
|
}
|
|
@@ -1515,7 +1562,7 @@ function assertWritableAgentPath(relPath, atClaudeRoot) {
|
|
|
1515
1562
|
function listDirectoryForBrowse(rootPath, relPath) {
|
|
1516
1563
|
return listDirectory(rootPath, relPath).map((entry) => ({
|
|
1517
1564
|
...entry,
|
|
1518
|
-
hasGit: entry.type === "dir" && existsSync4(
|
|
1565
|
+
hasGit: entry.type === "dir" && existsSync4(join9(rootPath, entry.path, ".git"))
|
|
1519
1566
|
}));
|
|
1520
1567
|
}
|
|
1521
1568
|
var SAFE_CLONE_URL_PATTERN = /^(https?:\/\/|git:\/\/|ssh:\/\/|git@[\w.-]+:)\S+$/i;
|
|
@@ -1553,7 +1600,7 @@ async function handleCloneRequest(req, res, query) {
|
|
|
1553
1600
|
sendJson(res, 400, { error: "N\xE3o foi poss\xEDvel determinar o nome da pasta a partir da URL." });
|
|
1554
1601
|
return;
|
|
1555
1602
|
}
|
|
1556
|
-
const destPath =
|
|
1603
|
+
const destPath = join9(setupEntry.rootPath, folderName);
|
|
1557
1604
|
if (existsSync4(destPath)) {
|
|
1558
1605
|
sendJson(res, 409, { error: `J\xE1 existe uma pasta "${folderName}" na raiz permitida.` });
|
|
1559
1606
|
return;
|
|
@@ -1621,7 +1668,7 @@ function findBrandIconPath(repoRoot) {
|
|
|
1621
1668
|
if (entry.isSymbolicLink() || IGNORED_ENTRY_NAMES.has(entry.name)) continue;
|
|
1622
1669
|
const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
1623
1670
|
if (entry.isDirectory()) {
|
|
1624
|
-
walk(
|
|
1671
|
+
walk(join9(dir, entry.name), relPath, depth + 1);
|
|
1625
1672
|
} else if (entry.isFile()) {
|
|
1626
1673
|
const match = patterns.find(({ pattern }) => pattern.test(entry.name));
|
|
1627
1674
|
if (match) candidatesByBaseName.get(match.baseName).push({ relPath, depth });
|
|
@@ -1786,7 +1833,7 @@ async function getRepoDiffForTarget(label, cwd) {
|
|
|
1786
1833
|
async function handleDiffRequest(entry, res, projectId) {
|
|
1787
1834
|
try {
|
|
1788
1835
|
const children = statuslineSupabase && projectId ? await getProjectRepos(statuslineSupabase, projectId) : [];
|
|
1789
|
-
const targets = children.length > 0 ? children.map((child2) => ({ label: child2.subpath, cwd:
|
|
1836
|
+
const targets = children.length > 0 ? children.map((child2) => ({ label: child2.subpath, cwd: join9(entry.repoRoot, child2.subpath) })) : [{ label: null, cwd: entry.repoRoot }];
|
|
1790
1837
|
const repos = await Promise.all(targets.map((target) => getRepoDiffForTarget(target.label, target.cwd)));
|
|
1791
1838
|
sendJson(res, 200, { repos });
|
|
1792
1839
|
} catch (err) {
|
|
@@ -2094,7 +2141,7 @@ function attachAgentEventsServer() {
|
|
|
2094
2141
|
|
|
2095
2142
|
// src/columnActions.ts
|
|
2096
2143
|
import { exec } from "node:child_process";
|
|
2097
|
-
import { join as
|
|
2144
|
+
import { join as join10 } from "node:path";
|
|
2098
2145
|
import { promisify as promisify3 } from "node:util";
|
|
2099
2146
|
var execAsync = promisify3(exec);
|
|
2100
2147
|
var OUTPUT_TAIL_CHARS = 4e3;
|
|
@@ -2104,7 +2151,7 @@ function tail(text) {
|
|
|
2104
2151
|
function resolveTargets(project, children, repoRoot, column, timing) {
|
|
2105
2152
|
if (children.length > 0) {
|
|
2106
2153
|
return children.flatMap(
|
|
2107
|
-
(child2) => (child2.column_actions[column] ?? []).filter((action) => action.timing === timing).map((action) => ({ label: child2.subpath, cwd:
|
|
2154
|
+
(child2) => (child2.column_actions[column] ?? []).filter((action) => action.timing === timing).map((action) => ({ label: child2.subpath, cwd: join10(repoRoot, child2.subpath), command: action.command }))
|
|
2108
2155
|
);
|
|
2109
2156
|
}
|
|
2110
2157
|
return (project.column_actions[column] ?? []).filter((action) => action.timing === timing).map((action) => ({ label: null, cwd: repoRoot, command: action.command }));
|
|
@@ -2210,7 +2257,7 @@ async function checkPendingDeployActionReruns(supabase, config) {
|
|
|
2210
2257
|
}
|
|
2211
2258
|
|
|
2212
2259
|
// src/deploy.ts
|
|
2213
|
-
import { join as
|
|
2260
|
+
import { join as join13 } from "node:path";
|
|
2214
2261
|
|
|
2215
2262
|
// src/devServer.ts
|
|
2216
2263
|
import { spawn as spawn4 } from "node:child_process";
|
|
@@ -2219,7 +2266,7 @@ import { spawn as spawn4 } from "node:child_process";
|
|
|
2219
2266
|
import { spawn as spawn3 } from "node:child_process";
|
|
2220
2267
|
import { accessSync, constants } from "node:fs";
|
|
2221
2268
|
import { homedir as homedir5 } from "node:os";
|
|
2222
|
-
import { delimiter, join as
|
|
2269
|
+
import { delimiter, join as join11 } from "node:path";
|
|
2223
2270
|
import { bin as bundledBinaryPath, install as downloadCloudflared } from "cloudflared";
|
|
2224
2271
|
var TRYCLOUDFLARE_URL_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
|
|
2225
2272
|
var STARTUP_TIMEOUT_MS = 2e4;
|
|
@@ -2230,15 +2277,15 @@ function wellKnownDirectories() {
|
|
|
2230
2277
|
const home = homedir5();
|
|
2231
2278
|
if (IS_WINDOWS2) {
|
|
2232
2279
|
const candidates = [
|
|
2233
|
-
process.env.ProgramFiles &&
|
|
2234
|
-
process.env["ProgramFiles(x86)"] &&
|
|
2235
|
-
process.env.LOCALAPPDATA &&
|
|
2236
|
-
|
|
2237
|
-
process.env.ChocolateyInstall &&
|
|
2280
|
+
process.env.ProgramFiles && join11(process.env.ProgramFiles, "cloudflared"),
|
|
2281
|
+
process.env["ProgramFiles(x86)"] && join11(process.env["ProgramFiles(x86)"], "cloudflared"),
|
|
2282
|
+
process.env.LOCALAPPDATA && join11(process.env.LOCALAPPDATA, "Microsoft", "WinGet", "Links"),
|
|
2283
|
+
join11(home, "scoop", "shims"),
|
|
2284
|
+
process.env.ChocolateyInstall && join11(process.env.ChocolateyInstall, "bin")
|
|
2238
2285
|
];
|
|
2239
2286
|
return candidates.filter((dir) => Boolean(dir));
|
|
2240
2287
|
}
|
|
2241
|
-
return ["/usr/local/bin", "/usr/bin", "/opt/homebrew/bin",
|
|
2288
|
+
return ["/usr/local/bin", "/usr/bin", "/opt/homebrew/bin", join11(home, ".local", "bin")];
|
|
2242
2289
|
}
|
|
2243
2290
|
function pathDirectories() {
|
|
2244
2291
|
return (process.env.PATH ?? "").split(delimiter).map((dir) => dir.trim().replace(/^"|"$/g, "")).filter(Boolean);
|
|
@@ -2254,7 +2301,7 @@ function isExecutableFile(path) {
|
|
|
2254
2301
|
function findInstalledBinary() {
|
|
2255
2302
|
const override = process.env.AGENT_BOARD_CLOUDFLARED_PATH;
|
|
2256
2303
|
const directories = [...pathDirectories(), ...wellKnownDirectories()];
|
|
2257
|
-
const candidates = [...override ? [override] : [], ...directories.map((dir) =>
|
|
2304
|
+
const candidates = [...override ? [override] : [], ...directories.map((dir) => join11(dir, BINARY_NAME))];
|
|
2258
2305
|
return candidates.find(isExecutableFile) ?? null;
|
|
2259
2306
|
}
|
|
2260
2307
|
async function locateCloudflared() {
|
|
@@ -2485,6 +2532,8 @@ var SHIFT_TAB_PRESSES_TO_PLAN_MODE = 4;
|
|
|
2485
2532
|
var SHIFT_TAB_DELAY_MS = 400;
|
|
2486
2533
|
var PLAN_MODE_INDICATOR = /plan mode on/i;
|
|
2487
2534
|
function spawnAgentPty(repoPath, cardId, cardTitle, instructions, sessionId, ticket, planFirst) {
|
|
2535
|
+
ensureDirTrusted(repoPath);
|
|
2536
|
+
ensureMcpServersTrusted(repoPath);
|
|
2488
2537
|
const claudeSessionId = randomUUID6();
|
|
2489
2538
|
const pty = spawn5(
|
|
2490
2539
|
CLAUDE_BINARY,
|
|
@@ -2581,6 +2630,8 @@ function sendContinue(pty, cardId) {
|
|
|
2581
2630
|
}
|
|
2582
2631
|
}
|
|
2583
2632
|
function resumeAgentPty(repoPath, claudeSessionId, sessionId, ticket) {
|
|
2633
|
+
ensureDirTrusted(repoPath);
|
|
2634
|
+
ensureMcpServersTrusted(repoPath);
|
|
2584
2635
|
return spawn5(
|
|
2585
2636
|
CLAUDE_BINARY,
|
|
2586
2637
|
[
|
|
@@ -2604,8 +2655,8 @@ function resumeAgentPty(repoPath, claudeSessionId, sessionId, ticket) {
|
|
|
2604
2655
|
// src/cardAttachments.ts
|
|
2605
2656
|
import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
|
|
2606
2657
|
import { tmpdir as tmpdir6 } from "node:os";
|
|
2607
|
-
import { extname, join as
|
|
2608
|
-
var ATTACHMENTS_ROOT =
|
|
2658
|
+
import { extname, join as join12 } from "node:path";
|
|
2659
|
+
var ATTACHMENTS_ROOT = join12(tmpdir6(), "agent-board", "card-attachments");
|
|
2609
2660
|
function sanitizeFileName(fileName) {
|
|
2610
2661
|
return fileName.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
2611
2662
|
}
|
|
@@ -2617,13 +2668,13 @@ async function downloadCardAttachments(supabase, cardId) {
|
|
|
2617
2668
|
}
|
|
2618
2669
|
const rows = data ?? [];
|
|
2619
2670
|
if (rows.length === 0) return [];
|
|
2620
|
-
const targetDir =
|
|
2671
|
+
const targetDir = join12(ATTACHMENTS_ROOT, cardId);
|
|
2621
2672
|
await mkdir2(targetDir, { recursive: true });
|
|
2622
2673
|
const paths = [];
|
|
2623
2674
|
for (const row of rows) {
|
|
2624
2675
|
const baseName = sanitizeFileName(row.file_name);
|
|
2625
2676
|
const fileName = extname(baseName) ? baseName : `${baseName}${extname(row.storage_path)}`;
|
|
2626
|
-
const localPath =
|
|
2677
|
+
const localPath = join12(targetDir, `${row.id}-${fileName}`);
|
|
2627
2678
|
try {
|
|
2628
2679
|
const { data: blob, error: downloadError } = await supabase.storage.from(CARD_ATTACHMENTS_BUCKET).download(row.storage_path);
|
|
2629
2680
|
if (downloadError) throw downloadError;
|
|
@@ -2697,6 +2748,7 @@ function cancelUsageResume(sessionId) {
|
|
|
2697
2748
|
|
|
2698
2749
|
// src/orchestrator.ts
|
|
2699
2750
|
var TERMINAL_TICKET_TTL_MS = 60 * 60 * 1e3;
|
|
2751
|
+
var RESUME_CONVERSATION_MISSING_PATTERN = /no conversation found with session id/i;
|
|
2700
2752
|
var inFlightCardIds = /* @__PURE__ */ new Set();
|
|
2701
2753
|
var activeSessionByCard = /* @__PURE__ */ new Map();
|
|
2702
2754
|
var intentionallyStoppedSessionIds = /* @__PURE__ */ new Set();
|
|
@@ -2770,11 +2822,17 @@ async function dispatchRunnerQueue(supabase, config) {
|
|
|
2770
2822
|
try {
|
|
2771
2823
|
const queued = await listQueuedSessionsForRunner(supabase, config.ownerUserId);
|
|
2772
2824
|
if (queued.length === 0) return;
|
|
2825
|
+
const queuedProjectIds = new Set(queued.map((entry) => entry.cards.project_id));
|
|
2773
2826
|
const maxConcurrentProjects = await getRunnerMaxConcurrentProjects(supabase, config.runnerId);
|
|
2774
2827
|
const runningProjectIds = /* @__PURE__ */ new Set();
|
|
2775
2828
|
for (const project of config.registeredProjects) {
|
|
2776
|
-
const
|
|
2777
|
-
if (
|
|
2829
|
+
const running = await getRunningSessionForProject(supabase, project.id, config.ownerUserId);
|
|
2830
|
+
if (!running) continue;
|
|
2831
|
+
if (running.agentState === "done" && queuedProjectIds.has(project.id)) {
|
|
2832
|
+
await pauseFinishedSession(supabase, config, project.id, running.cardId, running.id);
|
|
2833
|
+
continue;
|
|
2834
|
+
}
|
|
2835
|
+
runningProjectIds.add(project.id);
|
|
2778
2836
|
}
|
|
2779
2837
|
let freeProjectSlots = maxConcurrentProjects - runningProjectIds.size;
|
|
2780
2838
|
if (freeProjectSlots <= 0) return;
|
|
@@ -2828,9 +2886,13 @@ async function spawnSessionPty(supabase, config, entry) {
|
|
|
2828
2886
|
const ticket = randomUUID7();
|
|
2829
2887
|
let pty;
|
|
2830
2888
|
let newClaudeSessionId = null;
|
|
2889
|
+
let resumeConversationMissing = false;
|
|
2831
2890
|
try {
|
|
2832
2891
|
if (claudeSessionId) {
|
|
2833
2892
|
pty = resumeAgentPty(registered.repoPath, claudeSessionId, sessionId, ticket);
|
|
2893
|
+
pty.onData((chunk) => {
|
|
2894
|
+
if (RESUME_CONVERSATION_MISSING_PATTERN.test(normalizeTerminalOutput(chunk))) resumeConversationMissing = true;
|
|
2895
|
+
});
|
|
2834
2896
|
} else {
|
|
2835
2897
|
const attachmentPaths = await downloadCardAttachments(supabase, cardId);
|
|
2836
2898
|
const instructions = buildInstructionsWithMcpContext(
|
|
@@ -2878,11 +2940,24 @@ async function spawnSessionPty(supabase, config, entry) {
|
|
|
2878
2940
|
);
|
|
2879
2941
|
pty.onExit(({ exitCode }) => {
|
|
2880
2942
|
const wasIntentional = intentionallyStoppedSessionIds.delete(sessionId);
|
|
2881
|
-
logger.card.warn(cardId, `Session exited (code ${exitCode}) \u2014 this only happens if the process crashed or was killed, the card doesn't auto-advance.`);
|
|
2882
2943
|
activityWatcher.stop();
|
|
2883
2944
|
unregisterAgentActivityWatcher(sessionId);
|
|
2884
2945
|
cancelUsageResume(sessionId);
|
|
2885
2946
|
activeSessionByCard.delete(cardId);
|
|
2947
|
+
if (resumeConversationMissing) {
|
|
2948
|
+
logger.card.warn(
|
|
2949
|
+
cardId,
|
|
2950
|
+
`Session ${sessionId}: --resume ${claudeSessionId} found no such conversation \u2014 clearing it so the next attempt starts a fresh one instead of repeating the same failure.`
|
|
2951
|
+
);
|
|
2952
|
+
void updateSession(
|
|
2953
|
+
supabase,
|
|
2954
|
+
sessionId,
|
|
2955
|
+
{ status: "failed", endedAt: (/* @__PURE__ */ new Date()).toISOString(), claudeSessionId: null },
|
|
2956
|
+
{ projectId: card.project_id, cardId, userId: config.ownerUserId }
|
|
2957
|
+
);
|
|
2958
|
+
return;
|
|
2959
|
+
}
|
|
2960
|
+
logger.card.warn(cardId, `Session exited (code ${exitCode}) \u2014 this only happens if the process crashed or was killed, the card doesn't auto-advance.`);
|
|
2886
2961
|
if (!wasIntentional && exitCode !== 0) {
|
|
2887
2962
|
publishAgentState(supabase, card.project_id, cardId, sessionId, "error");
|
|
2888
2963
|
}
|
|
@@ -2903,7 +2978,7 @@ function watchAgentActivity(supabase, config, projectId, cardId, sessionId, pty)
|
|
|
2903
2978
|
} else {
|
|
2904
2979
|
cancelUsageResume(sessionId);
|
|
2905
2980
|
}
|
|
2906
|
-
if (state === "done") void
|
|
2981
|
+
if (state === "done") void dispatchRunnerQueue(supabase, config);
|
|
2907
2982
|
});
|
|
2908
2983
|
pty.onData((chunk) => watcher.push(chunk));
|
|
2909
2984
|
registerAgentActivityWatcher(sessionId, watcher);
|
|
@@ -2913,8 +2988,7 @@ async function pauseFinishedSession(supabase, config, projectId, cardId, session
|
|
|
2913
2988
|
const paused = await pauseRunningSession(supabase, sessionId, { projectId, cardId, userId: config.ownerUserId });
|
|
2914
2989
|
if (!paused) return;
|
|
2915
2990
|
setPtySessionReadOnly(sessionId, true);
|
|
2916
|
-
logger.card.info(cardId, `Session ${sessionId} paused (agent reached "done"
|
|
2917
|
-
await dispatchRunnerQueue(supabase, config);
|
|
2991
|
+
logger.card.info(cardId, `Session ${sessionId} paused (agent reached "done", another session needs this project's WIP slot).`);
|
|
2918
2992
|
}
|
|
2919
2993
|
async function requeuePausedCard(supabase, config, card) {
|
|
2920
2994
|
const active = await getActiveSessionForCard(supabase, card.id);
|
|
@@ -2992,6 +3066,24 @@ async function stopCardSession(supabase, config, card) {
|
|
|
2992
3066
|
logger.card.info(card.id, `Session ${sessionId} stopped.`);
|
|
2993
3067
|
await dispatchRunnerQueue(supabase, config);
|
|
2994
3068
|
}
|
|
3069
|
+
async function restartCardSession(supabase, config, card) {
|
|
3070
|
+
const sessionId = activeSessionByCard.get(card.id) ?? (await getActiveSessionForCard(supabase, card.id))?.id;
|
|
3071
|
+
if (sessionId) {
|
|
3072
|
+
intentionallyStoppedSessionIds.add(sessionId);
|
|
3073
|
+
stopPtySession(sessionId);
|
|
3074
|
+
cancelUsageResume(sessionId);
|
|
3075
|
+
activeSessionByCard.delete(card.id);
|
|
3076
|
+
await updateSession(
|
|
3077
|
+
supabase,
|
|
3078
|
+
sessionId,
|
|
3079
|
+
{ status: "failed", claudeSessionId: null, endedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
3080
|
+
{ projectId: card.project_id, cardId: card.id, userId: config.ownerUserId }
|
|
3081
|
+
);
|
|
3082
|
+
logger.card.info(card.id, `Session ${sessionId} abandoned by "Reiniciar sess\xE3o"; starting a fresh one.`);
|
|
3083
|
+
}
|
|
3084
|
+
await updateCard(supabase, card.id, { restartRequestedAt: null });
|
|
3085
|
+
await enqueueCard(supabase, config, card);
|
|
3086
|
+
}
|
|
2995
3087
|
|
|
2996
3088
|
// src/deploy.ts
|
|
2997
3089
|
async function handleDeployRequested(supabase, config, project) {
|
|
@@ -3009,7 +3101,7 @@ async function handleDeployRequested(supabase, config, project) {
|
|
|
3009
3101
|
}
|
|
3010
3102
|
const children = await getProjectRepos(supabase, project.id);
|
|
3011
3103
|
const repoRoot = resolveRepoPath(config, project.repo_path);
|
|
3012
|
-
const targets = children.length > 0 ? children.map((child2) => ({ label: child2.subpath, path:
|
|
3104
|
+
const targets = children.length > 0 ? children.map((child2) => ({ label: child2.subpath, path: join13(repoRoot, child2.subpath) })) : [{ label: null, path: repoRoot }];
|
|
3013
3105
|
const failures = [];
|
|
3014
3106
|
for (const target of targets) {
|
|
3015
3107
|
const result = await pushBranchToMain(target.path);
|
|
@@ -3143,7 +3235,7 @@ async function syncRegisteredProjects(supabase, config, registeredPaths) {
|
|
|
3143
3235
|
}
|
|
3144
3236
|
|
|
3145
3237
|
// src/review.ts
|
|
3146
|
-
import { join as
|
|
3238
|
+
import { join as join14 } from "node:path";
|
|
3147
3239
|
var inFlightCardIds2 = /* @__PURE__ */ new Set();
|
|
3148
3240
|
async function handleCardEnteredRevisao(supabase, config, card) {
|
|
3149
3241
|
if (inFlightCardIds2.has(card.id)) return;
|
|
@@ -3152,7 +3244,7 @@ async function handleCardEnteredRevisao(supabase, config, card) {
|
|
|
3152
3244
|
const project = await getProject(supabase, card.project_id);
|
|
3153
3245
|
const children = await getProjectRepos(supabase, project.id);
|
|
3154
3246
|
const repoRoot = resolveRepoPath(config, project.repo_path);
|
|
3155
|
-
const targets = children.length > 0 ? children.filter((child2) => child2.dev_command).map((child2) => ({ label: child2.subpath, cwd:
|
|
3247
|
+
const targets = children.length > 0 ? children.filter((child2) => child2.dev_command).map((child2) => ({ label: child2.subpath, cwd: join14(repoRoot, child2.subpath), command: child2.dev_command })) : project.dev_command ? [{ label: "", cwd: repoRoot, command: project.dev_command }] : [];
|
|
3156
3248
|
if (targets.length === 0) {
|
|
3157
3249
|
logger.card.info(card.id, `No dev_command configured for project ${project.id}, skipping automatic preview.`);
|
|
3158
3250
|
return;
|
|
@@ -3238,7 +3330,7 @@ function attachTerminalServer() {
|
|
|
3238
3330
|
const buffer = data;
|
|
3239
3331
|
const frameType = buffer[0];
|
|
3240
3332
|
if (frameType === FRAME_TYPE_IMAGE) {
|
|
3241
|
-
if (handle.readOnly) return;
|
|
3333
|
+
if (handle.readOnly || handle.exited) return;
|
|
3242
3334
|
const extCode = buffer[1];
|
|
3243
3335
|
const bytes = buffer.subarray(2);
|
|
3244
3336
|
savePastedImage(sessionId, extCode, bytes).then((path) => handle.pty.write(`${path} `)).catch((err) => logger.runner.warn("TERMINAL", `Terminal WS: failed to save pasted image for session ${sessionId}:`, err));
|
|
@@ -3256,7 +3348,7 @@ function attachTerminalServer() {
|
|
|
3256
3348
|
logger.runner.warn("TERMINAL", `Terminal WS: ignoring binary frame with unknown type ${frameType} for session ${sessionId}.`);
|
|
3257
3349
|
return;
|
|
3258
3350
|
}
|
|
3259
|
-
if (handle.readOnly) return;
|
|
3351
|
+
if (handle.readOnly || handle.exited) return;
|
|
3260
3352
|
handle.pty.write(data.toString());
|
|
3261
3353
|
});
|
|
3262
3354
|
socket.on("close", () => {
|
|
@@ -3378,6 +3470,7 @@ async function startRunner() {
|
|
|
3378
3470
|
const registeredPaths = new Set(config.registeredProjects.map((project) => project.id));
|
|
3379
3471
|
const lastKnownColumn = /* @__PURE__ */ new Map();
|
|
3380
3472
|
const lastKnownRequeueRequestedAt = /* @__PURE__ */ new Map();
|
|
3473
|
+
const lastKnownRestartRequestedAt = /* @__PURE__ */ new Map();
|
|
3381
3474
|
await syncRegisteredProjects(supabase, config, registeredPaths).catch((err) => logger.runner.error("PROJECT_SYNC", "syncRegisteredProjects failed:", err));
|
|
3382
3475
|
logger.runner.info("BOOT", `Agent Board runner "${config.runnerName}" starting (${registeredPaths.size} project(s) registered).`);
|
|
3383
3476
|
for (const project of config.registeredProjects) {
|
|
@@ -3473,6 +3566,11 @@ async function startRunner() {
|
|
|
3473
3566
|
if (card.requeue_requested_at && card.requeue_requested_at !== previousRequeueRequestedAt) {
|
|
3474
3567
|
void requeuePausedCard(supabase, config, card);
|
|
3475
3568
|
}
|
|
3569
|
+
const previousRestartRequestedAt = lastKnownRestartRequestedAt.get(card.id);
|
|
3570
|
+
lastKnownRestartRequestedAt.set(card.id, card.restart_requested_at);
|
|
3571
|
+
if (card.restart_requested_at && card.restart_requested_at !== previousRestartRequestedAt) {
|
|
3572
|
+
void restartCardSession(supabase, config, card);
|
|
3573
|
+
}
|
|
3476
3574
|
if (previousColumn !== void 0 && previousColumn !== card.column_name) {
|
|
3477
3575
|
void clearColumnActionStatus(supabase, card);
|
|
3478
3576
|
void handleColumnEntered(supabase, config, card);
|
package/dist/index.js
CHANGED
|
@@ -26,7 +26,7 @@ import { execFile as execFile2 } from "node:child_process";
|
|
|
26
26
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
27
27
|
import { existsSync as existsSync2, lstatSync, mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
28
28
|
import { homedir as homedir3 } from "node:os";
|
|
29
|
-
import { join as
|
|
29
|
+
import { join as join7, resolve, sep } from "node:path";
|
|
30
30
|
import { parse } from "node:url";
|
|
31
31
|
import { promisify as promisify2 } from "node:util";
|
|
32
32
|
|
|
@@ -250,7 +250,10 @@ async function savePastedImage(sessionId, extCode, bytes) {
|
|
|
250
250
|
return path;
|
|
251
251
|
}
|
|
252
252
|
function registerPtySession(sessionId, pty, ticket) {
|
|
253
|
-
const handle = { pty, ticket, outputBuffer: [], sockets: /* @__PURE__ */ new Set(), readOnly: false };
|
|
253
|
+
const handle = { pty, ticket, outputBuffer: [], sockets: /* @__PURE__ */ new Set(), readOnly: false, exited: false };
|
|
254
|
+
pty.onExit(() => {
|
|
255
|
+
handle.exited = true;
|
|
256
|
+
});
|
|
254
257
|
pty.onData((data) => {
|
|
255
258
|
handle.outputBuffer.push(data);
|
|
256
259
|
if (handle.outputBuffer.length > MAX_BUFFERED_CHUNKS) handle.outputBuffer.shift();
|
|
@@ -269,7 +272,7 @@ function setPtySessionReadOnly(sessionId, readOnly) {
|
|
|
269
272
|
}
|
|
270
273
|
function resizePtySession(sessionId, cols, rows) {
|
|
271
274
|
const handle = registry.get(sessionId);
|
|
272
|
-
if (!handle) return;
|
|
275
|
+
if (!handle || handle.exited) return;
|
|
273
276
|
handle.pty.resize(cols, rows);
|
|
274
277
|
}
|
|
275
278
|
function stopPtySession(sessionId) {
|
|
@@ -573,10 +576,12 @@ async function logActivity(supabase, params) {
|
|
|
573
576
|
}
|
|
574
577
|
|
|
575
578
|
// src/sessions.ts
|
|
576
|
-
async function
|
|
577
|
-
const {
|
|
579
|
+
async function getRunningSessionForProject(supabase, projectId, ownerUserId) {
|
|
580
|
+
const { data, error } = await supabase.from("sessions").select("id, card_id, agent_state, cards!card_id!inner(project_id, assigned_user_id)").eq("cards.project_id", projectId).eq("cards.assigned_user_id", ownerUserId).eq("status", "running").limit(1).maybeSingle();
|
|
578
581
|
if (error) throw error;
|
|
579
|
-
|
|
582
|
+
if (!data) return null;
|
|
583
|
+
const row = data;
|
|
584
|
+
return { id: row.id, cardId: row.card_id, agentState: row.agent_state };
|
|
580
585
|
}
|
|
581
586
|
async function listQueuedSessionsForRunner(supabase, ownerUserId) {
|
|
582
587
|
const { data, error } = await supabase.from("sessions").select(
|
|
@@ -650,7 +655,8 @@ async function updateCard(supabase, cardId, changes) {
|
|
|
650
655
|
...changes.deployError !== void 0 ? { deploy_error: changes.deployError } : {},
|
|
651
656
|
...changes.customActionStatus !== void 0 ? { custom_action_status: changes.customActionStatus } : {},
|
|
652
657
|
...changes.customActionOutput !== void 0 ? { custom_action_output: changes.customActionOutput } : {},
|
|
653
|
-
...changes.requeueRequestedAt !== void 0 ? { requeue_requested_at: changes.requeueRequestedAt } : {}
|
|
658
|
+
...changes.requeueRequestedAt !== void 0 ? { requeue_requested_at: changes.requeueRequestedAt } : {},
|
|
659
|
+
...changes.restartRequestedAt !== void 0 ? { restart_requested_at: changes.restartRequestedAt } : {}
|
|
654
660
|
}).eq("id", cardId);
|
|
655
661
|
if (error) throw error;
|
|
656
662
|
}
|
|
@@ -666,7 +672,7 @@ async function getProjectRepos(supabase, projectId) {
|
|
|
666
672
|
}
|
|
667
673
|
async function listCardsInColumn(supabase, projectId, column) {
|
|
668
674
|
const { data, error } = await supabase.from("cards").select(
|
|
669
|
-
"id, project_id, title, instructions, column_name, assigned_user_id, deploy_error, file_references, preview_urls, custom_action_status, requeue_requested_at"
|
|
675
|
+
"id, project_id, title, instructions, column_name, assigned_user_id, deploy_error, file_references, preview_urls, custom_action_status, requeue_requested_at, restart_requested_at"
|
|
670
676
|
).eq("project_id", projectId).eq("column_name", column);
|
|
671
677
|
if (error) throw error;
|
|
672
678
|
return data;
|
|
@@ -749,9 +755,9 @@ async function upsertClaudeUsage(supabase, userId, changes) {
|
|
|
749
755
|
|
|
750
756
|
// src/usageMonitor.ts
|
|
751
757
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
752
|
-
import {
|
|
753
|
-
import {
|
|
754
|
-
import { join as
|
|
758
|
+
import { mkdirSync } from "node:fs";
|
|
759
|
+
import { tmpdir as tmpdir5 } from "node:os";
|
|
760
|
+
import { join as join6 } from "node:path";
|
|
755
761
|
import { spawn as spawn2 } from "node-pty";
|
|
756
762
|
|
|
757
763
|
// src/agentMomentHookScript.ts
|
|
@@ -1101,38 +1107,79 @@ function claudeCliSettingsArgs() {
|
|
|
1101
1107
|
];
|
|
1102
1108
|
}
|
|
1103
1109
|
|
|
1104
|
-
// src/
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
var IDLE_MS = 2e3;
|
|
1110
|
-
var HARD_TIMEOUT_MS = 6e4;
|
|
1111
|
-
function usageMonitorSessionId() {
|
|
1112
|
-
return SESSION_ID;
|
|
1113
|
-
}
|
|
1114
|
-
function usageMonitorTicket() {
|
|
1115
|
-
return TICKET;
|
|
1116
|
-
}
|
|
1117
|
-
function ensureScratchDirTrusted() {
|
|
1110
|
+
// src/claudeTrust.ts
|
|
1111
|
+
import { existsSync, readFileSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1112
|
+
import { homedir as homedir2 } from "node:os";
|
|
1113
|
+
import { join as join5 } from "node:path";
|
|
1114
|
+
function ensureDirTrusted(dirPath) {
|
|
1118
1115
|
const claudeJsonPath = join5(homedir2(), ".claude.json");
|
|
1119
1116
|
if (!existsSync(claudeJsonPath)) return;
|
|
1120
1117
|
try {
|
|
1121
1118
|
const raw = readFileSync(claudeJsonPath, "utf8");
|
|
1122
1119
|
const data = JSON.parse(raw);
|
|
1123
1120
|
data.projects ??= {};
|
|
1124
|
-
const keys = [
|
|
1121
|
+
const keys = [dirPath, dirPath.replace(/\\/g, "/")];
|
|
1125
1122
|
const alreadyTrusted = keys.every((key) => data.projects[key]?.hasTrustDialogAccepted === true);
|
|
1126
1123
|
if (alreadyTrusted) return;
|
|
1127
1124
|
for (const key of keys) {
|
|
1128
1125
|
data.projects[key] = { ...data.projects[key], hasTrustDialogAccepted: true };
|
|
1129
1126
|
}
|
|
1130
1127
|
writeFileSync4(claudeJsonPath, JSON.stringify(data, null, 2));
|
|
1131
|
-
logger.runner.info("
|
|
1128
|
+
logger.runner.info("TRUST", `Pre-trusted ${dirPath} in ~/.claude.json.`);
|
|
1129
|
+
} catch (err) {
|
|
1130
|
+
logger.runner.error("TRUST", `Failed to pre-trust ${dirPath}:`, err);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
function ensureMcpServersTrusted(dirPath) {
|
|
1134
|
+
const mcpJsonPath = join5(dirPath, ".mcp.json");
|
|
1135
|
+
if (!existsSync(mcpJsonPath)) return;
|
|
1136
|
+
let serverNames;
|
|
1137
|
+
try {
|
|
1138
|
+
const mcpConfig = JSON.parse(readFileSync(mcpJsonPath, "utf8"));
|
|
1139
|
+
serverNames = Object.keys(mcpConfig.mcpServers ?? {});
|
|
1140
|
+
} catch (err) {
|
|
1141
|
+
logger.runner.error("TRUST", `Failed to read ${mcpJsonPath}:`, err);
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
if (serverNames.length === 0) return;
|
|
1145
|
+
const claudeJsonPath = join5(homedir2(), ".claude.json");
|
|
1146
|
+
if (!existsSync(claudeJsonPath)) return;
|
|
1147
|
+
try {
|
|
1148
|
+
const raw = readFileSync(claudeJsonPath, "utf8");
|
|
1149
|
+
const data = JSON.parse(raw);
|
|
1150
|
+
data.projects ??= {};
|
|
1151
|
+
const keys = [dirPath, dirPath.replace(/\\/g, "/")];
|
|
1152
|
+
const alreadyApproved = keys.every(
|
|
1153
|
+
(key) => serverNames.every((name) => data.projects[key]?.enabledMcpjsonServers?.includes(name))
|
|
1154
|
+
);
|
|
1155
|
+
if (alreadyApproved) return;
|
|
1156
|
+
for (const key of keys) {
|
|
1157
|
+
const project = data.projects[key] ??= {};
|
|
1158
|
+
project.enabledMcpjsonServers = [.../* @__PURE__ */ new Set([...project.enabledMcpjsonServers ?? [], ...serverNames])];
|
|
1159
|
+
project.disabledMcpjsonServers = (project.disabledMcpjsonServers ?? []).filter(
|
|
1160
|
+
(name) => !serverNames.includes(name)
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
writeFileSync4(claudeJsonPath, JSON.stringify(data, null, 2));
|
|
1164
|
+
logger.runner.info("TRUST", `Pre-approved MCP servers [${serverNames.join(", ")}] for ${dirPath}.`);
|
|
1132
1165
|
} catch (err) {
|
|
1133
|
-
logger.runner.error("
|
|
1166
|
+
logger.runner.error("TRUST", `Failed to pre-approve MCP servers for ${dirPath}:`, err);
|
|
1134
1167
|
}
|
|
1135
1168
|
}
|
|
1169
|
+
|
|
1170
|
+
// src/usageMonitor.ts
|
|
1171
|
+
var SESSION_ID = "usage-monitor";
|
|
1172
|
+
var TICKET = randomUUID3();
|
|
1173
|
+
var SCRATCH_DIR = join6(tmpdir5(), "agent-board-usage-monitor");
|
|
1174
|
+
var POLL_INTERVAL_MS = Number(process.env.AGENT_BOARD_USAGE_POLL_INTERVAL_MS ?? 15 * 6e4);
|
|
1175
|
+
var IDLE_MS = 2e3;
|
|
1176
|
+
var HARD_TIMEOUT_MS = 6e4;
|
|
1177
|
+
function usageMonitorSessionId() {
|
|
1178
|
+
return SESSION_ID;
|
|
1179
|
+
}
|
|
1180
|
+
function usageMonitorTicket() {
|
|
1181
|
+
return TICKET;
|
|
1182
|
+
}
|
|
1136
1183
|
function pollUsageOnce() {
|
|
1137
1184
|
let pty;
|
|
1138
1185
|
try {
|
|
@@ -1179,7 +1226,7 @@ function pollUsageOnce() {
|
|
|
1179
1226
|
}
|
|
1180
1227
|
function startUsageMonitor() {
|
|
1181
1228
|
mkdirSync(SCRATCH_DIR, { recursive: true });
|
|
1182
|
-
|
|
1229
|
+
ensureDirTrusted(SCRATCH_DIR);
|
|
1183
1230
|
pollUsageOnce();
|
|
1184
1231
|
setInterval(pollUsageOnce, POLL_INTERVAL_MS);
|
|
1185
1232
|
}
|
|
@@ -1195,7 +1242,7 @@ var IGNORED_ENTRY_NAMES = /* @__PURE__ */ new Set([".git", ".claude", "node_modu
|
|
|
1195
1242
|
var AGENT_SAFE_SUBDIR_NAMES = /* @__PURE__ */ new Set(["skills", "commands", "agents", "rules"]);
|
|
1196
1243
|
function ensureSkillsDirExists(claudeDir) {
|
|
1197
1244
|
try {
|
|
1198
|
-
mkdirSync2(
|
|
1245
|
+
mkdirSync2(join7(claudeDir, "skills"), { recursive: true });
|
|
1199
1246
|
} catch (err) {
|
|
1200
1247
|
logger.runner.warn("FILES", "Could not create .claude/skills:", err);
|
|
1201
1248
|
}
|
|
@@ -1322,7 +1369,7 @@ function listSafeAgentDirectory(baseDir, relPath, atClaudeRoot) {
|
|
|
1322
1369
|
}
|
|
1323
1370
|
const isRepoRootLevel = segments.length === 0 || segments.length === 1 && segments[0] !== ".claude";
|
|
1324
1371
|
if (isRepoRootLevel) {
|
|
1325
|
-
ensureSkillsDirExists(
|
|
1372
|
+
ensureSkillsDirExists(join7(baseDir, relPath, ".claude"));
|
|
1326
1373
|
const entries2 = listDirectory(baseDir, relPath, { includeDotClaude: true });
|
|
1327
1374
|
return entries2.filter((entry) => entry.type === "file" && entry.name === "CLAUDE.md" || entry.type === "dir" && entry.name === ".claude");
|
|
1328
1375
|
}
|
|
@@ -1353,7 +1400,7 @@ function assertWritableAgentPath(relPath, atClaudeRoot) {
|
|
|
1353
1400
|
function listDirectoryForBrowse(rootPath, relPath) {
|
|
1354
1401
|
return listDirectory(rootPath, relPath).map((entry) => ({
|
|
1355
1402
|
...entry,
|
|
1356
|
-
hasGit: entry.type === "dir" && existsSync2(
|
|
1403
|
+
hasGit: entry.type === "dir" && existsSync2(join7(rootPath, entry.path, ".git"))
|
|
1357
1404
|
}));
|
|
1358
1405
|
}
|
|
1359
1406
|
var SAFE_CLONE_URL_PATTERN = /^(https?:\/\/|git:\/\/|ssh:\/\/|git@[\w.-]+:)\S+$/i;
|
|
@@ -1391,7 +1438,7 @@ async function handleCloneRequest(req, res, query) {
|
|
|
1391
1438
|
sendJson(res, 400, { error: "N\xE3o foi poss\xEDvel determinar o nome da pasta a partir da URL." });
|
|
1392
1439
|
return;
|
|
1393
1440
|
}
|
|
1394
|
-
const destPath =
|
|
1441
|
+
const destPath = join7(setupEntry.rootPath, folderName);
|
|
1395
1442
|
if (existsSync2(destPath)) {
|
|
1396
1443
|
sendJson(res, 409, { error: `J\xE1 existe uma pasta "${folderName}" na raiz permitida.` });
|
|
1397
1444
|
return;
|
|
@@ -1459,7 +1506,7 @@ function findBrandIconPath(repoRoot) {
|
|
|
1459
1506
|
if (entry.isSymbolicLink() || IGNORED_ENTRY_NAMES.has(entry.name)) continue;
|
|
1460
1507
|
const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
1461
1508
|
if (entry.isDirectory()) {
|
|
1462
|
-
walk(
|
|
1509
|
+
walk(join7(dir, entry.name), relPath, depth + 1);
|
|
1463
1510
|
} else if (entry.isFile()) {
|
|
1464
1511
|
const match = patterns.find(({ pattern }) => pattern.test(entry.name));
|
|
1465
1512
|
if (match) candidatesByBaseName.get(match.baseName).push({ relPath, depth });
|
|
@@ -1624,7 +1671,7 @@ async function getRepoDiffForTarget(label, cwd) {
|
|
|
1624
1671
|
async function handleDiffRequest(entry, res, projectId) {
|
|
1625
1672
|
try {
|
|
1626
1673
|
const children = statuslineSupabase && projectId ? await getProjectRepos(statuslineSupabase, projectId) : [];
|
|
1627
|
-
const targets = children.length > 0 ? children.map((child2) => ({ label: child2.subpath, cwd:
|
|
1674
|
+
const targets = children.length > 0 ? children.map((child2) => ({ label: child2.subpath, cwd: join7(entry.repoRoot, child2.subpath) })) : [{ label: null, cwd: entry.repoRoot }];
|
|
1628
1675
|
const repos = await Promise.all(targets.map((target) => getRepoDiffForTarget(target.label, target.cwd)));
|
|
1629
1676
|
sendJson(res, 200, { repos });
|
|
1630
1677
|
} catch (err) {
|
|
@@ -1933,15 +1980,15 @@ function attachAgentEventsServer() {
|
|
|
1933
1980
|
// src/auth.ts
|
|
1934
1981
|
import { createClient } from "@supabase/supabase-js";
|
|
1935
1982
|
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "node:fs";
|
|
1936
|
-
import { join as
|
|
1983
|
+
import { join as join9 } from "node:path";
|
|
1937
1984
|
import open from "open";
|
|
1938
1985
|
|
|
1939
1986
|
// src/config.ts
|
|
1940
1987
|
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync6 } from "node:fs";
|
|
1941
1988
|
import { hostname, homedir as homedir4 } from "node:os";
|
|
1942
|
-
import { join as
|
|
1943
|
-
var CONFIG_DIR =
|
|
1944
|
-
var CONFIG_FILE =
|
|
1989
|
+
import { join as join8, resolve as resolve2 } from "node:path";
|
|
1990
|
+
var CONFIG_DIR = join8(homedir4(), ".agent-board");
|
|
1991
|
+
var CONFIG_FILE = join8(CONFIG_DIR, "credentials.json");
|
|
1945
1992
|
function loadConfig() {
|
|
1946
1993
|
if (!existsSync3(CONFIG_FILE)) return null;
|
|
1947
1994
|
return JSON.parse(readFileSync3(CONFIG_FILE, "utf-8"));
|
|
@@ -1974,7 +2021,7 @@ function resolveRepoPath(config, relativeRepoPath) {
|
|
|
1974
2021
|
}
|
|
1975
2022
|
|
|
1976
2023
|
// src/auth.ts
|
|
1977
|
-
var SESSION_FILE =
|
|
2024
|
+
var SESSION_FILE = join9(CONFIG_DIR, "session.json");
|
|
1978
2025
|
var DEFAULT_SUPABASE_URL = "https://brpxshvnmxgkbffgdfcc.supabase.co";
|
|
1979
2026
|
var DEFAULT_SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJycHhzaHZubXhna2JmZmdkZmNjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODc1OTQwMTEsImV4cCI6MjEwMzE3MDAxMX0.ErsAGZysLFgNvijetR7w1YvIziXC-1D2sx04Hm2UW9c";
|
|
1980
2027
|
function resolveSupabaseUrl() {
|
|
@@ -2033,7 +2080,7 @@ async function requireSession(supabase) {
|
|
|
2033
2080
|
|
|
2034
2081
|
// src/columnActions.ts
|
|
2035
2082
|
import { exec } from "node:child_process";
|
|
2036
|
-
import { join as
|
|
2083
|
+
import { join as join10 } from "node:path";
|
|
2037
2084
|
import { promisify as promisify3 } from "node:util";
|
|
2038
2085
|
var execAsync = promisify3(exec);
|
|
2039
2086
|
var OUTPUT_TAIL_CHARS = 4e3;
|
|
@@ -2043,7 +2090,7 @@ function tail(text) {
|
|
|
2043
2090
|
function resolveTargets(project, children, repoRoot, column, timing) {
|
|
2044
2091
|
if (children.length > 0) {
|
|
2045
2092
|
return children.flatMap(
|
|
2046
|
-
(child2) => (child2.column_actions[column] ?? []).filter((action) => action.timing === timing).map((action) => ({ label: child2.subpath, cwd:
|
|
2093
|
+
(child2) => (child2.column_actions[column] ?? []).filter((action) => action.timing === timing).map((action) => ({ label: child2.subpath, cwd: join10(repoRoot, child2.subpath), command: action.command }))
|
|
2047
2094
|
);
|
|
2048
2095
|
}
|
|
2049
2096
|
return (project.column_actions[column] ?? []).filter((action) => action.timing === timing).map((action) => ({ label: null, cwd: repoRoot, command: action.command }));
|
|
@@ -2149,7 +2196,7 @@ async function checkPendingDeployActionReruns(supabase, config) {
|
|
|
2149
2196
|
}
|
|
2150
2197
|
|
|
2151
2198
|
// src/deploy.ts
|
|
2152
|
-
import { join as
|
|
2199
|
+
import { join as join13 } from "node:path";
|
|
2153
2200
|
|
|
2154
2201
|
// src/devServer.ts
|
|
2155
2202
|
import { spawn as spawn4 } from "node:child_process";
|
|
@@ -2158,7 +2205,7 @@ import { spawn as spawn4 } from "node:child_process";
|
|
|
2158
2205
|
import { spawn as spawn3 } from "node:child_process";
|
|
2159
2206
|
import { accessSync, constants } from "node:fs";
|
|
2160
2207
|
import { homedir as homedir5 } from "node:os";
|
|
2161
|
-
import { delimiter, join as
|
|
2208
|
+
import { delimiter, join as join11 } from "node:path";
|
|
2162
2209
|
import { bin as bundledBinaryPath, install as downloadCloudflared } from "cloudflared";
|
|
2163
2210
|
var TRYCLOUDFLARE_URL_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
|
|
2164
2211
|
var STARTUP_TIMEOUT_MS = 2e4;
|
|
@@ -2169,15 +2216,15 @@ function wellKnownDirectories() {
|
|
|
2169
2216
|
const home = homedir5();
|
|
2170
2217
|
if (IS_WINDOWS2) {
|
|
2171
2218
|
const candidates = [
|
|
2172
|
-
process.env.ProgramFiles &&
|
|
2173
|
-
process.env["ProgramFiles(x86)"] &&
|
|
2174
|
-
process.env.LOCALAPPDATA &&
|
|
2175
|
-
|
|
2176
|
-
process.env.ChocolateyInstall &&
|
|
2219
|
+
process.env.ProgramFiles && join11(process.env.ProgramFiles, "cloudflared"),
|
|
2220
|
+
process.env["ProgramFiles(x86)"] && join11(process.env["ProgramFiles(x86)"], "cloudflared"),
|
|
2221
|
+
process.env.LOCALAPPDATA && join11(process.env.LOCALAPPDATA, "Microsoft", "WinGet", "Links"),
|
|
2222
|
+
join11(home, "scoop", "shims"),
|
|
2223
|
+
process.env.ChocolateyInstall && join11(process.env.ChocolateyInstall, "bin")
|
|
2177
2224
|
];
|
|
2178
2225
|
return candidates.filter((dir) => Boolean(dir));
|
|
2179
2226
|
}
|
|
2180
|
-
return ["/usr/local/bin", "/usr/bin", "/opt/homebrew/bin",
|
|
2227
|
+
return ["/usr/local/bin", "/usr/bin", "/opt/homebrew/bin", join11(home, ".local", "bin")];
|
|
2181
2228
|
}
|
|
2182
2229
|
function pathDirectories() {
|
|
2183
2230
|
return (process.env.PATH ?? "").split(delimiter).map((dir) => dir.trim().replace(/^"|"$/g, "")).filter(Boolean);
|
|
@@ -2193,7 +2240,7 @@ function isExecutableFile(path) {
|
|
|
2193
2240
|
function findInstalledBinary() {
|
|
2194
2241
|
const override = process.env.AGENT_BOARD_CLOUDFLARED_PATH;
|
|
2195
2242
|
const directories = [...pathDirectories(), ...wellKnownDirectories()];
|
|
2196
|
-
const candidates = [...override ? [override] : [], ...directories.map((dir) =>
|
|
2243
|
+
const candidates = [...override ? [override] : [], ...directories.map((dir) => join11(dir, BINARY_NAME))];
|
|
2197
2244
|
return candidates.find(isExecutableFile) ?? null;
|
|
2198
2245
|
}
|
|
2199
2246
|
async function locateCloudflared() {
|
|
@@ -2424,6 +2471,8 @@ var SHIFT_TAB_PRESSES_TO_PLAN_MODE = 4;
|
|
|
2424
2471
|
var SHIFT_TAB_DELAY_MS = 400;
|
|
2425
2472
|
var PLAN_MODE_INDICATOR = /plan mode on/i;
|
|
2426
2473
|
function spawnAgentPty(repoPath, cardId, cardTitle, instructions, sessionId, ticket, planFirst) {
|
|
2474
|
+
ensureDirTrusted(repoPath);
|
|
2475
|
+
ensureMcpServersTrusted(repoPath);
|
|
2427
2476
|
const claudeSessionId = randomUUID5();
|
|
2428
2477
|
const pty = spawn5(
|
|
2429
2478
|
CLAUDE_BINARY,
|
|
@@ -2520,6 +2569,8 @@ function sendContinue(pty, cardId) {
|
|
|
2520
2569
|
}
|
|
2521
2570
|
}
|
|
2522
2571
|
function resumeAgentPty(repoPath, claudeSessionId, sessionId, ticket) {
|
|
2572
|
+
ensureDirTrusted(repoPath);
|
|
2573
|
+
ensureMcpServersTrusted(repoPath);
|
|
2523
2574
|
return spawn5(
|
|
2524
2575
|
CLAUDE_BINARY,
|
|
2525
2576
|
[
|
|
@@ -2543,8 +2594,8 @@ function resumeAgentPty(repoPath, claudeSessionId, sessionId, ticket) {
|
|
|
2543
2594
|
// src/cardAttachments.ts
|
|
2544
2595
|
import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
|
|
2545
2596
|
import { tmpdir as tmpdir6 } from "node:os";
|
|
2546
|
-
import { extname, join as
|
|
2547
|
-
var ATTACHMENTS_ROOT =
|
|
2597
|
+
import { extname, join as join12 } from "node:path";
|
|
2598
|
+
var ATTACHMENTS_ROOT = join12(tmpdir6(), "agent-board", "card-attachments");
|
|
2548
2599
|
function sanitizeFileName(fileName) {
|
|
2549
2600
|
return fileName.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
2550
2601
|
}
|
|
@@ -2556,13 +2607,13 @@ async function downloadCardAttachments(supabase, cardId) {
|
|
|
2556
2607
|
}
|
|
2557
2608
|
const rows = data ?? [];
|
|
2558
2609
|
if (rows.length === 0) return [];
|
|
2559
|
-
const targetDir =
|
|
2610
|
+
const targetDir = join12(ATTACHMENTS_ROOT, cardId);
|
|
2560
2611
|
await mkdir2(targetDir, { recursive: true });
|
|
2561
2612
|
const paths = [];
|
|
2562
2613
|
for (const row of rows) {
|
|
2563
2614
|
const baseName = sanitizeFileName(row.file_name);
|
|
2564
2615
|
const fileName = extname(baseName) ? baseName : `${baseName}${extname(row.storage_path)}`;
|
|
2565
|
-
const localPath =
|
|
2616
|
+
const localPath = join12(targetDir, `${row.id}-${fileName}`);
|
|
2566
2617
|
try {
|
|
2567
2618
|
const { data: blob, error: downloadError } = await supabase.storage.from(CARD_ATTACHMENTS_BUCKET).download(row.storage_path);
|
|
2568
2619
|
if (downloadError) throw downloadError;
|
|
@@ -2636,6 +2687,7 @@ function cancelUsageResume(sessionId) {
|
|
|
2636
2687
|
|
|
2637
2688
|
// src/orchestrator.ts
|
|
2638
2689
|
var TERMINAL_TICKET_TTL_MS = 60 * 60 * 1e3;
|
|
2690
|
+
var RESUME_CONVERSATION_MISSING_PATTERN = /no conversation found with session id/i;
|
|
2639
2691
|
var inFlightCardIds = /* @__PURE__ */ new Set();
|
|
2640
2692
|
var activeSessionByCard = /* @__PURE__ */ new Map();
|
|
2641
2693
|
var intentionallyStoppedSessionIds = /* @__PURE__ */ new Set();
|
|
@@ -2709,11 +2761,17 @@ async function dispatchRunnerQueue(supabase, config) {
|
|
|
2709
2761
|
try {
|
|
2710
2762
|
const queued = await listQueuedSessionsForRunner(supabase, config.ownerUserId);
|
|
2711
2763
|
if (queued.length === 0) return;
|
|
2764
|
+
const queuedProjectIds = new Set(queued.map((entry) => entry.cards.project_id));
|
|
2712
2765
|
const maxConcurrentProjects = await getRunnerMaxConcurrentProjects(supabase, config.runnerId);
|
|
2713
2766
|
const runningProjectIds = /* @__PURE__ */ new Set();
|
|
2714
2767
|
for (const project of config.registeredProjects) {
|
|
2715
|
-
const
|
|
2716
|
-
if (
|
|
2768
|
+
const running = await getRunningSessionForProject(supabase, project.id, config.ownerUserId);
|
|
2769
|
+
if (!running) continue;
|
|
2770
|
+
if (running.agentState === "done" && queuedProjectIds.has(project.id)) {
|
|
2771
|
+
await pauseFinishedSession(supabase, config, project.id, running.cardId, running.id);
|
|
2772
|
+
continue;
|
|
2773
|
+
}
|
|
2774
|
+
runningProjectIds.add(project.id);
|
|
2717
2775
|
}
|
|
2718
2776
|
let freeProjectSlots = maxConcurrentProjects - runningProjectIds.size;
|
|
2719
2777
|
if (freeProjectSlots <= 0) return;
|
|
@@ -2767,9 +2825,13 @@ async function spawnSessionPty(supabase, config, entry) {
|
|
|
2767
2825
|
const ticket = randomUUID6();
|
|
2768
2826
|
let pty;
|
|
2769
2827
|
let newClaudeSessionId = null;
|
|
2828
|
+
let resumeConversationMissing = false;
|
|
2770
2829
|
try {
|
|
2771
2830
|
if (claudeSessionId) {
|
|
2772
2831
|
pty = resumeAgentPty(registered.repoPath, claudeSessionId, sessionId, ticket);
|
|
2832
|
+
pty.onData((chunk) => {
|
|
2833
|
+
if (RESUME_CONVERSATION_MISSING_PATTERN.test(normalizeTerminalOutput(chunk))) resumeConversationMissing = true;
|
|
2834
|
+
});
|
|
2773
2835
|
} else {
|
|
2774
2836
|
const attachmentPaths = await downloadCardAttachments(supabase, cardId);
|
|
2775
2837
|
const instructions = buildInstructionsWithMcpContext(
|
|
@@ -2817,11 +2879,24 @@ async function spawnSessionPty(supabase, config, entry) {
|
|
|
2817
2879
|
);
|
|
2818
2880
|
pty.onExit(({ exitCode }) => {
|
|
2819
2881
|
const wasIntentional = intentionallyStoppedSessionIds.delete(sessionId);
|
|
2820
|
-
logger.card.warn(cardId, `Session exited (code ${exitCode}) \u2014 this only happens if the process crashed or was killed, the card doesn't auto-advance.`);
|
|
2821
2882
|
activityWatcher.stop();
|
|
2822
2883
|
unregisterAgentActivityWatcher(sessionId);
|
|
2823
2884
|
cancelUsageResume(sessionId);
|
|
2824
2885
|
activeSessionByCard.delete(cardId);
|
|
2886
|
+
if (resumeConversationMissing) {
|
|
2887
|
+
logger.card.warn(
|
|
2888
|
+
cardId,
|
|
2889
|
+
`Session ${sessionId}: --resume ${claudeSessionId} found no such conversation \u2014 clearing it so the next attempt starts a fresh one instead of repeating the same failure.`
|
|
2890
|
+
);
|
|
2891
|
+
void updateSession(
|
|
2892
|
+
supabase,
|
|
2893
|
+
sessionId,
|
|
2894
|
+
{ status: "failed", endedAt: (/* @__PURE__ */ new Date()).toISOString(), claudeSessionId: null },
|
|
2895
|
+
{ projectId: card.project_id, cardId, userId: config.ownerUserId }
|
|
2896
|
+
);
|
|
2897
|
+
return;
|
|
2898
|
+
}
|
|
2899
|
+
logger.card.warn(cardId, `Session exited (code ${exitCode}) \u2014 this only happens if the process crashed or was killed, the card doesn't auto-advance.`);
|
|
2825
2900
|
if (!wasIntentional && exitCode !== 0) {
|
|
2826
2901
|
publishAgentState(supabase, card.project_id, cardId, sessionId, "error");
|
|
2827
2902
|
}
|
|
@@ -2842,7 +2917,7 @@ function watchAgentActivity(supabase, config, projectId, cardId, sessionId, pty)
|
|
|
2842
2917
|
} else {
|
|
2843
2918
|
cancelUsageResume(sessionId);
|
|
2844
2919
|
}
|
|
2845
|
-
if (state === "done") void
|
|
2920
|
+
if (state === "done") void dispatchRunnerQueue(supabase, config);
|
|
2846
2921
|
});
|
|
2847
2922
|
pty.onData((chunk) => watcher.push(chunk));
|
|
2848
2923
|
registerAgentActivityWatcher(sessionId, watcher);
|
|
@@ -2852,8 +2927,7 @@ async function pauseFinishedSession(supabase, config, projectId, cardId, session
|
|
|
2852
2927
|
const paused = await pauseRunningSession(supabase, sessionId, { projectId, cardId, userId: config.ownerUserId });
|
|
2853
2928
|
if (!paused) return;
|
|
2854
2929
|
setPtySessionReadOnly(sessionId, true);
|
|
2855
|
-
logger.card.info(cardId, `Session ${sessionId} paused (agent reached "done"
|
|
2856
|
-
await dispatchRunnerQueue(supabase, config);
|
|
2930
|
+
logger.card.info(cardId, `Session ${sessionId} paused (agent reached "done", another session needs this project's WIP slot).`);
|
|
2857
2931
|
}
|
|
2858
2932
|
async function requeuePausedCard(supabase, config, card) {
|
|
2859
2933
|
const active = await getActiveSessionForCard(supabase, card.id);
|
|
@@ -2931,6 +3005,24 @@ async function stopCardSession(supabase, config, card) {
|
|
|
2931
3005
|
logger.card.info(card.id, `Session ${sessionId} stopped.`);
|
|
2932
3006
|
await dispatchRunnerQueue(supabase, config);
|
|
2933
3007
|
}
|
|
3008
|
+
async function restartCardSession(supabase, config, card) {
|
|
3009
|
+
const sessionId = activeSessionByCard.get(card.id) ?? (await getActiveSessionForCard(supabase, card.id))?.id;
|
|
3010
|
+
if (sessionId) {
|
|
3011
|
+
intentionallyStoppedSessionIds.add(sessionId);
|
|
3012
|
+
stopPtySession(sessionId);
|
|
3013
|
+
cancelUsageResume(sessionId);
|
|
3014
|
+
activeSessionByCard.delete(card.id);
|
|
3015
|
+
await updateSession(
|
|
3016
|
+
supabase,
|
|
3017
|
+
sessionId,
|
|
3018
|
+
{ status: "failed", claudeSessionId: null, endedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
3019
|
+
{ projectId: card.project_id, cardId: card.id, userId: config.ownerUserId }
|
|
3020
|
+
);
|
|
3021
|
+
logger.card.info(card.id, `Session ${sessionId} abandoned by "Reiniciar sess\xE3o"; starting a fresh one.`);
|
|
3022
|
+
}
|
|
3023
|
+
await updateCard(supabase, card.id, { restartRequestedAt: null });
|
|
3024
|
+
await enqueueCard(supabase, config, card);
|
|
3025
|
+
}
|
|
2934
3026
|
|
|
2935
3027
|
// src/deploy.ts
|
|
2936
3028
|
async function handleDeployRequested(supabase, config, project) {
|
|
@@ -2948,7 +3040,7 @@ async function handleDeployRequested(supabase, config, project) {
|
|
|
2948
3040
|
}
|
|
2949
3041
|
const children = await getProjectRepos(supabase, project.id);
|
|
2950
3042
|
const repoRoot = resolveRepoPath(config, project.repo_path);
|
|
2951
|
-
const targets = children.length > 0 ? children.map((child2) => ({ label: child2.subpath, path:
|
|
3043
|
+
const targets = children.length > 0 ? children.map((child2) => ({ label: child2.subpath, path: join13(repoRoot, child2.subpath) })) : [{ label: null, path: repoRoot }];
|
|
2952
3044
|
const failures = [];
|
|
2953
3045
|
for (const target of targets) {
|
|
2954
3046
|
const result = await pushBranchToMain(target.path);
|
|
@@ -3082,7 +3174,7 @@ async function syncRegisteredProjects(supabase, config, registeredPaths) {
|
|
|
3082
3174
|
}
|
|
3083
3175
|
|
|
3084
3176
|
// src/review.ts
|
|
3085
|
-
import { join as
|
|
3177
|
+
import { join as join14 } from "node:path";
|
|
3086
3178
|
var inFlightCardIds2 = /* @__PURE__ */ new Set();
|
|
3087
3179
|
async function handleCardEnteredRevisao(supabase, config, card) {
|
|
3088
3180
|
if (inFlightCardIds2.has(card.id)) return;
|
|
@@ -3091,7 +3183,7 @@ async function handleCardEnteredRevisao(supabase, config, card) {
|
|
|
3091
3183
|
const project = await getProject(supabase, card.project_id);
|
|
3092
3184
|
const children = await getProjectRepos(supabase, project.id);
|
|
3093
3185
|
const repoRoot = resolveRepoPath(config, project.repo_path);
|
|
3094
|
-
const targets = children.length > 0 ? children.filter((child2) => child2.dev_command).map((child2) => ({ label: child2.subpath, cwd:
|
|
3186
|
+
const targets = children.length > 0 ? children.filter((child2) => child2.dev_command).map((child2) => ({ label: child2.subpath, cwd: join14(repoRoot, child2.subpath), command: child2.dev_command })) : project.dev_command ? [{ label: "", cwd: repoRoot, command: project.dev_command }] : [];
|
|
3095
3187
|
if (targets.length === 0) {
|
|
3096
3188
|
logger.card.info(card.id, `No dev_command configured for project ${project.id}, skipping automatic preview.`);
|
|
3097
3189
|
return;
|
|
@@ -3177,7 +3269,7 @@ function attachTerminalServer() {
|
|
|
3177
3269
|
const buffer = data;
|
|
3178
3270
|
const frameType = buffer[0];
|
|
3179
3271
|
if (frameType === FRAME_TYPE_IMAGE) {
|
|
3180
|
-
if (handle.readOnly) return;
|
|
3272
|
+
if (handle.readOnly || handle.exited) return;
|
|
3181
3273
|
const extCode = buffer[1];
|
|
3182
3274
|
const bytes = buffer.subarray(2);
|
|
3183
3275
|
savePastedImage(sessionId, extCode, bytes).then((path) => handle.pty.write(`${path} `)).catch((err) => logger.runner.warn("TERMINAL", `Terminal WS: failed to save pasted image for session ${sessionId}:`, err));
|
|
@@ -3195,7 +3287,7 @@ function attachTerminalServer() {
|
|
|
3195
3287
|
logger.runner.warn("TERMINAL", `Terminal WS: ignoring binary frame with unknown type ${frameType} for session ${sessionId}.`);
|
|
3196
3288
|
return;
|
|
3197
3289
|
}
|
|
3198
|
-
if (handle.readOnly) return;
|
|
3290
|
+
if (handle.readOnly || handle.exited) return;
|
|
3199
3291
|
handle.pty.write(data.toString());
|
|
3200
3292
|
});
|
|
3201
3293
|
socket.on("close", () => {
|
|
@@ -3317,6 +3409,7 @@ async function startRunner() {
|
|
|
3317
3409
|
const registeredPaths = new Set(config.registeredProjects.map((project) => project.id));
|
|
3318
3410
|
const lastKnownColumn = /* @__PURE__ */ new Map();
|
|
3319
3411
|
const lastKnownRequeueRequestedAt = /* @__PURE__ */ new Map();
|
|
3412
|
+
const lastKnownRestartRequestedAt = /* @__PURE__ */ new Map();
|
|
3320
3413
|
await syncRegisteredProjects(supabase, config, registeredPaths).catch((err) => logger.runner.error("PROJECT_SYNC", "syncRegisteredProjects failed:", err));
|
|
3321
3414
|
logger.runner.info("BOOT", `Agent Board runner "${config.runnerName}" starting (${registeredPaths.size} project(s) registered).`);
|
|
3322
3415
|
for (const project of config.registeredProjects) {
|
|
@@ -3412,6 +3505,11 @@ async function startRunner() {
|
|
|
3412
3505
|
if (card.requeue_requested_at && card.requeue_requested_at !== previousRequeueRequestedAt) {
|
|
3413
3506
|
void requeuePausedCard(supabase, config, card);
|
|
3414
3507
|
}
|
|
3508
|
+
const previousRestartRequestedAt = lastKnownRestartRequestedAt.get(card.id);
|
|
3509
|
+
lastKnownRestartRequestedAt.set(card.id, card.restart_requested_at);
|
|
3510
|
+
if (card.restart_requested_at && card.restart_requested_at !== previousRestartRequestedAt) {
|
|
3511
|
+
void restartCardSession(supabase, config, card);
|
|
3512
|
+
}
|
|
3415
3513
|
if (previousColumn !== void 0 && previousColumn !== card.column_name) {
|
|
3416
3514
|
void clearColumnActionStatus(supabase, card);
|
|
3417
3515
|
void handleColumnEntered(supabase, config, card);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-board-runner",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Local runner for Agent Board — signs in with your own Supabase account (no shared secrets) and drives Claude Code against repos under your local repos root.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|