patchcord 0.3.46 → 0.3.48

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 +38 -50
  2. package/package.json +1 -1
package/bin/patchcord.mjs CHANGED
@@ -24,6 +24,18 @@ function isSafeToken(t) {
24
24
  return /^[A-Za-z0-9_\-=+/.]+$/.test(t) && t.length < 200;
25
25
  }
26
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
+
27
39
  if (cmd === "help" || cmd === "--help" || cmd === "-h") {
28
40
  console.log(`patchcord — agent messaging for AI coding agents
29
41
 
@@ -47,6 +59,16 @@ if (!cmd || cmd === "install" || cmd === "agent") {
47
59
  const fullStatusline = flags.includes("--full");
48
60
  const { readFileSync, writeFileSync } = await import("fs");
49
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
+
50
72
  console.log(`
51
73
  ___ ____ ___ ____ _ _ ____ ____ ____ ___
52
74
  |__] |__| | | |__| | | | |__/ | \\
@@ -236,38 +258,7 @@ if (!cmd || cmd === "install" || cmd === "agent") {
236
258
  const toolLabel = isZed ? "Zed" : isWindsurf ? "Windsurf" : "Gemini CLI";
237
259
  console.log(`\n ${yellow}Note: ${toolLabel} uses global config — applies to all projects.${r}`);
238
260
  } else {
239
- // Detect if this looks like a project folder
240
- const isHome = cwd === HOME || cwd === HOME + "/";
241
- const hasGit = existsSync(join(cwd, ".git"));
242
- const hasProjectFile = [
243
- "package.json", "Cargo.toml", "go.mod", "pyproject.toml", "pom.xml",
244
- "build.gradle", "Makefile", "CMakeLists.txt", ".sln", "Gemfile",
245
- "composer.json", "mix.exs", "Pipfile", "requirements.txt", "setup.py",
246
- ].some(f => existsSync(join(cwd, f)));
247
- const isRoot = cwd === "/" || cwd === "C:\\" || cwd === "C:/";
248
- const isTmp = cwd.startsWith("/tmp") || cwd.includes("/temp");
249
-
250
- if (isHome || isRoot) {
251
- console.log(`\n ${red}⚠ This is your home directory, not a project folder!${r}`);
252
- console.log(` ${yellow}The config will only work for the project folder where it's created.${r}`);
253
- console.log(` ${yellow}cd into your project first, then run npx patchcord@latest again.${r}\n`);
254
- const force = (await ask(` ${dim}Set up here anyway? (y/N):${r} `)).trim().toLowerCase();
255
- if (force !== "y" && force !== "yes") {
256
- rl.close();
257
- process.exit(0);
258
- }
259
- } else if (!hasGit && !hasProjectFile && !isTmp) {
260
- console.log(`\n ${yellow}⚠ This doesn't look like a project folder${r} ${dim}(no .git or project files)${r}`);
261
- console.log(` ${dim}${cwd}${r}`);
262
- console.log(` ${dim}Make sure you're in the right folder — the agent only works here.${r}`);
263
- const proceed = (await ask(` ${dim}Continue? (y/N):${r} `)).trim().toLowerCase();
264
- if (proceed !== "y" && proceed !== "yes") {
265
- rl.close();
266
- process.exit(0);
267
- }
268
- } else {
269
- console.log(`\n${dim}Project:${r} ${bold}${cwd}${r}`);
270
- }
261
+ console.log(`\n${dim}Project:${r} ${bold}${cwd}${r}`);
271
262
  }
272
263
 
273
264
 
@@ -341,7 +332,7 @@ if (!cmd || cmd === "install" || cmd === "agent") {
341
332
  const geminiPath = join(HOME, ".gemini", "settings.json");
342
333
  if (existsSync(geminiPath)) {
343
334
  try {
344
- const existing = JSON.parse(readFileSync(geminiPath, "utf-8"));
335
+ const existing = safeReadJson(geminiPath) || {};
345
336
  if (existing.mcpServers?.patchcord) {
346
337
  console.log(`\n ${yellow}⚠ Gemini CLI already configured${r}`);
347
338
  console.log(` ${dim}${geminiPath}${r}`);
@@ -377,7 +368,7 @@ if (!cmd || cmd === "install" || cmd === "agent") {
377
368
  : join(HOME, ".config", "zed", "settings.json");
378
369
  if (existsSync(zedPath)) {
379
370
  try {
380
- const existing = JSON.parse(readFileSync(zedPath, "utf-8"));
371
+ const existing = safeReadJson(zedPath) || {};
381
372
  if (existing.context_servers?.patchcord) {
382
373
  console.log(`\n ${yellow}⚠ Zed already configured${r}`);
383
374
  console.log(` ${dim}${zedPath}${r}`);
@@ -418,7 +409,7 @@ if (!cmd || cmd === "install" || cmd === "agent") {
418
409
  console.log(` ${yellow}This overrides per-project config and causes conflicts.${r}`);
419
410
  const cleanGlobal = (await ask(` ${dim}Remove patchcord from global config? (Y/n):${r} `)).trim().toLowerCase();
420
411
  if (cleanGlobal !== "n" && cleanGlobal !== "no") {
421
- const cleaned = globalContent.replace(/\[mcp_servers\.patchcord\][^\[]*/s, "").replace(/\n{3,}/g, "\n\n").trim();
412
+ const cleaned = globalContent.replace(/\[mcp_servers\.patchcord\]\n(?:(?!\[)[^\n]*\n?)*/g, "").replace(/\n{3,}/g, "\n\n").trim();
422
413
  writeFileSync(globalCodexConfig, cleaned + "\n");
423
414
  console.log(` ${green}✓${r} Removed from global config`);
424
415
  }
@@ -484,7 +475,14 @@ if (!cmd || cmd === "install" || cmd === "agent") {
484
475
  const customUrl = (await ask(`\n${dim}Custom server URL? (y/N):${r} `)).trim().toLowerCase();
485
476
  if (customUrl === "y" || customUrl === "yes") {
486
477
  const url = (await ask("Server URL: ")).trim();
487
- if (url) serverUrl = url;
478
+ if (url) {
479
+ if (!isSafeUrl(url)) {
480
+ console.error("Invalid URL. Must start with https:// or http://");
481
+ rl.close();
482
+ process.exit(1);
483
+ }
484
+ serverUrl = url;
485
+ }
488
486
  }
489
487
 
490
488
  rl.close();
@@ -564,12 +562,7 @@ if (!cmd || cmd === "install" || cmd === "agent") {
564
562
  } else if (isGemini) {
565
563
  // Gemini CLI: global only (~/.gemini/settings.json)
566
564
  const geminiPath = join(HOME, ".gemini", "settings.json");
567
- let geminiSettings = {};
568
- if (existsSync(geminiPath)) {
569
- try {
570
- geminiSettings = JSON.parse(readFileSync(geminiPath, "utf-8"));
571
- } catch {}
572
- }
565
+ let geminiSettings = (existsSync(geminiPath) && safeReadJson(geminiPath)) || {};
573
566
  if (!geminiSettings.mcpServers) geminiSettings.mcpServers = {};
574
567
  geminiSettings.mcpServers.patchcord = {
575
568
  httpUrl: `${serverUrl}/mcp`,
@@ -592,12 +585,7 @@ if (!cmd || cmd === "install" || cmd === "agent") {
592
585
  const zedPath = process.platform === "darwin"
593
586
  ? join(HOME, "Library", "Application Support", "Zed", "settings.json")
594
587
  : join(HOME, ".config", "zed", "settings.json");
595
- let zedSettings = {};
596
- if (existsSync(zedPath)) {
597
- try {
598
- zedSettings = JSON.parse(readFileSync(zedPath, "utf-8"));
599
- } catch {}
600
- }
588
+ let zedSettings = (existsSync(zedPath) && safeReadJson(zedPath)) || {};
601
589
  if (!zedSettings.context_servers) zedSettings.context_servers = {};
602
590
  zedSettings.context_servers.patchcord = {
603
591
  url: `${serverUrl}/mcp`,
@@ -676,7 +664,7 @@ if (!cmd || cmd === "install" || cmd === "agent") {
676
664
  const configPath = join(codexDir, "config.toml");
677
665
  let existing = existsSync(configPath) ? readFileSync(configPath, "utf-8") : "";
678
666
  // Remove old patchcord config block if present
679
- existing = existing.replace(/\[mcp_servers\.patchcord\][^\[]*/s, "").replace(/\n{3,}/g, "\n\n").trim();
667
+ existing = existing.replace(/\[mcp_servers\.patchcord\]\n(?:(?!\[)[^\n]*\n?)*/g, "").replace(/\n{3,}/g, "\n\n").trim();
680
668
  existing = existing.trimEnd() + `\n\n[mcp_servers.patchcord]\nurl = "${serverUrl}/mcp/bearer"\nhttp_headers = { "Authorization" = "Bearer ${token}", "X-Patchcord-Machine" = "${hostname}" }\n`;
681
669
  writeFileSync(configPath, existing);
682
670
  // Clean up any PATCHCORD_TOKEN we previously wrote to .env
@@ -799,7 +787,7 @@ if (cmd === "skill") {
799
787
  }
800
788
  } catch {}
801
789
 
802
- if (!namespace || !agentId) {
790
+ if (!namespace || !agentId || !isSafeId(namespace) || !isSafeId(agentId)) {
803
791
  console.error("Cannot determine agent identity. Check your token.");
804
792
  process.exit(1);
805
793
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.3.46",
3
+ "version": "0.3.48",
4
4
  "description": "Cross-machine agent messaging for Claude Code and Codex",
5
5
  "author": "ppravdin",
6
6
  "license": "MIT",