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.
Files changed (2) hide show
  1. package/bin/patchcord.mjs +75 -50
  2. 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 = JSON.parse(readFileSync(geminiPath, "utf-8"));
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 = JSON.parse(readFileSync(zedPath, "utf-8"));
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\][^\[]*/s, "").replace(/\n{3,}/g, "\n\n").trim();
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) serverUrl = url;
478
-
479
- // Re-validate against custom server if identity wasn't found
480
- if (!identity) {
481
- console.log("Validating token...");
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(` ${green}✓${r} Workflows installed: ${dim}/patchcord${r}, ${dim}/patchcord-wait${r}`);
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\][^\[]*/s, "").replace(/\n{3,}/g, "\n\n").trim();
689
- // Codex requires bearer_token via env var http_headers not supported for auth
690
- const envName = "PATCHCORD_TOKEN";
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
- let envContent = existsSync(envPath) ? readFileSync(envPath, "utf-8") : "";
693
- // Replace or add token in .env
694
- if (envContent.includes(envName)) {
695
- envContent = envContent.replace(new RegExp(`${envName}=.*`), `${envName}=${token}`);
696
- } else {
697
- envContent = envContent.trimEnd() + `\n${envName}=${token}\n`;
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.3.44",
3
+ "version": "0.3.47",
4
4
  "description": "Cross-machine agent messaging for Claude Code and Codex",
5
5
  "author": "ppravdin",
6
6
  "license": "MIT",