patchcord 0.3.44 → 0.3.47
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 +75 -50
- package/package.json +1 -1
package/bin/patchcord.mjs
CHANGED
|
@@ -20,6 +20,22 @@ function run(cmd) {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
function isSafeToken(t) {
|
|
24
|
+
return /^[A-Za-z0-9_\-=+/.]+$/.test(t) && t.length < 200;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isSafeUrl(u) {
|
|
28
|
+
try {
|
|
29
|
+
const parsed = new URL(u);
|
|
30
|
+
return parsed.protocol === "https:" || parsed.protocol === "http:";
|
|
31
|
+
} catch { return false; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function isSafeId(s) {
|
|
35
|
+
return /^[A-Za-z0-9_\-]+$/.test(s) && s.length < 100;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
23
39
|
if (cmd === "help" || cmd === "--help" || cmd === "-h") {
|
|
24
40
|
console.log(`patchcord — agent messaging for AI coding agents
|
|
25
41
|
|
|
@@ -43,6 +59,16 @@ if (!cmd || cmd === "install" || cmd === "agent") {
|
|
|
43
59
|
const fullStatusline = flags.includes("--full");
|
|
44
60
|
const { readFileSync, writeFileSync } = await import("fs");
|
|
45
61
|
|
|
62
|
+
function safeReadJson(filePath) {
|
|
63
|
+
try {
|
|
64
|
+
let content = readFileSync(filePath, "utf-8");
|
|
65
|
+
// Strip JSONC comments (Zed, Gemini use JSONC)
|
|
66
|
+
content = content.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
67
|
+
content = content.replace(/,\s*([}\]])/g, "$1");
|
|
68
|
+
return JSON.parse(content);
|
|
69
|
+
} catch { return null; }
|
|
70
|
+
}
|
|
71
|
+
|
|
46
72
|
console.log(`
|
|
47
73
|
___ ____ ___ ____ _ _ ____ ____ ____ ___
|
|
48
74
|
|__] |__| | | |__| | | | |__/ | \\
|
|
@@ -337,7 +363,7 @@ if (!cmd || cmd === "install" || cmd === "agent") {
|
|
|
337
363
|
const geminiPath = join(HOME, ".gemini", "settings.json");
|
|
338
364
|
if (existsSync(geminiPath)) {
|
|
339
365
|
try {
|
|
340
|
-
const existing =
|
|
366
|
+
const existing = safeReadJson(geminiPath) || {};
|
|
341
367
|
if (existing.mcpServers?.patchcord) {
|
|
342
368
|
console.log(`\n ${yellow}⚠ Gemini CLI already configured${r}`);
|
|
343
369
|
console.log(` ${dim}${geminiPath}${r}`);
|
|
@@ -373,7 +399,7 @@ if (!cmd || cmd === "install" || cmd === "agent") {
|
|
|
373
399
|
: join(HOME, ".config", "zed", "settings.json");
|
|
374
400
|
if (existsSync(zedPath)) {
|
|
375
401
|
try {
|
|
376
|
-
const existing =
|
|
402
|
+
const existing = safeReadJson(zedPath) || {};
|
|
377
403
|
if (existing.context_servers?.patchcord) {
|
|
378
404
|
console.log(`\n ${yellow}⚠ Zed already configured${r}`);
|
|
379
405
|
console.log(` ${dim}${zedPath}${r}`);
|
|
@@ -414,7 +440,7 @@ if (!cmd || cmd === "install" || cmd === "agent") {
|
|
|
414
440
|
console.log(` ${yellow}This overrides per-project config and causes conflicts.${r}`);
|
|
415
441
|
const cleanGlobal = (await ask(` ${dim}Remove patchcord from global config? (Y/n):${r} `)).trim().toLowerCase();
|
|
416
442
|
if (cleanGlobal !== "n" && cleanGlobal !== "no") {
|
|
417
|
-
const cleaned = globalContent.replace(/\[mcp_servers\.patchcord\][^\
|
|
443
|
+
const cleaned = globalContent.replace(/\[mcp_servers\.patchcord\]\n(?:(?!\[)[^\n]*\n?)*/g, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
418
444
|
writeFileSync(globalCodexConfig, cleaned + "\n");
|
|
419
445
|
console.log(` ${green}✓${r} Removed from global config`);
|
|
420
446
|
}
|
|
@@ -452,6 +478,12 @@ if (!cmd || cmd === "install" || cmd === "agent") {
|
|
|
452
478
|
process.exit(1);
|
|
453
479
|
}
|
|
454
480
|
|
|
481
|
+
if (!isSafeToken(token)) {
|
|
482
|
+
console.log(` ${red}✗${r} Invalid token format`);
|
|
483
|
+
rl.close();
|
|
484
|
+
process.exit(1);
|
|
485
|
+
}
|
|
486
|
+
|
|
455
487
|
console.log("Validating...");
|
|
456
488
|
const validateResp = run(`curl -sf --max-time 5 -H "Authorization: Bearer ${token}" "${serverUrl}/api/inbox?limit=0"`);
|
|
457
489
|
if (validateResp) {
|
|
@@ -474,19 +506,13 @@ if (!cmd || cmd === "install" || cmd === "agent") {
|
|
|
474
506
|
const customUrl = (await ask(`\n${dim}Custom server URL? (y/N):${r} `)).trim().toLowerCase();
|
|
475
507
|
if (customUrl === "y" || customUrl === "yes") {
|
|
476
508
|
const url = (await ask("Server URL: ")).trim();
|
|
477
|
-
if (url)
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
const resp2 = run(`curl -sf --max-time 5 -H "Authorization: Bearer ${token}" "${serverUrl}/api/inbox?limit=0"`);
|
|
483
|
-
if (resp2) {
|
|
484
|
-
try {
|
|
485
|
-
const data = JSON.parse(resp2);
|
|
486
|
-
identity = `${data.agent_id}@${data.namespace_id}`;
|
|
487
|
-
console.log(` ${green}✓${r} ${bold}${identity}${r}`);
|
|
488
|
-
} catch {}
|
|
509
|
+
if (url) {
|
|
510
|
+
if (!isSafeUrl(url)) {
|
|
511
|
+
console.error("Invalid URL. Must start with https:// or http://");
|
|
512
|
+
rl.close();
|
|
513
|
+
process.exit(1);
|
|
489
514
|
}
|
|
515
|
+
serverUrl = url;
|
|
490
516
|
}
|
|
491
517
|
}
|
|
492
518
|
|
|
@@ -505,7 +531,7 @@ if (!cmd || cmd === "install" || cmd === "agent") {
|
|
|
505
531
|
command: "npx",
|
|
506
532
|
args: [
|
|
507
533
|
"-y", "mcp-remote",
|
|
508
|
-
serverUrl
|
|
534
|
+
`${serverUrl}/mcp`,
|
|
509
535
|
"--header",
|
|
510
536
|
`Authorization: Bearer ${token}`,
|
|
511
537
|
"--header",
|
|
@@ -538,7 +564,7 @@ if (!cmd || cmd === "install" || cmd === "agent") {
|
|
|
538
564
|
command: "npx",
|
|
539
565
|
args: [
|
|
540
566
|
"-y", "mcp-remote",
|
|
541
|
-
serverUrl
|
|
567
|
+
`${serverUrl}/mcp`,
|
|
542
568
|
"--header",
|
|
543
569
|
`Authorization: Bearer ${token}`,
|
|
544
570
|
"--header",
|
|
@@ -562,23 +588,12 @@ if (!cmd || cmd === "install" || cmd === "agent") {
|
|
|
562
588
|
mkdirSync(join(HOME, ".codeium", "windsurf"), { recursive: true });
|
|
563
589
|
writeFileSync(wsPath, JSON.stringify(wsConfig, null, 2) + "\n");
|
|
564
590
|
}
|
|
565
|
-
// Install workflows as slash commands (.windsurf/workflows/) — per-project
|
|
566
|
-
const wsWorkflowDir = join(cwd, ".windsurf", "workflows");
|
|
567
|
-
mkdirSync(wsWorkflowDir, { recursive: true });
|
|
568
|
-
cpSync(join(pluginRoot, "skills", "inbox", "SKILL.md"), join(wsWorkflowDir, "patchcord.md"));
|
|
569
|
-
cpSync(join(pluginRoot, "skills", "wait", "SKILL.md"), join(wsWorkflowDir, "patchcord-wait.md"));
|
|
570
591
|
console.log(`\n ${green}✓${r} Windsurf configured: ${dim}${wsPath}${r}`);
|
|
571
|
-
console.log(` ${
|
|
572
|
-
console.log(` ${yellow}MCP config is global — all Windsurf projects share this agent.${r}`);
|
|
592
|
+
console.log(` ${yellow}Global config — all Windsurf projects share this agent.${r}`);
|
|
573
593
|
} else if (isGemini) {
|
|
574
594
|
// Gemini CLI: global only (~/.gemini/settings.json)
|
|
575
595
|
const geminiPath = join(HOME, ".gemini", "settings.json");
|
|
576
|
-
let geminiSettings = {};
|
|
577
|
-
if (existsSync(geminiPath)) {
|
|
578
|
-
try {
|
|
579
|
-
geminiSettings = JSON.parse(readFileSync(geminiPath, "utf-8"));
|
|
580
|
-
} catch {}
|
|
581
|
-
}
|
|
596
|
+
let geminiSettings = (existsSync(geminiPath) && safeReadJson(geminiPath)) || {};
|
|
582
597
|
if (!geminiSettings.mcpServers) geminiSettings.mcpServers = {};
|
|
583
598
|
geminiSettings.mcpServers.patchcord = {
|
|
584
599
|
httpUrl: `${serverUrl}/mcp`,
|
|
@@ -601,12 +616,7 @@ if (!cmd || cmd === "install" || cmd === "agent") {
|
|
|
601
616
|
const zedPath = process.platform === "darwin"
|
|
602
617
|
? join(HOME, "Library", "Application Support", "Zed", "settings.json")
|
|
603
618
|
: join(HOME, ".config", "zed", "settings.json");
|
|
604
|
-
let zedSettings = {};
|
|
605
|
-
if (existsSync(zedPath)) {
|
|
606
|
-
try {
|
|
607
|
-
zedSettings = JSON.parse(readFileSync(zedPath, "utf-8"));
|
|
608
|
-
} catch {}
|
|
609
|
-
}
|
|
619
|
+
let zedSettings = (existsSync(zedPath) && safeReadJson(zedPath)) || {};
|
|
610
620
|
if (!zedSettings.context_servers) zedSettings.context_servers = {};
|
|
611
621
|
zedSettings.context_servers.patchcord = {
|
|
612
622
|
url: `${serverUrl}/mcp`,
|
|
@@ -685,20 +695,19 @@ if (!cmd || cmd === "install" || cmd === "agent") {
|
|
|
685
695
|
const configPath = join(codexDir, "config.toml");
|
|
686
696
|
let existing = existsSync(configPath) ? readFileSync(configPath, "utf-8") : "";
|
|
687
697
|
// Remove old patchcord config block if present
|
|
688
|
-
existing = existing.replace(/\[mcp_servers\.patchcord\][^\
|
|
689
|
-
|
|
690
|
-
|
|
698
|
+
existing = existing.replace(/\[mcp_servers\.patchcord\]\n(?:(?!\[)[^\n]*\n?)*/g, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
699
|
+
existing = existing.trimEnd() + `\n\n[mcp_servers.patchcord]\nurl = "${serverUrl}/mcp/bearer"\nhttp_headers = { "Authorization" = "Bearer ${token}", "X-Patchcord-Machine" = "${hostname}" }\n`;
|
|
700
|
+
writeFileSync(configPath, existing);
|
|
701
|
+
// Clean up any PATCHCORD_TOKEN we previously wrote to .env
|
|
691
702
|
const envPath = join(cwd, ".env");
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
703
|
+
if (existsSync(envPath)) {
|
|
704
|
+
const envContent = readFileSync(envPath, "utf-8");
|
|
705
|
+
if (envContent.includes("PATCHCORD_TOKEN=")) {
|
|
706
|
+
const cleaned = envContent.replace(/^PATCHCORD_TOKEN=.*\n?/gm, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
707
|
+
writeFileSync(envPath, cleaned ? cleaned + "\n" : "");
|
|
708
|
+
console.log(` ${green}✓${r} Cleaned PATCHCORD_TOKEN from .env`);
|
|
709
|
+
}
|
|
698
710
|
}
|
|
699
|
-
writeFileSync(envPath, envContent);
|
|
700
|
-
existing = existing.trimEnd() + `\n\n[mcp_servers.patchcord]\nurl = "${serverUrl}/mcp/bearer"\nbearer_token_env_var = "${envName}"\n`;
|
|
701
|
-
writeFileSync(configPath, existing);
|
|
702
711
|
// Slash commands (.codex/prompts/)
|
|
703
712
|
const codexPromptsDir = join(codexDir, "prompts");
|
|
704
713
|
mkdirSync(codexPromptsDir, { recursive: true });
|
|
@@ -737,6 +746,22 @@ if (!cmd || cmd === "install" || cmd === "agent") {
|
|
|
737
746
|
console.log(`\n ${green}✓${r} Claude Code configured: ${dim}${mcpPath}${r}`);
|
|
738
747
|
}
|
|
739
748
|
|
|
749
|
+
// Warn about gitignore for per-project configs with tokens
|
|
750
|
+
if (!isWindsurf && !isGemini && !isZed) {
|
|
751
|
+
const gitignorePath = join(cwd, ".gitignore");
|
|
752
|
+
const configFile = isCodex ? ".codex/config.toml" : isCursor ? ".cursor/mcp.json" : isVSCode ? ".vscode/mcp.json" : isOpenCode ? "opencode.json" : ".mcp.json";
|
|
753
|
+
let needsWarning = true;
|
|
754
|
+
if (existsSync(gitignorePath)) {
|
|
755
|
+
const gi = readFileSync(gitignorePath, "utf-8");
|
|
756
|
+
if (gi.includes(configFile) || gi.includes(".mcp.json") || gi.includes(".codex/") || gi.includes(".cursor/")) {
|
|
757
|
+
needsWarning = false;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
if (needsWarning) {
|
|
761
|
+
console.log(`\n ${yellow}⚠ Add ${configFile} to .gitignore — it contains your token${r}`);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
740
765
|
const toolName = isOpenCode ? "OpenCode" : isZed ? "Zed" : isVSCode ? "VS Code" : isGemini ? "Gemini CLI" : isWindsurf ? "Windsurf" : isCursor ? "Cursor" : isCodex ? "Codex" : "Claude Code";
|
|
741
766
|
console.log(`\n${dim}Restart your ${toolName} session, then run:${r} ${bold}inbox()${r}`);
|
|
742
767
|
process.exit(0);
|
|
@@ -777,7 +802,7 @@ if (cmd === "skill") {
|
|
|
777
802
|
const baseUrl = mcpUrl.replace(/\/mcp(\/bearer)?$/, "");
|
|
778
803
|
const token = auth.replace(/^Bearer\s+/, "");
|
|
779
804
|
|
|
780
|
-
if (!baseUrl || !token) {
|
|
805
|
+
if (!baseUrl || !token || !isSafeToken(token)) {
|
|
781
806
|
console.error("Cannot read patchcord URL/token from .mcp.json");
|
|
782
807
|
process.exit(1);
|
|
783
808
|
}
|
|
@@ -793,7 +818,7 @@ if (cmd === "skill") {
|
|
|
793
818
|
}
|
|
794
819
|
} catch {}
|
|
795
820
|
|
|
796
|
-
if (!namespace || !agentId) {
|
|
821
|
+
if (!namespace || !agentId || !isSafeId(namespace) || !isSafeId(agentId)) {
|
|
797
822
|
console.error("Cannot determine agent identity. Check your token.");
|
|
798
823
|
process.exit(1);
|
|
799
824
|
}
|