patchcord 0.5.60 → 0.5.62
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 +11 -1
- package/package.json +1 -1
- package/scripts/subscribe.mjs +66 -16
package/bin/patchcord.mjs
CHANGED
|
@@ -457,16 +457,26 @@ if (cmd === "upload") {
|
|
|
457
457
|
// of a wall of bash. subscribe.mjs handles its own pidfile guard
|
|
458
458
|
// (exits with code 2 + "already running" if another listener is up),
|
|
459
459
|
// so this command needs zero pre-checks.
|
|
460
|
+
//
|
|
461
|
+
// We resolve the bearer here (all 11 tool configs) and inject it as
|
|
462
|
+
// env vars so subscribe.mjs works regardless of which MCP client is
|
|
463
|
+
// running — OpenCode, Codex, Cursor, etc. — not just Claude Code.
|
|
460
464
|
if (cmd === "subscribe") {
|
|
461
465
|
const subscribeScript = join(pluginRoot, "scripts", "subscribe.mjs");
|
|
462
466
|
if (!existsSync(subscribeScript)) {
|
|
463
467
|
console.error(`subscribe.mjs not found at ${subscribeScript}`);
|
|
464
468
|
process.exit(1);
|
|
465
469
|
}
|
|
470
|
+
const bearerInfo = await _resolveBearer();
|
|
471
|
+
const spawnEnv = { ...process.env };
|
|
472
|
+
if (bearerInfo) {
|
|
473
|
+
spawnEnv.PATCHCORD_BASE_URL = bearerInfo.baseUrl;
|
|
474
|
+
spawnEnv.PATCHCORD_BEARER_TOKEN = bearerInfo.token;
|
|
475
|
+
}
|
|
466
476
|
const { spawnSync } = await import("child_process");
|
|
467
477
|
const result = spawnSync(process.execPath, [subscribeScript], {
|
|
468
478
|
stdio: "inherit",
|
|
469
|
-
env:
|
|
479
|
+
env: spawnEnv,
|
|
470
480
|
});
|
|
471
481
|
process.exit(result.status ?? (result.signal ? 1 : 0));
|
|
472
482
|
}
|
package/package.json
CHANGED
package/scripts/subscribe.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { readFileSync, writeFileSync, unlinkSync, existsSync } from "node:fs";
|
|
|
11
11
|
import { request as httpsRequest } from "node:https";
|
|
12
12
|
import { request as httpRequest } from "node:http";
|
|
13
13
|
import { URL } from "node:url";
|
|
14
|
+
import { dirname } from "node:path";
|
|
14
15
|
import { connect as wsConnect } from "./lib/ws.mjs";
|
|
15
16
|
|
|
16
17
|
const JWT_REFRESH_SAFETY_MARGIN_SEC = 120;
|
|
@@ -53,24 +54,36 @@ function die(msg, code = 1) {
|
|
|
53
54
|
}
|
|
54
55
|
|
|
55
56
|
function readMcpConfig(cwd) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
json = JSON.parse(readFileSync(path, "utf8"));
|
|
61
|
-
} catch (e) {
|
|
62
|
-
die(`.mcp.json parse error: ${e.message}`);
|
|
57
|
+
// Prefer env vars injected by patchcord.mjs, which already ran _resolveBearer()
|
|
58
|
+
// and supports all 11 tool configs (Claude Code, OpenCode, Codex, Cursor, etc.).
|
|
59
|
+
if (process.env.PATCHCORD_BASE_URL && process.env.PATCHCORD_BEARER_TOKEN) {
|
|
60
|
+
return { baseUrl: process.env.PATCHCORD_BASE_URL, token: process.env.PATCHCORD_BEARER_TOKEN };
|
|
63
61
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
62
|
+
// Fallback: direct invocation — try .mcp.json walking up from cwd.
|
|
63
|
+
let dir = cwd;
|
|
64
|
+
while (dir && dir !== "/") {
|
|
65
|
+
const path = `${dir}/.mcp.json`;
|
|
66
|
+
if (existsSync(path)) {
|
|
67
|
+
let json;
|
|
68
|
+
try {
|
|
69
|
+
json = JSON.parse(readFileSync(path, "utf8"));
|
|
70
|
+
} catch (e) {
|
|
71
|
+
die(`.mcp.json parse error: ${e.message}`);
|
|
72
|
+
}
|
|
73
|
+
const pc = json?.mcpServers?.patchcord;
|
|
74
|
+
if (!pc?.url || !pc?.headers?.Authorization) {
|
|
75
|
+
die(".mcp.json missing mcpServers.patchcord.url or Authorization");
|
|
76
|
+
}
|
|
77
|
+
let baseUrl = pc.url.replace(/\/mcp\/bearer$/, "").replace(/\/mcp$/, "");
|
|
78
|
+
const auth = pc.headers.Authorization;
|
|
79
|
+
const token = auth.startsWith("Bearer ") ? auth.slice(7) : auth;
|
|
80
|
+
return { baseUrl, token };
|
|
81
|
+
}
|
|
82
|
+
const parent = dirname(dir);
|
|
83
|
+
if (parent === dir) break;
|
|
84
|
+
dir = parent;
|
|
67
85
|
}
|
|
68
|
-
|
|
69
|
-
// Strip known MCP path suffixes to get the API base
|
|
70
|
-
baseUrl = baseUrl.replace(/\/mcp\/bearer$/, "").replace(/\/mcp$/, "");
|
|
71
|
-
const auth = pc.headers.Authorization;
|
|
72
|
-
const token = auth.startsWith("Bearer ") ? auth.slice(7) : auth;
|
|
73
|
-
return { baseUrl, token };
|
|
86
|
+
die(`no patchcord config found — run from a project directory or use 'patchcord subscribe'`);
|
|
74
87
|
}
|
|
75
88
|
|
|
76
89
|
function httpJson(urlStr, { method = "GET", headers = {}, body = null } = {}) {
|
|
@@ -393,6 +406,43 @@ function runOnce(ticket, baseUrl, token, refreshTicket) {
|
|
|
393
406
|
} catch {
|
|
394
407
|
return;
|
|
395
408
|
}
|
|
409
|
+
|
|
410
|
+
// Surface channel-state failures so silent server-side channel
|
|
411
|
+
// termination becomes visible. Previously we discarded every
|
|
412
|
+
// non-postgres_changes frame, which masked phx_join rejections,
|
|
413
|
+
// channel_error system messages, and post-refresh access_token
|
|
414
|
+
// rejections — exactly the cases where the WS stays "open" but
|
|
415
|
+
// realtime delivery is dead. Force a reconnect on any of those.
|
|
416
|
+
if (frame.event === "phx_reply") {
|
|
417
|
+
const status = frame.payload?.status;
|
|
418
|
+
if (status === "error") {
|
|
419
|
+
const reason = frame.payload?.response?.reason || frame.payload?.response || "unknown";
|
|
420
|
+
logErr(`subscribe: phx_reply error on topic=${frame.topic} ref=${frame.ref}: ${JSON.stringify(reason)}`);
|
|
421
|
+
done(new Error(`phx_reply error: ${JSON.stringify(reason)}`));
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (frame.event === "system") {
|
|
427
|
+
const status = frame.payload?.status;
|
|
428
|
+
const message = frame.payload?.message || frame.payload?.extension || "";
|
|
429
|
+
if (status === "error" || status === "channel_error") {
|
|
430
|
+
logErr(`subscribe: system error on topic=${frame.topic}: ${message}`);
|
|
431
|
+
done(new Error(`system error: ${message}`));
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
// Successful subscribe ack — log once for confirmation, then quiet.
|
|
435
|
+
if (status === "ok" && message) {
|
|
436
|
+
logErr(`subscribe: system ok on ${frame.topic}: ${message}`);
|
|
437
|
+
}
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (frame.event === "phx_error" || frame.event === "phx_close") {
|
|
441
|
+
logErr(`subscribe: ${frame.event} on topic=${frame.topic}: ${JSON.stringify(frame.payload || {})}`);
|
|
442
|
+
done(new Error(`${frame.event} on ${frame.topic}`));
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
396
446
|
if (frame.event !== "postgres_changes") return;
|
|
397
447
|
const data = frame.payload?.data;
|
|
398
448
|
if (!data || data.type !== "INSERT") return;
|