patchcord 0.3.77 → 0.3.79

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
@@ -39,7 +39,7 @@ const PROJECT_MARKERS = [
39
39
  ".git", "package.json", "package-lock.json", "Cargo.toml", "go.mod", "go.sum",
40
40
  "pyproject.toml", "pom.xml", "build.gradle", "Makefile", "CMakeLists.txt",
41
41
  "Gemfile", "composer.json", "mix.exs", "requirements.txt", "setup.py",
42
- ".claude", ".codex", ".cursor", ".vscode",
42
+ ".claude", ".codex", ".cursor", ".vscode", ".openclaw",
43
43
  ];
44
44
 
45
45
  function detectFolder(dir) {
@@ -258,7 +258,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd === "--token" || cmd ===
258
258
 
259
259
  const CLIENT_TYPE_MAP = {
260
260
  "claude_code": "1", "codex": "2", "cursor": "3", "windsurf": "4",
261
- "gemini": "5", "vscode": "6", "zed": "7", "opencode": "8",
261
+ "gemini": "5", "vscode": "6", "zed": "7", "opencode": "8", "openclaw": "9",
262
262
  };
263
263
 
264
264
 
@@ -292,9 +292,10 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd === "--token" || cmd ===
292
292
  console.log(` ${cyan}1.${r} Claude Code ${cyan}5.${r} Gemini CLI`);
293
293
  console.log(` ${cyan}2.${r} Codex CLI ${cyan}6.${r} VS Code`);
294
294
  console.log(` ${cyan}3.${r} Cursor ${cyan}7.${r} Zed`);
295
- console.log(` ${cyan}4.${r} Windsurf ${cyan}8.${r} OpenCode\n`);
296
- choice = (await ask(`${dim}Choose (1-8):${r} `)).trim();
297
- if (!["1","2","3","4","5","6","7","8"].includes(choice)) {
295
+ console.log(` ${cyan}4.${r} Windsurf ${cyan}8.${r} OpenCode`);
296
+ console.log(` ${cyan}9.${r} OpenClaw\n`);
297
+ choice = (await ask(`${dim}Choose (1-9):${r} `)).trim();
298
+ if (!["1","2","3","4","5","6","7","8","9"].includes(choice)) {
298
299
  console.error("Invalid choice.");
299
300
  rl.close();
300
301
  process.exit(1);
@@ -346,30 +347,92 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd === "--token" || cmd ===
346
347
  }
347
348
  } catch {}
348
349
  }
350
+ if (!existingToken) {
351
+ const openclawJsonPath = join(HOME, ".openclaw", "openclaw.json");
352
+ if (existsSync(openclawJsonPath)) {
353
+ try {
354
+ const oc = JSON.parse(readFileSync(openclawJsonPath, "utf-8"));
355
+ const pt = oc?.mcp?.servers?.patchcord;
356
+ if (pt?.headers?.Authorization) {
357
+ existingToken = pt.headers.Authorization.replace(/^Bearer\s+/i, "");
358
+ existingConfigFile = openclawJsonPath;
359
+ }
360
+ } catch {}
361
+ }
362
+ }
349
363
  if (existingToken) {
350
- console.log(`\n ${dim}Existing patchcord token found in ${existingConfigFile}${r}`);
364
+ // Figure out which tool is already configured
365
+ const existingToolName = existingConfigFile.includes(".codex") ? "Codex"
366
+ : existingConfigFile.includes("openclaw") ? "OpenClaw"
367
+ : existingConfigFile.includes(".cursor") ? "Cursor"
368
+ : existingConfigFile.includes(".vscode") ? "VS Code"
369
+ : "Claude Code";
370
+
371
+ // Validate the existing token to get identity
372
+ let existingIdentity = "";
373
+ const validateResp = run(`curl -sf --max-time 5 -H "Authorization: Bearer ${existingToken}" "${serverUrl}/api/inbox?limit=0&count_only=1"`);
374
+ if (validateResp) {
375
+ try {
376
+ const data = JSON.parse(validateResp);
377
+ existingIdentity = `${data.agent_id}@${data.namespace_id}`;
378
+ } catch {}
379
+ }
380
+
381
+ const identityStr = existingIdentity ? ` (${bold}${existingIdentity}${r}${dim})` : "";
382
+ console.log(`\n ${dim}${existingToolName} agent is already configured in this project${identityStr}${r}`);
383
+
384
+ if (rl) rl.close();
351
385
  const { createInterface: createRLU } = await import("readline");
352
386
  const rlU = createRLU({ input: process.stdin, output: process.stdout });
353
387
  const askU = (q) => new Promise((resolve) => rlU.question(q, resolve));
354
- const answer = (await askU(` ${bold}Update config and keep existing token? (Y/n):${r} `)).trim().toLowerCase();
355
- rlU.close();
356
- if (answer !== "n" && answer !== "no") {
388
+
389
+ const updateAnswer = (await askU(` ${bold}Update ${existingToolName} agent? (Y/n):${r} `)).trim().toLowerCase();
390
+ if (updateAnswer !== "n" && updateAnswer !== "no") {
357
391
  token = existingToken;
358
- // Validate existing token
359
- const validateResp = run(`curl -sf --max-time 5 -H "Authorization: Bearer ${token}" "${serverUrl}/api/inbox?limit=0&count_only=1"`);
360
- if (validateResp) {
361
- try {
362
- const data = JSON.parse(validateResp);
363
- identity = `${data.agent_id}@${data.namespace_id}`;
364
- clientType = data.client_type || "";
365
- choice = CLIENT_TYPE_MAP[clientType] || "";
366
- console.log(` ${green}✓${r} ${bold}${identity}${r} — token valid`);
367
- } catch {}
368
- }
369
- if (!identity) {
392
+ if (existingIdentity) {
393
+ identity = existingIdentity;
394
+ const vResp = validateResp ? JSON.parse(validateResp) : {};
395
+ clientType = vResp.client_type || "";
396
+ choice = CLIENT_TYPE_MAP[clientType] || "";
397
+ console.log(` ${green}✓${r} ${bold}${identity}${r} — token valid`);
398
+ } else {
370
399
  console.log(` ${yellow}⚠${r} Token expired or invalid. Starting fresh setup.`);
371
400
  token = "";
372
401
  }
402
+ rlU.close();
403
+ } else {
404
+ // Offer to add a different tool
405
+ const addAnswer = (await askU(` ${bold}Add another agent to this project? (y/N):${r} `)).trim().toLowerCase();
406
+ rlU.close();
407
+ if (addAnswer === "y" || addAnswer === "yes") {
408
+ // Use existing token but let user pick a different tool
409
+ token = existingToken;
410
+ if (existingIdentity) {
411
+ identity = existingIdentity;
412
+ console.log(` ${green}✓${r} ${bold}${identity}${r}`);
413
+ } else {
414
+ console.log(` ${yellow}⚠${r} Token expired or invalid. Starting fresh setup.`);
415
+ token = "";
416
+ }
417
+ // Show tool picker
418
+ if (token) {
419
+ const { createInterface: createRL4 } = await import("readline");
420
+ const rl4 = createRL4({ input: process.stdin, output: process.stdout });
421
+ const ask4 = (q) => new Promise((resolve) => rl4.question(q, resolve));
422
+ console.log(`\n${bold}Which agent are you adding?${r}\n`);
423
+ console.log(` ${cyan}1.${r} Claude Code ${cyan}5.${r} Gemini CLI`);
424
+ console.log(` ${cyan}2.${r} Codex CLI ${cyan}6.${r} VS Code`);
425
+ console.log(` ${cyan}3.${r} Cursor ${cyan}7.${r} Zed`);
426
+ console.log(` ${cyan}4.${r} Windsurf ${cyan}8.${r} OpenCode`);
427
+ console.log(` ${cyan}9.${r} OpenClaw\n`);
428
+ choice = (await ask4(`${dim}Choose (1-9):${r} `)).trim();
429
+ rl4.close();
430
+ if (!["1","2","3","4","5","6","7","8","9"].includes(choice)) {
431
+ console.error("Invalid choice.");
432
+ process.exit(1);
433
+ }
434
+ }
435
+ }
373
436
  }
374
437
  }
375
438
 
@@ -519,10 +582,11 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd === "--token" || cmd ===
519
582
  console.log(` ${cyan}1.${r} Claude Code ${cyan}5.${r} Gemini CLI`);
520
583
  console.log(` ${cyan}2.${r} Codex CLI ${cyan}6.${r} VS Code`);
521
584
  console.log(` ${cyan}3.${r} Cursor ${cyan}7.${r} Zed`);
522
- console.log(` ${cyan}4.${r} Windsurf ${cyan}8.${r} OpenCode\n`);
523
- choice = (await ask3(`${dim}Choose (1-8):${r} `)).trim();
585
+ console.log(` ${cyan}4.${r} Windsurf ${cyan}8.${r} OpenCode`);
586
+ console.log(` ${cyan}9.${r} OpenClaw\n`);
587
+ choice = (await ask3(`${dim}Choose (1-9):${r} `)).trim();
524
588
  rl3.close();
525
- if (!["1","2","3","4","5","6","7","8"].includes(choice)) {
589
+ if (!["1","2","3","4","5","6","7","8","9"].includes(choice)) {
526
590
  console.error("Invalid choice.");
527
591
  process.exit(1);
528
592
  }
@@ -538,6 +602,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd === "--token" || cmd ===
538
602
  const isVSCode = choice === "6";
539
603
  const isZed = choice === "7";
540
604
  const isOpenCode = choice === "8";
605
+ const isOpenClaw = choice === "9";
541
606
 
542
607
  const hostname = run("hostname -s") || run("hostname") || "unknown";
543
608
 
@@ -665,6 +730,52 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd === "--token" || cmd ===
665
730
  };
666
731
  writeFileSync(ocPath, JSON.stringify(ocConfig, null, 2) + "\n");
667
732
  console.log(`\n ${green}✓${r} OpenCode configured: ${dim}${ocPath}${r}`);
733
+ } else if (isOpenClaw) {
734
+ // OpenClaw: global ~/.openclaw/openclaw.json → mcp.servers
735
+ // Try CLI first, fall back to direct file write
736
+ const openclawServerEntry = JSON.stringify({
737
+ url: `${serverUrl}/mcp`,
738
+ transport: "streamable-http",
739
+ headers: {
740
+ Authorization: `Bearer ${token}`,
741
+ "X-Patchcord-Machine": hostname,
742
+ },
743
+ connectionTimeoutMs: 300000,
744
+ });
745
+ const cliResult = run(`openclaw mcp set patchcord '${openclawServerEntry.replace(/'/g, "'\\''")}'`);
746
+ if (cliResult !== null) {
747
+ console.log(`\n ${green}✓${r} OpenClaw configured via CLI: ${dim}openclaw mcp set${r}`);
748
+ } else {
749
+ // CLI not available — write config directly
750
+ const openclawDir = join(HOME, ".openclaw");
751
+ const openclawPath = join(openclawDir, "openclaw.json");
752
+ let openclawConfig = {};
753
+ if (existsSync(openclawPath)) {
754
+ try {
755
+ openclawConfig = JSON.parse(readFileSync(openclawPath, "utf-8"));
756
+ } catch {}
757
+ }
758
+ if (!openclawConfig.mcp) openclawConfig.mcp = {};
759
+ if (!openclawConfig.mcp.servers) openclawConfig.mcp.servers = {};
760
+ openclawConfig.mcp.servers.patchcord = {
761
+ url: `${serverUrl}/mcp`,
762
+ transport: "streamable-http",
763
+ headers: {
764
+ Authorization: `Bearer ${token}`,
765
+ "X-Patchcord-Machine": hostname,
766
+ },
767
+ connectionTimeoutMs: 300000,
768
+ };
769
+ mkdirSync(openclawDir, { recursive: true });
770
+ writeFileSync(openclawPath, JSON.stringify(openclawConfig, null, 2) + "\n");
771
+ console.log(`\n ${green}✓${r} OpenClaw configured: ${dim}${openclawPath}${r}`);
772
+ }
773
+ console.log(` ${yellow}Global config — all OpenClaw channels share this agent.${r}`);
774
+ console.log(` ${dim}Run: openclaw gateway restart${r}`);
775
+ // mcp-remote fallback note for older OpenClaw versions
776
+ console.log(`\n ${dim}If tools don't appear after restart, your OpenClaw may be too old${r}`);
777
+ console.log(` ${dim}for streamable-http. Update to v2026.3.31+ or use mcp-remote:${r}`);
778
+ console.log(` ${dim}openclaw mcp set patchcord '{"command":"npx","args":["mcp-remote","${serverUrl}/mcp","--header","Authorization: Bearer ${token}"],"transport":"stdio"}'${r}`);
668
779
  } else if (isVSCode) {
669
780
  // VS Code: write .vscode/mcp.json (per-project)
670
781
  const vscodeDir = join(cwd, ".vscode");
@@ -840,7 +951,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd === "--token" || cmd ===
840
951
  }
841
952
 
842
953
  // Warn about gitignore for per-project configs with tokens
843
- if (!isWindsurf && !isGemini && !isZed) {
954
+ if (!isWindsurf && !isGemini && !isZed && !isOpenClaw) {
844
955
  const gitignorePath = join(cwd, ".gitignore");
845
956
  const configFile = isCodex ? ".codex/config.toml" : isCursor ? ".cursor/mcp.json" : isVSCode ? ".vscode/mcp.json" : isOpenCode ? "opencode.json" : ".mcp.json";
846
957
  let needsWarning = true;
@@ -855,14 +966,18 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd === "--token" || cmd ===
855
966
  }
856
967
  }
857
968
 
858
- const toolName = isOpenCode ? "OpenCode" : isZed ? "Zed" : isVSCode ? "VS Code" : isGemini ? "Gemini CLI" : isWindsurf ? "Windsurf" : isCursor ? "Cursor" : isCodex ? "Codex" : "Claude Code";
969
+ const toolName = isOpenClaw ? "OpenClaw" : isOpenCode ? "OpenCode" : isZed ? "Zed" : isVSCode ? "VS Code" : isGemini ? "Gemini CLI" : isWindsurf ? "Windsurf" : isCursor ? "Cursor" : isCodex ? "Codex" : "Claude Code";
859
970
 
860
- if (!isWindsurf && !isGemini && !isZed) {
971
+ if (!isWindsurf && !isGemini && !isZed && !isOpenClaw) {
861
972
  console.log(`\n ${dim}To connect a second agent:${r}`);
862
973
  console.log(` ${dim}cd into another project and run${r} ${bold}npx patchcord@latest${r} ${dim}there.${r}`);
863
974
  }
864
975
 
865
- console.log(`\n${dim}Restart your ${toolName} session, then say:${r} ${bold}check inbox${r}`);
976
+ if (isOpenClaw) {
977
+ console.log(`\n${dim}Run${r} ${bold}openclaw gateway restart${r}${dim}, then tools will be available in your channels.${r}`);
978
+ } else {
979
+ console.log(`\n${dim}Restart your ${toolName} session, then say:${r} ${bold}check inbox${r}`);
980
+ }
866
981
  process.exit(0);
867
982
  }
868
983
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.3.77",
3
+ "version": "0.3.79",
4
4
  "description": "Cross-machine agent messaging for Claude Code and Codex",
5
5
  "author": "ppravdin",
6
6
  "license": "MIT",
@@ -2,14 +2,13 @@
2
2
  set -euo pipefail
3
3
 
4
4
  find_patchcord_mcp_json() {
5
+ # Only check CWD itself — don't walk up.
6
+ # Walking up leaks agent identity from parent dirs into unrelated projects.
5
7
  local dir="${1:-$PWD}"
6
- while [ -n "$dir" ] && [ "$dir" != "/" ]; do
7
- if [ -f "$dir/.mcp.json" ]; then
8
- printf '%s\n' "$dir/.mcp.json"
9
- return 0
10
- fi
11
- dir=$(dirname "$dir")
12
- done
8
+ if [ -f "$dir/.mcp.json" ]; then
9
+ printf '%s\n' "$dir/.mcp.json"
10
+ return 0
11
+ fi
13
12
  return 1
14
13
  }
15
14
 
@@ -13,14 +13,13 @@ for arg in "$@"; do
13
13
  done
14
14
 
15
15
  find_patchcord_mcp_json() {
16
+ # Only check CWD itself — don't walk up.
17
+ # Walking up leaks agent identity from parent dirs into unrelated projects.
16
18
  local dir="$1"
17
- while [ -n "$dir" ] && [ "$dir" != "/" ]; do
18
- if [ -f "$dir/.mcp.json" ]; then
19
- printf '%s\n' "$dir/.mcp.json"
20
- return 0
21
- fi
22
- dir=$(dirname "$dir")
23
- done
19
+ if [ -f "$dir/.mcp.json" ]; then
20
+ printf '%s\n' "$dir/.mcp.json"
21
+ return 0
22
+ fi
24
23
  return 1
25
24
  }
26
25
 
@@ -87,16 +87,18 @@ attachment(relay=true, path_or_url="https://example.com/file.md", filename="file
87
87
  ```
88
88
  Server fetches the URL and stores it. ~50 tokens instead of thousands for the file content.
89
89
 
90
- **Presigned upload (for local files):**
90
+ **Presigned upload (preferred for local files):**
91
91
  ```
92
- attachment(upload=true, filename="report.md") -> returns presigned URL
92
+ attachment(upload=true, filename="report.md") -> returns {url, path}
93
+ curl -X PUT -H "Content-Type: text/markdown" --data-binary @/path/to/report.md "<url>"
93
94
  ```
94
- PUT the file to the returned URL.
95
+ Then send the `path` to the other agent. No base64, no token waste.
95
96
 
96
- **Inline base64 upload (for generated content):**
97
+ **Inline base64 (last resort — small generated content only):**
97
98
  ```
98
- attachment(upload=true, filename="report.md", file_data="<base64>")
99
+ attachment(upload=true, filename="notes.txt", file_data="<base64>")
99
100
  ```
101
+ Never use for files on disk — use presigned upload above instead.
100
102
 
101
103
  **Downloading:**
102
104
  ```
@@ -75,17 +75,18 @@ attachment(relay=true, path_or_url="https://example.com/file.md", filename="file
75
75
  ```
76
76
  Server fetches the URL and stores it. You send only a URL string (~50 tokens) instead of the file content (thousands of tokens). Always prefer relay when the file is at a public URL.
77
77
 
78
- **Presigned upload (for local files):**
78
+ **Presigned upload (preferred for local files):**
79
79
  ```
80
- attachment(upload=true, filename="report.md") -> returns presigned URL
80
+ attachment(upload=true, filename="report.md") -> returns {url, path}
81
+ curl -X PUT -H "Content-Type: text/markdown" --data-binary @/path/to/report.md "<url>"
81
82
  ```
82
- PUT the file to the returned URL. Best for files already on disk.
83
+ Then send the `path` to the other agent. No base64, no token waste.
83
84
 
84
- **Inline base64 upload (for generated content):**
85
+ **Inline base64 (last resort — small generated content only):**
85
86
  ```
86
- attachment(upload=true, filename="report.md", file_data="<base64>")
87
+ attachment(upload=true, filename="notes.txt", file_data="<base64>")
87
88
  ```
88
- Upload directly with content embedded. Base64 adds ~33% overhead - keep files reasonable.
89
+ Base64 adds ~33% overhead and wastes context tokens. Never use this for files on disk — use presigned upload above instead.
89
90
 
90
91
  **Downloading:**
91
92
  ```