patchcord 0.6.10 → 0.6.11
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/bin/patchcord.mjs
CHANGED
|
@@ -10,6 +10,7 @@ const HOME = homedir();
|
|
|
10
10
|
|
|
11
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
12
|
const pluginRoot = join(__dirname, "..");
|
|
13
|
+
import { resolveProjectBearer, harnessContext } from "../scripts/lib/resolve-project-bearer.mjs";
|
|
13
14
|
const cmd = process.argv[2];
|
|
14
15
|
|
|
15
16
|
if (cmd === "--version" || cmd === "-v") {
|
|
@@ -134,13 +135,7 @@ if (cmd === "plugin-path") {
|
|
|
134
135
|
// opencode (per-project) + windsurf, gemini, zed, openclaw, antigravity,
|
|
135
136
|
// cline, kimi (global). Each tool stores the bearer in its own shape.
|
|
136
137
|
function _kimiContextRequested(options = {}) {
|
|
137
|
-
|
|
138
|
-
return options.preferKimi === true
|
|
139
|
-
|| forceTool === "kimi"
|
|
140
|
-
|| Boolean(process.env.KIMI_CLI)
|
|
141
|
-
|| Boolean(process.env.KIMI_SESSION_ID)
|
|
142
|
-
|| Boolean(process.env.KIMI_CONFIG_FILE)
|
|
143
|
-
|| Boolean(process.env.KIMI_MCP_CONFIG_FILE);
|
|
138
|
+
return harnessContext(process.env, options).preferKimi;
|
|
144
139
|
}
|
|
145
140
|
|
|
146
141
|
// Strip legacy Python kimi-cli per-project artifacts. Kimi Code (Node) uses
|
|
@@ -268,7 +263,9 @@ function readGrokTomlShape(path) {
|
|
|
268
263
|
|
|
269
264
|
async function _resolveBearer(options = {}) {
|
|
270
265
|
const { readFileSync } = await import("fs");
|
|
271
|
-
|
|
266
|
+
|
|
267
|
+
const projectFound = resolveProjectBearer(process.cwd(), options);
|
|
268
|
+
if (projectFound) return projectFound;
|
|
272
269
|
|
|
273
270
|
// Strict JSON first; fall back to JSONC stripping for zed/gemini-style
|
|
274
271
|
// settings (which allow // /* */ trailing commas). The fallback strips
|
|
@@ -357,30 +354,6 @@ async function _resolveBearer(options = {}) {
|
|
|
357
354
|
} catch { return null; }
|
|
358
355
|
};
|
|
359
356
|
|
|
360
|
-
// Per-project (walk up from cwd). First win.
|
|
361
|
-
const kimiProjectReader = (cwd) => readJsonAt(join(cwd, ".kimi", "mcp.json"), ["mcpServers", "patchcord"], "kimi");
|
|
362
|
-
const kimiCodeProjectReader = (cwd) => readJsonAt(join(cwd, ".kimi-code", "mcp.json"), ["mcpServers", "patchcord"], "kimi");
|
|
363
|
-
const defaultProjectReaders = [
|
|
364
|
-
(cwd) => readJsonAt(join(cwd, ".mcp.json"), ["mcpServers", "patchcord"], "claude_code"),
|
|
365
|
-
(cwd) => readJsonAt(join(cwd, ".cursor", "mcp.json"), ["mcpServers", "patchcord"], "cursor"),
|
|
366
|
-
(cwd) => readJsonAt(join(cwd, ".vscode", "mcp.json"), ["servers", "patchcord"], "vscode"),
|
|
367
|
-
(cwd) => readJsonAt(join(cwd, "opencode.json"), ["mcp", "patchcord"], "opencode"),
|
|
368
|
-
(cwd) => readJsonAt(join(cwd, ".agents", "mcp_config.json"), ["mcpServers", "patchcord"], "antigravity"),
|
|
369
|
-
(cwd) => readGrokTomlShape(join(cwd, ".grok", "config.toml")),
|
|
370
|
-
(cwd) => readCodexTomlShape(join(cwd, ".codex", "config.toml")),
|
|
371
|
-
];
|
|
372
|
-
const projectReaders = preferKimi
|
|
373
|
-
? [kimiProjectReader, kimiCodeProjectReader, ...defaultProjectReaders]
|
|
374
|
-
: [...defaultProjectReaders, kimiProjectReader, kimiCodeProjectReader];
|
|
375
|
-
let dir = process.cwd();
|
|
376
|
-
while (dir && dir !== "/") {
|
|
377
|
-
for (const r of projectReaders) {
|
|
378
|
-
const found = r(dir);
|
|
379
|
-
if (found) { found.scope = "project"; return found; }
|
|
380
|
-
}
|
|
381
|
-
dir = dirname(dir);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
357
|
// Global fallbacks. Order by likelihood for current install base.
|
|
385
358
|
const zedPath = process.platform === "darwin"
|
|
386
359
|
? join(HOME, "Library", "Application Support", "Zed", "settings.json")
|
package/package.json
CHANGED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// Shared per-project bearer resolution for patchcord.mjs and subscribe.mjs.
|
|
2
|
+
// When multiple tool configs exist in one directory (common: .mcp.json from
|
|
3
|
+
// Claude Code + .cursor/mcp.json from Cursor), pick the config matching the
|
|
4
|
+
// invoking harness instead of always preferring .mcp.json first.
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
|
|
9
|
+
function stripJsoncOutsideStrings(raw) {
|
|
10
|
+
let out = "";
|
|
11
|
+
let i = 0;
|
|
12
|
+
let inStr = false;
|
|
13
|
+
let strCh = "";
|
|
14
|
+
let prev = "";
|
|
15
|
+
while (i < raw.length) {
|
|
16
|
+
const c = raw[i];
|
|
17
|
+
if (inStr) {
|
|
18
|
+
out += c;
|
|
19
|
+
if (c === strCh && prev !== "\\") inStr = false;
|
|
20
|
+
prev = c;
|
|
21
|
+
i++;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (c === '"' || c === "'") {
|
|
25
|
+
inStr = true;
|
|
26
|
+
strCh = c;
|
|
27
|
+
out += c;
|
|
28
|
+
prev = c;
|
|
29
|
+
i++;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (c === "/" && raw[i + 1] === "/") {
|
|
33
|
+
while (i < raw.length && raw[i] !== "\n") i++;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (c === "/" && raw[i + 1] === "*") {
|
|
37
|
+
i += 2;
|
|
38
|
+
while (i < raw.length && !(raw[i] === "*" && raw[i + 1] === "/")) i++;
|
|
39
|
+
i += 2;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
out += c;
|
|
43
|
+
prev = c;
|
|
44
|
+
i++;
|
|
45
|
+
}
|
|
46
|
+
return out.replace(/,(\s*[}\]])/g, "$1");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parseJsonc(raw) {
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(raw);
|
|
52
|
+
} catch {}
|
|
53
|
+
return JSON.parse(stripJsoncOutsideStrings(raw));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function extractBearer(entry) {
|
|
57
|
+
if (!entry) return null;
|
|
58
|
+
const auth = entry?.headers?.Authorization;
|
|
59
|
+
const url = entry.url || entry.httpUrl || entry.serverUrl;
|
|
60
|
+
if (!auth || !url) return null;
|
|
61
|
+
return {
|
|
62
|
+
token: auth.replace(/^Bearer\s+/i, ""),
|
|
63
|
+
baseUrl: url.replace(/\/mcp(\/bearer)?$/, ""),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function readJsonAt(path, keyPath, tool) {
|
|
68
|
+
if (!existsSync(path)) return null;
|
|
69
|
+
try {
|
|
70
|
+
const obj = parseJsonc(readFileSync(path, "utf-8"));
|
|
71
|
+
const entry = keyPath.reduce((o, k) => o?.[k], obj);
|
|
72
|
+
const b = extractBearer(entry);
|
|
73
|
+
return b ? { ...b, configFile: path, tool } : null;
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function readCodexTomlShape(path) {
|
|
80
|
+
if (!existsSync(path)) return null;
|
|
81
|
+
try {
|
|
82
|
+
const content = readFileSync(path, "utf-8");
|
|
83
|
+
const block = content.match(/\[mcp_servers\.patchcord[-\w]*\]([\s\S]*?)(?=\n\[|$)/);
|
|
84
|
+
if (!block) return null;
|
|
85
|
+
const urlMatch = block[1].match(/url\s*=\s*"([^"]+)"/);
|
|
86
|
+
const tokenMatch = block[1].match(/Bearer\s+([^\s"]+)/);
|
|
87
|
+
if (!urlMatch || !tokenMatch) return null;
|
|
88
|
+
return {
|
|
89
|
+
token: tokenMatch[1],
|
|
90
|
+
baseUrl: urlMatch[1].replace(/\/mcp(\/bearer)?$/, ""),
|
|
91
|
+
configFile: path,
|
|
92
|
+
tool: "codex",
|
|
93
|
+
};
|
|
94
|
+
} catch {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function readGrokTomlShape(path) {
|
|
100
|
+
if (!existsSync(path)) return null;
|
|
101
|
+
try {
|
|
102
|
+
const content = readFileSync(path, "utf-8");
|
|
103
|
+
const header = content.match(/^\[mcp_servers\.patchcord(?:[-\w]*)?\]\s*$/m);
|
|
104
|
+
if (!header) return null;
|
|
105
|
+
const rest = content.slice(header.index + header[0].length);
|
|
106
|
+
const nextSection = rest.search(/^\[(?!mcp_servers\.patchcord(?:\.|[-\w]*\]))/m);
|
|
107
|
+
const block = nextSection === -1 ? rest : rest.slice(0, nextSection);
|
|
108
|
+
const urlMatch = block.match(/^\s*url\s*=\s*["']([^"']+)["']/m);
|
|
109
|
+
const tokenMatch = block.match(/["']?Authorization["']?\s*=\s*["']Bearer\s+([^"']+)["']/i);
|
|
110
|
+
if (!urlMatch || !tokenMatch) return null;
|
|
111
|
+
return {
|
|
112
|
+
token: tokenMatch[1],
|
|
113
|
+
baseUrl: urlMatch[1].replace(/\/mcp(\/bearer)?$/, ""),
|
|
114
|
+
configFile: path,
|
|
115
|
+
tool: "grok",
|
|
116
|
+
};
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const claudeReader = (cwd) => readJsonAt(join(cwd, ".mcp.json"), ["mcpServers", "patchcord"], "claude_code");
|
|
123
|
+
const cursorReader = (cwd) => readJsonAt(join(cwd, ".cursor", "mcp.json"), ["mcpServers", "patchcord"], "cursor");
|
|
124
|
+
const vscodeReader = (cwd) => readJsonAt(join(cwd, ".vscode", "mcp.json"), ["servers", "patchcord"], "vscode");
|
|
125
|
+
const opencodeReader = (cwd) => readJsonAt(join(cwd, "opencode.json"), ["mcp", "patchcord"], "opencode");
|
|
126
|
+
const antigravityReader = (cwd) => readJsonAt(join(cwd, ".agents", "mcp_config.json"), ["mcpServers", "patchcord"], "antigravity");
|
|
127
|
+
const grokReader = (cwd) => readGrokTomlShape(join(cwd, ".grok", "config.toml"));
|
|
128
|
+
const codexReader = (cwd) => readCodexTomlShape(join(cwd, ".codex", "config.toml"));
|
|
129
|
+
const kimiReader = (cwd) => readJsonAt(join(cwd, ".kimi", "mcp.json"), ["mcpServers", "patchcord"], "kimi");
|
|
130
|
+
const kimiCodeReader = (cwd) => readJsonAt(join(cwd, ".kimi-code", "mcp.json"), ["mcpServers", "patchcord"], "kimi");
|
|
131
|
+
|
|
132
|
+
/** Detect which harness launched this process (env + explicit overrides). */
|
|
133
|
+
export function harnessContext(env = process.env, options = {}) {
|
|
134
|
+
const forceTool = (env.PATCHCORD_FORCE_TOOL || env.PATCHCORD_TOOL || "").toLowerCase();
|
|
135
|
+
const preferKimi =
|
|
136
|
+
options.preferKimi === true
|
|
137
|
+
|| forceTool === "kimi"
|
|
138
|
+
|| Boolean(env.KIMI_CLI)
|
|
139
|
+
|| Boolean(env.KIMI_SESSION_ID)
|
|
140
|
+
|| Boolean(env.KIMI_CONFIG_FILE)
|
|
141
|
+
|| Boolean(env.KIMI_MCP_CONFIG_FILE);
|
|
142
|
+
const preferCursor =
|
|
143
|
+
options.preferCursor === true
|
|
144
|
+
|| forceTool === "cursor"
|
|
145
|
+
|| Boolean(env.CURSOR_AGENT);
|
|
146
|
+
return { forceTool, preferKimi, preferCursor };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Ordered per-project readers for the active harness. */
|
|
150
|
+
export function projectReadersForContext(ctx) {
|
|
151
|
+
const defaultReaders = [
|
|
152
|
+
claudeReader,
|
|
153
|
+
cursorReader,
|
|
154
|
+
vscodeReader,
|
|
155
|
+
opencodeReader,
|
|
156
|
+
antigravityReader,
|
|
157
|
+
grokReader,
|
|
158
|
+
codexReader,
|
|
159
|
+
];
|
|
160
|
+
const kimiReaders = [kimiReader, kimiCodeReader];
|
|
161
|
+
|
|
162
|
+
if (ctx.preferKimi) {
|
|
163
|
+
return [...kimiReaders, ...defaultReaders];
|
|
164
|
+
}
|
|
165
|
+
if (ctx.preferCursor) {
|
|
166
|
+
// Cursor CLI/IDE: .cursor/mcp.json must win over stale .mcp.json in the
|
|
167
|
+
// same repo (e.g. leftover Claude Code token from an old provision).
|
|
168
|
+
return [
|
|
169
|
+
cursorReader,
|
|
170
|
+
vscodeReader,
|
|
171
|
+
opencodeReader,
|
|
172
|
+
antigravityReader,
|
|
173
|
+
grokReader,
|
|
174
|
+
codexReader,
|
|
175
|
+
claudeReader,
|
|
176
|
+
...kimiReaders,
|
|
177
|
+
];
|
|
178
|
+
}
|
|
179
|
+
return [...defaultReaders, ...kimiReaders];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Walk up from startDir; return first matching project bearer or null. */
|
|
183
|
+
export function resolveProjectBearer(startDir, options = {}) {
|
|
184
|
+
const ctx = harnessContext(process.env, options);
|
|
185
|
+
const readers = projectReadersForContext(ctx);
|
|
186
|
+
let dir = startDir;
|
|
187
|
+
while (dir && dir !== "/") {
|
|
188
|
+
for (const r of readers) {
|
|
189
|
+
const found = r(dir);
|
|
190
|
+
if (found) {
|
|
191
|
+
found.scope = "project";
|
|
192
|
+
return found;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const parent = dirname(dir);
|
|
196
|
+
if (parent === dir) break;
|
|
197
|
+
dir = parent;
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
package/scripts/subscribe.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import { request as httpRequest } from "node:http";
|
|
|
13
13
|
import { URL } from "node:url";
|
|
14
14
|
import { dirname } from "node:path";
|
|
15
15
|
import { connect as wsConnect } from "./lib/ws.mjs";
|
|
16
|
+
import { resolveProjectBearer } from "./lib/resolve-project-bearer.mjs";
|
|
16
17
|
|
|
17
18
|
// --- Hermes webhook bridge mode -------------------------------------------
|
|
18
19
|
// Default mode writes "PATCHCORD: ..." lines to stdout for Claude Code's
|
|
@@ -70,35 +71,15 @@ function die(msg, code = 1) {
|
|
|
70
71
|
|
|
71
72
|
function readMcpConfig(cwd) {
|
|
72
73
|
// Prefer env vars injected by patchcord.mjs, which already ran _resolveBearer()
|
|
73
|
-
// and supports all
|
|
74
|
+
// and supports all tool configs with harness-aware ordering.
|
|
74
75
|
if (process.env.PATCHCORD_BASE_URL && process.env.PATCHCORD_BEARER_TOKEN) {
|
|
75
76
|
return { baseUrl: process.env.PATCHCORD_BASE_URL, token: process.env.PATCHCORD_BEARER_TOKEN };
|
|
76
77
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const path = `${dir}/.mcp.json`;
|
|
81
|
-
if (existsSync(path)) {
|
|
82
|
-
let json;
|
|
83
|
-
try {
|
|
84
|
-
json = JSON.parse(readFileSync(path, "utf8"));
|
|
85
|
-
} catch (e) {
|
|
86
|
-
die(`.mcp.json parse error: ${e.message}`);
|
|
87
|
-
}
|
|
88
|
-
const pc = json?.mcpServers?.patchcord;
|
|
89
|
-
if (!pc?.url || !pc?.headers?.Authorization) {
|
|
90
|
-
die(".mcp.json missing mcpServers.patchcord.url or Authorization");
|
|
91
|
-
}
|
|
92
|
-
let baseUrl = pc.url.replace(/\/mcp\/bearer$/, "").replace(/\/mcp$/, "");
|
|
93
|
-
const auth = pc.headers.Authorization;
|
|
94
|
-
const token = auth.startsWith("Bearer ") ? auth.slice(7) : auth;
|
|
95
|
-
return { baseUrl, token };
|
|
96
|
-
}
|
|
97
|
-
const parent = dirname(dir);
|
|
98
|
-
if (parent === dir) break;
|
|
99
|
-
dir = parent;
|
|
78
|
+
const found = resolveProjectBearer(cwd);
|
|
79
|
+
if (!found) {
|
|
80
|
+
die(`no patchcord config found — run from a project directory or use 'patchcord subscribe'`);
|
|
100
81
|
}
|
|
101
|
-
|
|
82
|
+
return { baseUrl: found.baseUrl, token: found.token };
|
|
102
83
|
}
|
|
103
84
|
|
|
104
85
|
function httpJson(urlStr, { method = "GET", headers = {}, body = null } = {}) {
|