@integrity-labs/agt-cli 0.19.15 → 0.19.16

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.
@@ -22,7 +22,7 @@ import {
22
22
  resolveChannels,
23
23
  resolveDmTarget,
24
24
  wrapScheduledTaskPrompt
25
- } from "../chunk-DVWBVANP.js";
25
+ } from "../chunk-NT26H4DO.js";
26
26
  import {
27
27
  findTaskByTemplate,
28
28
  getProjectDir,
@@ -60,10 +60,18 @@ import { join as join4, dirname } from "path";
60
60
  import { homedir as homedir3 } from "os";
61
61
  import { fileURLToPath } from "url";
62
62
 
63
+ // src/lib/mcp-config-drift.ts
64
+ function decideMcpDriftAction(currentHash, knownHash) {
65
+ if (!currentHash) return { kind: "no-config" };
66
+ if (!knownHash) return { kind: "baseline", hash: currentHash };
67
+ if (knownHash === currentHash) return { kind: "no-drift" };
68
+ return { kind: "drift", previous: knownHash, current: currentHash };
69
+ }
70
+
63
71
  // src/lib/stale-mcp-reaper.ts
64
72
  import { execFileSync } from "child_process";
65
- function parseEnvIntegrationsVars(content) {
66
- const names = /* @__PURE__ */ new Set();
73
+ function parseEnvIntegrationsEntries(content) {
74
+ const entries = /* @__PURE__ */ new Map();
67
75
  for (const raw of content.split(/\r?\n/)) {
68
76
  const line = raw.trim();
69
77
  if (!line || line.startsWith("#")) continue;
@@ -71,9 +79,22 @@ function parseEnvIntegrationsVars(content) {
71
79
  if (eq <= 0) continue;
72
80
  const name = line.slice(0, eq).trim();
73
81
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) continue;
74
- names.add(name);
82
+ const value = line.slice(eq + 1);
83
+ entries.set(name, value);
84
+ }
85
+ return entries;
86
+ }
87
+ function diffEnvIntegrations(oldContent, newContent) {
88
+ const oldEntries = oldContent === void 0 ? /* @__PURE__ */ new Map() : parseEnvIntegrationsEntries(oldContent);
89
+ const newEntries = parseEnvIntegrationsEntries(newContent);
90
+ const changed = /* @__PURE__ */ new Set();
91
+ for (const [name, value] of newEntries) {
92
+ if (oldEntries.get(name) !== value) changed.add(name);
93
+ }
94
+ for (const name of oldEntries.keys()) {
95
+ if (!newEntries.has(name)) changed.add(name);
75
96
  }
76
- return [...names];
97
+ return [...changed];
77
98
  }
78
99
  function findMcpServersUsingVars(mcp, changedVars) {
79
100
  const changedSet = new Set(changedVars);
@@ -102,14 +123,48 @@ function findMcpServersUsingVars(mcp, changedVars) {
102
123
  }
103
124
  return result;
104
125
  }
105
- function buildArgvMatchers(serverKeys) {
126
+ function buildArgvMatchersForEntry(key, entry) {
127
+ const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
106
128
  const patterns = [];
107
- for (const key of serverKeys) {
108
- const safe = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
109
- patterns.push(new RegExp(`\\b${safe}\\b`));
129
+ const args = entry?.args ?? [];
130
+ for (let i = args.length - 1; i >= 0; i--) {
131
+ const arg = args[i];
132
+ if (typeof arg !== "string" || !arg) continue;
133
+ if (arg.startsWith("-")) continue;
134
+ const stripped = arg.replace(
135
+ /@[^/@]*$/,
136
+ (m) => (
137
+ // @latest, @1.2.3, etc. — drop. But @scope at the START of a
138
+ // package spec must NOT be stripped, so we only strip a tail
139
+ // `@...` if it doesn't itself start with a slash inside.
140
+ // The regex above only matches a single trailing `@...`
141
+ // segment after the last `/`, so this is safe for `@scope/pkg`.
142
+ m.includes("/") ? m : ""
143
+ )
144
+ );
145
+ const tail = "(?![A-Za-z0-9_-])";
146
+ if (/^@?[a-z0-9]([a-z0-9._-]*\/)?[a-z0-9._-]+$/i.test(stripped)) {
147
+ patterns.push(new RegExp(`${escapeRe(stripped)}${tail}`));
148
+ const basename = stripped.split("/").pop();
149
+ if (basename && basename !== stripped) {
150
+ patterns.push(new RegExp(`${escapeRe(basename)}${tail}`));
151
+ }
152
+ break;
153
+ }
154
+ if (stripped.includes("/")) {
155
+ const basename = stripped.split("/").pop();
156
+ if (basename) {
157
+ patterns.push(new RegExp(`${escapeRe(basename)}${tail}`));
158
+ break;
159
+ }
160
+ }
161
+ }
162
+ if (patterns.length === 0) {
163
+ const safe = escapeRe(key);
164
+ patterns.push(new RegExp(`(?<![A-Za-z0-9_])${safe}(?![A-Za-z0-9_])`));
110
165
  if (safe.includes("_")) {
111
166
  const dashed = safe.replace(/_/g, "-");
112
- patterns.push(new RegExp(`\\b${dashed}\\b`));
167
+ patterns.push(new RegExp(`(?<![A-Za-z0-9_])${dashed}(?![A-Za-z0-9_])`));
113
168
  }
114
169
  }
115
170
  return patterns;
@@ -119,11 +174,15 @@ function buildClaudeAgentMatcher(codeName) {
119
174
  return new RegExp(`\\bclaude\\b.*--name\\s+agt-${safe}(?=\\s|$)`);
120
175
  }
121
176
  function findMcpChildrenForAgent(args) {
122
- const { rows, codeName, serverKeys } = args;
177
+ const { rows, codeName, serverKeys, mcpJson } = args;
123
178
  const maxDepth = args.maxDepth ?? 8;
124
179
  if (serverKeys.length === 0 || rows.length === 0) return [];
125
180
  const claudeMatcher = buildClaudeAgentMatcher(codeName);
126
- const argvMatchers = buildArgvMatchers(serverKeys);
181
+ const argvMatchers = [];
182
+ for (const key of serverKeys) {
183
+ const entry = mcpJson?.mcpServers?.[key];
184
+ argvMatchers.push(...buildArgvMatchersForEntry(key, entry));
185
+ }
127
186
  const byPid = new Map(rows.map((r) => [r.pid, r]));
128
187
  const matched = [];
129
188
  for (const row of rows) {
@@ -144,7 +203,7 @@ function findMcpChildrenForAgent(args) {
144
203
  return matched;
145
204
  }
146
205
  function reapStaleMcpChildren(args) {
147
- const { log: log2, codeName, serverKeys, graceMs = 5e3 } = args;
206
+ const { log: log2, codeName, serverKeys, mcpJson, graceMs = 5e3 } = args;
148
207
  if (serverKeys.length === 0) return [];
149
208
  const runPs = args.runPs ?? (() => execFileSync("ps", ["-eo", "pid,ppid,args"], { encoding: "utf-8", timeout: 5e3 }));
150
209
  const killProcess = args.killProcess ?? ((pid, signal) => {
@@ -169,7 +228,7 @@ function reapStaleMcpChildren(args) {
169
228
  return [];
170
229
  }
171
230
  const rows = parsePsRows(psOutput);
172
- const targets = findMcpChildrenForAgent({ rows, codeName, serverKeys });
231
+ const targets = findMcpChildrenForAgent({ rows, codeName, serverKeys, mcpJson });
173
232
  if (targets.length === 0) return [];
174
233
  const byPid = new Map(rows.map((r) => [r.pid, r]));
175
234
  const describe = (pid) => {
@@ -196,7 +255,8 @@ function reapStaleMcpChildren(args) {
196
255
  findMcpChildrenForAgent({
197
256
  rows: parsePsRows(freshPsOutput),
198
257
  codeName,
199
- serverKeys
258
+ serverKeys,
259
+ mcpJson
200
260
  })
201
261
  );
202
262
  const stragglers = targets.filter((pid) => isAlive(pid) && stillOwned.has(pid));
@@ -1748,6 +1808,7 @@ function scheduleSessionRestart(codeName, delayMs, reason) {
1748
1808
  const timer = setTimeout(() => {
1749
1809
  pendingSessionRestarts.delete(codeName);
1750
1810
  stopPersistentSession(codeName, log);
1811
+ runningMcpHashes.delete(codeName);
1751
1812
  log(`[hot-reload] Session stopped for '${codeName}' \u2014 will respawn with ${reason}`);
1752
1813
  }, delayMs);
1753
1814
  timer.unref?.();
@@ -1762,6 +1823,38 @@ function cancelPendingSessionRestart(codeName) {
1762
1823
  }
1763
1824
  var writtenHashes = /* @__PURE__ */ new Map();
1764
1825
  var knownSecretsHashes = /* @__PURE__ */ new Map();
1826
+ var runningMcpHashes = /* @__PURE__ */ new Map();
1827
+ function projectMcpHash(codeName, projectDir) {
1828
+ try {
1829
+ return createHash("sha256").update(readFileSync3(join4(projectDir, ".mcp.json"))).digest("hex");
1830
+ } catch {
1831
+ return null;
1832
+ }
1833
+ }
1834
+ function stopPersistentSessionAndForgetMcpBaseline(codeName) {
1835
+ cancelPendingSessionRestart(codeName);
1836
+ stopPersistentSession(codeName, log);
1837
+ runningMcpHashes.delete(codeName);
1838
+ }
1839
+ function checkMcpConfigDriftAndScheduleRestart(codeName, projectDir) {
1840
+ const currentHash = projectMcpHash(codeName, projectDir);
1841
+ const action = decideMcpDriftAction(currentHash, runningMcpHashes.get(codeName));
1842
+ switch (action.kind) {
1843
+ case "no-config":
1844
+ case "no-drift":
1845
+ return;
1846
+ case "baseline":
1847
+ runningMcpHashes.set(codeName, action.hash);
1848
+ return;
1849
+ case "drift":
1850
+ log(
1851
+ `[hot-reload] .mcp.json content changed for '${codeName}' (${action.previous.slice(0, 12)} \u2192 ${action.current.slice(0, 12)}); scheduling restart (ENG-4897)`
1852
+ );
1853
+ scheduleSessionRestart(codeName, 0, ".mcp.json content change (ENG-4897)");
1854
+ runningMcpHashes.delete(codeName);
1855
+ return;
1856
+ }
1857
+ }
1765
1858
  var knownChannelConfigHashes = /* @__PURE__ */ new Map();
1766
1859
  var knownModels = /* @__PURE__ */ new Map();
1767
1860
  var knownTasksHashes = /* @__PURE__ */ new Map();
@@ -1836,7 +1929,7 @@ function clearAgentCaches(agentId, codeName) {
1836
1929
  var cachedFrameworkVersion = null;
1837
1930
  var lastVersionCheckAt = 0;
1838
1931
  var VERSION_CHECK_INTERVAL_MS = 5 * 60 * 1e3;
1839
- var agtCliVersion = true ? "0.19.15" : "dev";
1932
+ var agtCliVersion = true ? "0.19.16" : "dev";
1840
1933
  function resolveBrewPath(execFileSync3) {
1841
1934
  try {
1842
1935
  const out = execFileSync3("which", ["brew"], { timeout: 5e3 }).toString().trim();
@@ -2679,8 +2772,7 @@ async function pollCycle() {
2679
2772
  log,
2680
2773
  resolveFramework: (codeName) => agentFrameworkCache.get(codeName) ?? null,
2681
2774
  stopSession: (codeName) => {
2682
- cancelPendingSessionRestart(codeName);
2683
- stopPersistentSession(codeName, log);
2775
+ stopPersistentSessionAndForgetMcpBaseline(codeName);
2684
2776
  persistentSessionAgents.delete(codeName);
2685
2777
  claudeAuthTupleBySession.delete(codeName);
2686
2778
  },
@@ -2840,8 +2932,7 @@ async function pollCycle() {
2840
2932
  log(`Agent '${prev.codeName}' removed from host (deleted or unassigned)`);
2841
2933
  const adapter = resolveAgentFramework(prev.codeName);
2842
2934
  await stopGatewayIfRunning(prev.codeName, adapter);
2843
- cancelPendingSessionRestart(prev.codeName);
2844
- stopPersistentSession(prev.codeName, log);
2935
+ stopPersistentSessionAndForgetMcpBaseline(prev.codeName);
2845
2936
  try {
2846
2937
  const { execSync: es } = await import("child_process");
2847
2938
  es(`tmux kill-session -t agt-${prev.codeName} 2>/dev/null`, { stdio: "ignore" });
@@ -2978,8 +3069,7 @@ async function processAgent(agent, agentStates) {
2978
3069
  if (agent.status === "draft" || agent.status === "paused") {
2979
3070
  log(`Agent '${agent.code_name}' is ${agent.status}, skipping provisioning`);
2980
3071
  await stopGatewayIfRunning(agent.code_name, adapter);
2981
- cancelPendingSessionRestart(agent.code_name);
2982
- stopPersistentSession(agent.code_name, log);
3072
+ stopPersistentSessionAndForgetMcpBaseline(agent.code_name);
2983
3073
  try {
2984
3074
  const { execSync: es } = await import("child_process");
2985
3075
  es(`tmux kill-session -t agt-${agent.code_name} 2>/dev/null`, { stdio: "ignore" });
@@ -3007,8 +3097,7 @@ async function processAgent(agent, agentStates) {
3007
3097
  if (agent.status === "revoked") {
3008
3098
  log(`Agent '${agent.code_name}' is revoked, cleaning up`);
3009
3099
  await stopGatewayIfRunning(agent.code_name, adapter);
3010
- cancelPendingSessionRestart(agent.code_name);
3011
- stopPersistentSession(agent.code_name, log);
3100
+ stopPersistentSessionAndForgetMcpBaseline(agent.code_name);
3012
3101
  try {
3013
3102
  const { execSync: es } = await import("child_process");
3014
3103
  es(`tmux kill-session -t agt-${agent.code_name} 2>/dev/null`, { stdio: "ignore" });
@@ -3481,40 +3570,50 @@ async function processAgent(agent, agentStates) {
3481
3570
  const intHash = createHash("sha256").update(JSON.stringify(integrations.map((i) => `${i.definition_id}:${JSON.stringify(i.credentials)}`))).digest("hex").slice(0, 16);
3482
3571
  const prevIntHash = knownIntegrationHashes.get(agent.agent_id);
3483
3572
  if (intHash !== prevIntHash) {
3573
+ const projectDir = join4(homedir3(), ".augmented", agent.code_name, "project");
3574
+ const envIntPath = join4(projectDir, ".env.integrations");
3575
+ let preWriteEnv;
3576
+ try {
3577
+ preWriteEnv = readFileSync3(envIntPath, "utf-8");
3578
+ } catch {
3579
+ preWriteEnv = void 0;
3580
+ }
3484
3581
  if (frameworkAdapter.writeIntegrations) {
3485
3582
  frameworkAdapter.writeIntegrations(agent.code_name, integrations);
3486
3583
  }
3487
- knownIntegrationHashes.set(agent.agent_id, intHash);
3488
3584
  log(`Integrations provisioned for '${agent.code_name}' (${integrations.length} integration(s))`);
3489
3585
  const fw = agentFrameworkCache.get(agent.code_name) ?? "openclaw";
3586
+ let rotationHandled = true;
3490
3587
  if (fw === "claude-code" && isSessionHealthy(agent.code_name)) {
3491
- let affectedServerKeys = [];
3492
3588
  try {
3493
- const projectDir = join4(homedir3(), ".augmented", agent.code_name, "project");
3494
- const envIntPath = join4(projectDir, ".env.integrations");
3495
3589
  const projectMcpPath = join4(projectDir, ".mcp.json");
3496
- const envContent = readFileSync3(envIntPath, "utf-8");
3590
+ const postWriteEnv = readFileSync3(envIntPath, "utf-8");
3497
3591
  const mcpContent = readFileSync3(projectMcpPath, "utf-8");
3498
- const changedVars = parseEnvIntegrationsVars(envContent);
3499
- const mcpJson = JSON.parse(mcpContent);
3500
- affectedServerKeys = findMcpServersUsingVars(mcpJson, changedVars);
3501
- } catch (err) {
3502
- log(`[hot-reload] Failed to compute affected MCP servers for '${agent.code_name}': ${err.message}`);
3503
- }
3504
- if (affectedServerKeys.length > 0) {
3505
- reapStaleMcpChildren({
3506
- log,
3507
- codeName: agent.code_name,
3508
- serverKeys: affectedServerKeys
3592
+ const changedVars = diffEnvIntegrations(preWriteEnv, postWriteEnv);
3593
+ const mcpJsonForReap = JSON.parse(mcpContent);
3594
+ const affectedServerKeys = findMcpServersUsingVars(mcpJsonForReap, changedVars);
3595
+ if (affectedServerKeys.length > 0) {
3596
+ reapStaleMcpChildren({
3597
+ log,
3598
+ codeName: agent.code_name,
3599
+ serverKeys: affectedServerKeys,
3600
+ mcpJson: mcpJsonForReap
3601
+ });
3602
+ }
3603
+ const names = integrations.map((i) => i.display_name || i.definition_id).join(", ");
3604
+ const reapNote = affectedServerKeys.length > 0 ? ` The MCP servers that depend on rotating credentials (${affectedServerKeys.join(", ")}) have been signalled to reconnect.` : "";
3605
+ injectMessage(agent.code_name, "system", `Your integrations have been refreshed. You have access to: ${names}.${reapNote}`, {
3606
+ task_name: "integration-update"
3607
+ }, log).catch(() => {
3509
3608
  });
3609
+ log(`[hot-reload] Notified '${agent.code_name}' about integration update: ${names} (reaped ${affectedServerKeys.length} stale MCP server(s))`);
3610
+ } catch (err) {
3611
+ rotationHandled = false;
3612
+ log(`[hot-reload] Failed to compute / reap affected MCP servers for '${agent.code_name}': ${err.message} \u2014 will retry next tick`);
3510
3613
  }
3511
- const names = integrations.map((i) => i.display_name || i.definition_id).join(", ");
3512
- const reapNote = affectedServerKeys.length > 0 ? ` The MCP servers that depend on rotating credentials (${affectedServerKeys.join(", ")}) have been signalled to reconnect.` : "";
3513
- injectMessage(agent.code_name, "system", `Your integrations have been refreshed. You have access to: ${names}.${reapNote}`, {
3514
- task_name: "integration-update"
3515
- }, log).catch(() => {
3516
- });
3517
- log(`[hot-reload] Notified '${agent.code_name}' about integration update: ${names} (reaped ${affectedServerKeys.length} stale MCP server(s))`);
3614
+ }
3615
+ if (rotationHandled) {
3616
+ knownIntegrationHashes.set(agent.agent_id, intHash);
3518
3617
  }
3519
3618
  needsGatewayRestart = true;
3520
3619
  }
@@ -3832,6 +3931,11 @@ async function processAgent(agent, agentStates) {
3832
3931
  const sessionMode = refreshData.agent.session_mode ?? "oneshot";
3833
3932
  if (agentFw === "claude-code" && sessionMode === "persistent") {
3834
3933
  await ensurePersistentSession(agent, tasks, boardItems, refreshData);
3934
+ try {
3935
+ checkMcpConfigDriftAndScheduleRestart(agent.code_name, getProjectDir2(agent.code_name));
3936
+ } catch (err) {
3937
+ log(`[hot-reload] .mcp.json drift check failed for '${agent.code_name}': ${err.message}`);
3938
+ }
3835
3939
  } else if (agentFw === "claude-code" && tasks.length > 0) {
3836
3940
  await syncAndCheckClaudeScheduler(agent, tasks, boardItems, refreshData);
3837
3941
  } else if (frameworkAdapter.syncScheduledTasks && gatewayRunning && gatewayPort) {
@@ -4583,8 +4687,7 @@ async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
4583
4687
  const msg = err.message;
4584
4688
  log(`[persistent-session] Failed to resolve auth for '${codeName}': ${msg} \u2014 refusing to spawn`);
4585
4689
  if (isSessionHealthy(codeName)) {
4586
- cancelPendingSessionRestart(codeName);
4587
- stopPersistentSession(codeName, log);
4690
+ stopPersistentSessionAndForgetMcpBaseline(codeName);
4588
4691
  persistentSessionAgents.delete(codeName);
4589
4692
  claudeAuthTupleBySession.delete(codeName);
4590
4693
  }
@@ -4606,8 +4709,7 @@ async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
4606
4709
  const recordedAuthTuple = claudeAuthTupleBySession.get(codeName);
4607
4710
  if (recordedAuthTuple && recordedAuthTuple !== currentAuthTuple && isSessionHealthy(codeName)) {
4608
4711
  log(`[persistent-session] Auth config changed for '${codeName}' (${recordedAuthTuple} \u2192 ${currentAuthTuple}) \u2014 restarting session`);
4609
- cancelPendingSessionRestart(codeName);
4610
- stopPersistentSession(codeName, log);
4712
+ stopPersistentSessionAndForgetMcpBaseline(codeName);
4611
4713
  persistentSessionAgents.delete(codeName);
4612
4714
  }
4613
4715
  if (isStaleForToday(codeName) && isSessionHealthy(codeName)) {
@@ -4618,8 +4720,7 @@ async function ensurePersistentSession(agent, tasks, boardItems, refreshData) {
4618
4720
  log(
4619
4721
  `[persistent-session] Day rollover for '${codeName}' (yesterday=${current.date}) \u2014 agent idle, restarting to mint fresh session`
4620
4722
  );
4621
- cancelPendingSessionRestart(codeName);
4622
- stopPersistentSession(codeName, log);
4723
+ stopPersistentSessionAndForgetMcpBaseline(codeName);
4623
4724
  persistentSessionAgents.delete(codeName);
4624
4725
  } else {
4625
4726
  log(