@integrity-labs/agt-cli 0.7.6 → 0.7.8
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.
|
@@ -6,9 +6,10 @@ import {
|
|
|
6
6
|
getFramework,
|
|
7
7
|
getHostId,
|
|
8
8
|
provision,
|
|
9
|
+
provisionStopHook,
|
|
9
10
|
requireHost,
|
|
10
11
|
resolveChannels
|
|
11
|
-
} from "../chunk-
|
|
12
|
+
} from "../chunk-B47JW4MM.js";
|
|
12
13
|
import {
|
|
13
14
|
findTaskByTemplate,
|
|
14
15
|
getProjectDir,
|
|
@@ -20,10 +21,42 @@ import {
|
|
|
20
21
|
|
|
21
22
|
// src/lib/manager-worker.ts
|
|
22
23
|
import { createHash } from "crypto";
|
|
23
|
-
import { readFileSync as
|
|
24
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync, existsSync as existsSync2, rmSync, readdirSync, statSync, unlinkSync } from "fs";
|
|
24
25
|
import https from "https";
|
|
25
26
|
import { join as join2 } from "path";
|
|
26
27
|
|
|
28
|
+
// src/lib/mcp-sanitize.ts
|
|
29
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
30
|
+
function sanitizeMcpJson(mcpConfigPath, apiHost) {
|
|
31
|
+
try {
|
|
32
|
+
const mcpRaw = JSON.parse(readFileSync(mcpConfigPath, "utf-8"));
|
|
33
|
+
const servers = mcpRaw.mcpServers;
|
|
34
|
+
if (!servers) return false;
|
|
35
|
+
let changed = false;
|
|
36
|
+
for (const [key, val] of Object.entries(servers)) {
|
|
37
|
+
if (typeof val?.url !== "string") continue;
|
|
38
|
+
if (val.url.startsWith("/")) {
|
|
39
|
+
if (apiHost) {
|
|
40
|
+
val.url = `${apiHost}${val.url}`;
|
|
41
|
+
changed = true;
|
|
42
|
+
} else {
|
|
43
|
+
delete servers[key];
|
|
44
|
+
changed = true;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (!val.type) {
|
|
49
|
+
val.type = "sse";
|
|
50
|
+
changed = true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (changed) writeFileSync(mcpConfigPath, JSON.stringify(mcpRaw, null, 2));
|
|
54
|
+
return changed;
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
27
60
|
// src/lib/gateway-client.ts
|
|
28
61
|
import { EventEmitter } from "events";
|
|
29
62
|
import WebSocket from "ws";
|
|
@@ -297,7 +330,7 @@ var GatewayClientPool = class extends EventEmitter {
|
|
|
297
330
|
import { spawn, execSync } from "child_process";
|
|
298
331
|
import { join } from "path";
|
|
299
332
|
import { homedir } from "os";
|
|
300
|
-
import { existsSync, readFileSync } from "fs";
|
|
333
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
301
334
|
var sessions = /* @__PURE__ */ new Map();
|
|
302
335
|
function startPersistentSession(config2) {
|
|
303
336
|
const existing = sessions.get(config2.codeName);
|
|
@@ -318,6 +351,7 @@ function startPersistentSession(config2) {
|
|
|
318
351
|
}
|
|
319
352
|
function spawnSession(config2, session) {
|
|
320
353
|
const { codeName, projectDir, mcpConfigPath, claudeMdPath, channels, devChannels, log: log2 } = config2;
|
|
354
|
+
sanitizeMcpJson(mcpConfigPath);
|
|
321
355
|
const args = [];
|
|
322
356
|
if (channels.length > 0) {
|
|
323
357
|
args.push("--channels", ...channels);
|
|
@@ -346,7 +380,7 @@ function spawnSession(config2, session) {
|
|
|
346
380
|
let envPrefix = "";
|
|
347
381
|
if (existsSync(envIntegrationsPath)) {
|
|
348
382
|
try {
|
|
349
|
-
const envContent =
|
|
383
|
+
const envContent = readFileSync2(envIntegrationsPath, "utf-8");
|
|
350
384
|
const envVars = envContent.split("\n").filter((line) => line && !line.startsWith("#") && line.includes("=")).map((line) => {
|
|
351
385
|
const eqIdx = line.indexOf("=");
|
|
352
386
|
const key = line.slice(0, eqIdx);
|
|
@@ -490,6 +524,32 @@ function resetRestartCount(codeName) {
|
|
|
490
524
|
const session = sessions.get(codeName);
|
|
491
525
|
if (session) session.restartCount = 0;
|
|
492
526
|
}
|
|
527
|
+
async function stopAllSessionsAndWait(log2, opts) {
|
|
528
|
+
const codeNames = [...sessions.keys()];
|
|
529
|
+
if (codeNames.length === 0) return;
|
|
530
|
+
const exitPromises = [];
|
|
531
|
+
for (const codeName of codeNames) {
|
|
532
|
+
const session = sessions.get(codeName);
|
|
533
|
+
const proc = session?.process;
|
|
534
|
+
stopPersistentSession(codeName, log2);
|
|
535
|
+
if (proc && !proc.killed && proc.exitCode === null) {
|
|
536
|
+
exitPromises.push(
|
|
537
|
+
new Promise((resolve) => {
|
|
538
|
+
proc.on("close", resolve);
|
|
539
|
+
proc.on("error", resolve);
|
|
540
|
+
})
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (exitPromises.length === 0) return;
|
|
545
|
+
await Promise.race([
|
|
546
|
+
Promise.all(exitPromises),
|
|
547
|
+
new Promise((resolve) => setTimeout(() => {
|
|
548
|
+
log2(`[persistent-session] Shutdown timeout (${opts.timeoutMs}ms), force-killing remaining sessions`);
|
|
549
|
+
resolve();
|
|
550
|
+
}, opts.timeoutMs))
|
|
551
|
+
]);
|
|
552
|
+
}
|
|
493
553
|
function getProjectDir2(codeName) {
|
|
494
554
|
return join(homedir(), ".augmented", codeName, "project");
|
|
495
555
|
}
|
|
@@ -934,14 +994,14 @@ function checkClaudeAuth(execFileSync) {
|
|
|
934
994
|
}
|
|
935
995
|
function loadGatewayPorts() {
|
|
936
996
|
try {
|
|
937
|
-
return JSON.parse(
|
|
997
|
+
return JSON.parse(readFileSync3(GATEWAY_PORTS_FILE, "utf-8"));
|
|
938
998
|
} catch {
|
|
939
999
|
return {};
|
|
940
1000
|
}
|
|
941
1001
|
}
|
|
942
1002
|
function saveGatewayPorts(ports) {
|
|
943
1003
|
mkdirSync(AUGMENTED_DIR, { recursive: true });
|
|
944
|
-
|
|
1004
|
+
writeFileSync2(GATEWAY_PORTS_FILE, JSON.stringify(ports, null, 2));
|
|
945
1005
|
}
|
|
946
1006
|
function allocatePort(codeName) {
|
|
947
1007
|
const ports = loadGatewayPorts();
|
|
@@ -967,7 +1027,7 @@ var STATE_FILE = join2(process.env["HOME"] ?? "/tmp", ".augmented", "manager-sta
|
|
|
967
1027
|
function send(msg) {
|
|
968
1028
|
if (msg.type === "state-update") {
|
|
969
1029
|
try {
|
|
970
|
-
|
|
1030
|
+
writeFileSync2(STATE_FILE, JSON.stringify(msg.state, null, 2));
|
|
971
1031
|
} catch {
|
|
972
1032
|
}
|
|
973
1033
|
}
|
|
@@ -989,7 +1049,7 @@ function sha256(content) {
|
|
|
989
1049
|
}
|
|
990
1050
|
function hashFile(filePath) {
|
|
991
1051
|
try {
|
|
992
|
-
const content =
|
|
1052
|
+
const content = readFileSync3(filePath, "utf-8");
|
|
993
1053
|
return sha256(content);
|
|
994
1054
|
} catch {
|
|
995
1055
|
return null;
|
|
@@ -1000,7 +1060,7 @@ async function migrateToProfiles() {
|
|
|
1000
1060
|
const sharedConfigPath = join2(homeDir, ".openclaw", "openclaw.json");
|
|
1001
1061
|
let sharedConfig;
|
|
1002
1062
|
try {
|
|
1003
|
-
sharedConfig = JSON.parse(
|
|
1063
|
+
sharedConfig = JSON.parse(readFileSync3(sharedConfigPath, "utf-8"));
|
|
1004
1064
|
} catch {
|
|
1005
1065
|
return;
|
|
1006
1066
|
}
|
|
@@ -1024,8 +1084,8 @@ async function migrateToProfiles() {
|
|
|
1024
1084
|
const authFile = join2(sharedAuthDir, "auth-profiles.json");
|
|
1025
1085
|
if (existsSync2(authFile)) {
|
|
1026
1086
|
mkdirSync(profileAuthDir, { recursive: true });
|
|
1027
|
-
const authContent =
|
|
1028
|
-
|
|
1087
|
+
const authContent = readFileSync3(authFile, "utf-8");
|
|
1088
|
+
writeFileSync2(join2(profileAuthDir, "auth-profiles.json"), authContent);
|
|
1029
1089
|
}
|
|
1030
1090
|
allocatePort(codeName);
|
|
1031
1091
|
migrated++;
|
|
@@ -1063,7 +1123,7 @@ function readGatewayToken(codeName) {
|
|
|
1063
1123
|
}
|
|
1064
1124
|
const homeDir = process.env["HOME"] ?? "/tmp";
|
|
1065
1125
|
try {
|
|
1066
|
-
const cfg = JSON.parse(
|
|
1126
|
+
const cfg = JSON.parse(readFileSync3(join2(homeDir, `.openclaw-${codeName}`, "openclaw.json"), "utf-8"));
|
|
1067
1127
|
return cfg?.gateway?.auth?.token;
|
|
1068
1128
|
} catch {
|
|
1069
1129
|
return void 0;
|
|
@@ -1075,7 +1135,7 @@ function isGatewayHung(codeName) {
|
|
|
1075
1135
|
const jobsPath = join2(homeDir, `.openclaw-${codeName}`, "cron", "jobs.json");
|
|
1076
1136
|
if (!existsSync2(jobsPath)) return false;
|
|
1077
1137
|
try {
|
|
1078
|
-
const data = JSON.parse(
|
|
1138
|
+
const data = JSON.parse(readFileSync3(jobsPath, "utf-8"));
|
|
1079
1139
|
const jobs = data.jobs ?? data;
|
|
1080
1140
|
if (!Array.isArray(jobs)) return false;
|
|
1081
1141
|
const now = Date.now();
|
|
@@ -1116,11 +1176,11 @@ async function ensureGatewayRunning(codeName, adapter) {
|
|
|
1116
1176
|
const homeDir = process.env["HOME"] ?? "/tmp";
|
|
1117
1177
|
const configPath = join2(homeDir, `.openclaw-${codeName}`, "openclaw.json");
|
|
1118
1178
|
if (existsSync2(configPath)) {
|
|
1119
|
-
const cfg = JSON.parse(
|
|
1179
|
+
const cfg = JSON.parse(readFileSync3(configPath, "utf-8"));
|
|
1120
1180
|
if (cfg.gateway?.port !== status.port) {
|
|
1121
1181
|
if (!cfg.gateway) cfg.gateway = {};
|
|
1122
1182
|
cfg.gateway.port = status.port;
|
|
1123
|
-
|
|
1183
|
+
writeFileSync2(configPath, JSON.stringify(cfg, null, 2));
|
|
1124
1184
|
}
|
|
1125
1185
|
}
|
|
1126
1186
|
} catch {
|
|
@@ -1142,10 +1202,10 @@ async function ensureGatewayRunning(codeName, adapter) {
|
|
|
1142
1202
|
const homeDir = process.env["HOME"] ?? "/tmp";
|
|
1143
1203
|
const configPath = join2(homeDir, `.openclaw-${codeName}`, "openclaw.json");
|
|
1144
1204
|
if (existsSync2(configPath)) {
|
|
1145
|
-
const cfg = JSON.parse(
|
|
1205
|
+
const cfg = JSON.parse(readFileSync3(configPath, "utf-8"));
|
|
1146
1206
|
if (!cfg.gateway) cfg.gateway = {};
|
|
1147
1207
|
cfg.gateway.port = port;
|
|
1148
|
-
|
|
1208
|
+
writeFileSync2(configPath, JSON.stringify(cfg, null, 2));
|
|
1149
1209
|
}
|
|
1150
1210
|
} catch {
|
|
1151
1211
|
}
|
|
@@ -1308,10 +1368,13 @@ async function pollCycle() {
|
|
|
1308
1368
|
const currentIds = new Set(agents.map((a) => a.agent_id));
|
|
1309
1369
|
for (const prev of state.agents) {
|
|
1310
1370
|
if (!currentIds.has(prev.agentId)) {
|
|
1311
|
-
log(`Agent '${prev.codeName}'
|
|
1371
|
+
log(`Agent '${prev.codeName}' removed from host (deleted or unassigned)`);
|
|
1312
1372
|
const adapter = resolveAgentFramework(prev.codeName);
|
|
1313
|
-
clearAgentCaches(prev.agentId, prev.codeName);
|
|
1314
1373
|
await stopGatewayIfRunning(prev.codeName, adapter);
|
|
1374
|
+
freePort(prev.codeName);
|
|
1375
|
+
const agentDir = join2(config.configDir, prev.codeName, "provision");
|
|
1376
|
+
await cleanupAgentFiles(prev.codeName, agentDir);
|
|
1377
|
+
clearAgentCaches(prev.agentId, prev.codeName);
|
|
1315
1378
|
}
|
|
1316
1379
|
}
|
|
1317
1380
|
await healthCheckGateways(agentStates);
|
|
@@ -1542,7 +1605,7 @@ async function processAgent(agent, agentStates) {
|
|
|
1542
1605
|
const fileNames = changedFiles.map((f) => f.relativePath).join(", ");
|
|
1543
1606
|
log(`${verb} '${agent.code_name}': ${fileNames}`);
|
|
1544
1607
|
for (const file of changedFiles) {
|
|
1545
|
-
|
|
1608
|
+
writeFileSync2(join2(agentDir, file.relativePath), file.content);
|
|
1546
1609
|
}
|
|
1547
1610
|
lastProvisionAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1548
1611
|
knownVersions.set(agent.agent_id, { charterVersion, toolsVersion });
|
|
@@ -1734,6 +1797,44 @@ async function processAgent(agent, agentStates) {
|
|
|
1734
1797
|
}
|
|
1735
1798
|
knownIntegrationHashes.set(agent.agent_id, intHash);
|
|
1736
1799
|
log(`Integrations provisioned for '${agent.code_name}' (${integrations.length} integration(s))`);
|
|
1800
|
+
const managedIntegrations = integrations.filter((i) => i.auth_type === "managed");
|
|
1801
|
+
if (managedIntegrations.length > 0 && frameworkAdapter.writeMcpServer) {
|
|
1802
|
+
try {
|
|
1803
|
+
const toolkitData = await api.post("/host/managed-toolkits", { agent_ids: [agent.agent_id] });
|
|
1804
|
+
const expectedServerIds = /* @__PURE__ */ new Set();
|
|
1805
|
+
for (const tk of toolkitData.toolkits) {
|
|
1806
|
+
if (tk.agent_id !== agent.agent_id) continue;
|
|
1807
|
+
const serverId = tk.toolkit_id.replace(/\//g, "-");
|
|
1808
|
+
expectedServerIds.add(serverId);
|
|
1809
|
+
const proxyUrl = tk.proxy_url.startsWith("/") ? `${requireHost()}${tk.proxy_url}` : tk.proxy_url;
|
|
1810
|
+
frameworkAdapter.writeMcpServer(agent.code_name, serverId, {
|
|
1811
|
+
url: proxyUrl
|
|
1812
|
+
});
|
|
1813
|
+
log(`[managed-toolkit] ${agent.code_name}: ${tk.toolkit_name} \u2192 ${proxyUrl}`);
|
|
1814
|
+
}
|
|
1815
|
+
if (frameworkAdapter.removeMcpServer) {
|
|
1816
|
+
try {
|
|
1817
|
+
const { readFileSync: readFileSync4 } = await import("fs");
|
|
1818
|
+
const { join: join3 } = await import("path");
|
|
1819
|
+
const { homedir: homedir2 } = await import("os");
|
|
1820
|
+
const mcpPath = join3(homedir2(), ".augmented", "agents", agent.code_name, "provision", ".mcp.json");
|
|
1821
|
+
const mcpConfig = JSON.parse(readFileSync4(mcpPath, "utf-8"));
|
|
1822
|
+
if (mcpConfig.mcpServers) {
|
|
1823
|
+
const managedPrefixes = ["composio-", "one-", "nango-", "paragon-"];
|
|
1824
|
+
for (const key of Object.keys(mcpConfig.mcpServers)) {
|
|
1825
|
+
if (managedPrefixes.some((p) => key.startsWith(p)) && !expectedServerIds.has(key)) {
|
|
1826
|
+
frameworkAdapter.removeMcpServer(agent.code_name, key);
|
|
1827
|
+
log(`[managed-toolkit] Removed stale MCP server '${key}' for '${agent.code_name}'`);
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
} catch {
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
} catch (err) {
|
|
1835
|
+
log(`[managed-toolkit] Failed to provision for '${agent.code_name}': ${err.message}`);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1737
1838
|
needsGatewayRestart = true;
|
|
1738
1839
|
const hasLcm = integrations.some((i) => i.definition_id === "lossless-claw");
|
|
1739
1840
|
if (hasLcm && !losslessClawInstalled.get(agent.code_name)) {
|
|
@@ -1980,8 +2081,6 @@ async function processAgent(agent, agentStates) {
|
|
|
1980
2081
|
agentDisplayName: agent.display_name
|
|
1981
2082
|
});
|
|
1982
2083
|
}
|
|
1983
|
-
const ccInFlight = agentFw === "claude-code" ? claudeTaskConcurrency.get(agent.code_name) ?? 0 : null;
|
|
1984
|
-
log(`[${agent.code_name}] Harvest gate: gatewayRunning=${gatewayRunning} gatewayPort=${gatewayPort} tasks=${tasks.length} fw=${agentFw}${ccInFlight !== null ? ` claude-p=${ccInFlight}/${MAX_CLAUDE_CONCURRENCY}` : ""}`);
|
|
1985
2084
|
if (agentFw === "openclaw" && gatewayRunning && gatewayPort && tasks.length > 0) {
|
|
1986
2085
|
const lastHarvest = lastHarvestAt.get(agent.code_name) ?? 0;
|
|
1987
2086
|
if (Date.now() - lastHarvest >= HARVEST_INTERVAL_MS) {
|
|
@@ -2003,7 +2102,7 @@ async function processAgent(agent, agentStates) {
|
|
|
2003
2102
|
const jobsPath = join2(homeDir, `.openclaw-${agent.code_name}`, "cron", "jobs.json");
|
|
2004
2103
|
if (existsSync2(jobsPath)) {
|
|
2005
2104
|
try {
|
|
2006
|
-
const jobsData = JSON.parse(
|
|
2105
|
+
const jobsData = JSON.parse(readFileSync3(jobsPath, "utf-8"));
|
|
2007
2106
|
const kanbanJob = (jobsData.jobs ?? []).find(
|
|
2008
2107
|
(j) => typeof j.name === "string" && j.name.includes("kanban-work")
|
|
2009
2108
|
);
|
|
@@ -2016,7 +2115,20 @@ async function processAgent(agent, agentStates) {
|
|
|
2016
2115
|
}
|
|
2017
2116
|
}
|
|
2018
2117
|
} else if (agentFw === "claude-code") {
|
|
2019
|
-
|
|
2118
|
+
if (sessionMode === "persistent") {
|
|
2119
|
+
if (isSessionHealthy(agent.code_name)) {
|
|
2120
|
+
const todayItem = boardItems.find((b) => b.status === "today");
|
|
2121
|
+
const taskHint = todayItem ? ` Top item: "${todayItem.title}" (priority ${todayItem.priority}).` : "";
|
|
2122
|
+
injectMessage(agent.code_name, "task", `New work triggered. Check your kanban board with kanban_list and pick up the next item.${taskHint}`, {
|
|
2123
|
+
task_name: "kanban-work-trigger"
|
|
2124
|
+
});
|
|
2125
|
+
log(`[persistent-session] Work trigger injected for '${agent.code_name}'`);
|
|
2126
|
+
} else {
|
|
2127
|
+
log(`[persistent-session] Work trigger skipped for '${agent.code_name}' \u2014 session not yet healthy`);
|
|
2128
|
+
}
|
|
2129
|
+
} else {
|
|
2130
|
+
fireClaudeWorkTrigger(agent.code_name, agent.agent_id, boardItems);
|
|
2131
|
+
}
|
|
2020
2132
|
}
|
|
2021
2133
|
}
|
|
2022
2134
|
}
|
|
@@ -2147,7 +2259,7 @@ function cleanupCronSessions(sessionsDir, keepCount) {
|
|
|
2147
2259
|
const indexPath = join2(sessionsDir, "sessions.json");
|
|
2148
2260
|
if (!existsSync2(indexPath)) return;
|
|
2149
2261
|
try {
|
|
2150
|
-
const raw =
|
|
2262
|
+
const raw = readFileSync3(indexPath, "utf-8");
|
|
2151
2263
|
const index = JSON.parse(raw);
|
|
2152
2264
|
const cronRunKeys = Object.keys(index).filter((k) => k.includes(":cron:") && k.includes(":run:")).map((k) => ({
|
|
2153
2265
|
key: k,
|
|
@@ -2192,7 +2304,7 @@ function cleanupCronSessions(sessionsDir, keepCount) {
|
|
|
2192
2304
|
}
|
|
2193
2305
|
}
|
|
2194
2306
|
}
|
|
2195
|
-
|
|
2307
|
+
writeFileSync2(indexPath, JSON.stringify(index));
|
|
2196
2308
|
if (toDelete.length > 0) {
|
|
2197
2309
|
log(`Cleaned ${toDelete.length} cron session(s) and ${deletedFiles} file(s) from ${sessionsDir}`);
|
|
2198
2310
|
}
|
|
@@ -2203,7 +2315,7 @@ var STALE_RUN_TIMEOUT_MS = 5 * 6e4;
|
|
|
2203
2315
|
function clearStaleCronRunState(jobsPath) {
|
|
2204
2316
|
if (!existsSync2(jobsPath)) return;
|
|
2205
2317
|
try {
|
|
2206
|
-
const raw =
|
|
2318
|
+
const raw = readFileSync3(jobsPath, "utf-8");
|
|
2207
2319
|
const data = JSON.parse(raw);
|
|
2208
2320
|
const jobs = data.jobs ?? data;
|
|
2209
2321
|
if (!Array.isArray(jobs)) return;
|
|
@@ -2228,7 +2340,7 @@ function clearStaleCronRunState(jobsPath) {
|
|
|
2228
2340
|
}
|
|
2229
2341
|
}
|
|
2230
2342
|
if (changed) {
|
|
2231
|
-
|
|
2343
|
+
writeFileSync2(jobsPath, JSON.stringify(data, null, 2));
|
|
2232
2344
|
}
|
|
2233
2345
|
} catch {
|
|
2234
2346
|
}
|
|
@@ -2331,6 +2443,7 @@ async function syncAndCheckClaudeScheduler(agent, tasks, boardItems, refreshData
|
|
|
2331
2443
|
async function executeAndProcessClaudeTask(codeName, agentId, task, prompt) {
|
|
2332
2444
|
const projectDir = getProjectDir(codeName);
|
|
2333
2445
|
const mcpConfigPath = join2(projectDir, ".mcp.json");
|
|
2446
|
+
sanitizeMcpJson(mcpConfigPath, requireHost());
|
|
2334
2447
|
try {
|
|
2335
2448
|
const claudeMdPath = join2(projectDir, "CLAUDE.md");
|
|
2336
2449
|
const claudeArgs = [
|
|
@@ -2461,6 +2574,11 @@ async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
|
|
|
2461
2574
|
if (persistentSessionAgents.has(codeName)) {
|
|
2462
2575
|
log(`[persistent-session] Session for '${codeName}' is unhealthy, will restart`);
|
|
2463
2576
|
}
|
|
2577
|
+
try {
|
|
2578
|
+
provisionStopHook(codeName);
|
|
2579
|
+
} catch (err) {
|
|
2580
|
+
log(`[persistent-session] Failed to provision Stop hook for '${codeName}': ${err.message}`);
|
|
2581
|
+
}
|
|
2464
2582
|
startPersistentSession({
|
|
2465
2583
|
codeName,
|
|
2466
2584
|
agentId: agent.agent_id,
|
|
@@ -2502,6 +2620,21 @@ async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
|
|
|
2502
2620
|
}
|
|
2503
2621
|
}
|
|
2504
2622
|
log(`[persistent-session] Injecting task '${task.name}' into '${codeName}'`);
|
|
2623
|
+
const projectDir2 = getProjectDir2(codeName);
|
|
2624
|
+
const markerDir = join2(projectDir2, ".claude");
|
|
2625
|
+
const markerPath = join2(markerDir, ".agt-pending-task.json");
|
|
2626
|
+
try {
|
|
2627
|
+
mkdirSync(markerDir, { recursive: true });
|
|
2628
|
+
writeFileSync2(markerPath, JSON.stringify({
|
|
2629
|
+
agent_id: agent.agent_id,
|
|
2630
|
+
task_id: task.taskId,
|
|
2631
|
+
template_id: task.templateId,
|
|
2632
|
+
task_name: task.name,
|
|
2633
|
+
injected_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
2634
|
+
}), "utf-8");
|
|
2635
|
+
} catch (err) {
|
|
2636
|
+
log(`[persistent-session] Failed to write task marker for '${codeName}': ${err.message}`);
|
|
2637
|
+
}
|
|
2505
2638
|
const injected = await injectMessage(codeName, "task", prompt, {
|
|
2506
2639
|
task_id: task.taskId,
|
|
2507
2640
|
template_id: task.templateId,
|
|
@@ -2545,8 +2678,23 @@ var realtimeDriftStarted = false;
|
|
|
2545
2678
|
var realtimeKanbanStarted = false;
|
|
2546
2679
|
var realtimeAssignStarted = false;
|
|
2547
2680
|
var realtimeConfigStarted = false;
|
|
2681
|
+
var realtimeSubscribedAgentIds = /* @__PURE__ */ new Set();
|
|
2548
2682
|
function ensureRealtimeStarted(agentStates) {
|
|
2549
|
-
|
|
2683
|
+
const currentActiveIds = new Set(
|
|
2684
|
+
agentStates.filter((a) => a.status === "active").map((a) => a.agentId)
|
|
2685
|
+
);
|
|
2686
|
+
if (realtimeStarted) {
|
|
2687
|
+
const sameSize = currentActiveIds.size === realtimeSubscribedAgentIds.size;
|
|
2688
|
+
const sameMembers = sameSize && [...currentActiveIds].every((id) => realtimeSubscribedAgentIds.has(id));
|
|
2689
|
+
if (sameMembers) return;
|
|
2690
|
+
log("[realtime] Agent set changed \u2014 reconnecting subscriptions");
|
|
2691
|
+
stopRealtimeChat();
|
|
2692
|
+
realtimeStarted = false;
|
|
2693
|
+
realtimeDriftStarted = false;
|
|
2694
|
+
realtimeAssignStarted = false;
|
|
2695
|
+
realtimeConfigStarted = false;
|
|
2696
|
+
realtimeKanbanStarted = false;
|
|
2697
|
+
}
|
|
2550
2698
|
const activeAgentIds = agentStates.filter((a) => a.status === "active").map((a) => a.agentId);
|
|
2551
2699
|
if (activeAgentIds.length === 0) return;
|
|
2552
2700
|
const apiKey = process.env["AGT_API_KEY"];
|
|
@@ -2590,6 +2738,7 @@ function ensureRealtimeStarted(agentStates) {
|
|
|
2590
2738
|
log
|
|
2591
2739
|
});
|
|
2592
2740
|
realtimeStarted = true;
|
|
2741
|
+
realtimeSubscribedAgentIds = new Set(activeAgentIds);
|
|
2593
2742
|
log(`[realtime-chat] Started for ${activeAgentIds.length} agent(s)`);
|
|
2594
2743
|
}).catch((err) => {
|
|
2595
2744
|
log(`[realtime-chat] Failed to start: ${err.message}`);
|
|
@@ -3022,7 +3171,7 @@ function getBuiltInSkillContent(skillId) {
|
|
|
3022
3171
|
];
|
|
3023
3172
|
for (const candidate of candidates) {
|
|
3024
3173
|
if (existsSync2(candidate)) {
|
|
3025
|
-
const content =
|
|
3174
|
+
const content = readFileSync3(candidate, "utf-8");
|
|
3026
3175
|
const files = [{ relativePath: "SKILL.md", content }];
|
|
3027
3176
|
builtInSkillCache.set(skillId, files);
|
|
3028
3177
|
return files;
|
|
@@ -3540,16 +3689,50 @@ function scheduleNext() {
|
|
|
3540
3689
|
void pollCycle().then(scheduleNext);
|
|
3541
3690
|
}, config.intervalMs);
|
|
3542
3691
|
}
|
|
3692
|
+
async function killAllAgtTmuxSessions() {
|
|
3693
|
+
try {
|
|
3694
|
+
const { stdout: output } = await execFilePromiseLong(
|
|
3695
|
+
"tmux",
|
|
3696
|
+
["list-sessions", "-F", "#{session_name}"],
|
|
3697
|
+
{ timeout: 2e3, stdin: "ignore" }
|
|
3698
|
+
);
|
|
3699
|
+
const sessions2 = output.trim().split("\n").filter((s) => s.startsWith("agt-"));
|
|
3700
|
+
for (const session of sessions2) {
|
|
3701
|
+
try {
|
|
3702
|
+
await execFilePromiseLong("tmux", ["kill-session", "-t", session], {
|
|
3703
|
+
timeout: 2e3,
|
|
3704
|
+
stdin: "ignore"
|
|
3705
|
+
});
|
|
3706
|
+
log(`Killed tmux session '${session}'`);
|
|
3707
|
+
} catch {
|
|
3708
|
+
}
|
|
3709
|
+
}
|
|
3710
|
+
if (sessions2.length > 0) {
|
|
3711
|
+
log(`Cleaned up ${sessions2.length} agt-* tmux session(s)`);
|
|
3712
|
+
}
|
|
3713
|
+
} catch {
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3543
3716
|
async function stopPolling() {
|
|
3544
3717
|
running = false;
|
|
3545
3718
|
if (pollTimer) {
|
|
3546
3719
|
clearTimeout(pollTimer);
|
|
3547
3720
|
pollTimer = null;
|
|
3548
3721
|
}
|
|
3722
|
+
const shutdownTimer = setTimeout(() => {
|
|
3723
|
+
log("Shutdown timeout exceeded (5s), forcing exit");
|
|
3724
|
+
process.exit(1);
|
|
3725
|
+
}, 5e3);
|
|
3726
|
+
shutdownTimer.unref();
|
|
3549
3727
|
stopCaffeinate();
|
|
3550
3728
|
stopRealtimeChat();
|
|
3551
3729
|
stopGatewayPool();
|
|
3730
|
+
log("Stopping persistent sessions...");
|
|
3731
|
+
await stopAllSessionsAndWait(log, { timeoutMs: 4e3 });
|
|
3732
|
+
log("Stopping gateway processes...");
|
|
3552
3733
|
await stopAllGateways();
|
|
3734
|
+
await killAllAgtTmuxSessions();
|
|
3735
|
+
clearTimeout(shutdownTimer);
|
|
3553
3736
|
}
|
|
3554
3737
|
function startManager(opts) {
|
|
3555
3738
|
config = opts;
|