patchcord 0.5.123 → 0.5.125

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "patchcord",
3
3
  "description": "Cross-machine agent messaging. Messages from other agents land in the inbox and wake the agent to reply.",
4
- "version": "0.5.123",
4
+ "version": "0.5.125",
5
5
  "author": {
6
6
  "name": "ppravdin"
7
7
  },
package/bin/patchcord.mjs CHANGED
@@ -349,18 +349,19 @@ async function _resolveBearer(options = {}) {
349
349
  );
350
350
  })();
351
351
 
352
- // NO global kimi/kimi-code reader. Kimi is a PER-PROJECT identity
353
- // (.kimi-code/mcp.json in the worktree); a global ~/.kimi(-code)/mcp.json would
354
- // make a leftover token hijack every kimi session everywhere. A kimi outside a
352
+ // NO global reader for PER-PROJECT tools. Kimi (.kimi-code/mcp.json) and
353
+ // Antigravity (.agents/mcp_config.json) are per-project identities: a leftover
354
+ // token in a global config (~/.kimi(-code)/mcp.json, ~/.gemini/antigravity/
355
+ // mcp_config.json) would hijack every session everywhere and make the agent
356
+ // "check inbox" in projects it has nothing to do with. Such a tool outside a
355
357
  // patchcord worktree is simply not an agent. (The tools below are global-ONLY
356
- // by nature — Windsurf/Gemini/Zed/etc. store MCP config globally — so they keep
357
- // their global readers.)
358
+ // by nature — Windsurf/Gemini CLI/Zed/etc. store MCP config globally — so they
359
+ // keep their global readers.)
358
360
  const defaultGlobalCandidates = [
359
361
  () => readJsonAt(join(HOME, ".codeium", "windsurf", "mcp_config.json"), ["mcpServers", "patchcord"], "windsurf"),
360
362
  () => readJsonAt(join(HOME, ".gemini", "settings.json"), ["mcpServers", "patchcord"], "gemini"),
361
363
  () => readJsonAt(zedPath, ["context_servers", "patchcord"], "zed"),
362
364
  () => readJsonAt(join(HOME, ".openclaw", "openclaw.json"), ["mcp", "servers", "patchcord"], "openclaw"),
363
- () => readJsonAt(join(HOME, ".gemini", "antigravity", "mcp_config.json"), ["mcpServers", "patchcord"], "antigravity"),
364
365
  () => readHermesShape(join(HOME, ".hermes", "config.yaml")),
365
366
  ...clinePaths.map((p) => () => readJsonAt(p, ["mcpServers", "patchcord"], "cline")),
366
367
  ];
@@ -1669,13 +1670,33 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
1669
1670
  if (windsurfChanged) globalChanges.push("Windsurf skills installed");
1670
1671
  }
1671
1672
 
1672
- // Gemini CLI + Antigravity (they SHARE ~/.gemini/skills and ~/.gemini/commands).
1673
- // Seed the patchcord skill ONLY when patchcord MCP is actually configured for
1674
- // one of these surfaces. Otherwise a bare session (esp. Antigravity, which
1675
- // inherits this dir) reads a skill with no backing MCP tool and improvises
1676
- // destructively scavenging other agents' bearer tokens off disk and curling
1677
- // the API. Invariant: skill present patchcord MCP present under ~/.gemini.
1673
+ // Gemini CLI + Antigravity SHARE ~/.gemini/skills and ~/.gemini/commands, but
1674
+ // their MCP config is NOT shared: Gemini CLI stores patchcord globally
1675
+ // (~/.gemini/settings.json) while Antigravity is PER-PROJECT
1676
+ // (<project>/.agents/mcp_config.json). So the shared global skill must be
1677
+ // seeded ONLY off Gemini CLI's global config — never off an Antigravity config.
1678
+ // A patchcord entry in the global ~/.gemini/antigravity(-cli)/mcp_config.json is
1679
+ // a leaked per-project token: it makes agy load the patchcord MCP (and its
1680
+ // "check inbox first" instruction) in EVERY project, and it kept this skill
1681
+ // seeded everywhere. We strip that leak below and gate the skill on Gemini only.
1678
1682
  if (existsSync(join(HOME, ".gemini"))) {
1683
+ // Remove any leaked patchcord server from Antigravity's GLOBAL configs so a
1684
+ // per-project token stops hijacking every agy session. Preserve other MCP
1685
+ // servers; only drop patchcord.
1686
+ for (const agGlobal of [
1687
+ join(HOME, ".gemini", "antigravity", "mcp_config.json"),
1688
+ join(HOME, ".gemini", "antigravity-cli", "mcp_config.json"),
1689
+ ]) {
1690
+ if (!existsSync(agGlobal)) continue;
1691
+ try {
1692
+ const j = JSON.parse(readFileSync(agGlobal, "utf-8"));
1693
+ if (j?.mcpServers?.patchcord) {
1694
+ delete j.mcpServers.patchcord;
1695
+ writeFileSync(agGlobal, JSON.stringify(j, null, 2) + "\n");
1696
+ globalChanges.push("Removed leaked global Antigravity patchcord MCP (agy is per-project)");
1697
+ }
1698
+ } catch {}
1699
+ }
1679
1700
  // true = configured, false = file absent (definitely not), null = unparseable (unknown)
1680
1701
  const hasPatchcordMcp = (p, keyPath) => {
1681
1702
  if (!existsSync(p)) return false;
@@ -1684,8 +1705,6 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
1684
1705
  };
1685
1706
  const checks = [
1686
1707
  hasPatchcordMcp(join(HOME, ".gemini", "settings.json"), ["mcpServers", "patchcord"]),
1687
- hasPatchcordMcp(join(HOME, ".gemini", "antigravity", "mcp_config.json"), ["mcpServers", "patchcord"]),
1688
- hasPatchcordMcp(join(HOME, ".gemini", "antigravity-cli", "mcp_config.json"), ["mcpServers", "patchcord"]),
1689
1708
  ];
1690
1709
  const mcpConfigured = checks.some((c) => c === true);
1691
1710
  const mcpUnknown = checks.some((c) => c === null);
@@ -1767,6 +1786,33 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
1767
1786
  // Kimi Code (Node) reads ~/.kimi-code/skills. The legacy Python kimi-cli
1768
1787
  // (~/.kimi) is deprecated — install only into Kimi Code's home.
1769
1788
  const kimiSkillRoots = [join(kimiCodeHome, "skills")];
1789
+
1790
+ // Remove [[hooks]] array-of-tables blocks whose body matches `predicate`,
1791
+ // preserving everything else. A block runs from a `[[hooks]]` header line
1792
+ // until the next TOML table / array-of-tables header (`[...]` / `[[...]]`)
1793
+ // or EOF — so trailing sections like [providers.*] and [models.*] are NOT
1794
+ // swallowed. (The old split("[[hooks]]")+filter approach dropped the whole
1795
+ // segment after the patchcord hook, deleting kimi's model/provider defs and
1796
+ // leaving "Model not configured" on every host on the next install.)
1797
+ const removeHookBlocks = (toml, predicate) => {
1798
+ const lines = toml.split("\n");
1799
+ const out = [];
1800
+ let i = 0;
1801
+ while (i < lines.length) {
1802
+ if (/^\s*\[\[hooks\]\]\s*$/.test(lines[i])) {
1803
+ const block = [lines[i]];
1804
+ let j = i + 1;
1805
+ while (j < lines.length && !/^\s*\[/.test(lines[j])) { block.push(lines[j]); j++; }
1806
+ if (!predicate(block.join("\n"))) out.push(...block);
1807
+ i = j;
1808
+ } else {
1809
+ out.push(lines[i]); i++;
1810
+ }
1811
+ }
1812
+ return out.join("\n");
1813
+ };
1814
+ const isPatchcordHook = (b) => /patchcord-(stop-hook|session-start|prompt-hook)/.test(b);
1815
+
1770
1816
  for (const root of kimiSkillRoots) {
1771
1817
  for (const stale of ["patchcord", "patchcord-wait", "patchcord:inbox", "patchcord:wait", "patchcord:subscribe"]) {
1772
1818
  const d = join(root, stale);
@@ -1796,11 +1842,6 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
1796
1842
  // Remove old inline hooks = [] (conflicts with [[hooks]] array-of-tables)
1797
1843
  kimiConfig = kimiConfig.replace(/^hooks\s*=\s*\[\]\n?/gm, "");
1798
1844
 
1799
- // Remove existing patchcord stop hook blocks
1800
- const segments = kimiConfig.split("[[hooks]]");
1801
- const kept = segments.filter((seg, idx) => idx === 0 || !seg.includes("patchcord-stop-hook"));
1802
- kimiConfig = kept.join("[[hooks]]").replace(/\n{3,}/g, "\n\n").trim();
1803
-
1804
1845
  // Install session-start hook
1805
1846
  const kimiSessionSrc = join(pluginRoot, "scripts", "kimi-session-start.sh");
1806
1847
  const kimiSessionDest = join(kimiCodeHome, "patchcord-session-start.sh");
@@ -1809,10 +1850,9 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
1809
1850
  chmodSync(kimiSessionDest, 0o755);
1810
1851
  }
1811
1852
 
1812
- // Remove existing patchcord session-start hook blocks
1813
- const segments2 = kimiConfig.split("[[hooks]]");
1814
- const kept2 = segments2.filter((seg, idx) => idx === 0 || !seg.includes("patchcord-session-start") && !seg.includes("patchcord-prompt-hook"));
1815
- kimiConfig = kept2.join("[[hooks]]").replace(/\n{3,}/g, "\n\n").trim();
1853
+ // Remove any existing patchcord hook blocks (stop / session-start /
1854
+ // legacy prompt) in one pass, preserving all other tables.
1855
+ kimiConfig = removeHookBlocks(kimiConfig, isPatchcordHook).replace(/\n{3,}/g, "\n\n").trim();
1816
1856
 
1817
1857
  // Append new hooks
1818
1858
  kimiConfig = kimiConfig.trimEnd() + `\n\n[[hooks]]\nevent = "SessionStart"\ncommand = "${kimiSessionDest}"\ntimeout = 10\n\n[[hooks]]\nevent = "Stop"\ncommand = "${kimiHookDest}"\ntimeout = 10\n`;
@@ -1834,9 +1874,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
1834
1874
  if (existsSync(legacyConfigPath)) {
1835
1875
  try {
1836
1876
  let lc = readFileSync(legacyConfigPath, "utf-8");
1837
- const segs = lc.split("[[hooks]]");
1838
- const keptLegacy = segs.filter((seg, idx) => idx === 0 || !seg.includes("patchcord-"));
1839
- lc = keptLegacy.join("[[hooks]]").replace(/\n{3,}/g, "\n\n").trim();
1877
+ lc = removeHookBlocks(lc, (b) => /patchcord-/.test(b)).replace(/\n{3,}/g, "\n\n").trim();
1840
1878
  writeFileSync(legacyConfigPath, lc ? lc + "\n" : "");
1841
1879
  } catch {}
1842
1880
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.5.123",
3
+ "version": "0.5.125",
4
4
  "description": "Cross-machine agent messaging for Claude Code and Codex",
5
5
  "scripts": {
6
6
  "version": "node scripts/sync-plugin-version.mjs && git add .claude-plugin/plugin.json"