patchcord 0.6.23 → 0.6.25

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.23",
4
+ "version": "0.6.25",
5
5
  "author": {
6
6
  "name": "ppravdin"
7
7
  },
package/bin/patchcord.mjs CHANGED
@@ -4,6 +4,7 @@ import { existsSync, mkdirSync, cpSync, readdirSync, readFileSync, writeFileSync
4
4
  import { join, dirname, basename } from "path";
5
5
  import { fileURLToPath } from "url";
6
6
  import { execSync } from "child_process";
7
+ import { createHash } from "crypto";
7
8
  import { homedir } from "os";
8
9
 
9
10
  const HOME = homedir();
@@ -28,6 +29,85 @@ function run(cmd) {
28
29
  }
29
30
  }
30
31
 
32
+ /**
33
+ * Cursor CLI gates project MCP behind per-project approval
34
+ * (`mcp list` → "not loaded (needs approval)"). permissions.json mcpAllowlist
35
+ * only covers IDE auto-run AFTER the server is loaded — it does not clear that
36
+ * gate. Approve via `cursor-agent mcp enable` (+ CLI allowlist + approvals file).
37
+ */
38
+ function _approveCursorProjectMcp(projectDir) {
39
+ if (!projectDir || !existsSync(join(projectDir, ".cursor", "mcp.json"))) return false;
40
+ let changed = false;
41
+
42
+ // CLI tool allow — official format in cli-config.json (separate from permissions.json).
43
+ const cliConfigPath = join(HOME, ".cursor", "cli-config.json");
44
+ try {
45
+ mkdirSync(dirname(cliConfigPath), { recursive: true });
46
+ let obj = {};
47
+ if (existsSync(cliConfigPath)) {
48
+ try { obj = JSON.parse(readFileSync(cliConfigPath, "utf-8")); } catch { obj = {}; }
49
+ }
50
+ obj.permissions = obj.permissions || {};
51
+ if (!Array.isArray(obj.permissions.allow)) obj.permissions.allow = [];
52
+ const pat = "Mcp(patchcord:*)";
53
+ if (!obj.permissions.allow.includes(pat)) {
54
+ obj.permissions.allow.push(pat);
55
+ writeFileSync(cliConfigPath, JSON.stringify(obj, null, 2) + "\n");
56
+ changed = true;
57
+ }
58
+ } catch {}
59
+
60
+ // Official CLI API: records approval under ~/.cursor/projects/<slug>/mcp-approvals.json
61
+ if (run("which cursor-agent")) {
62
+ try {
63
+ execSync("cursor-agent mcp enable patchcord", {
64
+ cwd: projectDir,
65
+ stdio: "pipe",
66
+ encoding: "utf-8",
67
+ timeout: 20000,
68
+ });
69
+ changed = true;
70
+ } catch {
71
+ // Fall through to writing mcp-approvals.json directly.
72
+ }
73
+ }
74
+
75
+ // Fallback: write the same approval key cursor-agent expects (URL MCP servers).
76
+ try {
77
+ const mcpPath = join(projectDir, ".cursor", "mcp.json");
78
+ const cfg = JSON.parse(readFileSync(mcpPath, "utf-8"));
79
+ const server = cfg?.mcpServers?.patchcord;
80
+ const url = server?.url;
81
+ if (url) {
82
+ const slug = projectDir.replace(/[^A-Za-z0-9]+/g, "-").replace(/^-|-$/g, "");
83
+ const approvalsDir = join(HOME, ".cursor", "projects", slug);
84
+ mkdirSync(approvalsDir, { recursive: true });
85
+ const approvalsPath = join(approvalsDir, "mcp-approvals.json");
86
+ let approvals = [];
87
+ if (existsSync(approvalsPath)) {
88
+ try {
89
+ const raw = JSON.parse(readFileSync(approvalsPath, "utf-8"));
90
+ if (Array.isArray(raw)) approvals = raw;
91
+ } catch {}
92
+ }
93
+ // Cursor hashes { path, server: { url } } and sometimes the full server object.
94
+ const keys = [
95
+ `patchcord-${createHash("sha256").update(JSON.stringify({ path: projectDir, server: { url } })).digest("hex").slice(0, 16)}`,
96
+ `patchcord-${createHash("sha256").update(JSON.stringify({ path: projectDir, server })).digest("hex").slice(0, 16)}`,
97
+ ];
98
+ for (const key of keys) {
99
+ if (!approvals.includes(key)) {
100
+ approvals.push(key);
101
+ changed = true;
102
+ }
103
+ }
104
+ writeFileSync(approvalsPath, JSON.stringify(approvals, null, 2) + "\n");
105
+ }
106
+ } catch {}
107
+
108
+ return changed;
109
+ }
110
+
31
111
  function isSafeToken(t) {
32
112
  return /^[A-Za-z0-9_\-=+/.]+$/.test(t) && t.length < 200;
33
113
  }
@@ -177,11 +257,14 @@ function _purgeLegacyKimiProjectConfig(dir) {
177
257
  } catch {}
178
258
  }
179
259
 
180
- // Cursor slash menu duplicates come from (a) stale combined/no-hyphen dirs under
181
- // skills-cursor and (b) Claude colon-named plugin skills bundled inside
182
- // cursor-agent's node_modules/patchcord. NEVER touch ~/.agents/skills — that tree
183
- // is Codex/Antigravity-owned (global + per-project); Cursor update must not
184
- // delete other harnesses' skills.
260
+ // Cursor slash menu duplicates come from multiple trees Cursor CLI indexes:
261
+ // (a) stale no-hyphen / colon dirs under skills-cursor
262
+ // (b) cursor-agent's bundled node_modules/patchcord both skills/ (Claude
263
+ // colon names /patchcordinbox) AND per-project-skills/cursor (exact
264
+ // dupes of skills-cursor /patchcord-inbox)
265
+ // (c) ~/.agents/skills Codex copies (/patchcord) — Cursor indexes Agent Skills
266
+ // NEVER delete ~/.agents/skills content (Codex owns that tree). We only strip
267
+ // the cursor-agent-bundled copies Cursor should not show.
185
268
  function _purgeCursorSkillDuplicates() {
186
269
  const staleCursorOnly = [
187
270
  "patchcord",
@@ -198,8 +281,22 @@ function _purgeCursorSkillDuplicates() {
198
281
  if (!existsSync(d)) continue;
199
282
  try { rmSync(d, { recursive: true, force: true }); } catch {}
200
283
  }
201
- // cursor-agent ships patchcord@* with Claude plugin skills/ Cursor must not
202
- // index those; only per-project skills-cursor copies are valid for Cursor CLI.
284
+ // Keep ONLY the three hyphenated Cursor skills under skills-cursor.
285
+ // Anything else named patchcord* there is leftover junk.
286
+ if (existsSync(cursorSkillsRoot)) {
287
+ const keep = new Set(["patchcord-inbox", "patchcord-wait", "patchcord-subscribe"]);
288
+ try {
289
+ for (const ent of readdirSync(cursorSkillsRoot, { withFileTypes: true })) {
290
+ if (!ent.isDirectory()) continue;
291
+ if (!/^patchcord/i.test(ent.name)) continue;
292
+ if (keep.has(ent.name)) continue;
293
+ try { rmSync(join(cursorSkillsRoot, ent.name), { recursive: true, force: true }); } catch {}
294
+ }
295
+ } catch {}
296
+ }
297
+ // cursor-agent ships the full npm package. Cursor indexes SKILL.md under it,
298
+ // so strip BOTH skills/ and per-project-skills/ from every bundled copy.
299
+ // Canonical Cursor skills live only in ~/.cursor/skills-cursor/.
203
300
  const cursorAgentRoot = join(HOME, ".local", "share", "cursor-agent");
204
301
  if (existsSync(cursorAgentRoot)) {
205
302
  const walk = (dir, depth = 0) => {
@@ -210,8 +307,8 @@ function _purgeCursorSkillDuplicates() {
210
307
  const p = join(dir, ent.name);
211
308
  if (ent.isDirectory()) {
212
309
  if (ent.name === "patchcord" && dir.endsWith("node_modules")) {
213
- for (const pluginSkill of ["inbox", "wait", "subscribe"]) {
214
- const sd = join(p, "skills", pluginSkill);
310
+ for (const sub of ["skills", "per-project-skills"]) {
311
+ const sd = join(p, sub);
215
312
  if (existsSync(sd)) {
216
313
  try { rmSync(sd, { recursive: true, force: true }); } catch {}
217
314
  }
@@ -1900,6 +1997,11 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
1900
1997
  } else if (allowlistChanged) {
1901
1998
  globalChanges.push("Cursor MCP allowlist configured");
1902
1999
  }
2000
+
2001
+ // Existing projects: clear "not loaded (needs approval)" on update.
2002
+ if (_approveCursorProjectMcp(process.cwd())) {
2003
+ globalChanges.push("Cursor MCP approved for this project");
2004
+ }
1903
2005
  }
1904
2006
  }
1905
2007
 
@@ -2729,6 +2831,9 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
2729
2831
  console.log(`\n ${green}✓${r} Cursor configured: ${dim}${cursorPath}${r}`);
2730
2832
  console.log(` ${dim}Per-project only — other projects won't see this agent.${r}`);
2731
2833
  }
2834
+ if (_approveCursorProjectMcp(cwd)) {
2835
+ console.log(` ${green}✓${r} Cursor MCP approved (cursor-agent mcp enable)`);
2836
+ }
2732
2837
  } else if (isGrok) {
2733
2838
  // Grok CLI: project-scoped .grok/config.toml. Grok loads this file from
2734
2839
  // the current directory up through the git root, so the bearer remains
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.6.23",
3
+ "version": "0.6.25",
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"