patchcord 0.6.22 → 0.6.24
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 +86 -52
- 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
|
}
|
|
@@ -1901,27 +1981,9 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
1901
1981
|
globalChanges.push("Cursor MCP allowlist configured");
|
|
1902
1982
|
}
|
|
1903
1983
|
|
|
1904
|
-
//
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
let cliAllowChanged = false;
|
|
1908
|
-
const cliOk = updateJsonConfig(cliConfigPath, (obj) => {
|
|
1909
|
-
obj.permissions = obj.permissions || {};
|
|
1910
|
-
if (!obj.permissions.allow) obj.permissions.allow = [];
|
|
1911
|
-
if (!Array.isArray(obj.permissions.allow)) {
|
|
1912
|
-
console.log(`\n ${yellow}⚠${r} Skipped Cursor CLI MCP allow — ${cliConfigPath} permissions.allow is not an array.`);
|
|
1913
|
-
return;
|
|
1914
|
-
}
|
|
1915
|
-
const pat = "Mcp(patchcord:*)";
|
|
1916
|
-
if (!obj.permissions.allow.includes(pat)) {
|
|
1917
|
-
obj.permissions.allow.push(pat);
|
|
1918
|
-
cliAllowChanged = true;
|
|
1919
|
-
}
|
|
1920
|
-
});
|
|
1921
|
-
if (!cliOk) {
|
|
1922
|
-
globalChanges.push("✗ Cursor CLI permissions error");
|
|
1923
|
-
} else if (cliAllowChanged) {
|
|
1924
|
-
globalChanges.push("Cursor CLI MCP allowlist configured");
|
|
1984
|
+
// Existing projects: clear "not loaded (needs approval)" on update.
|
|
1985
|
+
if (_approveCursorProjectMcp(process.cwd())) {
|
|
1986
|
+
globalChanges.push("Cursor MCP approved for this project");
|
|
1925
1987
|
}
|
|
1926
1988
|
}
|
|
1927
1989
|
}
|
|
@@ -2360,12 +2422,6 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
2360
2422
|
}
|
|
2361
2423
|
toolSlug = toolFlag; // preserved as-is for the URL param
|
|
2362
2424
|
}
|
|
2363
|
-
// cursor-agent --yolo runs npx patchcord without --tool=cursor; auto-select
|
|
2364
|
-
// Cursor so provision writes .cursor/mcp.json for this harness.
|
|
2365
|
-
if (!choice && process.env.CURSOR_AGENT) {
|
|
2366
|
-
choice = CLIENT_TYPE_MAP.cursor;
|
|
2367
|
-
toolSlug = toolSlug || "cursor";
|
|
2368
|
-
}
|
|
2369
2425
|
|
|
2370
2426
|
// --token bypass for power users / CI / self-hosters
|
|
2371
2427
|
const tokenFlag = flags.find(f => f.startsWith("--token="))?.split("=")[1]
|
|
@@ -2416,14 +2472,12 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
2416
2472
|
let existingToken = "";
|
|
2417
2473
|
let existingConfigFile = "";
|
|
2418
2474
|
const mcpJsonPath = join(cwd, ".mcp.json");
|
|
2419
|
-
const cursorMcpPath = join(cwd, ".cursor", "mcp.json");
|
|
2420
2475
|
const codexTomlPath = join(cwd, ".codex", "config.toml");
|
|
2421
2476
|
const grokTomlPath = join(cwd, ".grok", "config.toml");
|
|
2422
2477
|
const kimiJsonPath = join(cwd, ".kimi", "mcp.json");
|
|
2423
2478
|
|
|
2424
|
-
const slugForCheck = toolSlug ? toolSlug.replace(/-/g, "_") :
|
|
2479
|
+
const slugForCheck = toolSlug ? toolSlug.replace(/-/g, "_") : "";
|
|
2425
2480
|
const checkMcpJson = !slugForCheck || slugForCheck === "claude_code";
|
|
2426
|
-
const checkCursorJson = !slugForCheck || slugForCheck === "cursor";
|
|
2427
2481
|
const checkCodexToml = !slugForCheck || slugForCheck === "codex";
|
|
2428
2482
|
const checkGrokToml = !slugForCheck || ["grok", "grok_cli", "grok_build"].includes(slugForCheck);
|
|
2429
2483
|
const checkKimiJson = !slugForCheck || slugForCheck === "kimi";
|
|
@@ -2438,16 +2492,6 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
2438
2492
|
}
|
|
2439
2493
|
} catch {}
|
|
2440
2494
|
}
|
|
2441
|
-
if (!existingToken && checkCursorJson && existsSync(cursorMcpPath)) {
|
|
2442
|
-
try {
|
|
2443
|
-
const existing = JSON.parse(readFileSync(cursorMcpPath, "utf-8"));
|
|
2444
|
-
const pt = existing?.mcpServers?.patchcord;
|
|
2445
|
-
if (pt?.headers?.Authorization) {
|
|
2446
|
-
existingToken = pt.headers.Authorization.replace(/^Bearer\s+/i, "");
|
|
2447
|
-
existingConfigFile = cursorMcpPath;
|
|
2448
|
-
}
|
|
2449
|
-
} catch {}
|
|
2450
|
-
}
|
|
2451
2495
|
if (!existingToken && checkCodexToml && existsSync(codexTomlPath)) {
|
|
2452
2496
|
try {
|
|
2453
2497
|
const content = readFileSync(codexTomlPath, "utf-8");
|
|
@@ -2770,16 +2814,8 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
2770
2814
|
console.log(`\n ${green}✓${r} Cursor configured: ${dim}${cursorPath}${r}`);
|
|
2771
2815
|
console.log(` ${dim}Per-project only — other projects won't see this agent.${r}`);
|
|
2772
2816
|
}
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
obj.permissions = obj.permissions || {};
|
|
2776
|
-
if (!obj.permissions.allow) obj.permissions.allow = [];
|
|
2777
|
-
if (!Array.isArray(obj.permissions.allow)) return;
|
|
2778
|
-
const pat = "Mcp(patchcord:*)";
|
|
2779
|
-
if (!obj.permissions.allow.includes(pat)) obj.permissions.allow.push(pat);
|
|
2780
|
-
});
|
|
2781
|
-
if (cursorCliOk) {
|
|
2782
|
-
console.log(` ${green}✓${r} Cursor CLI permissions: ${dim}${cursorCliPath}${r}`);
|
|
2817
|
+
if (_approveCursorProjectMcp(cwd)) {
|
|
2818
|
+
console.log(` ${green}✓${r} Cursor MCP approved (cursor-agent mcp enable)`);
|
|
2783
2819
|
}
|
|
2784
2820
|
} else if (isGrok) {
|
|
2785
2821
|
// Grok CLI: project-scoped .grok/config.toml. Grok loads this file from
|
|
@@ -3433,8 +3469,6 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
3433
3469
|
|
|
3434
3470
|
if (isOpenClaw) {
|
|
3435
3471
|
console.log(`\n${dim}Run${r} ${bold}openclaw gateway restart${r}${dim}, then tools will be available in your channels.${r}`);
|
|
3436
|
-
} else if (isCursor && hasCursorAgent) {
|
|
3437
|
-
console.log(`\n ${green}→${r} ${bold}Exit and start a new ${cyan}cursor-agent${r}${bold} session${r} ${dim}(MCP loads at session start)${r}`);
|
|
3438
3472
|
} else if (isKimiCode) {
|
|
3439
3473
|
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}`);
|
|
3440
3474
|
} else if (isKimi) {
|
package/package.json
CHANGED