patchcord 0.3.77 → 0.3.78

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,75 @@ 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
+
351
384
  const { createInterface: createRLU } = await import("readline");
352
385
  const rlU = createRLU({ input: process.stdin, output: process.stdout });
353
386
  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") {
387
+
388
+ const updateAnswer = (await askU(` ${bold}Update ${existingToolName} agent? (Y/n):${r} `)).trim().toLowerCase();
389
+ if (updateAnswer !== "n" && updateAnswer !== "no") {
357
390
  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) {
391
+ if (existingIdentity) {
392
+ identity = existingIdentity;
393
+ const vResp = validateResp ? JSON.parse(validateResp) : {};
394
+ clientType = vResp.client_type || "";
395
+ choice = CLIENT_TYPE_MAP[clientType] || "";
396
+ console.log(` ${green}✓${r} ${bold}${identity}${r} — token valid`);
397
+ } else {
370
398
  console.log(` ${yellow}⚠${r} Token expired or invalid. Starting fresh setup.`);
371
399
  token = "";
372
400
  }
401
+ rlU.close();
402
+ } else {
403
+ // Offer to add a different tool
404
+ const addAnswer = (await askU(` ${bold}Add another agent to this project? (y/N):${r} `)).trim().toLowerCase();
405
+ rlU.close();
406
+ if (addAnswer === "y" || addAnswer === "yes") {
407
+ // Use existing token but let user pick a different tool
408
+ token = existingToken;
409
+ if (existingIdentity) {
410
+ identity = existingIdentity;
411
+ console.log(` ${green}✓${r} ${bold}${identity}${r}`);
412
+ } else {
413
+ console.log(` ${yellow}⚠${r} Token expired or invalid. Starting fresh setup.`);
414
+ token = "";
415
+ }
416
+ // Force tool picker by clearing choice — will be asked below
417
+ choice = "";
418
+ }
373
419
  }
374
420
  }
375
421
 
@@ -519,10 +565,11 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd === "--token" || cmd ===
519
565
  console.log(` ${cyan}1.${r} Claude Code ${cyan}5.${r} Gemini CLI`);
520
566
  console.log(` ${cyan}2.${r} Codex CLI ${cyan}6.${r} VS Code`);
521
567
  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();
568
+ console.log(` ${cyan}4.${r} Windsurf ${cyan}8.${r} OpenCode`);
569
+ console.log(` ${cyan}9.${r} OpenClaw\n`);
570
+ choice = (await ask3(`${dim}Choose (1-9):${r} `)).trim();
524
571
  rl3.close();
525
- if (!["1","2","3","4","5","6","7","8"].includes(choice)) {
572
+ if (!["1","2","3","4","5","6","7","8","9"].includes(choice)) {
526
573
  console.error("Invalid choice.");
527
574
  process.exit(1);
528
575
  }
@@ -538,6 +585,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd === "--token" || cmd ===
538
585
  const isVSCode = choice === "6";
539
586
  const isZed = choice === "7";
540
587
  const isOpenCode = choice === "8";
588
+ const isOpenClaw = choice === "9";
541
589
 
542
590
  const hostname = run("hostname -s") || run("hostname") || "unknown";
543
591
 
@@ -665,6 +713,52 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd === "--token" || cmd ===
665
713
  };
666
714
  writeFileSync(ocPath, JSON.stringify(ocConfig, null, 2) + "\n");
667
715
  console.log(`\n ${green}✓${r} OpenCode configured: ${dim}${ocPath}${r}`);
716
+ } else if (isOpenClaw) {
717
+ // OpenClaw: global ~/.openclaw/openclaw.json → mcp.servers
718
+ // Try CLI first, fall back to direct file write
719
+ const openclawServerEntry = JSON.stringify({
720
+ url: `${serverUrl}/mcp`,
721
+ transport: "streamable-http",
722
+ headers: {
723
+ Authorization: `Bearer ${token}`,
724
+ "X-Patchcord-Machine": hostname,
725
+ },
726
+ connectionTimeoutMs: 300000,
727
+ });
728
+ const cliResult = run(`openclaw mcp set patchcord '${openclawServerEntry.replace(/'/g, "'\\''")}'`);
729
+ if (cliResult !== null) {
730
+ console.log(`\n ${green}✓${r} OpenClaw configured via CLI: ${dim}openclaw mcp set${r}`);
731
+ } else {
732
+ // CLI not available — write config directly
733
+ const openclawDir = join(HOME, ".openclaw");
734
+ const openclawPath = join(openclawDir, "openclaw.json");
735
+ let openclawConfig = {};
736
+ if (existsSync(openclawPath)) {
737
+ try {
738
+ openclawConfig = JSON.parse(readFileSync(openclawPath, "utf-8"));
739
+ } catch {}
740
+ }
741
+ if (!openclawConfig.mcp) openclawConfig.mcp = {};
742
+ if (!openclawConfig.mcp.servers) openclawConfig.mcp.servers = {};
743
+ openclawConfig.mcp.servers.patchcord = {
744
+ url: `${serverUrl}/mcp`,
745
+ transport: "streamable-http",
746
+ headers: {
747
+ Authorization: `Bearer ${token}`,
748
+ "X-Patchcord-Machine": hostname,
749
+ },
750
+ connectionTimeoutMs: 300000,
751
+ };
752
+ mkdirSync(openclawDir, { recursive: true });
753
+ writeFileSync(openclawPath, JSON.stringify(openclawConfig, null, 2) + "\n");
754
+ console.log(`\n ${green}✓${r} OpenClaw configured: ${dim}${openclawPath}${r}`);
755
+ }
756
+ console.log(` ${yellow}Global config — all OpenClaw channels share this agent.${r}`);
757
+ console.log(` ${dim}Run: openclaw gateway restart${r}`);
758
+ // mcp-remote fallback note for older OpenClaw versions
759
+ console.log(`\n ${dim}If tools don't appear after restart, your OpenClaw may be too old${r}`);
760
+ console.log(` ${dim}for streamable-http. Update to v2026.3.31+ or use mcp-remote:${r}`);
761
+ console.log(` ${dim}openclaw mcp set patchcord '{"command":"npx","args":["mcp-remote","${serverUrl}/mcp","--header","Authorization: Bearer ${token}"],"transport":"stdio"}'${r}`);
668
762
  } else if (isVSCode) {
669
763
  // VS Code: write .vscode/mcp.json (per-project)
670
764
  const vscodeDir = join(cwd, ".vscode");
@@ -840,7 +934,7 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd === "--token" || cmd ===
840
934
  }
841
935
 
842
936
  // Warn about gitignore for per-project configs with tokens
843
- if (!isWindsurf && !isGemini && !isZed) {
937
+ if (!isWindsurf && !isGemini && !isZed && !isOpenClaw) {
844
938
  const gitignorePath = join(cwd, ".gitignore");
845
939
  const configFile = isCodex ? ".codex/config.toml" : isCursor ? ".cursor/mcp.json" : isVSCode ? ".vscode/mcp.json" : isOpenCode ? "opencode.json" : ".mcp.json";
846
940
  let needsWarning = true;
@@ -855,14 +949,18 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd === "--token" || cmd ===
855
949
  }
856
950
  }
857
951
 
858
- const toolName = isOpenCode ? "OpenCode" : isZed ? "Zed" : isVSCode ? "VS Code" : isGemini ? "Gemini CLI" : isWindsurf ? "Windsurf" : isCursor ? "Cursor" : isCodex ? "Codex" : "Claude Code";
952
+ const toolName = isOpenClaw ? "OpenClaw" : isOpenCode ? "OpenCode" : isZed ? "Zed" : isVSCode ? "VS Code" : isGemini ? "Gemini CLI" : isWindsurf ? "Windsurf" : isCursor ? "Cursor" : isCodex ? "Codex" : "Claude Code";
859
953
 
860
- if (!isWindsurf && !isGemini && !isZed) {
954
+ if (!isWindsurf && !isGemini && !isZed && !isOpenClaw) {
861
955
  console.log(`\n ${dim}To connect a second agent:${r}`);
862
956
  console.log(` ${dim}cd into another project and run${r} ${bold}npx patchcord@latest${r} ${dim}there.${r}`);
863
957
  }
864
958
 
865
- console.log(`\n${dim}Restart your ${toolName} session, then say:${r} ${bold}check inbox${r}`);
959
+ if (isOpenClaw) {
960
+ console.log(`\n${dim}Run${r} ${bold}openclaw gateway restart${r}${dim}, then tools will be available in your channels.${r}`);
961
+ } else {
962
+ console.log(`\n${dim}Restart your ${toolName} session, then say:${r} ${bold}check inbox${r}`);
963
+ }
866
964
  process.exit(0);
867
965
  }
868
966
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.3.77",
3
+ "version": "0.3.78",
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
  ```