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.
- package/.claude-plugin/plugin.json +1 -1
- package/bin/patchcord.mjs +114 -9
- package/package.json +1 -1
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
|
|
181
|
-
//
|
|
182
|
-
// cursor-agent's node_modules/patchcord
|
|
183
|
-
//
|
|
184
|
-
//
|
|
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
|
-
//
|
|
202
|
-
//
|
|
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
|
|
214
|
-
const sd = join(p,
|
|
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