agent-board-runner 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +168 -65
- package/dist/index.js +173 -70
- 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
|
|
@@ -1163,11 +1169,16 @@ function formatPercent(value) {
|
|
|
1163
1169
|
}
|
|
1164
1170
|
|
|
1165
1171
|
function buildDisplayText(data) {
|
|
1166
|
-
const rateLimits = data && data.rate_limits;
|
|
1167
|
-
if (!rateLimits) return "";
|
|
1168
1172
|
const parts = [];
|
|
1169
|
-
|
|
1170
|
-
if (
|
|
1173
|
+
const contextWindow = data && data.context_window;
|
|
1174
|
+
if (contextWindow && typeof contextWindow.used_percentage === "number") {
|
|
1175
|
+
parts.push(\`ctx \${formatPercent(contextWindow.used_percentage)}\`);
|
|
1176
|
+
}
|
|
1177
|
+
const rateLimits = data && data.rate_limits;
|
|
1178
|
+
if (rateLimits) {
|
|
1179
|
+
if (rateLimits.five_hour) parts.push(\`5h \${formatPercent(rateLimits.five_hour.used_percentage)}\`);
|
|
1180
|
+
if (rateLimits.seven_day) parts.push(\`7d \${formatPercent(rateLimits.seven_day.used_percentage)}\`);
|
|
1181
|
+
}
|
|
1171
1182
|
return parts.join(" \xB7 ");
|
|
1172
1183
|
}
|
|
1173
1184
|
|
|
@@ -1263,38 +1274,79 @@ function claudeCliSettingsArgs() {
|
|
|
1263
1274
|
];
|
|
1264
1275
|
}
|
|
1265
1276
|
|
|
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() {
|
|
1277
|
+
// src/claudeTrust.ts
|
|
1278
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync6 } from "node:fs";
|
|
1279
|
+
import { homedir as homedir3 } from "node:os";
|
|
1280
|
+
import { join as join7 } from "node:path";
|
|
1281
|
+
function ensureDirTrusted(dirPath) {
|
|
1280
1282
|
const claudeJsonPath = join7(homedir3(), ".claude.json");
|
|
1281
1283
|
if (!existsSync3(claudeJsonPath)) return;
|
|
1282
1284
|
try {
|
|
1283
1285
|
const raw = readFileSync3(claudeJsonPath, "utf8");
|
|
1284
1286
|
const data = JSON.parse(raw);
|
|
1285
1287
|
data.projects ??= {};
|
|
1286
|
-
const keys = [
|
|
1288
|
+
const keys = [dirPath, dirPath.replace(/\\/g, "/")];
|
|
1287
1289
|
const alreadyTrusted = keys.every((key) => data.projects[key]?.hasTrustDialogAccepted === true);
|
|
1288
1290
|
if (alreadyTrusted) return;
|
|
1289
1291
|
for (const key of keys) {
|
|
1290
1292
|
data.projects[key] = { ...data.projects[key], hasTrustDialogAccepted: true };
|
|
1291
1293
|
}
|
|
1292
1294
|
writeFileSync6(claudeJsonPath, JSON.stringify(data, null, 2));
|
|
1293
|
-
logger.runner.info("
|
|
1295
|
+
logger.runner.info("TRUST", `Pre-trusted ${dirPath} in ~/.claude.json.`);
|
|
1296
|
+
} catch (err) {
|
|
1297
|
+
logger.runner.error("TRUST", `Failed to pre-trust ${dirPath}:`, err);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
function ensureMcpServersTrusted(dirPath) {
|
|
1301
|
+
const mcpJsonPath = join7(dirPath, ".mcp.json");
|
|
1302
|
+
if (!existsSync3(mcpJsonPath)) return;
|
|
1303
|
+
let serverNames;
|
|
1304
|
+
try {
|
|
1305
|
+
const mcpConfig = JSON.parse(readFileSync3(mcpJsonPath, "utf8"));
|
|
1306
|
+
serverNames = Object.keys(mcpConfig.mcpServers ?? {});
|
|
1307
|
+
} catch (err) {
|
|
1308
|
+
logger.runner.error("TRUST", `Failed to read ${mcpJsonPath}:`, err);
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
if (serverNames.length === 0) return;
|
|
1312
|
+
const claudeJsonPath = join7(homedir3(), ".claude.json");
|
|
1313
|
+
if (!existsSync3(claudeJsonPath)) return;
|
|
1314
|
+
try {
|
|
1315
|
+
const raw = readFileSync3(claudeJsonPath, "utf8");
|
|
1316
|
+
const data = JSON.parse(raw);
|
|
1317
|
+
data.projects ??= {};
|
|
1318
|
+
const keys = [dirPath, dirPath.replace(/\\/g, "/")];
|
|
1319
|
+
const alreadyApproved = keys.every(
|
|
1320
|
+
(key) => serverNames.every((name) => data.projects[key]?.enabledMcpjsonServers?.includes(name))
|
|
1321
|
+
);
|
|
1322
|
+
if (alreadyApproved) return;
|
|
1323
|
+
for (const key of keys) {
|
|
1324
|
+
const project = data.projects[key] ??= {};
|
|
1325
|
+
project.enabledMcpjsonServers = [.../* @__PURE__ */ new Set([...project.enabledMcpjsonServers ?? [], ...serverNames])];
|
|
1326
|
+
project.disabledMcpjsonServers = (project.disabledMcpjsonServers ?? []).filter(
|
|
1327
|
+
(name) => !serverNames.includes(name)
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
writeFileSync6(claudeJsonPath, JSON.stringify(data, null, 2));
|
|
1331
|
+
logger.runner.info("TRUST", `Pre-approved MCP servers [${serverNames.join(", ")}] for ${dirPath}.`);
|
|
1294
1332
|
} catch (err) {
|
|
1295
|
-
logger.runner.error("
|
|
1333
|
+
logger.runner.error("TRUST", `Failed to pre-approve MCP servers for ${dirPath}:`, err);
|
|
1296
1334
|
}
|
|
1297
1335
|
}
|
|
1336
|
+
|
|
1337
|
+
// src/usageMonitor.ts
|
|
1338
|
+
var SESSION_ID = "usage-monitor";
|
|
1339
|
+
var TICKET = randomUUID4();
|
|
1340
|
+
var SCRATCH_DIR = join8(tmpdir5(), "agent-board-usage-monitor");
|
|
1341
|
+
var POLL_INTERVAL_MS = Number(process.env.AGENT_BOARD_USAGE_POLL_INTERVAL_MS ?? 15 * 6e4);
|
|
1342
|
+
var IDLE_MS = 2e3;
|
|
1343
|
+
var HARD_TIMEOUT_MS = 6e4;
|
|
1344
|
+
function usageMonitorSessionId() {
|
|
1345
|
+
return SESSION_ID;
|
|
1346
|
+
}
|
|
1347
|
+
function usageMonitorTicket() {
|
|
1348
|
+
return TICKET;
|
|
1349
|
+
}
|
|
1298
1350
|
function pollUsageOnce() {
|
|
1299
1351
|
let pty;
|
|
1300
1352
|
try {
|
|
@@ -1341,7 +1393,7 @@ function pollUsageOnce() {
|
|
|
1341
1393
|
}
|
|
1342
1394
|
function startUsageMonitor() {
|
|
1343
1395
|
mkdirSync3(SCRATCH_DIR, { recursive: true });
|
|
1344
|
-
|
|
1396
|
+
ensureDirTrusted(SCRATCH_DIR);
|
|
1345
1397
|
pollUsageOnce();
|
|
1346
1398
|
setInterval(pollUsageOnce, POLL_INTERVAL_MS);
|
|
1347
1399
|
}
|
|
@@ -1357,7 +1409,7 @@ var IGNORED_ENTRY_NAMES = /* @__PURE__ */ new Set([".git", ".claude", "node_modu
|
|
|
1357
1409
|
var AGENT_SAFE_SUBDIR_NAMES = /* @__PURE__ */ new Set(["skills", "commands", "agents", "rules"]);
|
|
1358
1410
|
function ensureSkillsDirExists(claudeDir) {
|
|
1359
1411
|
try {
|
|
1360
|
-
mkdirSync4(
|
|
1412
|
+
mkdirSync4(join9(claudeDir, "skills"), { recursive: true });
|
|
1361
1413
|
} catch (err) {
|
|
1362
1414
|
logger.runner.warn("FILES", "Could not create .claude/skills:", err);
|
|
1363
1415
|
}
|
|
@@ -1484,7 +1536,7 @@ function listSafeAgentDirectory(baseDir, relPath, atClaudeRoot) {
|
|
|
1484
1536
|
}
|
|
1485
1537
|
const isRepoRootLevel = segments.length === 0 || segments.length === 1 && segments[0] !== ".claude";
|
|
1486
1538
|
if (isRepoRootLevel) {
|
|
1487
|
-
ensureSkillsDirExists(
|
|
1539
|
+
ensureSkillsDirExists(join9(baseDir, relPath, ".claude"));
|
|
1488
1540
|
const entries2 = listDirectory(baseDir, relPath, { includeDotClaude: true });
|
|
1489
1541
|
return entries2.filter((entry) => entry.type === "file" && entry.name === "CLAUDE.md" || entry.type === "dir" && entry.name === ".claude");
|
|
1490
1542
|
}
|
|
@@ -1515,7 +1567,7 @@ function assertWritableAgentPath(relPath, atClaudeRoot) {
|
|
|
1515
1567
|
function listDirectoryForBrowse(rootPath, relPath) {
|
|
1516
1568
|
return listDirectory(rootPath, relPath).map((entry) => ({
|
|
1517
1569
|
...entry,
|
|
1518
|
-
hasGit: entry.type === "dir" && existsSync4(
|
|
1570
|
+
hasGit: entry.type === "dir" && existsSync4(join9(rootPath, entry.path, ".git"))
|
|
1519
1571
|
}));
|
|
1520
1572
|
}
|
|
1521
1573
|
var SAFE_CLONE_URL_PATTERN = /^(https?:\/\/|git:\/\/|ssh:\/\/|git@[\w.-]+:)\S+$/i;
|
|
@@ -1553,7 +1605,7 @@ async function handleCloneRequest(req, res, query) {
|
|
|
1553
1605
|
sendJson(res, 400, { error: "N\xE3o foi poss\xEDvel determinar o nome da pasta a partir da URL." });
|
|
1554
1606
|
return;
|
|
1555
1607
|
}
|
|
1556
|
-
const destPath =
|
|
1608
|
+
const destPath = join9(setupEntry.rootPath, folderName);
|
|
1557
1609
|
if (existsSync4(destPath)) {
|
|
1558
1610
|
sendJson(res, 409, { error: `J\xE1 existe uma pasta "${folderName}" na raiz permitida.` });
|
|
1559
1611
|
return;
|
|
@@ -1621,7 +1673,7 @@ function findBrandIconPath(repoRoot) {
|
|
|
1621
1673
|
if (entry.isSymbolicLink() || IGNORED_ENTRY_NAMES.has(entry.name)) continue;
|
|
1622
1674
|
const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
1623
1675
|
if (entry.isDirectory()) {
|
|
1624
|
-
walk(
|
|
1676
|
+
walk(join9(dir, entry.name), relPath, depth + 1);
|
|
1625
1677
|
} else if (entry.isFile()) {
|
|
1626
1678
|
const match = patterns.find(({ pattern }) => pattern.test(entry.name));
|
|
1627
1679
|
if (match) candidatesByBaseName.get(match.baseName).push({ relPath, depth });
|
|
@@ -1786,7 +1838,7 @@ async function getRepoDiffForTarget(label, cwd) {
|
|
|
1786
1838
|
async function handleDiffRequest(entry, res, projectId) {
|
|
1787
1839
|
try {
|
|
1788
1840
|
const children = statuslineSupabase && projectId ? await getProjectRepos(statuslineSupabase, projectId) : [];
|
|
1789
|
-
const targets = children.length > 0 ? children.map((child2) => ({ label: child2.subpath, cwd:
|
|
1841
|
+
const targets = children.length > 0 ? children.map((child2) => ({ label: child2.subpath, cwd: join9(entry.repoRoot, child2.subpath) })) : [{ label: null, cwd: entry.repoRoot }];
|
|
1790
1842
|
const repos = await Promise.all(targets.map((target) => getRepoDiffForTarget(target.label, target.cwd)));
|
|
1791
1843
|
sendJson(res, 200, { repos });
|
|
1792
1844
|
} catch (err) {
|
|
@@ -2094,7 +2146,7 @@ function attachAgentEventsServer() {
|
|
|
2094
2146
|
|
|
2095
2147
|
// src/columnActions.ts
|
|
2096
2148
|
import { exec } from "node:child_process";
|
|
2097
|
-
import { join as
|
|
2149
|
+
import { join as join10 } from "node:path";
|
|
2098
2150
|
import { promisify as promisify3 } from "node:util";
|
|
2099
2151
|
var execAsync = promisify3(exec);
|
|
2100
2152
|
var OUTPUT_TAIL_CHARS = 4e3;
|
|
@@ -2104,7 +2156,7 @@ function tail(text) {
|
|
|
2104
2156
|
function resolveTargets(project, children, repoRoot, column, timing) {
|
|
2105
2157
|
if (children.length > 0) {
|
|
2106
2158
|
return children.flatMap(
|
|
2107
|
-
(child2) => (child2.column_actions[column] ?? []).filter((action) => action.timing === timing).map((action) => ({ label: child2.subpath, cwd:
|
|
2159
|
+
(child2) => (child2.column_actions[column] ?? []).filter((action) => action.timing === timing).map((action) => ({ label: child2.subpath, cwd: join10(repoRoot, child2.subpath), command: action.command }))
|
|
2108
2160
|
);
|
|
2109
2161
|
}
|
|
2110
2162
|
return (project.column_actions[column] ?? []).filter((action) => action.timing === timing).map((action) => ({ label: null, cwd: repoRoot, command: action.command }));
|
|
@@ -2210,7 +2262,7 @@ async function checkPendingDeployActionReruns(supabase, config) {
|
|
|
2210
2262
|
}
|
|
2211
2263
|
|
|
2212
2264
|
// src/deploy.ts
|
|
2213
|
-
import { join as
|
|
2265
|
+
import { join as join13 } from "node:path";
|
|
2214
2266
|
|
|
2215
2267
|
// src/devServer.ts
|
|
2216
2268
|
import { spawn as spawn4 } from "node:child_process";
|
|
@@ -2219,7 +2271,7 @@ import { spawn as spawn4 } from "node:child_process";
|
|
|
2219
2271
|
import { spawn as spawn3 } from "node:child_process";
|
|
2220
2272
|
import { accessSync, constants } from "node:fs";
|
|
2221
2273
|
import { homedir as homedir5 } from "node:os";
|
|
2222
|
-
import { delimiter, join as
|
|
2274
|
+
import { delimiter, join as join11 } from "node:path";
|
|
2223
2275
|
import { bin as bundledBinaryPath, install as downloadCloudflared } from "cloudflared";
|
|
2224
2276
|
var TRYCLOUDFLARE_URL_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
|
|
2225
2277
|
var STARTUP_TIMEOUT_MS = 2e4;
|
|
@@ -2230,15 +2282,15 @@ function wellKnownDirectories() {
|
|
|
2230
2282
|
const home = homedir5();
|
|
2231
2283
|
if (IS_WINDOWS2) {
|
|
2232
2284
|
const candidates = [
|
|
2233
|
-
process.env.ProgramFiles &&
|
|
2234
|
-
process.env["ProgramFiles(x86)"] &&
|
|
2235
|
-
process.env.LOCALAPPDATA &&
|
|
2236
|
-
|
|
2237
|
-
process.env.ChocolateyInstall &&
|
|
2285
|
+
process.env.ProgramFiles && join11(process.env.ProgramFiles, "cloudflared"),
|
|
2286
|
+
process.env["ProgramFiles(x86)"] && join11(process.env["ProgramFiles(x86)"], "cloudflared"),
|
|
2287
|
+
process.env.LOCALAPPDATA && join11(process.env.LOCALAPPDATA, "Microsoft", "WinGet", "Links"),
|
|
2288
|
+
join11(home, "scoop", "shims"),
|
|
2289
|
+
process.env.ChocolateyInstall && join11(process.env.ChocolateyInstall, "bin")
|
|
2238
2290
|
];
|
|
2239
2291
|
return candidates.filter((dir) => Boolean(dir));
|
|
2240
2292
|
}
|
|
2241
|
-
return ["/usr/local/bin", "/usr/bin", "/opt/homebrew/bin",
|
|
2293
|
+
return ["/usr/local/bin", "/usr/bin", "/opt/homebrew/bin", join11(home, ".local", "bin")];
|
|
2242
2294
|
}
|
|
2243
2295
|
function pathDirectories() {
|
|
2244
2296
|
return (process.env.PATH ?? "").split(delimiter).map((dir) => dir.trim().replace(/^"|"$/g, "")).filter(Boolean);
|
|
@@ -2254,7 +2306,7 @@ function isExecutableFile(path) {
|
|
|
2254
2306
|
function findInstalledBinary() {
|
|
2255
2307
|
const override = process.env.AGENT_BOARD_CLOUDFLARED_PATH;
|
|
2256
2308
|
const directories = [...pathDirectories(), ...wellKnownDirectories()];
|
|
2257
|
-
const candidates = [...override ? [override] : [], ...directories.map((dir) =>
|
|
2309
|
+
const candidates = [...override ? [override] : [], ...directories.map((dir) => join11(dir, BINARY_NAME))];
|
|
2258
2310
|
return candidates.find(isExecutableFile) ?? null;
|
|
2259
2311
|
}
|
|
2260
2312
|
async function locateCloudflared() {
|
|
@@ -2485,6 +2537,8 @@ var SHIFT_TAB_PRESSES_TO_PLAN_MODE = 4;
|
|
|
2485
2537
|
var SHIFT_TAB_DELAY_MS = 400;
|
|
2486
2538
|
var PLAN_MODE_INDICATOR = /plan mode on/i;
|
|
2487
2539
|
function spawnAgentPty(repoPath, cardId, cardTitle, instructions, sessionId, ticket, planFirst) {
|
|
2540
|
+
ensureDirTrusted(repoPath);
|
|
2541
|
+
ensureMcpServersTrusted(repoPath);
|
|
2488
2542
|
const claudeSessionId = randomUUID6();
|
|
2489
2543
|
const pty = spawn5(
|
|
2490
2544
|
CLAUDE_BINARY,
|
|
@@ -2581,6 +2635,8 @@ function sendContinue(pty, cardId) {
|
|
|
2581
2635
|
}
|
|
2582
2636
|
}
|
|
2583
2637
|
function resumeAgentPty(repoPath, claudeSessionId, sessionId, ticket) {
|
|
2638
|
+
ensureDirTrusted(repoPath);
|
|
2639
|
+
ensureMcpServersTrusted(repoPath);
|
|
2584
2640
|
return spawn5(
|
|
2585
2641
|
CLAUDE_BINARY,
|
|
2586
2642
|
[
|
|
@@ -2604,8 +2660,8 @@ function resumeAgentPty(repoPath, claudeSessionId, sessionId, ticket) {
|
|
|
2604
2660
|
// src/cardAttachments.ts
|
|
2605
2661
|
import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
|
|
2606
2662
|
import { tmpdir as tmpdir6 } from "node:os";
|
|
2607
|
-
import { extname, join as
|
|
2608
|
-
var ATTACHMENTS_ROOT =
|
|
2663
|
+
import { extname, join as join12 } from "node:path";
|
|
2664
|
+
var ATTACHMENTS_ROOT = join12(tmpdir6(), "agent-board", "card-attachments");
|
|
2609
2665
|
function sanitizeFileName(fileName) {
|
|
2610
2666
|
return fileName.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
2611
2667
|
}
|
|
@@ -2617,13 +2673,13 @@ async function downloadCardAttachments(supabase, cardId) {
|
|
|
2617
2673
|
}
|
|
2618
2674
|
const rows = data ?? [];
|
|
2619
2675
|
if (rows.length === 0) return [];
|
|
2620
|
-
const targetDir =
|
|
2676
|
+
const targetDir = join12(ATTACHMENTS_ROOT, cardId);
|
|
2621
2677
|
await mkdir2(targetDir, { recursive: true });
|
|
2622
2678
|
const paths = [];
|
|
2623
2679
|
for (const row of rows) {
|
|
2624
2680
|
const baseName = sanitizeFileName(row.file_name);
|
|
2625
2681
|
const fileName = extname(baseName) ? baseName : `${baseName}${extname(row.storage_path)}`;
|
|
2626
|
-
const localPath =
|
|
2682
|
+
const localPath = join12(targetDir, `${row.id}-${fileName}`);
|
|
2627
2683
|
try {
|
|
2628
2684
|
const { data: blob, error: downloadError } = await supabase.storage.from(CARD_ATTACHMENTS_BUCKET).download(row.storage_path);
|
|
2629
2685
|
if (downloadError) throw downloadError;
|
|
@@ -2697,6 +2753,7 @@ function cancelUsageResume(sessionId) {
|
|
|
2697
2753
|
|
|
2698
2754
|
// src/orchestrator.ts
|
|
2699
2755
|
var TERMINAL_TICKET_TTL_MS = 60 * 60 * 1e3;
|
|
2756
|
+
var RESUME_CONVERSATION_MISSING_PATTERN = /no conversation found with session id/i;
|
|
2700
2757
|
var inFlightCardIds = /* @__PURE__ */ new Set();
|
|
2701
2758
|
var activeSessionByCard = /* @__PURE__ */ new Map();
|
|
2702
2759
|
var intentionallyStoppedSessionIds = /* @__PURE__ */ new Set();
|
|
@@ -2770,11 +2827,17 @@ async function dispatchRunnerQueue(supabase, config) {
|
|
|
2770
2827
|
try {
|
|
2771
2828
|
const queued = await listQueuedSessionsForRunner(supabase, config.ownerUserId);
|
|
2772
2829
|
if (queued.length === 0) return;
|
|
2830
|
+
const queuedProjectIds = new Set(queued.map((entry) => entry.cards.project_id));
|
|
2773
2831
|
const maxConcurrentProjects = await getRunnerMaxConcurrentProjects(supabase, config.runnerId);
|
|
2774
2832
|
const runningProjectIds = /* @__PURE__ */ new Set();
|
|
2775
2833
|
for (const project of config.registeredProjects) {
|
|
2776
|
-
const
|
|
2777
|
-
if (
|
|
2834
|
+
const running = await getRunningSessionForProject(supabase, project.id, config.ownerUserId);
|
|
2835
|
+
if (!running) continue;
|
|
2836
|
+
if (running.agentState === "done" && queuedProjectIds.has(project.id)) {
|
|
2837
|
+
await pauseFinishedSession(supabase, config, project.id, running.cardId, running.id);
|
|
2838
|
+
continue;
|
|
2839
|
+
}
|
|
2840
|
+
runningProjectIds.add(project.id);
|
|
2778
2841
|
}
|
|
2779
2842
|
let freeProjectSlots = maxConcurrentProjects - runningProjectIds.size;
|
|
2780
2843
|
if (freeProjectSlots <= 0) return;
|
|
@@ -2828,9 +2891,13 @@ async function spawnSessionPty(supabase, config, entry) {
|
|
|
2828
2891
|
const ticket = randomUUID7();
|
|
2829
2892
|
let pty;
|
|
2830
2893
|
let newClaudeSessionId = null;
|
|
2894
|
+
let resumeConversationMissing = false;
|
|
2831
2895
|
try {
|
|
2832
2896
|
if (claudeSessionId) {
|
|
2833
2897
|
pty = resumeAgentPty(registered.repoPath, claudeSessionId, sessionId, ticket);
|
|
2898
|
+
pty.onData((chunk) => {
|
|
2899
|
+
if (RESUME_CONVERSATION_MISSING_PATTERN.test(normalizeTerminalOutput(chunk))) resumeConversationMissing = true;
|
|
2900
|
+
});
|
|
2834
2901
|
} else {
|
|
2835
2902
|
const attachmentPaths = await downloadCardAttachments(supabase, cardId);
|
|
2836
2903
|
const instructions = buildInstructionsWithMcpContext(
|
|
@@ -2878,11 +2945,24 @@ async function spawnSessionPty(supabase, config, entry) {
|
|
|
2878
2945
|
);
|
|
2879
2946
|
pty.onExit(({ exitCode }) => {
|
|
2880
2947
|
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
2948
|
activityWatcher.stop();
|
|
2883
2949
|
unregisterAgentActivityWatcher(sessionId);
|
|
2884
2950
|
cancelUsageResume(sessionId);
|
|
2885
2951
|
activeSessionByCard.delete(cardId);
|
|
2952
|
+
if (resumeConversationMissing) {
|
|
2953
|
+
logger.card.warn(
|
|
2954
|
+
cardId,
|
|
2955
|
+
`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.`
|
|
2956
|
+
);
|
|
2957
|
+
void updateSession(
|
|
2958
|
+
supabase,
|
|
2959
|
+
sessionId,
|
|
2960
|
+
{ status: "failed", endedAt: (/* @__PURE__ */ new Date()).toISOString(), claudeSessionId: null },
|
|
2961
|
+
{ projectId: card.project_id, cardId, userId: config.ownerUserId }
|
|
2962
|
+
);
|
|
2963
|
+
return;
|
|
2964
|
+
}
|
|
2965
|
+
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
2966
|
if (!wasIntentional && exitCode !== 0) {
|
|
2887
2967
|
publishAgentState(supabase, card.project_id, cardId, sessionId, "error");
|
|
2888
2968
|
}
|
|
@@ -2903,7 +2983,7 @@ function watchAgentActivity(supabase, config, projectId, cardId, sessionId, pty)
|
|
|
2903
2983
|
} else {
|
|
2904
2984
|
cancelUsageResume(sessionId);
|
|
2905
2985
|
}
|
|
2906
|
-
if (state === "done") void
|
|
2986
|
+
if (state === "done") void dispatchRunnerQueue(supabase, config);
|
|
2907
2987
|
});
|
|
2908
2988
|
pty.onData((chunk) => watcher.push(chunk));
|
|
2909
2989
|
registerAgentActivityWatcher(sessionId, watcher);
|
|
@@ -2913,8 +2993,7 @@ async function pauseFinishedSession(supabase, config, projectId, cardId, session
|
|
|
2913
2993
|
const paused = await pauseRunningSession(supabase, sessionId, { projectId, cardId, userId: config.ownerUserId });
|
|
2914
2994
|
if (!paused) return;
|
|
2915
2995
|
setPtySessionReadOnly(sessionId, true);
|
|
2916
|
-
logger.card.info(cardId, `Session ${sessionId} paused (agent reached "done"
|
|
2917
|
-
await dispatchRunnerQueue(supabase, config);
|
|
2996
|
+
logger.card.info(cardId, `Session ${sessionId} paused (agent reached "done", another session needs this project's WIP slot).`);
|
|
2918
2997
|
}
|
|
2919
2998
|
async function requeuePausedCard(supabase, config, card) {
|
|
2920
2999
|
const active = await getActiveSessionForCard(supabase, card.id);
|
|
@@ -2992,6 +3071,24 @@ async function stopCardSession(supabase, config, card) {
|
|
|
2992
3071
|
logger.card.info(card.id, `Session ${sessionId} stopped.`);
|
|
2993
3072
|
await dispatchRunnerQueue(supabase, config);
|
|
2994
3073
|
}
|
|
3074
|
+
async function restartCardSession(supabase, config, card) {
|
|
3075
|
+
const sessionId = activeSessionByCard.get(card.id) ?? (await getActiveSessionForCard(supabase, card.id))?.id;
|
|
3076
|
+
if (sessionId) {
|
|
3077
|
+
intentionallyStoppedSessionIds.add(sessionId);
|
|
3078
|
+
stopPtySession(sessionId);
|
|
3079
|
+
cancelUsageResume(sessionId);
|
|
3080
|
+
activeSessionByCard.delete(card.id);
|
|
3081
|
+
await updateSession(
|
|
3082
|
+
supabase,
|
|
3083
|
+
sessionId,
|
|
3084
|
+
{ status: "failed", claudeSessionId: null, endedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
3085
|
+
{ projectId: card.project_id, cardId: card.id, userId: config.ownerUserId }
|
|
3086
|
+
);
|
|
3087
|
+
logger.card.info(card.id, `Session ${sessionId} abandoned by "Reiniciar sess\xE3o"; starting a fresh one.`);
|
|
3088
|
+
}
|
|
3089
|
+
await updateCard(supabase, card.id, { restartRequestedAt: null });
|
|
3090
|
+
await enqueueCard(supabase, config, card);
|
|
3091
|
+
}
|
|
2995
3092
|
|
|
2996
3093
|
// src/deploy.ts
|
|
2997
3094
|
async function handleDeployRequested(supabase, config, project) {
|
|
@@ -3009,7 +3106,7 @@ async function handleDeployRequested(supabase, config, project) {
|
|
|
3009
3106
|
}
|
|
3010
3107
|
const children = await getProjectRepos(supabase, project.id);
|
|
3011
3108
|
const repoRoot = resolveRepoPath(config, project.repo_path);
|
|
3012
|
-
const targets = children.length > 0 ? children.map((child2) => ({ label: child2.subpath, path:
|
|
3109
|
+
const targets = children.length > 0 ? children.map((child2) => ({ label: child2.subpath, path: join13(repoRoot, child2.subpath) })) : [{ label: null, path: repoRoot }];
|
|
3013
3110
|
const failures = [];
|
|
3014
3111
|
for (const target of targets) {
|
|
3015
3112
|
const result = await pushBranchToMain(target.path);
|
|
@@ -3143,7 +3240,7 @@ async function syncRegisteredProjects(supabase, config, registeredPaths) {
|
|
|
3143
3240
|
}
|
|
3144
3241
|
|
|
3145
3242
|
// src/review.ts
|
|
3146
|
-
import { join as
|
|
3243
|
+
import { join as join14 } from "node:path";
|
|
3147
3244
|
var inFlightCardIds2 = /* @__PURE__ */ new Set();
|
|
3148
3245
|
async function handleCardEnteredRevisao(supabase, config, card) {
|
|
3149
3246
|
if (inFlightCardIds2.has(card.id)) return;
|
|
@@ -3152,7 +3249,7 @@ async function handleCardEnteredRevisao(supabase, config, card) {
|
|
|
3152
3249
|
const project = await getProject(supabase, card.project_id);
|
|
3153
3250
|
const children = await getProjectRepos(supabase, project.id);
|
|
3154
3251
|
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:
|
|
3252
|
+
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
3253
|
if (targets.length === 0) {
|
|
3157
3254
|
logger.card.info(card.id, `No dev_command configured for project ${project.id}, skipping automatic preview.`);
|
|
3158
3255
|
return;
|
|
@@ -3238,7 +3335,7 @@ function attachTerminalServer() {
|
|
|
3238
3335
|
const buffer = data;
|
|
3239
3336
|
const frameType = buffer[0];
|
|
3240
3337
|
if (frameType === FRAME_TYPE_IMAGE) {
|
|
3241
|
-
if (handle.readOnly) return;
|
|
3338
|
+
if (handle.readOnly || handle.exited) return;
|
|
3242
3339
|
const extCode = buffer[1];
|
|
3243
3340
|
const bytes = buffer.subarray(2);
|
|
3244
3341
|
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 +3353,7 @@ function attachTerminalServer() {
|
|
|
3256
3353
|
logger.runner.warn("TERMINAL", `Terminal WS: ignoring binary frame with unknown type ${frameType} for session ${sessionId}.`);
|
|
3257
3354
|
return;
|
|
3258
3355
|
}
|
|
3259
|
-
if (handle.readOnly) return;
|
|
3356
|
+
if (handle.readOnly || handle.exited) return;
|
|
3260
3357
|
handle.pty.write(data.toString());
|
|
3261
3358
|
});
|
|
3262
3359
|
socket.on("close", () => {
|
|
@@ -3378,6 +3475,7 @@ async function startRunner() {
|
|
|
3378
3475
|
const registeredPaths = new Set(config.registeredProjects.map((project) => project.id));
|
|
3379
3476
|
const lastKnownColumn = /* @__PURE__ */ new Map();
|
|
3380
3477
|
const lastKnownRequeueRequestedAt = /* @__PURE__ */ new Map();
|
|
3478
|
+
const lastKnownRestartRequestedAt = /* @__PURE__ */ new Map();
|
|
3381
3479
|
await syncRegisteredProjects(supabase, config, registeredPaths).catch((err) => logger.runner.error("PROJECT_SYNC", "syncRegisteredProjects failed:", err));
|
|
3382
3480
|
logger.runner.info("BOOT", `Agent Board runner "${config.runnerName}" starting (${registeredPaths.size} project(s) registered).`);
|
|
3383
3481
|
for (const project of config.registeredProjects) {
|
|
@@ -3473,6 +3571,11 @@ async function startRunner() {
|
|
|
3473
3571
|
if (card.requeue_requested_at && card.requeue_requested_at !== previousRequeueRequestedAt) {
|
|
3474
3572
|
void requeuePausedCard(supabase, config, card);
|
|
3475
3573
|
}
|
|
3574
|
+
const previousRestartRequestedAt = lastKnownRestartRequestedAt.get(card.id);
|
|
3575
|
+
lastKnownRestartRequestedAt.set(card.id, card.restart_requested_at);
|
|
3576
|
+
if (card.restart_requested_at && card.restart_requested_at !== previousRestartRequestedAt) {
|
|
3577
|
+
void restartCardSession(supabase, config, card);
|
|
3578
|
+
}
|
|
3476
3579
|
if (previousColumn !== void 0 && previousColumn !== card.column_name) {
|
|
3477
3580
|
void clearColumnActionStatus(supabase, card);
|
|
3478
3581
|
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
|
|
@@ -1001,11 +1007,16 @@ function formatPercent(value) {
|
|
|
1001
1007
|
}
|
|
1002
1008
|
|
|
1003
1009
|
function buildDisplayText(data) {
|
|
1004
|
-
const rateLimits = data && data.rate_limits;
|
|
1005
|
-
if (!rateLimits) return "";
|
|
1006
1010
|
const parts = [];
|
|
1007
|
-
|
|
1008
|
-
if (
|
|
1011
|
+
const contextWindow = data && data.context_window;
|
|
1012
|
+
if (contextWindow && typeof contextWindow.used_percentage === "number") {
|
|
1013
|
+
parts.push(\`ctx \${formatPercent(contextWindow.used_percentage)}\`);
|
|
1014
|
+
}
|
|
1015
|
+
const rateLimits = data && data.rate_limits;
|
|
1016
|
+
if (rateLimits) {
|
|
1017
|
+
if (rateLimits.five_hour) parts.push(\`5h \${formatPercent(rateLimits.five_hour.used_percentage)}\`);
|
|
1018
|
+
if (rateLimits.seven_day) parts.push(\`7d \${formatPercent(rateLimits.seven_day.used_percentage)}\`);
|
|
1019
|
+
}
|
|
1009
1020
|
return parts.join(" \xB7 ");
|
|
1010
1021
|
}
|
|
1011
1022
|
|
|
@@ -1101,38 +1112,79 @@ function claudeCliSettingsArgs() {
|
|
|
1101
1112
|
];
|
|
1102
1113
|
}
|
|
1103
1114
|
|
|
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() {
|
|
1115
|
+
// src/claudeTrust.ts
|
|
1116
|
+
import { existsSync, readFileSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1117
|
+
import { homedir as homedir2 } from "node:os";
|
|
1118
|
+
import { join as join5 } from "node:path";
|
|
1119
|
+
function ensureDirTrusted(dirPath) {
|
|
1118
1120
|
const claudeJsonPath = join5(homedir2(), ".claude.json");
|
|
1119
1121
|
if (!existsSync(claudeJsonPath)) return;
|
|
1120
1122
|
try {
|
|
1121
1123
|
const raw = readFileSync(claudeJsonPath, "utf8");
|
|
1122
1124
|
const data = JSON.parse(raw);
|
|
1123
1125
|
data.projects ??= {};
|
|
1124
|
-
const keys = [
|
|
1126
|
+
const keys = [dirPath, dirPath.replace(/\\/g, "/")];
|
|
1125
1127
|
const alreadyTrusted = keys.every((key) => data.projects[key]?.hasTrustDialogAccepted === true);
|
|
1126
1128
|
if (alreadyTrusted) return;
|
|
1127
1129
|
for (const key of keys) {
|
|
1128
1130
|
data.projects[key] = { ...data.projects[key], hasTrustDialogAccepted: true };
|
|
1129
1131
|
}
|
|
1130
1132
|
writeFileSync4(claudeJsonPath, JSON.stringify(data, null, 2));
|
|
1131
|
-
logger.runner.info("
|
|
1133
|
+
logger.runner.info("TRUST", `Pre-trusted ${dirPath} in ~/.claude.json.`);
|
|
1134
|
+
} catch (err) {
|
|
1135
|
+
logger.runner.error("TRUST", `Failed to pre-trust ${dirPath}:`, err);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
function ensureMcpServersTrusted(dirPath) {
|
|
1139
|
+
const mcpJsonPath = join5(dirPath, ".mcp.json");
|
|
1140
|
+
if (!existsSync(mcpJsonPath)) return;
|
|
1141
|
+
let serverNames;
|
|
1142
|
+
try {
|
|
1143
|
+
const mcpConfig = JSON.parse(readFileSync(mcpJsonPath, "utf8"));
|
|
1144
|
+
serverNames = Object.keys(mcpConfig.mcpServers ?? {});
|
|
1145
|
+
} catch (err) {
|
|
1146
|
+
logger.runner.error("TRUST", `Failed to read ${mcpJsonPath}:`, err);
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
if (serverNames.length === 0) return;
|
|
1150
|
+
const claudeJsonPath = join5(homedir2(), ".claude.json");
|
|
1151
|
+
if (!existsSync(claudeJsonPath)) return;
|
|
1152
|
+
try {
|
|
1153
|
+
const raw = readFileSync(claudeJsonPath, "utf8");
|
|
1154
|
+
const data = JSON.parse(raw);
|
|
1155
|
+
data.projects ??= {};
|
|
1156
|
+
const keys = [dirPath, dirPath.replace(/\\/g, "/")];
|
|
1157
|
+
const alreadyApproved = keys.every(
|
|
1158
|
+
(key) => serverNames.every((name) => data.projects[key]?.enabledMcpjsonServers?.includes(name))
|
|
1159
|
+
);
|
|
1160
|
+
if (alreadyApproved) return;
|
|
1161
|
+
for (const key of keys) {
|
|
1162
|
+
const project = data.projects[key] ??= {};
|
|
1163
|
+
project.enabledMcpjsonServers = [.../* @__PURE__ */ new Set([...project.enabledMcpjsonServers ?? [], ...serverNames])];
|
|
1164
|
+
project.disabledMcpjsonServers = (project.disabledMcpjsonServers ?? []).filter(
|
|
1165
|
+
(name) => !serverNames.includes(name)
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
writeFileSync4(claudeJsonPath, JSON.stringify(data, null, 2));
|
|
1169
|
+
logger.runner.info("TRUST", `Pre-approved MCP servers [${serverNames.join(", ")}] for ${dirPath}.`);
|
|
1132
1170
|
} catch (err) {
|
|
1133
|
-
logger.runner.error("
|
|
1171
|
+
logger.runner.error("TRUST", `Failed to pre-approve MCP servers for ${dirPath}:`, err);
|
|
1134
1172
|
}
|
|
1135
1173
|
}
|
|
1174
|
+
|
|
1175
|
+
// src/usageMonitor.ts
|
|
1176
|
+
var SESSION_ID = "usage-monitor";
|
|
1177
|
+
var TICKET = randomUUID3();
|
|
1178
|
+
var SCRATCH_DIR = join6(tmpdir5(), "agent-board-usage-monitor");
|
|
1179
|
+
var POLL_INTERVAL_MS = Number(process.env.AGENT_BOARD_USAGE_POLL_INTERVAL_MS ?? 15 * 6e4);
|
|
1180
|
+
var IDLE_MS = 2e3;
|
|
1181
|
+
var HARD_TIMEOUT_MS = 6e4;
|
|
1182
|
+
function usageMonitorSessionId() {
|
|
1183
|
+
return SESSION_ID;
|
|
1184
|
+
}
|
|
1185
|
+
function usageMonitorTicket() {
|
|
1186
|
+
return TICKET;
|
|
1187
|
+
}
|
|
1136
1188
|
function pollUsageOnce() {
|
|
1137
1189
|
let pty;
|
|
1138
1190
|
try {
|
|
@@ -1179,7 +1231,7 @@ function pollUsageOnce() {
|
|
|
1179
1231
|
}
|
|
1180
1232
|
function startUsageMonitor() {
|
|
1181
1233
|
mkdirSync(SCRATCH_DIR, { recursive: true });
|
|
1182
|
-
|
|
1234
|
+
ensureDirTrusted(SCRATCH_DIR);
|
|
1183
1235
|
pollUsageOnce();
|
|
1184
1236
|
setInterval(pollUsageOnce, POLL_INTERVAL_MS);
|
|
1185
1237
|
}
|
|
@@ -1195,7 +1247,7 @@ var IGNORED_ENTRY_NAMES = /* @__PURE__ */ new Set([".git", ".claude", "node_modu
|
|
|
1195
1247
|
var AGENT_SAFE_SUBDIR_NAMES = /* @__PURE__ */ new Set(["skills", "commands", "agents", "rules"]);
|
|
1196
1248
|
function ensureSkillsDirExists(claudeDir) {
|
|
1197
1249
|
try {
|
|
1198
|
-
mkdirSync2(
|
|
1250
|
+
mkdirSync2(join7(claudeDir, "skills"), { recursive: true });
|
|
1199
1251
|
} catch (err) {
|
|
1200
1252
|
logger.runner.warn("FILES", "Could not create .claude/skills:", err);
|
|
1201
1253
|
}
|
|
@@ -1322,7 +1374,7 @@ function listSafeAgentDirectory(baseDir, relPath, atClaudeRoot) {
|
|
|
1322
1374
|
}
|
|
1323
1375
|
const isRepoRootLevel = segments.length === 0 || segments.length === 1 && segments[0] !== ".claude";
|
|
1324
1376
|
if (isRepoRootLevel) {
|
|
1325
|
-
ensureSkillsDirExists(
|
|
1377
|
+
ensureSkillsDirExists(join7(baseDir, relPath, ".claude"));
|
|
1326
1378
|
const entries2 = listDirectory(baseDir, relPath, { includeDotClaude: true });
|
|
1327
1379
|
return entries2.filter((entry) => entry.type === "file" && entry.name === "CLAUDE.md" || entry.type === "dir" && entry.name === ".claude");
|
|
1328
1380
|
}
|
|
@@ -1353,7 +1405,7 @@ function assertWritableAgentPath(relPath, atClaudeRoot) {
|
|
|
1353
1405
|
function listDirectoryForBrowse(rootPath, relPath) {
|
|
1354
1406
|
return listDirectory(rootPath, relPath).map((entry) => ({
|
|
1355
1407
|
...entry,
|
|
1356
|
-
hasGit: entry.type === "dir" && existsSync2(
|
|
1408
|
+
hasGit: entry.type === "dir" && existsSync2(join7(rootPath, entry.path, ".git"))
|
|
1357
1409
|
}));
|
|
1358
1410
|
}
|
|
1359
1411
|
var SAFE_CLONE_URL_PATTERN = /^(https?:\/\/|git:\/\/|ssh:\/\/|git@[\w.-]+:)\S+$/i;
|
|
@@ -1391,7 +1443,7 @@ async function handleCloneRequest(req, res, query) {
|
|
|
1391
1443
|
sendJson(res, 400, { error: "N\xE3o foi poss\xEDvel determinar o nome da pasta a partir da URL." });
|
|
1392
1444
|
return;
|
|
1393
1445
|
}
|
|
1394
|
-
const destPath =
|
|
1446
|
+
const destPath = join7(setupEntry.rootPath, folderName);
|
|
1395
1447
|
if (existsSync2(destPath)) {
|
|
1396
1448
|
sendJson(res, 409, { error: `J\xE1 existe uma pasta "${folderName}" na raiz permitida.` });
|
|
1397
1449
|
return;
|
|
@@ -1459,7 +1511,7 @@ function findBrandIconPath(repoRoot) {
|
|
|
1459
1511
|
if (entry.isSymbolicLink() || IGNORED_ENTRY_NAMES.has(entry.name)) continue;
|
|
1460
1512
|
const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
1461
1513
|
if (entry.isDirectory()) {
|
|
1462
|
-
walk(
|
|
1514
|
+
walk(join7(dir, entry.name), relPath, depth + 1);
|
|
1463
1515
|
} else if (entry.isFile()) {
|
|
1464
1516
|
const match = patterns.find(({ pattern }) => pattern.test(entry.name));
|
|
1465
1517
|
if (match) candidatesByBaseName.get(match.baseName).push({ relPath, depth });
|
|
@@ -1624,7 +1676,7 @@ async function getRepoDiffForTarget(label, cwd) {
|
|
|
1624
1676
|
async function handleDiffRequest(entry, res, projectId) {
|
|
1625
1677
|
try {
|
|
1626
1678
|
const children = statuslineSupabase && projectId ? await getProjectRepos(statuslineSupabase, projectId) : [];
|
|
1627
|
-
const targets = children.length > 0 ? children.map((child2) => ({ label: child2.subpath, cwd:
|
|
1679
|
+
const targets = children.length > 0 ? children.map((child2) => ({ label: child2.subpath, cwd: join7(entry.repoRoot, child2.subpath) })) : [{ label: null, cwd: entry.repoRoot }];
|
|
1628
1680
|
const repos = await Promise.all(targets.map((target) => getRepoDiffForTarget(target.label, target.cwd)));
|
|
1629
1681
|
sendJson(res, 200, { repos });
|
|
1630
1682
|
} catch (err) {
|
|
@@ -1933,15 +1985,15 @@ function attachAgentEventsServer() {
|
|
|
1933
1985
|
// src/auth.ts
|
|
1934
1986
|
import { createClient } from "@supabase/supabase-js";
|
|
1935
1987
|
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync4, rmSync as rmSync2, writeFileSync as writeFileSync7 } from "node:fs";
|
|
1936
|
-
import { join as
|
|
1988
|
+
import { join as join9 } from "node:path";
|
|
1937
1989
|
import open from "open";
|
|
1938
1990
|
|
|
1939
1991
|
// src/config.ts
|
|
1940
1992
|
import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync6 } from "node:fs";
|
|
1941
1993
|
import { hostname, homedir as homedir4 } from "node:os";
|
|
1942
|
-
import { join as
|
|
1943
|
-
var CONFIG_DIR =
|
|
1944
|
-
var CONFIG_FILE =
|
|
1994
|
+
import { join as join8, resolve as resolve2 } from "node:path";
|
|
1995
|
+
var CONFIG_DIR = join8(homedir4(), ".agent-board");
|
|
1996
|
+
var CONFIG_FILE = join8(CONFIG_DIR, "credentials.json");
|
|
1945
1997
|
function loadConfig() {
|
|
1946
1998
|
if (!existsSync3(CONFIG_FILE)) return null;
|
|
1947
1999
|
return JSON.parse(readFileSync3(CONFIG_FILE, "utf-8"));
|
|
@@ -1974,7 +2026,7 @@ function resolveRepoPath(config, relativeRepoPath) {
|
|
|
1974
2026
|
}
|
|
1975
2027
|
|
|
1976
2028
|
// src/auth.ts
|
|
1977
|
-
var SESSION_FILE =
|
|
2029
|
+
var SESSION_FILE = join9(CONFIG_DIR, "session.json");
|
|
1978
2030
|
var DEFAULT_SUPABASE_URL = "https://brpxshvnmxgkbffgdfcc.supabase.co";
|
|
1979
2031
|
var DEFAULT_SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJycHhzaHZubXhna2JmZmdkZmNjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3ODc1OTQwMTEsImV4cCI6MjEwMzE3MDAxMX0.ErsAGZysLFgNvijetR7w1YvIziXC-1D2sx04Hm2UW9c";
|
|
1980
2032
|
function resolveSupabaseUrl() {
|
|
@@ -2033,7 +2085,7 @@ async function requireSession(supabase) {
|
|
|
2033
2085
|
|
|
2034
2086
|
// src/columnActions.ts
|
|
2035
2087
|
import { exec } from "node:child_process";
|
|
2036
|
-
import { join as
|
|
2088
|
+
import { join as join10 } from "node:path";
|
|
2037
2089
|
import { promisify as promisify3 } from "node:util";
|
|
2038
2090
|
var execAsync = promisify3(exec);
|
|
2039
2091
|
var OUTPUT_TAIL_CHARS = 4e3;
|
|
@@ -2043,7 +2095,7 @@ function tail(text) {
|
|
|
2043
2095
|
function resolveTargets(project, children, repoRoot, column, timing) {
|
|
2044
2096
|
if (children.length > 0) {
|
|
2045
2097
|
return children.flatMap(
|
|
2046
|
-
(child2) => (child2.column_actions[column] ?? []).filter((action) => action.timing === timing).map((action) => ({ label: child2.subpath, cwd:
|
|
2098
|
+
(child2) => (child2.column_actions[column] ?? []).filter((action) => action.timing === timing).map((action) => ({ label: child2.subpath, cwd: join10(repoRoot, child2.subpath), command: action.command }))
|
|
2047
2099
|
);
|
|
2048
2100
|
}
|
|
2049
2101
|
return (project.column_actions[column] ?? []).filter((action) => action.timing === timing).map((action) => ({ label: null, cwd: repoRoot, command: action.command }));
|
|
@@ -2149,7 +2201,7 @@ async function checkPendingDeployActionReruns(supabase, config) {
|
|
|
2149
2201
|
}
|
|
2150
2202
|
|
|
2151
2203
|
// src/deploy.ts
|
|
2152
|
-
import { join as
|
|
2204
|
+
import { join as join13 } from "node:path";
|
|
2153
2205
|
|
|
2154
2206
|
// src/devServer.ts
|
|
2155
2207
|
import { spawn as spawn4 } from "node:child_process";
|
|
@@ -2158,7 +2210,7 @@ import { spawn as spawn4 } from "node:child_process";
|
|
|
2158
2210
|
import { spawn as spawn3 } from "node:child_process";
|
|
2159
2211
|
import { accessSync, constants } from "node:fs";
|
|
2160
2212
|
import { homedir as homedir5 } from "node:os";
|
|
2161
|
-
import { delimiter, join as
|
|
2213
|
+
import { delimiter, join as join11 } from "node:path";
|
|
2162
2214
|
import { bin as bundledBinaryPath, install as downloadCloudflared } from "cloudflared";
|
|
2163
2215
|
var TRYCLOUDFLARE_URL_RE = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
|
|
2164
2216
|
var STARTUP_TIMEOUT_MS = 2e4;
|
|
@@ -2169,15 +2221,15 @@ function wellKnownDirectories() {
|
|
|
2169
2221
|
const home = homedir5();
|
|
2170
2222
|
if (IS_WINDOWS2) {
|
|
2171
2223
|
const candidates = [
|
|
2172
|
-
process.env.ProgramFiles &&
|
|
2173
|
-
process.env["ProgramFiles(x86)"] &&
|
|
2174
|
-
process.env.LOCALAPPDATA &&
|
|
2175
|
-
|
|
2176
|
-
process.env.ChocolateyInstall &&
|
|
2224
|
+
process.env.ProgramFiles && join11(process.env.ProgramFiles, "cloudflared"),
|
|
2225
|
+
process.env["ProgramFiles(x86)"] && join11(process.env["ProgramFiles(x86)"], "cloudflared"),
|
|
2226
|
+
process.env.LOCALAPPDATA && join11(process.env.LOCALAPPDATA, "Microsoft", "WinGet", "Links"),
|
|
2227
|
+
join11(home, "scoop", "shims"),
|
|
2228
|
+
process.env.ChocolateyInstall && join11(process.env.ChocolateyInstall, "bin")
|
|
2177
2229
|
];
|
|
2178
2230
|
return candidates.filter((dir) => Boolean(dir));
|
|
2179
2231
|
}
|
|
2180
|
-
return ["/usr/local/bin", "/usr/bin", "/opt/homebrew/bin",
|
|
2232
|
+
return ["/usr/local/bin", "/usr/bin", "/opt/homebrew/bin", join11(home, ".local", "bin")];
|
|
2181
2233
|
}
|
|
2182
2234
|
function pathDirectories() {
|
|
2183
2235
|
return (process.env.PATH ?? "").split(delimiter).map((dir) => dir.trim().replace(/^"|"$/g, "")).filter(Boolean);
|
|
@@ -2193,7 +2245,7 @@ function isExecutableFile(path) {
|
|
|
2193
2245
|
function findInstalledBinary() {
|
|
2194
2246
|
const override = process.env.AGENT_BOARD_CLOUDFLARED_PATH;
|
|
2195
2247
|
const directories = [...pathDirectories(), ...wellKnownDirectories()];
|
|
2196
|
-
const candidates = [...override ? [override] : [], ...directories.map((dir) =>
|
|
2248
|
+
const candidates = [...override ? [override] : [], ...directories.map((dir) => join11(dir, BINARY_NAME))];
|
|
2197
2249
|
return candidates.find(isExecutableFile) ?? null;
|
|
2198
2250
|
}
|
|
2199
2251
|
async function locateCloudflared() {
|
|
@@ -2424,6 +2476,8 @@ var SHIFT_TAB_PRESSES_TO_PLAN_MODE = 4;
|
|
|
2424
2476
|
var SHIFT_TAB_DELAY_MS = 400;
|
|
2425
2477
|
var PLAN_MODE_INDICATOR = /plan mode on/i;
|
|
2426
2478
|
function spawnAgentPty(repoPath, cardId, cardTitle, instructions, sessionId, ticket, planFirst) {
|
|
2479
|
+
ensureDirTrusted(repoPath);
|
|
2480
|
+
ensureMcpServersTrusted(repoPath);
|
|
2427
2481
|
const claudeSessionId = randomUUID5();
|
|
2428
2482
|
const pty = spawn5(
|
|
2429
2483
|
CLAUDE_BINARY,
|
|
@@ -2520,6 +2574,8 @@ function sendContinue(pty, cardId) {
|
|
|
2520
2574
|
}
|
|
2521
2575
|
}
|
|
2522
2576
|
function resumeAgentPty(repoPath, claudeSessionId, sessionId, ticket) {
|
|
2577
|
+
ensureDirTrusted(repoPath);
|
|
2578
|
+
ensureMcpServersTrusted(repoPath);
|
|
2523
2579
|
return spawn5(
|
|
2524
2580
|
CLAUDE_BINARY,
|
|
2525
2581
|
[
|
|
@@ -2543,8 +2599,8 @@ function resumeAgentPty(repoPath, claudeSessionId, sessionId, ticket) {
|
|
|
2543
2599
|
// src/cardAttachments.ts
|
|
2544
2600
|
import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
|
|
2545
2601
|
import { tmpdir as tmpdir6 } from "node:os";
|
|
2546
|
-
import { extname, join as
|
|
2547
|
-
var ATTACHMENTS_ROOT =
|
|
2602
|
+
import { extname, join as join12 } from "node:path";
|
|
2603
|
+
var ATTACHMENTS_ROOT = join12(tmpdir6(), "agent-board", "card-attachments");
|
|
2548
2604
|
function sanitizeFileName(fileName) {
|
|
2549
2605
|
return fileName.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
2550
2606
|
}
|
|
@@ -2556,13 +2612,13 @@ async function downloadCardAttachments(supabase, cardId) {
|
|
|
2556
2612
|
}
|
|
2557
2613
|
const rows = data ?? [];
|
|
2558
2614
|
if (rows.length === 0) return [];
|
|
2559
|
-
const targetDir =
|
|
2615
|
+
const targetDir = join12(ATTACHMENTS_ROOT, cardId);
|
|
2560
2616
|
await mkdir2(targetDir, { recursive: true });
|
|
2561
2617
|
const paths = [];
|
|
2562
2618
|
for (const row of rows) {
|
|
2563
2619
|
const baseName = sanitizeFileName(row.file_name);
|
|
2564
2620
|
const fileName = extname(baseName) ? baseName : `${baseName}${extname(row.storage_path)}`;
|
|
2565
|
-
const localPath =
|
|
2621
|
+
const localPath = join12(targetDir, `${row.id}-${fileName}`);
|
|
2566
2622
|
try {
|
|
2567
2623
|
const { data: blob, error: downloadError } = await supabase.storage.from(CARD_ATTACHMENTS_BUCKET).download(row.storage_path);
|
|
2568
2624
|
if (downloadError) throw downloadError;
|
|
@@ -2636,6 +2692,7 @@ function cancelUsageResume(sessionId) {
|
|
|
2636
2692
|
|
|
2637
2693
|
// src/orchestrator.ts
|
|
2638
2694
|
var TERMINAL_TICKET_TTL_MS = 60 * 60 * 1e3;
|
|
2695
|
+
var RESUME_CONVERSATION_MISSING_PATTERN = /no conversation found with session id/i;
|
|
2639
2696
|
var inFlightCardIds = /* @__PURE__ */ new Set();
|
|
2640
2697
|
var activeSessionByCard = /* @__PURE__ */ new Map();
|
|
2641
2698
|
var intentionallyStoppedSessionIds = /* @__PURE__ */ new Set();
|
|
@@ -2709,11 +2766,17 @@ async function dispatchRunnerQueue(supabase, config) {
|
|
|
2709
2766
|
try {
|
|
2710
2767
|
const queued = await listQueuedSessionsForRunner(supabase, config.ownerUserId);
|
|
2711
2768
|
if (queued.length === 0) return;
|
|
2769
|
+
const queuedProjectIds = new Set(queued.map((entry) => entry.cards.project_id));
|
|
2712
2770
|
const maxConcurrentProjects = await getRunnerMaxConcurrentProjects(supabase, config.runnerId);
|
|
2713
2771
|
const runningProjectIds = /* @__PURE__ */ new Set();
|
|
2714
2772
|
for (const project of config.registeredProjects) {
|
|
2715
|
-
const
|
|
2716
|
-
if (
|
|
2773
|
+
const running = await getRunningSessionForProject(supabase, project.id, config.ownerUserId);
|
|
2774
|
+
if (!running) continue;
|
|
2775
|
+
if (running.agentState === "done" && queuedProjectIds.has(project.id)) {
|
|
2776
|
+
await pauseFinishedSession(supabase, config, project.id, running.cardId, running.id);
|
|
2777
|
+
continue;
|
|
2778
|
+
}
|
|
2779
|
+
runningProjectIds.add(project.id);
|
|
2717
2780
|
}
|
|
2718
2781
|
let freeProjectSlots = maxConcurrentProjects - runningProjectIds.size;
|
|
2719
2782
|
if (freeProjectSlots <= 0) return;
|
|
@@ -2767,9 +2830,13 @@ async function spawnSessionPty(supabase, config, entry) {
|
|
|
2767
2830
|
const ticket = randomUUID6();
|
|
2768
2831
|
let pty;
|
|
2769
2832
|
let newClaudeSessionId = null;
|
|
2833
|
+
let resumeConversationMissing = false;
|
|
2770
2834
|
try {
|
|
2771
2835
|
if (claudeSessionId) {
|
|
2772
2836
|
pty = resumeAgentPty(registered.repoPath, claudeSessionId, sessionId, ticket);
|
|
2837
|
+
pty.onData((chunk) => {
|
|
2838
|
+
if (RESUME_CONVERSATION_MISSING_PATTERN.test(normalizeTerminalOutput(chunk))) resumeConversationMissing = true;
|
|
2839
|
+
});
|
|
2773
2840
|
} else {
|
|
2774
2841
|
const attachmentPaths = await downloadCardAttachments(supabase, cardId);
|
|
2775
2842
|
const instructions = buildInstructionsWithMcpContext(
|
|
@@ -2817,11 +2884,24 @@ async function spawnSessionPty(supabase, config, entry) {
|
|
|
2817
2884
|
);
|
|
2818
2885
|
pty.onExit(({ exitCode }) => {
|
|
2819
2886
|
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
2887
|
activityWatcher.stop();
|
|
2822
2888
|
unregisterAgentActivityWatcher(sessionId);
|
|
2823
2889
|
cancelUsageResume(sessionId);
|
|
2824
2890
|
activeSessionByCard.delete(cardId);
|
|
2891
|
+
if (resumeConversationMissing) {
|
|
2892
|
+
logger.card.warn(
|
|
2893
|
+
cardId,
|
|
2894
|
+
`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.`
|
|
2895
|
+
);
|
|
2896
|
+
void updateSession(
|
|
2897
|
+
supabase,
|
|
2898
|
+
sessionId,
|
|
2899
|
+
{ status: "failed", endedAt: (/* @__PURE__ */ new Date()).toISOString(), claudeSessionId: null },
|
|
2900
|
+
{ projectId: card.project_id, cardId, userId: config.ownerUserId }
|
|
2901
|
+
);
|
|
2902
|
+
return;
|
|
2903
|
+
}
|
|
2904
|
+
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
2905
|
if (!wasIntentional && exitCode !== 0) {
|
|
2826
2906
|
publishAgentState(supabase, card.project_id, cardId, sessionId, "error");
|
|
2827
2907
|
}
|
|
@@ -2842,7 +2922,7 @@ function watchAgentActivity(supabase, config, projectId, cardId, sessionId, pty)
|
|
|
2842
2922
|
} else {
|
|
2843
2923
|
cancelUsageResume(sessionId);
|
|
2844
2924
|
}
|
|
2845
|
-
if (state === "done") void
|
|
2925
|
+
if (state === "done") void dispatchRunnerQueue(supabase, config);
|
|
2846
2926
|
});
|
|
2847
2927
|
pty.onData((chunk) => watcher.push(chunk));
|
|
2848
2928
|
registerAgentActivityWatcher(sessionId, watcher);
|
|
@@ -2852,8 +2932,7 @@ async function pauseFinishedSession(supabase, config, projectId, cardId, session
|
|
|
2852
2932
|
const paused = await pauseRunningSession(supabase, sessionId, { projectId, cardId, userId: config.ownerUserId });
|
|
2853
2933
|
if (!paused) return;
|
|
2854
2934
|
setPtySessionReadOnly(sessionId, true);
|
|
2855
|
-
logger.card.info(cardId, `Session ${sessionId} paused (agent reached "done"
|
|
2856
|
-
await dispatchRunnerQueue(supabase, config);
|
|
2935
|
+
logger.card.info(cardId, `Session ${sessionId} paused (agent reached "done", another session needs this project's WIP slot).`);
|
|
2857
2936
|
}
|
|
2858
2937
|
async function requeuePausedCard(supabase, config, card) {
|
|
2859
2938
|
const active = await getActiveSessionForCard(supabase, card.id);
|
|
@@ -2931,6 +3010,24 @@ async function stopCardSession(supabase, config, card) {
|
|
|
2931
3010
|
logger.card.info(card.id, `Session ${sessionId} stopped.`);
|
|
2932
3011
|
await dispatchRunnerQueue(supabase, config);
|
|
2933
3012
|
}
|
|
3013
|
+
async function restartCardSession(supabase, config, card) {
|
|
3014
|
+
const sessionId = activeSessionByCard.get(card.id) ?? (await getActiveSessionForCard(supabase, card.id))?.id;
|
|
3015
|
+
if (sessionId) {
|
|
3016
|
+
intentionallyStoppedSessionIds.add(sessionId);
|
|
3017
|
+
stopPtySession(sessionId);
|
|
3018
|
+
cancelUsageResume(sessionId);
|
|
3019
|
+
activeSessionByCard.delete(card.id);
|
|
3020
|
+
await updateSession(
|
|
3021
|
+
supabase,
|
|
3022
|
+
sessionId,
|
|
3023
|
+
{ status: "failed", claudeSessionId: null, endedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
3024
|
+
{ projectId: card.project_id, cardId: card.id, userId: config.ownerUserId }
|
|
3025
|
+
);
|
|
3026
|
+
logger.card.info(card.id, `Session ${sessionId} abandoned by "Reiniciar sess\xE3o"; starting a fresh one.`);
|
|
3027
|
+
}
|
|
3028
|
+
await updateCard(supabase, card.id, { restartRequestedAt: null });
|
|
3029
|
+
await enqueueCard(supabase, config, card);
|
|
3030
|
+
}
|
|
2934
3031
|
|
|
2935
3032
|
// src/deploy.ts
|
|
2936
3033
|
async function handleDeployRequested(supabase, config, project) {
|
|
@@ -2948,7 +3045,7 @@ async function handleDeployRequested(supabase, config, project) {
|
|
|
2948
3045
|
}
|
|
2949
3046
|
const children = await getProjectRepos(supabase, project.id);
|
|
2950
3047
|
const repoRoot = resolveRepoPath(config, project.repo_path);
|
|
2951
|
-
const targets = children.length > 0 ? children.map((child2) => ({ label: child2.subpath, path:
|
|
3048
|
+
const targets = children.length > 0 ? children.map((child2) => ({ label: child2.subpath, path: join13(repoRoot, child2.subpath) })) : [{ label: null, path: repoRoot }];
|
|
2952
3049
|
const failures = [];
|
|
2953
3050
|
for (const target of targets) {
|
|
2954
3051
|
const result = await pushBranchToMain(target.path);
|
|
@@ -3082,7 +3179,7 @@ async function syncRegisteredProjects(supabase, config, registeredPaths) {
|
|
|
3082
3179
|
}
|
|
3083
3180
|
|
|
3084
3181
|
// src/review.ts
|
|
3085
|
-
import { join as
|
|
3182
|
+
import { join as join14 } from "node:path";
|
|
3086
3183
|
var inFlightCardIds2 = /* @__PURE__ */ new Set();
|
|
3087
3184
|
async function handleCardEnteredRevisao(supabase, config, card) {
|
|
3088
3185
|
if (inFlightCardIds2.has(card.id)) return;
|
|
@@ -3091,7 +3188,7 @@ async function handleCardEnteredRevisao(supabase, config, card) {
|
|
|
3091
3188
|
const project = await getProject(supabase, card.project_id);
|
|
3092
3189
|
const children = await getProjectRepos(supabase, project.id);
|
|
3093
3190
|
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:
|
|
3191
|
+
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
3192
|
if (targets.length === 0) {
|
|
3096
3193
|
logger.card.info(card.id, `No dev_command configured for project ${project.id}, skipping automatic preview.`);
|
|
3097
3194
|
return;
|
|
@@ -3177,7 +3274,7 @@ function attachTerminalServer() {
|
|
|
3177
3274
|
const buffer = data;
|
|
3178
3275
|
const frameType = buffer[0];
|
|
3179
3276
|
if (frameType === FRAME_TYPE_IMAGE) {
|
|
3180
|
-
if (handle.readOnly) return;
|
|
3277
|
+
if (handle.readOnly || handle.exited) return;
|
|
3181
3278
|
const extCode = buffer[1];
|
|
3182
3279
|
const bytes = buffer.subarray(2);
|
|
3183
3280
|
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 +3292,7 @@ function attachTerminalServer() {
|
|
|
3195
3292
|
logger.runner.warn("TERMINAL", `Terminal WS: ignoring binary frame with unknown type ${frameType} for session ${sessionId}.`);
|
|
3196
3293
|
return;
|
|
3197
3294
|
}
|
|
3198
|
-
if (handle.readOnly) return;
|
|
3295
|
+
if (handle.readOnly || handle.exited) return;
|
|
3199
3296
|
handle.pty.write(data.toString());
|
|
3200
3297
|
});
|
|
3201
3298
|
socket.on("close", () => {
|
|
@@ -3317,6 +3414,7 @@ async function startRunner() {
|
|
|
3317
3414
|
const registeredPaths = new Set(config.registeredProjects.map((project) => project.id));
|
|
3318
3415
|
const lastKnownColumn = /* @__PURE__ */ new Map();
|
|
3319
3416
|
const lastKnownRequeueRequestedAt = /* @__PURE__ */ new Map();
|
|
3417
|
+
const lastKnownRestartRequestedAt = /* @__PURE__ */ new Map();
|
|
3320
3418
|
await syncRegisteredProjects(supabase, config, registeredPaths).catch((err) => logger.runner.error("PROJECT_SYNC", "syncRegisteredProjects failed:", err));
|
|
3321
3419
|
logger.runner.info("BOOT", `Agent Board runner "${config.runnerName}" starting (${registeredPaths.size} project(s) registered).`);
|
|
3322
3420
|
for (const project of config.registeredProjects) {
|
|
@@ -3412,6 +3510,11 @@ async function startRunner() {
|
|
|
3412
3510
|
if (card.requeue_requested_at && card.requeue_requested_at !== previousRequeueRequestedAt) {
|
|
3413
3511
|
void requeuePausedCard(supabase, config, card);
|
|
3414
3512
|
}
|
|
3513
|
+
const previousRestartRequestedAt = lastKnownRestartRequestedAt.get(card.id);
|
|
3514
|
+
lastKnownRestartRequestedAt.set(card.id, card.restart_requested_at);
|
|
3515
|
+
if (card.restart_requested_at && card.restart_requested_at !== previousRestartRequestedAt) {
|
|
3516
|
+
void restartCardSession(supabase, config, card);
|
|
3517
|
+
}
|
|
3415
3518
|
if (previousColumn !== void 0 && previousColumn !== card.column_name) {
|
|
3416
3519
|
void clearColumnActionStatus(supabase, card);
|
|
3417
3520
|
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.4",
|
|
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": {
|