patchcord 0.6.14 → 0.6.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.
@@ -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.6.14",
4
+ "version": "0.6.16",
5
5
  "author": {
6
6
  "name": "ppravdin"
7
7
  },
package/bin/patchcord.mjs CHANGED
@@ -43,6 +43,59 @@ function isSafeId(s) {
43
43
  return /^[A-Za-z0-9_\-]+$/.test(s) && s.length < 100;
44
44
  }
45
45
 
46
+ /** Undo 0.6.14 mistake: Claude Code uses /mcp (not /mcp/bearer). */
47
+ function revertClaudeMcpBearerUrl(mcpPath) {
48
+ if (!existsSync(mcpPath)) return false;
49
+ try {
50
+ const obj = JSON.parse(readFileSync(mcpPath, "utf-8"));
51
+ const pt = obj?.mcpServers?.patchcord;
52
+ const url = pt?.url;
53
+ if (!url || !url.endsWith("/mcp/bearer")) return false;
54
+ pt.url = url.replace(/\/mcp\/bearer$/, "/mcp");
55
+ writeFileSync(mcpPath, JSON.stringify(obj, null, 2) + "\n");
56
+ return true;
57
+ } catch {
58
+ return false;
59
+ }
60
+ }
61
+
62
+ /** Patchcord uses project .mcp.json only — drop stale local-scope duplicates. */
63
+ function dedupeClaudePatchcordMcp(cwd) {
64
+ if (run("which claude")) {
65
+ run("claude mcp remove patchcord -s local");
66
+ }
67
+ const claudeJson = join(HOME, ".claude.json");
68
+ if (!existsSync(claudeJson)) return false;
69
+ try {
70
+ const obj = JSON.parse(readFileSync(claudeJson, "utf-8"));
71
+ let changed = false;
72
+ if (obj.mcpServers?.patchcord) {
73
+ delete obj.mcpServers.patchcord;
74
+ if (Object.keys(obj.mcpServers).length === 0) delete obj.mcpServers;
75
+ changed = true;
76
+ }
77
+ const proj = obj.projects?.[cwd];
78
+ if (proj?.mcpServers?.patchcord) {
79
+ delete proj.mcpServers.patchcord;
80
+ if (Object.keys(proj.mcpServers).length === 0) delete proj.mcpServers;
81
+ changed = true;
82
+ }
83
+ if (changed) {
84
+ writeFileSync(claudeJson, JSON.stringify(obj, null, 2) + "\n");
85
+ }
86
+ return changed;
87
+ } catch {
88
+ return false;
89
+ }
90
+ }
91
+
92
+ function fixClaudePatchcordMcp(cwd) {
93
+ const mcpPath = join(cwd, ".mcp.json");
94
+ const reverted = revertClaudeMcpBearerUrl(mcpPath);
95
+ const deduped = dedupeClaudePatchcordMcp(cwd);
96
+ return { reverted, deduped };
97
+ }
98
+
46
99
  // A trailing --help/-h on ANY subcommand must show help and exit — never run the
47
100
  // command. (patchcord orchestrator --help used to PROVISION an orchestrator;
48
101
  // patchcord login --help hung on the browser login. Side-effecting --help is a
@@ -708,6 +761,8 @@ if (cmd === "subscribe") {
708
761
  const forceKimi = args.includes("--kimi");
709
762
  const intervalArg = args.find((a) => a !== "--kimi") || "30";
710
763
 
764
+ fixClaudePatchcordMcp(process.cwd());
765
+
711
766
  // Kimi Code uses polling instead of WebSocket realtime.
712
767
  // Force Kimi mode with --kimi flag, or auto-detect from bearer config.
713
768
  let isKimi = forceKimi;
@@ -1172,13 +1227,9 @@ if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "
1172
1227
  // default: claude_code. type:"http" is REQUIRED — without it Claude Code
1173
1228
  // defaults to stdio transport and rejects the entry ("command: expected
1174
1229
  // string, received undefined").
1175
- // Use /mcp/bearer (not /mcp): Claude Code v2.1.113+ follows RFC 9728 OAuth
1176
- // discovery on /mcp, drops the static bearer header, and stalls on "Needs
1177
- // authentication". /mcp/bearer serves bearer-only resource metadata — same
1178
- // path Cursor and Kimi Code already use.
1179
1230
  return writeJson(join(dir, ".mcp.json"), (o) => {
1180
1231
  o.mcpServers = o.mcpServers || {};
1181
- o.mcpServers.patchcord = { type: "http", url: `${baseUrl}/mcp/bearer`, headers: hdr };
1232
+ o.mcpServers.patchcord = { type: "http", url: `${baseUrl}/mcp`, headers: hdr };
1182
1233
  });
1183
1234
  };
1184
1235
 
@@ -1665,22 +1716,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
1665
1716
  return out.replace(/,(\s*[}\]])/g, "$1");
1666
1717
  }
1667
1718
 
1668
- // Claude Code v2.1.113+ OAuth discovery on plain /mcp breaks bearer MCP.
1669
- // Rewrite legacy project configs to /mcp/bearer (Cursor/Kimi already use it).
1670
- function _migrateClaudeMcpBearerUrl(mcpPath) {
1671
- if (!existsSync(mcpPath)) return false;
1672
- try {
1673
- const obj = JSON.parse(readFileSync(mcpPath, "utf-8"));
1674
- const pt = obj?.mcpServers?.patchcord;
1675
- const url = pt?.url;
1676
- if (!url || !/\/mcp$/.test(url) || url.endsWith("/mcp/bearer")) return false;
1677
- pt.url = url.replace(/\/mcp$/, "/mcp/bearer");
1678
- writeFileSync(mcpPath, JSON.stringify(obj, null, 2) + "\n");
1679
- return true;
1680
- } catch {
1681
- return false;
1682
- }
1683
- }
1719
+ // dedupeClaudePatchcordMcp + revertClaudeMcpBearerUrl live at module scope.
1684
1720
 
1685
1721
  // Read+merge+write a JSON config file. If the file exists but its contents
1686
1722
  // can't be parsed, REFUSE to write — silently overwriting would erase
@@ -1740,8 +1776,13 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
1740
1776
  const bold = "\x1b[1m";
1741
1777
  const r = "\x1b[0m";
1742
1778
 
1743
- if (_migrateClaudeMcpBearerUrl(join(process.cwd(), ".mcp.json"))) {
1744
- console.log(` ${green}✓${r} Claude Code MCP URL migrated to /mcp/bearer`);
1779
+ const _installCwd = process.cwd();
1780
+ const _mcpFix = fixClaudePatchcordMcp(_installCwd);
1781
+ if (_mcpFix.reverted) {
1782
+ console.log(` ${green}✓${r} Claude Code MCP URL reverted to /mcp (0.6.14 /mcp/bearer was wrong for CC)`);
1783
+ }
1784
+ if (_mcpFix.deduped) {
1785
+ console.log(` ${green}✓${r} Removed stale local-scope patchcord MCP (use project .mcp.json only)`);
1745
1786
  }
1746
1787
 
1747
1788
  // ── Purge stale npx cache entries ────────────────────────────
@@ -1925,6 +1966,30 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
1925
1966
  } else if (allowlistChanged) {
1926
1967
  globalChanges.push("Cursor MCP allowlist configured");
1927
1968
  }
1969
+
1970
+ // cursor-agent v2026.07+ also reads ~/.cursor/cli-config.json permissions.allow
1971
+ // for MCP tool approval — permissions.json alone leaves patchcord at
1972
+ // "not loaded (needs approval)" even with --yolo.
1973
+ const cliConfigPath = join(HOME, ".cursor", "cli-config.json");
1974
+ let cliAllowChanged = false;
1975
+ const cliOk = updateJsonConfig(cliConfigPath, (obj) => {
1976
+ obj.permissions = obj.permissions || {};
1977
+ if (!obj.permissions.allow) obj.permissions.allow = [];
1978
+ if (!Array.isArray(obj.permissions.allow)) {
1979
+ console.log(`\n ${yellow}⚠${r} Skipped Cursor CLI MCP allow — ${cliConfigPath} permissions.allow is not an array.`);
1980
+ return;
1981
+ }
1982
+ const pat = "Mcp(patchcord:*)";
1983
+ if (!obj.permissions.allow.includes(pat)) {
1984
+ obj.permissions.allow.push(pat);
1985
+ cliAllowChanged = true;
1986
+ }
1987
+ });
1988
+ if (!cliOk) {
1989
+ globalChanges.push("✗ Cursor CLI permissions error");
1990
+ } else if (cliAllowChanged) {
1991
+ globalChanges.push("Cursor CLI MCP allowlist configured");
1992
+ }
1928
1993
  }
1929
1994
  }
1930
1995
 
@@ -2362,6 +2427,13 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
2362
2427
  }
2363
2428
  toolSlug = toolFlag; // preserved as-is for the URL param
2364
2429
  }
2430
+ // cursor-agent --yolo runs `npx patchcord` without --tool=cursor. Without
2431
+ // this, browser connect defaults to Claude and writes .mcp.json instead of
2432
+ // .cursor/mcp.json — MCP never loads in cursor-agent.
2433
+ if (!choice && process.env.CURSOR_AGENT) {
2434
+ choice = CLIENT_TYPE_MAP.cursor;
2435
+ toolSlug = toolSlug || "cursor";
2436
+ }
2365
2437
 
2366
2438
  // --token bypass for power users / CI / self-hosters
2367
2439
  const tokenFlag = flags.find(f => f.startsWith("--token="))?.split("=")[1]
@@ -2412,12 +2484,14 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
2412
2484
  let existingToken = "";
2413
2485
  let existingConfigFile = "";
2414
2486
  const mcpJsonPath = join(cwd, ".mcp.json");
2487
+ const cursorMcpPath = join(cwd, ".cursor", "mcp.json");
2415
2488
  const codexTomlPath = join(cwd, ".codex", "config.toml");
2416
2489
  const grokTomlPath = join(cwd, ".grok", "config.toml");
2417
2490
  const kimiJsonPath = join(cwd, ".kimi", "mcp.json");
2418
2491
 
2419
- const slugForCheck = toolSlug ? toolSlug.replace(/-/g, "_") : "";
2492
+ const slugForCheck = toolSlug ? toolSlug.replace(/-/g, "_") : (process.env.CURSOR_AGENT ? "cursor" : "");
2420
2493
  const checkMcpJson = !slugForCheck || slugForCheck === "claude_code";
2494
+ const checkCursorJson = !slugForCheck || slugForCheck === "cursor";
2421
2495
  const checkCodexToml = !slugForCheck || slugForCheck === "codex";
2422
2496
  const checkGrokToml = !slugForCheck || ["grok", "grok_cli", "grok_build"].includes(slugForCheck);
2423
2497
  const checkKimiJson = !slugForCheck || slugForCheck === "kimi";
@@ -2432,6 +2506,16 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
2432
2506
  }
2433
2507
  } catch {}
2434
2508
  }
2509
+ if (!existingToken && checkCursorJson && existsSync(cursorMcpPath)) {
2510
+ try {
2511
+ const existing = JSON.parse(readFileSync(cursorMcpPath, "utf-8"));
2512
+ const pt = existing?.mcpServers?.patchcord;
2513
+ if (pt?.headers?.Authorization) {
2514
+ existingToken = pt.headers.Authorization.replace(/^Bearer\s+/i, "");
2515
+ existingConfigFile = cursorMcpPath;
2516
+ }
2517
+ } catch {}
2518
+ }
2435
2519
  if (!existingToken && checkCodexToml && existsSync(codexTomlPath)) {
2436
2520
  try {
2437
2521
  const content = readFileSync(codexTomlPath, "utf-8");
@@ -3276,7 +3360,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
3276
3360
  mcpServers: {
3277
3361
  patchcord: {
3278
3362
  type: "http",
3279
- url: `${serverUrl}/mcp/bearer`,
3363
+ url: `${serverUrl}/mcp`,
3280
3364
  headers: {
3281
3365
  Authorization: `Bearer ${token}`,
3282
3366
  "X-Patchcord-Machine": hostname,
@@ -3295,6 +3379,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
3295
3379
  // their JSON and re-runs.
3296
3380
  process.exit(1);
3297
3381
  }
3382
+ dedupeClaudePatchcordMcp(cwd);
3298
3383
  console.log(`\n ${green}✓${r} Claude Code configured: ${dim}${mcpPath}${r}`);
3299
3384
 
3300
3385
  // Statusline: ask whether to enable full mode (unless --full flag passed or already configured).
@@ -3410,6 +3495,9 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
3410
3495
  console.log(`\n ${green}→${r} ${bold}Restart ${cyan}kimi${r}${bold} in this project (or run ${cyan}/reload${r}${bold}), then say: ${cyan}${bold}check inbox${r}`);
3411
3496
  } else if (isKimi) {
3412
3497
  console.log(`\n ${green}→${r} ${bold}Restart your ${toolName} session with ${cyan}kimi-pc${r}${bold}, then say: ${cyan}${bold}check inbox${r}`);
3498
+ } else if (isCursor && hasCursorAgent) {
3499
+ console.log(`\n ${green}→${r} ${bold}Exit and start a new ${cyan}cursor-agent${r}${bold} session${r} ${dim}(MCP loads at session start — not mid-session)${r}`);
3500
+ console.log(` ${dim}Then:${r} ${cyan}${bold}check inbox${r} ${dim}before${r} ${cyan}${bold}/patchcord-subscribe${r}`);
3413
3501
  } else {
3414
3502
  console.log(`\n ${green}→${r} ${bold}Restart your ${toolName} session${r}, then say: ${cyan}${bold}check inbox${r}`);
3415
3503
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.6.14",
3
+ "version": "0.6.16",
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"
@@ -15,12 +15,21 @@ Start the persistent listener in a background Shell.
15
15
  expose it as `mcp_patchcord_inbox`). Process every pending message according
16
16
  to the patchcord inbox skill before starting the listener.
17
17
 
18
+ **Just provisioned in this same session?** Cursor/`cursor-agent` loads MCP
19
+ servers at session start only — `inbox()` will not exist until the user
20
+ starts a **new** session after connect. Do not spin on missing MCP tools.
21
+ Skip inbox drain, start the listener (step 3), and say one line: *MCP inbox
22
+ needs a fresh cursor-agent session after connect; listener is running — restart
23
+ once, then say check inbox.*
24
+
18
25
  2. **Find the absolute project root.** Use the current session's git root or
19
26
  agent worktree. Do not rely on the ambient Shell cwd: the listener must read
20
27
  the `.cursor/mcp.json` for this project and therefore needs an explicit
21
28
  `cd`.
22
29
 
23
- 3. **Spawn the background listener** with Cursor's Shell tool:
30
+ 3. **Spawn the background listener** with Cursor's Shell tool. **Always**
31
+ `block_until_ms: 0` — never run subscribe in the foreground (that yields
32
+ "Wait skipped" and no wake notifications):
24
33
 
25
34
  ```text
26
35
  Shell(