patchcord 0.5.30 → 0.5.32

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 +103 -32
  2. package/package.json +1 -1
package/bin/patchcord.mjs CHANGED
@@ -67,7 +67,7 @@ Usage:
67
67
  npx patchcord@latest --token <token> Self-hosted / CI setup
68
68
  npx patchcord@latest --token <token> --server <url> Self-hosted with custom server
69
69
  npx patchcord@latest --full Same + full statusline
70
- npx patchcord@latest --rename <new-name> Rename this agent (paste from dashboard)
70
+ npx patchcord@latest --rename <new-name> [--tool <slug>] Rename this agent (paste from dashboard)
71
71
  npx patchcord@latest skill apply Fetch custom skill from web console`);
72
72
  process.exit(0);
73
73
  }
@@ -77,59 +77,130 @@ if (cmd === "plugin-path") {
77
77
  process.exit(0);
78
78
  }
79
79
 
80
- // ── --rename <new> [--expect-token <prefix>] ────────────────────
81
- // Renames the agent in cwd's .mcp.json. Dashboard generates the full
82
- // pasted command including --expect-token to prevent wrong-machine paste.
80
+ // ── --rename <new> [--tool <slug>] [--expect-token <prefix>] ────
81
+ // Renames the agent's bearer in its tool's per-project config. Supported
82
+ // tools: claude_code, codex, cursor, vscode, opencode. Dashboard generates
83
+ // the full pasted command including --tool (so we read the right file)
84
+ // and --expect-token (sha256 prefix, prevents wrong-machine paste).
83
85
  {
84
86
  const renameIdx = process.argv.indexOf("--rename");
85
87
  if (renameIdx !== -1) {
86
88
  const newName = (process.argv[renameIdx + 1] || "").trim().toLowerCase();
87
89
  const expectIdx = process.argv.indexOf("--expect-token");
88
90
  const expectPrefix = expectIdx !== -1 ? (process.argv[expectIdx + 1] || "").trim() : "";
91
+ const toolIdx = process.argv.indexOf("--tool");
92
+ const tool = toolIdx !== -1 ? (process.argv[toolIdx + 1] || "").trim().toLowerCase() : "";
89
93
 
90
94
  if (!newName || !/^[a-z][a-z0-9-]{1,49}$/.test(newName)) {
91
- console.error("Usage: npx patchcord@latest --rename <new-name> [--expect-token <prefix>]");
92
- console.error(" <new-name> must be lowercase letters, digits, dashes; 2-50 chars");
95
+ console.error("Usage: npx patchcord@latest --rename <new-name> [--tool <slug>] [--expect-token <prefix>]");
96
+ console.error(" <new-name>: lowercase letters, digits, dashes; 2-50 chars");
97
+ console.error(" <slug>: claude_code | codex | cursor | vscode | opencode");
93
98
  process.exit(1);
94
99
  }
95
100
 
96
- const cwd = process.cwd();
97
- let mcpJson = null;
98
- let dir = cwd;
99
- while (dir && dir !== "/") {
100
- const p = join(dir, ".mcp.json");
101
- if (existsSync(p)) { mcpJson = p; break; }
102
- dir = dirname(dir);
103
- }
104
- if (!mcpJson) {
105
- console.error(`No .mcp.json found above ${cwd}. Run from your patchcord project folder.`);
101
+ const { readFileSync } = await import("fs");
102
+
103
+ // Per-tool token readers. Each returns {token, baseUrl, configFile} or null.
104
+ const readMcpJsonShape = (path, key = "mcpServers") => {
105
+ if (!existsSync(path)) return null;
106
+ try {
107
+ const obj = JSON.parse(readFileSync(path, "utf-8"));
108
+ const pt = obj?.[key]?.patchcord;
109
+ if (!pt?.headers?.Authorization || !pt?.url) return null;
110
+ return {
111
+ token: pt.headers.Authorization.replace(/^Bearer\s+/i, ""),
112
+ baseUrl: pt.url.replace(/\/mcp(\/bearer)?$/, ""),
113
+ configFile: path,
114
+ };
115
+ } catch { return null; }
116
+ };
117
+ const readOpenCodeShape = (path) => {
118
+ if (!existsSync(path)) return null;
119
+ try {
120
+ const obj = JSON.parse(readFileSync(path, "utf-8"));
121
+ const pt = obj?.mcp?.patchcord;
122
+ if (!pt?.headers?.Authorization || !pt?.url) return null;
123
+ return {
124
+ token: pt.headers.Authorization.replace(/^Bearer\s+/i, ""),
125
+ baseUrl: pt.url.replace(/\/mcp(\/bearer)?$/, ""),
126
+ configFile: path,
127
+ };
128
+ } catch { return null; }
129
+ };
130
+ const readCodexTomlShape = (path) => {
131
+ if (!existsSync(path)) return null;
132
+ try {
133
+ const content = readFileSync(path, "utf-8");
134
+ const block = content.match(/\[mcp_servers\.patchcord[-\w]*\]([\s\S]*?)(?=\n\[|$)/);
135
+ if (!block) return null;
136
+ const urlMatch = block[1].match(/url\s*=\s*"([^"]+)"/);
137
+ const tokenMatch = block[1].match(/Bearer\s+([^\s"]+)/);
138
+ if (!urlMatch || !tokenMatch) return null;
139
+ return {
140
+ token: tokenMatch[1],
141
+ baseUrl: urlMatch[1].replace(/\/mcp(\/bearer)?$/, ""),
142
+ configFile: path,
143
+ };
144
+ } catch { return null; }
145
+ };
146
+
147
+ const READERS = {
148
+ claude_code: (cwd) => readMcpJsonShape(join(cwd, ".mcp.json")),
149
+ cursor: (cwd) => readMcpJsonShape(join(cwd, ".cursor", "mcp.json")),
150
+ vscode: (cwd) => readMcpJsonShape(join(cwd, ".vscode", "mcp.json"), "servers"),
151
+ opencode: (cwd) => readOpenCodeShape(join(cwd, "opencode.json")),
152
+ codex: (cwd) => readCodexTomlShape(join(cwd, ".codex", "config.toml")),
153
+ };
154
+
155
+ if (tool && !READERS[tool]) {
156
+ console.error(`✗ Rename not supported for --tool=${tool}.`);
157
+ console.error(` Supported: ${Object.keys(READERS).join(", ")}.`);
158
+ console.error(` This usually means the tool stores its config in a global location`);
159
+ console.error(` not yet wired up to --rename. Open an issue.`);
106
160
  process.exit(1);
107
161
  }
108
162
 
109
- const { readFileSync } = await import("fs");
110
- let config;
111
- try {
112
- config = JSON.parse(readFileSync(mcpJson, "utf-8"));
113
- } catch (e) {
114
- console.error(`Cannot parse ${mcpJson}: ${e.message}`);
163
+ // Walk up from cwd looking for any supported config. If --tool given,
164
+ // only check that tool; otherwise try all in priority order.
165
+ const cwd = process.cwd();
166
+ const tryReaders = tool ? [READERS[tool]] : Object.values(READERS);
167
+ let found = null;
168
+ let dir = cwd;
169
+ while (dir && dir !== "/" && !found) {
170
+ for (const r of tryReaders) {
171
+ const result = r(dir);
172
+ if (result) { found = result; break; }
173
+ }
174
+ if (!found) dir = dirname(dir);
175
+ }
176
+ if (!found) {
177
+ const whichFile = tool
178
+ ? `the ${tool} config file in or above ${cwd}`
179
+ : `any supported tool config (.mcp.json, .cursor/mcp.json, .vscode/mcp.json, opencode.json, .codex/config.toml) above ${cwd}`;
180
+ console.error(`No patchcord bearer found in ${whichFile}. Run from the agent's project folder.`);
115
181
  process.exit(1);
116
182
  }
117
- const mcpUrl = config?.mcpServers?.patchcord?.url || "";
118
- const auth = config?.mcpServers?.patchcord?.headers?.Authorization || "";
119
- const baseUrl = mcpUrl.replace(/\/mcp(\/bearer)?$/, "");
120
- const token = auth.replace(/^Bearer\s+/, "");
183
+ const { token, baseUrl, configFile: mcpJson } = found;
121
184
 
122
185
  if (!baseUrl || !token || !isSafeToken(token) || !isSafeUrl(baseUrl)) {
123
186
  console.error(`Cannot read patchcord URL/token from ${mcpJson}.`);
124
187
  process.exit(1);
125
188
  }
126
189
 
127
- if (expectPrefix && !token.startsWith(expectPrefix)) {
128
- console.error("✗ Token mismatch.");
129
- console.error(` The dashboard issued this rename for a different agent than what's`);
130
- console.error(` installed in ${dirname(mcpJson)}.`);
131
- console.error(` Maybe you clicked Rename while looking at a different machine?`);
132
- process.exit(2);
190
+ if (expectPrefix) {
191
+ // Compare sha256(token) prefix, not raw token prefix. The dashboard
192
+ // never sees the plaintext token (we don't store it server-side
193
+ // anymore) — it has token_hash from the bearer_tokens row, slices
194
+ // the first N chars of that, and we hash locally to compare.
195
+ const { createHash } = await import("node:crypto");
196
+ const tokenHash = createHash("sha256").update(token).digest("hex");
197
+ if (!tokenHash.startsWith(expectPrefix)) {
198
+ console.error("✗ Token mismatch.");
199
+ console.error(` The dashboard issued this rename for a different agent than what's`);
200
+ console.error(` installed in ${dirname(mcpJson)}.`);
201
+ console.error(` Maybe you clicked Rename while looking at a different machine?`);
202
+ process.exit(2);
203
+ }
133
204
  }
134
205
 
135
206
  // Call the server. Use curl + -w for status code so we can distinguish
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.5.30",
3
+ "version": "0.5.32",
4
4
  "description": "Cross-machine agent messaging for Claude Code and Codex",
5
5
  "author": "ppravdin",
6
6
  "license": "MIT",