agentcache 0.5.0-beta.1 → 0.5.0-beta.2

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > **Pick a coding session and continue it in another agent.**
4
4
 
5
- **AgentCache 0.5.0-beta.1 is a same-machine public beta.** It keeps a local,
5
+ **AgentCache 0.5.0-beta.2 is a same-machine public beta.** It keeps a local,
6
6
  portable timeline for an explicitly selected coding session, then hands a
7
7
  bounded checkpoint to another registered agent through MCP. There is no
8
8
  Markdown handoff file to maintain and no silent choice of “latest” session.
@@ -39,14 +39,16 @@ match.
39
39
  1. Install the pinned beta and run the explicit setup step.
40
40
 
41
41
  ```bash
42
- npm install -g agentcache@0.5.0-beta.1
42
+ npm install -g agentcache@0.5.0-beta.2
43
43
  agentcache setup
44
44
  ```
45
45
 
46
46
  Installation has no postinstall configuration hook. Setup is explicit:
47
47
  `agentcache setup` detects installed clients and adds a client-bound MCP
48
- entry such as `agentcache serve --adapter codex` when the existing config is
49
- unambiguous. Restart or reload clients after setup.
48
+ entry using the exact active Node executable and installed AgentCache CLI.
49
+ This prevents an IDE's bundled Node from loading a native dependency built
50
+ for another Node version. Rerun setup after changing Node versions, global
51
+ npm prefixes, or AgentCache installations, then restart or reload clients.
50
52
 
51
53
  2. Check what AgentCache actually detected and what each adapter can do.
52
54
 
@@ -18,7 +18,7 @@ import { dirname as dirname2, isAbsolute, join as join2, resolve as resolve2 } f
18
18
  import { homedir } from "os";
19
19
  import { TextDecoder } from "util";
20
20
  import { parse as parseToml } from "smol-toml";
21
- import { isMap, parseDocument } from "yaml";
21
+ import { isMap, isScalar, isSeq, parseDocument } from "yaml";
22
22
 
23
23
  // src/utils/atomic-file.ts
24
24
  import {
@@ -626,15 +626,22 @@ function isAgentCacheExecutable(value) {
626
626
  executableBasename(value) ?? ""
627
627
  );
628
628
  }
629
+ function isPortableAbsolutePath(value) {
630
+ return isAbsolute(value) || /^[A-Za-z]:[\\/]/.test(value) || /^\\\\/.test(value);
631
+ }
632
+ function isRecognizedAgentCacheScript(value) {
633
+ if (typeof value !== "string" || !isPortableAbsolutePath(value)) return false;
634
+ if (isAgentCacheExecutable(value)) return true;
635
+ const segments = value.toLowerCase().split(/[/\\]+/).filter(Boolean);
636
+ return segments.length >= 3 && segments.at(-3) === "agentcache" && segments.at(-2) === "dist" && segments.at(-1) === "cli.js";
637
+ }
629
638
  function isAgentCacheNodeWrapper(value, ide) {
630
- if (!isJsonObject(value) || !["node", "node.exe"].includes(executableBasename(value.command) ?? "")) {
639
+ if (!isJsonObject(value) || typeof value.command !== "string" || !isPortableAbsolutePath(value.command) || !["node", "node.exe"].includes(executableBasename(value.command) ?? "")) {
631
640
  return false;
632
641
  }
633
642
  if (!Array.isArray(value.args) || value.args.length < 2) return false;
634
643
  const script = value.args[0];
635
- const scriptName = executableBasename(script);
636
- const isAgentCacheScript = scriptName === "agentcache" || scriptName === "agentcache.cmd" || scriptName === "agentcache.exe" || scriptName === "cli.js" && typeof script === "string" && /agentcache/i.test(script);
637
- if (!isAgentCacheScript) return false;
644
+ if (!isRecognizedAgentCacheScript(script)) return false;
638
645
  return argsEqual(value.args.slice(1), ["serve"]) || argsEqual(value.args.slice(1), boundServeArgs(ide));
639
646
  }
640
647
  function registrationOwnership(config, ide, desired) {
@@ -649,7 +656,7 @@ function registrationOwnership(config, ide, desired) {
649
656
  if (isJsonObject(existing) && isAgentCacheExecutable(existing.command) && (argsEqual(existing.args, ["serve"]) || argsEqual(existing.args, boundServeArgs(ide)))) {
650
657
  return "owned";
651
658
  }
652
- if (isVscodeExtensionIde(ide) && isAgentCacheNodeWrapper(existing, ide)) {
659
+ if (isAgentCacheNodeWrapper(existing, ide)) {
653
660
  return "owned";
654
661
  }
655
662
  return "ambiguous";
@@ -845,7 +852,8 @@ function unregisterMcpServer(ide) {
845
852
  }
846
853
  function isMcpServerRegistered(ide) {
847
854
  if (ide.mcpConfigFormat === "claude-settings") {
848
- return isJsonMcpServerRegistered(ide, claudeServerConfig(ide));
855
+ const desired = claudeServerConfig(ide);
856
+ return desired ? isJsonMcpServerRegistered(ide, desired) : false;
849
857
  }
850
858
  if (ide.mcpConfigFormat === "mcp-json" || ide.mcpConfigFormat === "continue-dir") {
851
859
  const desired = serverConfig(ide);
@@ -859,8 +867,9 @@ function inspectMcpServerPresence(ide) {
859
867
  const obsoletePresence = inspectObsoleteJsonRegistrationPresence(ide);
860
868
  let currentPresence;
861
869
  if (ide.mcpConfigFormat === "claude-settings") {
870
+ const desired = claudeServerConfig(ide);
862
871
  currentPresence = combineRegistrationPresence(
863
- inspectJsonMcpServerPresence(ide, claudeServerConfig(ide)),
872
+ desired ? inspectJsonMcpServerPresence(ide, desired) : "unknown",
864
873
  inspectClaudeSettingsPresence(claudeSettingsPath(ide))
865
874
  );
866
875
  } else if (ide.mcpConfigFormat === "mcp-json" || ide.mcpConfigFormat === "continue-dir") {
@@ -991,6 +1000,7 @@ function registerClaudeCode(ide) {
991
1000
  const config = configFile.config;
992
1001
  const settings = settingsFile.config;
993
1002
  const desired = claudeServerConfig(ide);
1003
+ if (!desired) return skipped("trusted AgentCache CLI entrypoint unavailable");
994
1004
  if (registrationOwnership(config, ide, desired) === "ambiguous") {
995
1005
  return skipped("ambiguous AgentCache registration preserved");
996
1006
  }
@@ -1045,7 +1055,9 @@ function unregisterClaudeCode(ide) {
1045
1055
  if (!configFile || !settingsFile || !validServerMap(configFile.config.mcpServers)) return false;
1046
1056
  const config = configFile.config;
1047
1057
  const settings = settingsFile.config;
1048
- const ownership = registrationOwnership(config, ide, claudeServerConfig(ide));
1058
+ const desired = claudeServerConfig(ide);
1059
+ if (!desired) return false;
1060
+ const ownership = registrationOwnership(config, ide, desired);
1049
1061
  if (ownership === "ambiguous") return false;
1050
1062
  if (legacyClaudeHookOwnership(settings) === "ambiguous") return false;
1051
1063
  const legacyOwnership = legacyLoopRegistrationOwnership(config);
@@ -1073,24 +1085,26 @@ function unregisterClaudeCode(ide) {
1073
1085
  function boundServeArgs(ide) {
1074
1086
  return ["serve", "--adapter", ide.adapterId];
1075
1087
  }
1088
+ function pinnedServeCommand(ide) {
1089
+ if (!registrationCliEntrypoint) return void 0;
1090
+ return {
1091
+ command: process.execPath,
1092
+ args: [registrationCliEntrypoint, ...boundServeArgs(ide)]
1093
+ };
1094
+ }
1076
1095
  function claudeServerConfig(ide) {
1096
+ const pinned = pinnedServeCommand(ide);
1097
+ if (!pinned) return void 0;
1077
1098
  return {
1078
1099
  type: "stdio",
1079
- command: "agentcache",
1080
- args: boundServeArgs(ide),
1100
+ ...pinned,
1081
1101
  env: {}
1082
1102
  };
1083
1103
  }
1084
1104
  function serverConfig(ide) {
1085
- if (isVscodeExtensionIde(ide)) {
1086
- if (!registrationCliEntrypoint) return void 0;
1087
- return {
1088
- command: process.execPath,
1089
- args: [registrationCliEntrypoint, ...boundServeArgs(ide)],
1090
- disabled: false
1091
- };
1092
- }
1093
- return { command: "agentcache", args: boundServeArgs(ide) };
1105
+ const pinned = pinnedServeCommand(ide);
1106
+ if (!pinned) return void 0;
1107
+ return isVscodeExtensionIde(ide) ? { ...pinned, disabled: false } : pinned;
1094
1108
  }
1095
1109
  function isJsonMcpServerRegistered(ide, desired) {
1096
1110
  const config = parseJsonFileForInspection(ide.mcpConfigPath);
@@ -1225,22 +1239,28 @@ function restoreGooseTarget(target, published, original) {
1225
1239
  return !restore.conflict && !restore.unsupported && restore.changed;
1226
1240
  }
1227
1241
  function gooseServerConfig(ide) {
1242
+ const pinned = pinnedServeCommand(ide);
1243
+ if (!pinned) return void 0;
1228
1244
  return {
1229
1245
  enabled: true,
1230
1246
  name: GOOSE_EXTENSION_KEY,
1231
1247
  description: "AgentCache MCP server",
1232
1248
  type: "stdio",
1233
- cmd: "agentcache",
1234
- args: boundServeArgs(ide)
1249
+ cmd: pinned.command,
1250
+ args: pinned.args
1235
1251
  };
1236
1252
  }
1237
- function gooseCoreMatches(value, args) {
1238
- return isJsonObject(value) && value.name === GOOSE_EXTENSION_KEY && value.type === "stdio" && value.cmd === "agentcache" && argsEqual(value.args, args);
1253
+ function gooseCoreMatches(value, expected) {
1254
+ return isJsonObject(value) && value.name === GOOSE_EXTENSION_KEY && value.type === "stdio" && value.cmd === expected.cmd && argsEqual(value.args, expected.args);
1239
1255
  }
1240
- function gooseOwnership(value, ide) {
1256
+ function gooseOwnership(value, ide, desired) {
1241
1257
  if (value === void 0) return "absent";
1242
- if (gooseCoreMatches(value, boundServeArgs(ide))) return "owned";
1243
- if (gooseCoreMatches(value, ["serve"])) return "owned";
1258
+ if (desired && gooseCoreMatches(value, desired)) return "owned";
1259
+ if (gooseCoreMatches(value, { cmd: "agentcache", args: boundServeArgs(ide) })) return "owned";
1260
+ if (gooseCoreMatches(value, { cmd: "agentcache", args: ["serve"] })) return "owned";
1261
+ if (isJsonObject(value) && value.name === GOOSE_EXTENSION_KEY && value.type === "stdio" && isAgentCacheNodeWrapper({ command: value.cmd, args: value.args }, ide)) {
1262
+ return "owned";
1263
+ }
1244
1264
  return "ambiguous";
1245
1265
  }
1246
1266
  function parseGooseDocument(content) {
@@ -1289,11 +1309,12 @@ function registerGoose(ide) {
1289
1309
  } catch {
1290
1310
  return skipped("malformed or unsupported Goose YAML config preserved");
1291
1311
  }
1292
- const ownership = gooseOwnership(existing, ide);
1312
+ const desired = gooseServerConfig(ide);
1313
+ if (!desired) return skipped("trusted AgentCache CLI entrypoint unavailable");
1314
+ const ownership = gooseOwnership(existing, ide, desired);
1293
1315
  if (ownership === "ambiguous" || existingNode !== void 0 && !isMap(existingNode)) {
1294
1316
  return skipped("ambiguous AgentCache registration preserved");
1295
1317
  }
1296
- const desired = gooseServerConfig(ide);
1297
1318
  if (ownership === "owned" && Object.entries(desired).every(([key, value]) => {
1298
1319
  const existingValue = existing[key];
1299
1320
  return Array.isArray(value) ? argsEqual(existingValue, value) : existingValue === value;
@@ -1320,7 +1341,29 @@ function registerGoose(ide) {
1320
1341
  for (const [key, value] of Object.entries(desired)) {
1321
1342
  const existingValue = existing[key];
1322
1343
  const matches = Array.isArray(value) ? argsEqual(existingValue, value) : existingValue === value;
1323
- if (!matches) existingNode.set(key, document.createNode(value));
1344
+ if (!matches) {
1345
+ const currentNode = existingNode.get(key, true);
1346
+ if (isScalar(currentNode) && !Array.isArray(value)) currentNode.value = value;
1347
+ else {
1348
+ const replacement = document.createNode(value);
1349
+ if (isSeq(currentNode) && isSeq(replacement)) {
1350
+ replacement.comment = currentNode.comment;
1351
+ replacement.commentBefore = currentNode.commentBefore;
1352
+ replacement.spaceBefore = currentNode.spaceBefore;
1353
+ for (const replacementItem of replacement.items) {
1354
+ if (!isScalar(replacementItem)) continue;
1355
+ const previousItem = currentNode.items.find(
1356
+ (item) => isScalar(item) && item.value === replacementItem.value
1357
+ );
1358
+ if (!isScalar(previousItem)) continue;
1359
+ replacementItem.comment = previousItem.comment;
1360
+ replacementItem.commentBefore = previousItem.commentBefore;
1361
+ replacementItem.spaceBefore = previousItem.spaceBefore;
1362
+ }
1363
+ }
1364
+ existingNode.set(key, replacement);
1365
+ }
1366
+ }
1324
1367
  }
1325
1368
  } else {
1326
1369
  const desiredNode = createGooseMap(document, desired);
@@ -1364,7 +1407,7 @@ function unregisterGoose(ide) {
1364
1407
  } catch {
1365
1408
  return false;
1366
1409
  }
1367
- if (gooseOwnership(existing, ide) !== "owned") return false;
1410
+ if (gooseOwnership(existing, ide, gooseServerConfig(ide)) !== "owned") return false;
1368
1411
  extensions.delete(GOOSE_EXTENSION_KEY);
1369
1412
  if (!gooseTargetStillBound(target, file.expectation)) return false;
1370
1413
  const next = String(document);
@@ -1389,7 +1432,8 @@ function isGooseRegistered(ide) {
1389
1432
  const entry = extensions.get(GOOSE_EXTENSION_KEY, true);
1390
1433
  if (!entry || !isMap(entry)) return false;
1391
1434
  const value = entry.toJSON();
1392
- return isJsonObject(value) && value.enabled === true && gooseCoreMatches(value, boundServeArgs(ide));
1435
+ const desired = gooseServerConfig(ide);
1436
+ return !!desired && isJsonObject(value) && value.enabled === true && gooseCoreMatches(value, desired);
1393
1437
  } catch {
1394
1438
  return false;
1395
1439
  }
@@ -1409,21 +1453,25 @@ function inspectGoosePresence(ide) {
1409
1453
  const entry = extensions.get(GOOSE_EXTENSION_KEY, true);
1410
1454
  if (entry === void 0) return "absent";
1411
1455
  if (!isMap(entry)) return "unknown";
1412
- return gooseOwnership(entry.toJSON(), ide) === "owned" ? "present" : "unknown";
1456
+ return gooseOwnership(entry.toJSON(), ide, gooseServerConfig(ide)) === "owned" ? "present" : "unknown";
1413
1457
  } catch {
1414
1458
  return "unknown";
1415
1459
  }
1416
1460
  }
1417
- function codexBlock(ide, legacy = false) {
1461
+ function formatCodexBlock(command, args) {
1418
1462
  return [
1419
1463
  CODEX_MARKER_START,
1420
1464
  "[mcp_servers.agentcache]",
1421
- 'command = "agentcache"',
1422
- legacy ? 'args = ["serve"]' : `args = ["serve", "--adapter", ${JSON.stringify(ide.adapterId)}]`,
1465
+ `command = ${JSON.stringify(command)}`,
1466
+ `args = [${args.map((value) => JSON.stringify(value)).join(", ")}]`,
1423
1467
  CODEX_MARKER_END,
1424
1468
  ""
1425
1469
  ].join("\n");
1426
1470
  }
1471
+ function codexBlock(ide) {
1472
+ const pinned = pinnedServeCommand(ide);
1473
+ return pinned ? formatCodexBlock(pinned.command, pinned.args) : void 0;
1474
+ }
1427
1475
  function maskTomlStrings(content) {
1428
1476
  let state = "normal";
1429
1477
  let masked = "";
@@ -1500,6 +1548,42 @@ function maskTomlStrings(content) {
1500
1548
  }
1501
1549
  return state === "normal" || state === "comment" ? masked : void 0;
1502
1550
  }
1551
+ var TOML_HEADER_SENTINEL = "__agentcache_header_sentinel_4f20c6d1__";
1552
+ function tomlHeaderSentinelPaths(value) {
1553
+ if (!value || typeof value !== "object") return [];
1554
+ const paths = [];
1555
+ for (const [key, child] of Object.entries(value)) {
1556
+ if (key === TOML_HEADER_SENTINEL && child === true) {
1557
+ paths.push([]);
1558
+ continue;
1559
+ }
1560
+ const candidates = Array.isArray(child) ? child : [child];
1561
+ for (const candidate of candidates) {
1562
+ for (const nestedPath of tomlHeaderSentinelPaths(candidate)) {
1563
+ paths.push([key, ...nestedPath]);
1564
+ }
1565
+ }
1566
+ }
1567
+ return paths;
1568
+ }
1569
+ function parseTomlHeader(source) {
1570
+ try {
1571
+ const parsed = parseToml(`${source}
1572
+ ${TOML_HEADER_SENTINEL} = true
1573
+ `);
1574
+ const paths = tomlHeaderSentinelPaths(parsed);
1575
+ if (paths.length !== 1 || paths[0].length === 0) return void 0;
1576
+ return { path: paths[0], array: source.startsWith("[[") };
1577
+ } catch {
1578
+ return void 0;
1579
+ }
1580
+ }
1581
+ function isOwnedCodexHeader(header) {
1582
+ return header.path.length >= 2 && header.path[0] === "mcp_servers" && header.path[1] === "agentcache";
1583
+ }
1584
+ function isExactCodexHeader(header) {
1585
+ return !header.array && header.path.length === 2 && isOwnedCodexHeader(header);
1586
+ }
1503
1587
  function scanToml(content) {
1504
1588
  const masked = maskTomlStrings(content);
1505
1589
  if (masked === void 0) return void 0;
@@ -1513,6 +1597,7 @@ function scanToml(content) {
1513
1597
  while (offset < content.length) {
1514
1598
  const newline = content.indexOf("\n", offset);
1515
1599
  const end = newline < 0 ? content.length : newline + 1;
1600
+ const originalLine = content.slice(offset, end).replace(/\r?\n$/, "");
1516
1601
  const maskedLine = masked.slice(offset, end).replace(/\r?\n$/, "");
1517
1602
  const trimmed = maskedLine.trim();
1518
1603
  const marker = trimmed === CODEX_MARKER_START ? "start" : trimmed === CODEX_MARKER_END ? "end" : void 0;
@@ -1523,11 +1608,10 @@ function scanToml(content) {
1523
1608
  if (marker === "start") markerStarts.push(line);
1524
1609
  if (marker === "end") markerEnds.push(line);
1525
1610
  if (code.startsWith("[") && squareDepth === 0 && inlineTableDepth === 0) {
1526
- const table = code.match(/^\[([A-Za-z0-9_.-]+)\]$/);
1527
- const tableArray = code.match(/^\[\[([A-Za-z0-9_.-]+)\]\]$/);
1528
- const name = table?.[1] ?? tableArray?.[1];
1529
- if (!name) return void 0;
1530
- headers.push({ name, start: offset, bodyStart: end, end: content.length });
1611
+ const source = (commentAt < 0 ? originalLine : originalLine.slice(0, commentAt)).trim();
1612
+ const header = parseTomlHeader(source);
1613
+ if (!header) return void 0;
1614
+ headers.push({ ...header, start: offset, bodyStart: end, end: content.length });
1531
1615
  } else if (code) {
1532
1616
  const startsAtTopLevel = squareDepth === 0 && inlineTableDepth === 0;
1533
1617
  if (startsAtTopLevel && !code.includes("=")) return void 0;
@@ -1557,68 +1641,69 @@ function scanToml(content) {
1557
1641
  markerEnd: markerEnds[0]
1558
1642
  };
1559
1643
  }
1560
- function managedCodexRange(content, scan, ide) {
1644
+ function managedCodexRange(content, scan, ide, desiredBlock) {
1561
1645
  if (!scan.markerStart && !scan.markerEnd) return void 0;
1562
1646
  if (!scan.markerStart || !scan.markerEnd) return null;
1563
1647
  const range = { start: scan.markerStart.start, end: scan.markerEnd.end };
1564
1648
  const managedContent = content.slice(range.start, range.end);
1565
- const current = managedContent === codexBlock(ide);
1566
- if (!current && managedContent !== codexBlock(ide, true)) return null;
1567
- const ownedHeaders = scan.headers.filter(
1568
- (header) => header.name === "mcp_servers.agentcache" || header.name.startsWith("mcp_servers.agentcache.")
1649
+ const current = managedContent === desiredBlock;
1650
+ const ownedHeaders = scan.headers.filter(isOwnedCodexHeader);
1651
+ const headersInside = scan.headers.filter(
1652
+ (header) => header.start > scan.markerStart.start && header.start < scan.markerEnd.start
1569
1653
  );
1570
- if (ownedHeaders.length !== 1 || ownedHeaders[0].name !== "mcp_servers.agentcache" || ownedHeaders[0].start < range.start || ownedHeaders[0].start >= range.end) {
1654
+ if (ownedHeaders.length !== 1 || headersInside.length !== 1 || headersInside[0] !== ownedHeaders[0] || !isExactCodexHeader(ownedHeaders[0]) || ownedHeaders[0].start < range.start || ownedHeaders[0].start >= range.end) {
1571
1655
  return null;
1572
1656
  }
1657
+ const leadingTableContent = scan.lines.some(
1658
+ (line) => line.start >= scan.markerStart.end && line.start < ownedHeaders[0].start && line.code.length > 0
1659
+ );
1660
+ if (leadingTableContent) return null;
1573
1661
  const trailingTableContent = scan.lines.some(
1574
1662
  (line) => line.start >= scan.markerEnd.end && line.start < ownedHeaders[0].end && line.code.length > 0
1575
1663
  );
1576
1664
  if (trailingTableContent) return null;
1665
+ if (!legacyCodexRange(content, scan, ide, true)) return null;
1577
1666
  return { ...range, current };
1578
1667
  }
1579
- function legacyCodexRange(content, scan, ide) {
1580
- const ownedHeaders = scan.headers.filter(
1581
- (header2) => header2.name === "mcp_servers.agentcache" || header2.name.startsWith("mcp_servers.agentcache.")
1582
- );
1668
+ function legacyCodexRange(content, scan, ide, allowManagedNodeWrapper = false) {
1669
+ const ownedHeaders = scan.headers.filter(isOwnedCodexHeader);
1583
1670
  if (ownedHeaders.length === 0) return void 0;
1584
- if (ownedHeaders.length !== 1 || ownedHeaders[0].name !== "mcp_servers.agentcache") return null;
1671
+ if (ownedHeaders.length !== 1 || !isExactCodexHeader(ownedHeaders[0])) return null;
1585
1672
  const header = ownedHeaders[0];
1586
1673
  const bodyLines = scan.lines.filter(
1587
1674
  (line) => line.start >= header.bodyStart && line.start < header.end && line.code.length > 0
1588
1675
  );
1589
1676
  let hasCommand = false;
1590
1677
  let hasArgs = false;
1591
- let hasBoundArgs = false;
1592
1678
  let hasLegacyApproval = false;
1593
- let semanticCommand;
1679
+ let semanticServer;
1594
1680
  try {
1595
1681
  const parsed = parseToml(content);
1596
- semanticCommand = parsed.mcp_servers?.agentcache && parsed.mcp_servers.agentcache.command;
1682
+ const table = parsed.mcp_servers?.agentcache;
1683
+ if (!isJsonObject(table)) return null;
1684
+ semanticServer = { command: table.command, args: table.args };
1597
1685
  } catch {
1598
1686
  return null;
1599
1687
  }
1600
- const escapedAdapterId = ide.adapterId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1601
- const boundArgs = new RegExp(
1602
- `^args\\s*=\\s*\\[\\s*"serve"\\s*,\\s*"--adapter"\\s*,\\s*"${escapedAdapterId}"\\s*\\]\\s*(?:#.*)?$`
1603
- );
1604
1688
  for (const line of bodyLines) {
1605
1689
  const source = content.slice(line.start, line.end).trim();
1606
1690
  if (/^command\s*=/.test(source)) {
1607
- if (hasCommand || !isAgentCacheExecutable(semanticCommand)) return null;
1691
+ if (hasCommand) return null;
1608
1692
  hasCommand = true;
1609
- } else if (/^args\s*=\s*\[\s*"serve"\s*\]\s*(?:#.*)?$/.test(source)) {
1610
- hasArgs = true;
1611
- } else if (boundArgs.test(source)) {
1693
+ } else if (/^args\s*=/.test(source)) {
1694
+ if (hasArgs) return null;
1612
1695
  hasArgs = true;
1613
- hasBoundArgs = true;
1614
1696
  } else if (/^default_tools_approval_mode\s*=\s*"auto"\s*(?:#.*)?$/.test(source)) {
1615
1697
  hasLegacyApproval = true;
1616
1698
  } else return null;
1617
1699
  }
1618
- return hasCommand && hasArgs ? {
1700
+ const desired = pinnedServeCommand(ide);
1701
+ const current = !!desired && serverCoreMatches(semanticServer, desired);
1702
+ const owned = current || isAgentCacheExecutable(semanticServer.command) && (argsEqual(semanticServer.args, ["serve"]) || argsEqual(semanticServer.args, boundServeArgs(ide))) || allowManagedNodeWrapper && isAgentCacheNodeWrapper(semanticServer, ide);
1703
+ return hasCommand && hasArgs && owned ? {
1619
1704
  start: header.start,
1620
1705
  end: header.end,
1621
- current: hasBoundArgs && !hasLegacyApproval
1706
+ current: current && !hasLegacyApproval
1622
1707
  } : null;
1623
1708
  }
1624
1709
  function removeCodexRange(content, range) {
@@ -1627,6 +1712,8 @@ function removeCodexRange(content, range) {
1627
1712
  return content.slice(0, start) + content.slice(range.end);
1628
1713
  }
1629
1714
  function registerCodex(ide) {
1715
+ const desiredBlock = codexBlock(ide);
1716
+ if (!desiredBlock) return skipped("trusted AgentCache CLI entrypoint unavailable");
1630
1717
  const file = readBoundedRegistrationFile(ide.mcpConfigPath, true);
1631
1718
  if (!file) return skipped("malformed or ambiguous TOML config preserved");
1632
1719
  const original = file.content;
@@ -1635,14 +1722,14 @@ function registerCodex(ide) {
1635
1722
  }
1636
1723
  const scan = scanToml(original);
1637
1724
  if (!scan) return skipped("malformed or ambiguous TOML config preserved");
1638
- const managed = managedCodexRange(original, scan, ide);
1725
+ const managed = managedCodexRange(original, scan, ide, desiredBlock);
1639
1726
  if (managed === null) return skipped("malformed or ambiguous TOML config preserved");
1640
1727
  if (managed?.current) {
1641
1728
  return atomicWriteExpectationMatches(ide.mcpConfigPath, file.expectation) ? { status: "unchanged" } : skipped("Codex config changed during registration");
1642
1729
  }
1643
1730
  const legacy = managed ?? legacyCodexRange(original, scan, ide);
1644
1731
  if (legacy === null) return skipped("malformed or ambiguous TOML config preserved");
1645
- const next = legacy ? original.slice(0, legacy.start) + codexBlock(ide) + original.slice(legacy.end) : `${original}${original.length > 0 ? "\n\n" : ""}${codexBlock(ide)}`;
1732
+ const next = legacy ? original.slice(0, legacy.start) + desiredBlock + original.slice(legacy.end) : `${original}${original.length > 0 ? "\n\n" : ""}${desiredBlock}`;
1646
1733
  if (!isValidToml(next)) {
1647
1734
  return skipped("generated TOML would be invalid; existing config preserved");
1648
1735
  }
@@ -1652,13 +1739,15 @@ function registerCodex(ide) {
1652
1739
  );
1653
1740
  }
1654
1741
  function unregisterCodex(ide) {
1742
+ const desiredBlock = codexBlock(ide);
1743
+ if (!desiredBlock) return false;
1655
1744
  const file = readBoundedRegistrationFile(ide.mcpConfigPath);
1656
1745
  if (!file) return false;
1657
1746
  const original = file.content;
1658
1747
  if (!isValidToml(original)) return false;
1659
1748
  const scan = scanToml(original);
1660
1749
  if (!scan) return false;
1661
- const managed = managedCodexRange(original, scan, ide);
1750
+ const managed = managedCodexRange(original, scan, ide, desiredBlock);
1662
1751
  if (managed === null) return false;
1663
1752
  const legacy = managed ? void 0 : legacyCodexRange(original, scan, ide);
1664
1753
  if (legacy === null) return false;
@@ -1671,13 +1760,15 @@ function unregisterCodex(ide) {
1671
1760
  }
1672
1761
  function isCodexRegistered(ide) {
1673
1762
  try {
1763
+ const desiredBlock = codexBlock(ide);
1764
+ if (!desiredBlock) return false;
1674
1765
  const file = readBoundedRegistrationFile(ide.mcpConfigPath);
1675
1766
  if (!file) return false;
1676
1767
  const original = file.content;
1677
1768
  if (!isValidToml(original)) return false;
1678
1769
  const scan = scanToml(original);
1679
1770
  if (!scan) return false;
1680
- const managed = managedCodexRange(original, scan, ide);
1771
+ const managed = managedCodexRange(original, scan, ide, desiredBlock);
1681
1772
  if (managed === null) return false;
1682
1773
  if (managed) return managed.current;
1683
1774
  const recognized = legacyCodexRange(original, scan, ide);
@@ -1688,6 +1779,8 @@ function isCodexRegistered(ide) {
1688
1779
  }
1689
1780
  function inspectCodexPresence(ide) {
1690
1781
  try {
1782
+ const desiredBlock = codexBlock(ide);
1783
+ if (!desiredBlock) return "unknown";
1691
1784
  const file = readBoundedRegistrationFile(ide.mcpConfigPath, true);
1692
1785
  if (!file) return "unknown";
1693
1786
  if (!file.expectation.existed) return "absent";
@@ -1695,7 +1788,7 @@ function inspectCodexPresence(ide) {
1695
1788
  if (!isValidToml(original)) return "unknown";
1696
1789
  const scan = scanToml(original);
1697
1790
  if (!scan) return "unknown";
1698
- const managed = managedCodexRange(original, scan, ide);
1791
+ const managed = managedCodexRange(original, scan, ide, desiredBlock);
1699
1792
  if (managed === null) return "unknown";
1700
1793
  if (managed) return "present";
1701
1794
  const legacy = legacyCodexRange(original, scan, ide);
@@ -2,7 +2,7 @@ import {
2
2
  inspectMcpServerPresence,
3
3
  registerMcpServer,
4
4
  unregisterMcpServer
5
- } from "./chunk-ECMT4ANK.js";
5
+ } from "./chunk-YKG6CDGT.js";
6
6
  import {
7
7
  detectInstalledIdes
8
8
  } from "./chunk-RXGW4Q3G.js";
@@ -3343,7 +3343,8 @@ import {
3343
3343
  fstatSync as fstatSync2,
3344
3344
  lstatSync as lstatSync2,
3345
3345
  mkdirSync,
3346
- openSync as openSync2
3346
+ openSync as openSync2,
3347
+ unlinkSync
3347
3348
  } from "fs";
3348
3349
  import { dirname as dirname4, posix as posix2 } from "path";
3349
3350
  import Database from "better-sqlite3";
@@ -3717,7 +3718,8 @@ function prepareSessionDatabasePath(dbPath) {
3717
3718
  const directory = preparePrivateDirectory(dirname4(dbPath));
3718
3719
  let descriptor;
3719
3720
  try {
3720
- descriptor = openOrCreateRegularFile(dbPath);
3721
+ const openResult = openOrCreateRegularFile(dbPath);
3722
+ descriptor = openResult.descriptor;
3721
3723
  const opened = fstatSync2(descriptor);
3722
3724
  if (!isSafeDatabaseFile(opened)) {
3723
3725
  throw unsafeSessionStorage("Session database path must be a private regular file with one link");
@@ -3737,6 +3739,17 @@ function prepareSessionDatabasePath(dbPath) {
3737
3739
  assertPathIdentity(dbPath, opened, "Session database path changed during initialization");
3738
3740
  directory.verify();
3739
3741
  },
3742
+ removeCreatedFile() {
3743
+ if (closed || !openResult.created) return;
3744
+ try {
3745
+ const currentDescriptor = fstatSync2(descriptor);
3746
+ if (currentDescriptor.size !== 0 || !isSafeDatabaseFile(currentDescriptor) || !sameIdentity(opened, currentDescriptor)) return;
3747
+ assertPathIdentity(dbPath, opened, "Session database path changed during cleanup");
3748
+ directory.verify();
3749
+ unlinkSync(dbPath);
3750
+ } catch {
3751
+ }
3752
+ },
3740
3753
  close() {
3741
3754
  if (closed) return;
3742
3755
  closed = true;
@@ -3911,7 +3924,7 @@ function openOrCreateRegularFile(dbPath) {
3911
3924
  if (!isSafeDatabaseFile(opened) || initial && !sameIdentity(initial, opened)) {
3912
3925
  throw unsafeSessionStorage("Session database path changed during initialization");
3913
3926
  }
3914
- return descriptor;
3927
+ return { descriptor, created: !initial };
3915
3928
  } catch (error) {
3916
3929
  try {
3917
3930
  closeSync2(descriptor);
@@ -4047,7 +4060,12 @@ var SqliteSessionRepository = class {
4047
4060
  let database;
4048
4061
  try {
4049
4062
  if (dbPath !== ":memory:") validateSqliteSidecars(dbPath);
4050
- database = dbPath === ":memory:" ? new Database(dbPath) : new Database(dbPath, { fileMustExist: true });
4063
+ try {
4064
+ database = dbPath === ":memory:" ? new Database(dbPath) : new Database(dbPath, { fileMustExist: true });
4065
+ } catch (error) {
4066
+ pathGuard?.removeCreatedFile();
4067
+ throw error;
4068
+ }
4051
4069
  pathGuard?.verify();
4052
4070
  if (dbPath !== ":memory:") validateSqliteSidecars(dbPath);
4053
4071
  this.db = database;
package/dist/cli.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  resolveWorkspace,
13
13
  serializePublicResumeCapsule,
14
14
  validateReadBudget
15
- } from "./chunk-4T3I3ACZ.js";
15
+ } from "./chunk-YY7QXBG5.js";
16
16
  import {
17
17
  loadOrCreateDeviceId
18
18
  } from "./chunk-XRJ6QW6N.js";
@@ -20,7 +20,7 @@ import {
20
20
  configureMcpRegistrationEntrypoint,
21
21
  inspectLegacyClaudeHookPresence,
22
22
  isMcpServerRegistered
23
- } from "./chunk-ECMT4ANK.js";
23
+ } from "./chunk-YKG6CDGT.js";
24
24
  import "./chunk-RXGW4Q3G.js";
25
25
  import {
26
26
  getSessionDbPath
@@ -2495,7 +2495,7 @@ function probeSessionDatabase(path) {
2495
2495
  }
2496
2496
  program.name("agentcache").description("Same-machine cross-agent session continuity with explicit user-selected handoffs").version(PKG_VERSION);
2497
2497
  program.command("setup").description("Detect IDEs and register AgentCache").action(async () => {
2498
- const { runSetup } = await import("./setup-CXZFERQE.js");
2498
+ const { runSetup } = await import("./setup-7JJPW3VG.js");
2499
2499
  await runSetup();
2500
2500
  });
2501
2501
  program.command("serve").description("Start AgentCache MCP server (spawned by IDEs automatically)").requiredOption("--adapter <id>", "Trusted adapter identity for this MCP process").action(async (options) => {
package/dist/mcp.js CHANGED
@@ -7,8 +7,8 @@ import {
7
7
  redactPortableText,
8
8
  resolveWorkspace,
9
9
  serializePublicResumeCapsule
10
- } from "./chunk-4T3I3ACZ.js";
11
- import "./chunk-ECMT4ANK.js";
10
+ } from "./chunk-YY7QXBG5.js";
11
+ import "./chunk-YKG6CDGT.js";
12
12
  import "./chunk-RXGW4Q3G.js";
13
13
  import {
14
14
  getSessionDbPath
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  registerClaudeHooks,
3
3
  registerMcpServer
4
- } from "./chunk-ECMT4ANK.js";
4
+ } from "./chunk-YKG6CDGT.js";
5
5
  import {
6
6
  detectInstalledIdes
7
7
  } from "./chunk-RXGW4Q3G.js";
@@ -1,7 +1,7 @@
1
1
  # AgentCache beta compatibility
2
2
 
3
3
  This document describes the runtime registry shipped in AgentCache
4
- `0.5.0-beta.1`. The CLI remains the source of truth on an installed machine:
4
+ `0.5.0-beta.2`. The CLI remains the source of truth on an installed machine:
5
5
 
6
6
  ```bash
7
7
  agentcache adapter list --json
@@ -69,12 +69,18 @@ unsupported for all seven adapters. In practical terms:
69
69
 
70
70
  ## Registration formats
71
71
 
72
- Setup writes an adapter-bound command, never a bare untrusted identity:
72
+ Setup writes an adapter-bound launch vector, never a bare PATH-resolved
73
+ identity:
73
74
 
74
75
  ```text
75
- agentcache serve --adapter <adapter-id>
76
+ <absolute-node> <absolute-agentcache-cli> serve --adapter <adapter-id>
76
77
  ```
77
78
 
79
+ Both executable paths are captured from the AgentCache process performing
80
+ setup. Rerun setup after changing Node versions or version managers, changing
81
+ the global npm prefix, or reinstalling/upgrading AgentCache, then restart the
82
+ clients so they reload their MCP configuration.
83
+
78
84
  The supported configuration families are Claude Code JSON, Cursor/Windsurf/Roo
79
85
  Code MCP JSON, Continue's MCP JSON directory, Codex TOML, and Goose YAML.
80
86
  Existing files are updated atomically with a restrictive backup. Malformed or
@@ -9,7 +9,7 @@ Do not cut around an error or replace real output with a mock.
9
9
 
10
10
  ## Preflight, off camera
11
11
 
12
- - Install `agentcache@0.5.0-beta.1`, run `agentcache setup`, and restart Claude
12
+ - Install `agentcache@0.5.0-beta.2`, run `agentcache setup`, and restart Claude
13
13
  Code, Codex, and Cursor.
14
14
  - Open the same small demo repository in all three clients.
15
15
  - Run the global `agentcache doctor` to verify exact adapter-bound MCP
@@ -5,13 +5,11 @@ portable AgentCache state from provider-native session state.
5
5
 
6
6
  ## Release gate
7
7
 
8
- This is staged launch copy, not evidence that the release is available. The
9
- `0.5.0-beta.1` npm version remains unpublished and the GitHub repository remains
10
- private until the final human-controlled release step. Before using any copy
11
- below, verify that the exact npm artifact is published, its checksum matches the
12
- release candidate, and the GitHub repository plus beta issue form are accessible
13
- to the intended audience. Do not publish the package or change repository
14
- visibility without explicit release approval.
8
+ The GitHub repository is public and `0.5.0-beta.1` has been published under the
9
+ `beta` npm tag. The `0.5.0-beta.2` copy below remains staged until the corrected
10
+ artifact and matching GitHub prerelease are published. Before using it, verify
11
+ the exact npm artifact and checksum, the `beta` dist-tag, and the public beta
12
+ issue form.
15
13
 
16
14
  ## One line
17
15
 
@@ -20,7 +18,7 @@ context in another registered agent on the same machine.
20
18
 
21
19
  ## Short post
22
20
 
23
- AgentCache `0.5.0-beta.1` is available for testing.
21
+ AgentCache `0.5.0-beta.2` is available for testing.
24
22
 
25
23
  Pick a coding session, choose a destination agent, and continue with a bounded
26
24
  checkpoint of the goal, decisions, completed work, blockers, constraints, and
@@ -41,7 +39,7 @@ Treat that token as a temporary bearer secret.
41
39
  Install the pinned beta:
42
40
 
43
41
  ```bash
44
- npm install -g agentcache@0.5.0-beta.1
42
+ npm install -g agentcache@0.5.0-beta.2
45
43
  agentcache setup
46
44
  ```
47
45
 
@@ -51,7 +49,7 @@ AgentCache beta feedback issue form.
51
49
 
52
50
  ## GitHub release summary
53
51
 
54
- ### AgentCache 0.5.0-beta.1 — explicit cross-agent session continuity
52
+ ### AgentCache 0.5.0-beta.2 — explicit cross-agent session continuity
55
53
 
56
54
  This beta changes AgentCache's default product path from automatic knowledge
57
55
  compilation to explicit portable sessions.
package/docs/privacy.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Privacy and trust boundary
2
2
 
3
- AgentCache `0.5.0-beta.1` is local-first and same-machine only. Its v2 database
3
+ AgentCache `0.5.0-beta.2` is local-first and same-machine only. Its v2 database
4
4
  and coordination state live on your machine. That statement is about where
5
5
  AgentCache stores data; it is not a promise that an AI coding client or its
6
6
  model provider operates offline.
@@ -13,8 +13,11 @@ agentcache adapter list --json
13
13
  agentcache adapter doctor <adapter-id> --json
14
14
  ```
15
15
 
16
- AgentCache `0.5.0-beta.1` requires Node.js 22 or newer. After setup or a client
17
- upgrade, restart the client so it reloads its MCP configuration.
16
+ AgentCache `0.5.0-beta.2` requires Node.js 22 or newer. Setup pins the exact
17
+ active Node executable and installed AgentCache CLI into each client config.
18
+ After changing Node versions or version managers, changing the global npm
19
+ prefix, reinstalling/upgrading AgentCache, or upgrading a client, rerun
20
+ `agentcache setup` and restart the client so it reloads that configuration.
18
21
 
19
22
  The global `agentcache doctor` verifies exact adapter-bound MCP registration
20
23
  commands and, after initialization, the v2 data store. On a fresh setup before
@@ -59,7 +62,7 @@ specific registration command. AgentCache does not overwrite an entry it cannot
59
62
  prove it owns.
60
63
 
61
64
  Goose uses a YAML configuration. A JSON-only diagnostic from an older
62
- AgentCache build may misreport it; verify you are running `0.5.0-beta.1` before
65
+ AgentCache build may misreport it; verify you are running `0.5.0-beta.2` before
63
66
  reporting the result.
64
67
 
65
68
  ## Handoff errors
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentcache",
3
- "version": "0.5.0-beta.1",
3
+ "version": "0.5.0-beta.2",
4
4
  "description": "Same-machine cross-agent session continuity with explicit user-selected handoffs",
5
5
  "type": "module",
6
6
  "license": "MIT",