patchcord 0.5.54 → 0.5.56
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 +345 -11
- package/package.json +1 -1
- package/skills/inbox/SKILL.md +23 -0
package/bin/patchcord.mjs
CHANGED
|
@@ -74,6 +74,12 @@ Usage:
|
|
|
74
74
|
patchcord --token <token> [--server <url>] Headless / self-hosted setup
|
|
75
75
|
patchcord --full Same + full statusline
|
|
76
76
|
patchcord --rename <new-name> [--agent-type <type>] Rename this agent
|
|
77
|
+
patchcord whoami Show your identity + project
|
|
78
|
+
patchcord whoami --propose "<text>" Propose an update to your whoami
|
|
79
|
+
patchcord whoami --approve <id> Approve a pending proposal
|
|
80
|
+
patchcord whoami --reject <id> Reject a pending proposal
|
|
81
|
+
patchcord agents List every agent (with whoami)
|
|
82
|
+
patchcord agents <name> Show one agent's whoami
|
|
77
83
|
patchcord subscribe Start the realtime listener
|
|
78
84
|
patchcord update Update to the latest version
|
|
79
85
|
patchcord --version Show installed version
|
|
@@ -88,6 +94,312 @@ if (cmd === "plugin-path") {
|
|
|
88
94
|
process.exit(0);
|
|
89
95
|
}
|
|
90
96
|
|
|
97
|
+
// Shared bearer/base-url resolver. Project-local configs win over globals.
|
|
98
|
+
// Supports all 11 installer targets: claude_code, codex, cursor, vscode,
|
|
99
|
+
// opencode (per-project) + windsurf, gemini, zed, openclaw, antigravity,
|
|
100
|
+
// cline (global). Each tool stores the bearer in its own shape.
|
|
101
|
+
async function _resolveBearer() {
|
|
102
|
+
const { readFileSync } = await import("fs");
|
|
103
|
+
|
|
104
|
+
// Strict JSON first; fall back to JSONC stripping for zed/gemini-style
|
|
105
|
+
// settings (which allow // /* */ trailing commas). The fallback strips
|
|
106
|
+
// ONLY outside string literals so URLs like "https://..." survive.
|
|
107
|
+
const _stripJsoncOutsideStrings = (raw) => {
|
|
108
|
+
let out = "";
|
|
109
|
+
let i = 0;
|
|
110
|
+
let inStr = false;
|
|
111
|
+
let strCh = "";
|
|
112
|
+
let prev = "";
|
|
113
|
+
while (i < raw.length) {
|
|
114
|
+
const c = raw[i];
|
|
115
|
+
if (inStr) {
|
|
116
|
+
out += c;
|
|
117
|
+
if (c === strCh && prev !== "\\") inStr = false;
|
|
118
|
+
prev = c;
|
|
119
|
+
i++;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (c === '"' || c === "'") {
|
|
123
|
+
inStr = true;
|
|
124
|
+
strCh = c;
|
|
125
|
+
out += c;
|
|
126
|
+
prev = c;
|
|
127
|
+
i++;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (c === "/" && raw[i + 1] === "/") {
|
|
131
|
+
while (i < raw.length && raw[i] !== "\n") i++;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (c === "/" && raw[i + 1] === "*") {
|
|
135
|
+
i += 2;
|
|
136
|
+
while (i < raw.length && !(raw[i] === "*" && raw[i + 1] === "/")) i++;
|
|
137
|
+
i += 2;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
out += c;
|
|
141
|
+
prev = c;
|
|
142
|
+
i++;
|
|
143
|
+
}
|
|
144
|
+
return out.replace(/,(\s*[}\]])/g, "$1");
|
|
145
|
+
};
|
|
146
|
+
const _parseJsonc = (raw) => {
|
|
147
|
+
try { return JSON.parse(raw); } catch {}
|
|
148
|
+
return JSON.parse(_stripJsoncOutsideStrings(raw));
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const _extractBearer = (entry) => {
|
|
152
|
+
if (!entry) return null;
|
|
153
|
+
const auth = entry?.headers?.Authorization;
|
|
154
|
+
// Each tool has its own url key; check all known variants.
|
|
155
|
+
const url = entry.url || entry.httpUrl || entry.serverUrl;
|
|
156
|
+
if (!auth || !url) return null;
|
|
157
|
+
return {
|
|
158
|
+
token: auth.replace(/^Bearer\s+/i, ""),
|
|
159
|
+
baseUrl: url.replace(/\/mcp(\/bearer)?$/, ""),
|
|
160
|
+
};
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// Shape readers: each returns {token, baseUrl, configFile, tool} or null.
|
|
164
|
+
const readJsonAt = (path, keyPath, tool) => {
|
|
165
|
+
if (!existsSync(path)) return null;
|
|
166
|
+
try {
|
|
167
|
+
const obj = _parseJsonc(readFileSync(path, "utf-8"));
|
|
168
|
+
const entry = keyPath.reduce((o, k) => o?.[k], obj);
|
|
169
|
+
const b = _extractBearer(entry);
|
|
170
|
+
return b ? { ...b, configFile: path, tool } : null;
|
|
171
|
+
} catch { return null; }
|
|
172
|
+
};
|
|
173
|
+
const readCodexTomlShape = (path) => {
|
|
174
|
+
if (!existsSync(path)) return null;
|
|
175
|
+
try {
|
|
176
|
+
const content = readFileSync(path, "utf-8");
|
|
177
|
+
const block = content.match(/\[mcp_servers\.patchcord[-\w]*\]([\s\S]*?)(?=\n\[|$)/);
|
|
178
|
+
if (!block) return null;
|
|
179
|
+
const urlMatch = block[1].match(/url\s*=\s*"([^"]+)"/);
|
|
180
|
+
const tokenMatch = block[1].match(/Bearer\s+([^\s"]+)/);
|
|
181
|
+
if (!urlMatch || !tokenMatch) return null;
|
|
182
|
+
return {
|
|
183
|
+
token: tokenMatch[1],
|
|
184
|
+
baseUrl: urlMatch[1].replace(/\/mcp(\/bearer)?$/, ""),
|
|
185
|
+
configFile: path,
|
|
186
|
+
tool: "codex",
|
|
187
|
+
};
|
|
188
|
+
} catch { return null; }
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Per-project (walk up from cwd). First win.
|
|
192
|
+
const projectReaders = [
|
|
193
|
+
(cwd) => readJsonAt(join(cwd, ".mcp.json"), ["mcpServers", "patchcord"], "claude_code"),
|
|
194
|
+
(cwd) => readJsonAt(join(cwd, ".cursor", "mcp.json"), ["mcpServers", "patchcord"], "cursor"),
|
|
195
|
+
(cwd) => readJsonAt(join(cwd, ".vscode", "mcp.json"), ["servers", "patchcord"], "vscode"),
|
|
196
|
+
(cwd) => readJsonAt(join(cwd, "opencode.json"), ["mcp", "patchcord"], "opencode"),
|
|
197
|
+
(cwd) => readCodexTomlShape(join(cwd, ".codex", "config.toml")),
|
|
198
|
+
];
|
|
199
|
+
let dir = process.cwd();
|
|
200
|
+
while (dir && dir !== "/") {
|
|
201
|
+
for (const r of projectReaders) {
|
|
202
|
+
const found = r(dir);
|
|
203
|
+
if (found) return found;
|
|
204
|
+
}
|
|
205
|
+
dir = dirname(dir);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Global fallbacks. Order by likelihood for current install base.
|
|
209
|
+
const zedPath = process.platform === "darwin"
|
|
210
|
+
? join(HOME, "Library", "Application Support", "Zed", "settings.json")
|
|
211
|
+
: join(HOME, ".config", "zed", "settings.json");
|
|
212
|
+
|
|
213
|
+
// Cline lives in VS Code globalStorage — try stable / insiders / cursor.
|
|
214
|
+
const clinePaths = (() => {
|
|
215
|
+
const variants = process.platform === "darwin"
|
|
216
|
+
? [
|
|
217
|
+
join(HOME, "Library", "Application Support", "Code", "User", "globalStorage"),
|
|
218
|
+
join(HOME, "Library", "Application Support", "Code - Insiders", "User", "globalStorage"),
|
|
219
|
+
join(HOME, "Library", "Application Support", "Cursor", "User", "globalStorage"),
|
|
220
|
+
]
|
|
221
|
+
: process.platform === "win32"
|
|
222
|
+
? [
|
|
223
|
+
join(process.env.APPDATA || join(HOME, "AppData", "Roaming"), "Code", "User", "globalStorage"),
|
|
224
|
+
join(process.env.APPDATA || join(HOME, "AppData", "Roaming"), "Code - Insiders", "User", "globalStorage"),
|
|
225
|
+
join(process.env.APPDATA || join(HOME, "AppData", "Roaming"), "Cursor", "User", "globalStorage"),
|
|
226
|
+
]
|
|
227
|
+
: [
|
|
228
|
+
join(HOME, ".config", "Code", "User", "globalStorage"),
|
|
229
|
+
join(HOME, ".config", "Code - Insiders", "User", "globalStorage"),
|
|
230
|
+
join(HOME, ".config", "Cursor", "User", "globalStorage"),
|
|
231
|
+
];
|
|
232
|
+
return variants.map((b) =>
|
|
233
|
+
join(b, "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json")
|
|
234
|
+
);
|
|
235
|
+
})();
|
|
236
|
+
|
|
237
|
+
const globalCandidates = [
|
|
238
|
+
() => readJsonAt(join(HOME, ".codeium", "windsurf", "mcp_config.json"), ["mcpServers", "patchcord"], "windsurf"),
|
|
239
|
+
() => readJsonAt(join(HOME, ".gemini", "settings.json"), ["mcpServers", "patchcord"], "gemini"),
|
|
240
|
+
() => readJsonAt(zedPath, ["context_servers", "patchcord"], "zed"),
|
|
241
|
+
() => readJsonAt(join(HOME, ".openclaw", "openclaw.json"), ["mcp", "servers", "patchcord"], "openclaw"),
|
|
242
|
+
() => readJsonAt(join(HOME, ".gemini", "antigravity", "mcp_config.json"), ["mcpServers", "patchcord"], "antigravity"),
|
|
243
|
+
...clinePaths.map((p) => () => readJsonAt(p, ["mcpServers", "patchcord"], "cline")),
|
|
244
|
+
];
|
|
245
|
+
for (const r of globalCandidates) {
|
|
246
|
+
const found = r();
|
|
247
|
+
if (found) return found;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function _httpJSON(method, url, token, body) {
|
|
254
|
+
const tmpResp = `/tmp/patchcord-${process.pid}-${Date.now()}.json`;
|
|
255
|
+
const dataArg = body ? `-d '${JSON.stringify(body).replace(/'/g, "'\\''")}' ` : "";
|
|
256
|
+
const headers = `-H "Authorization: Bearer ${token}" -H "Content-Type: application/json" `;
|
|
257
|
+
const statusCode = run(
|
|
258
|
+
`curl -s -o "${tmpResp}" -w "%{http_code}" -X ${method} ${headers}${dataArg}"${url}"`
|
|
259
|
+
);
|
|
260
|
+
const { readFileSync } = await import("fs");
|
|
261
|
+
let respBody = "";
|
|
262
|
+
try { respBody = readFileSync(tmpResp, "utf-8"); } catch {}
|
|
263
|
+
try { (await import("fs")).unlinkSync(tmpResp); } catch {}
|
|
264
|
+
let json = null;
|
|
265
|
+
try { json = JSON.parse(respBody); } catch {}
|
|
266
|
+
return { status: statusCode, body: respBody, json };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ── whoami ────────────────────────────────────────────────────
|
|
270
|
+
if (cmd === "whoami") {
|
|
271
|
+
const args = process.argv.slice(3);
|
|
272
|
+
const found = await _resolveBearer();
|
|
273
|
+
if (!found) {
|
|
274
|
+
console.error("No patchcord config found in current directory or any parent. Run `npx patchcord@latest` from a project directory first.");
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
const { token, baseUrl } = found;
|
|
278
|
+
if (!isSafeToken(token) || !isSafeUrl(baseUrl)) {
|
|
279
|
+
console.error(`Invalid patchcord URL or token in config.`);
|
|
280
|
+
process.exit(1);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// --propose "<text>"
|
|
284
|
+
const proposeIdx = args.indexOf("--propose");
|
|
285
|
+
if (proposeIdx !== -1) {
|
|
286
|
+
const text = (args[proposeIdx + 1] || "").trim();
|
|
287
|
+
if (!text) {
|
|
288
|
+
console.error("Usage: patchcord whoami --propose \"<text>\"");
|
|
289
|
+
process.exit(1);
|
|
290
|
+
}
|
|
291
|
+
if (text.length > 300) {
|
|
292
|
+
console.error(`Whoami text exceeds 300 characters (got ${text.length}).`);
|
|
293
|
+
process.exit(1);
|
|
294
|
+
}
|
|
295
|
+
const { status, json, body } = await _httpJSON("POST", `${baseUrl}/api/agent/whoami/propose`, token, { text });
|
|
296
|
+
if (status !== "200") {
|
|
297
|
+
console.error(`✗ HTTP ${status}: ${(json && json.error) || body}`);
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
console.log(`proposal_id: ${json.proposal_id}`);
|
|
301
|
+
console.log(`status: ${json.status}`);
|
|
302
|
+
console.log(`awaiting human approval. another agent or the dashboard must approve.`);
|
|
303
|
+
process.exit(0);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// --approve <id> / --reject <id>
|
|
307
|
+
const approveIdx = args.indexOf("--approve");
|
|
308
|
+
const rejectIdx = args.indexOf("--reject");
|
|
309
|
+
if (approveIdx !== -1 || rejectIdx !== -1) {
|
|
310
|
+
const isApprove = approveIdx !== -1;
|
|
311
|
+
const idx = isApprove ? approveIdx : rejectIdx;
|
|
312
|
+
const id = (args[idx + 1] || "").trim();
|
|
313
|
+
if (!id) {
|
|
314
|
+
console.error(`Usage: patchcord whoami --${isApprove ? "approve" : "reject"} <proposal-id>`);
|
|
315
|
+
process.exit(1);
|
|
316
|
+
}
|
|
317
|
+
const { status, json, body } = await _httpJSON(
|
|
318
|
+
"POST",
|
|
319
|
+
`${baseUrl}/api/agent/whoami/approve`,
|
|
320
|
+
token,
|
|
321
|
+
{ proposal_id: id, action: isApprove ? "approve" : "reject" },
|
|
322
|
+
);
|
|
323
|
+
if (status !== "200") {
|
|
324
|
+
console.error(`✗ HTTP ${status}: ${(json && json.error) || body}`);
|
|
325
|
+
process.exit(1);
|
|
326
|
+
}
|
|
327
|
+
console.log(`✓ ${json.status}: ${json.agent} (proposal ${json.proposal_id})`);
|
|
328
|
+
process.exit(0);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// plain whoami (no flags)
|
|
332
|
+
const { status, json, body } = await _httpJSON("GET", `${baseUrl}/api/agent/whoami`, token);
|
|
333
|
+
if (status !== "200") {
|
|
334
|
+
console.error(`✗ HTTP ${status}: ${(json && json.error) || body}`);
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
console.log(`agent: ${json.agent}`);
|
|
338
|
+
console.log(`namespace: ${json.namespace}`);
|
|
339
|
+
if (json.project_summary) console.log(`project: ${json.project_summary}`);
|
|
340
|
+
if (json.whoami) {
|
|
341
|
+
console.log(`self: ${json.whoami}`);
|
|
342
|
+
} else {
|
|
343
|
+
console.log(`self: (empty — propose with: patchcord whoami --propose "<text>")`);
|
|
344
|
+
}
|
|
345
|
+
console.log();
|
|
346
|
+
console.log(`tip: \`patchcord agents\` returns whoami for all peers.`);
|
|
347
|
+
process.exit(0);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ── agents [name] ────────────────────────────────────────────
|
|
351
|
+
if (cmd === "agents") {
|
|
352
|
+
const name = (process.argv[3] || "").trim().toLowerCase();
|
|
353
|
+
const found = await _resolveBearer();
|
|
354
|
+
if (!found) {
|
|
355
|
+
console.error("No patchcord config found in current directory or any parent. Run `npx patchcord@latest` from a project directory first.");
|
|
356
|
+
process.exit(1);
|
|
357
|
+
}
|
|
358
|
+
const { token, baseUrl } = found;
|
|
359
|
+
if (!isSafeToken(token) || !isSafeUrl(baseUrl)) {
|
|
360
|
+
console.error(`Invalid patchcord URL or token in config.`);
|
|
361
|
+
process.exit(1);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (name) {
|
|
365
|
+
if (!/^[a-z][a-z0-9-]{0,49}$/.test(name)) {
|
|
366
|
+
console.error("Invalid agent name. Lowercase letters, digits, and dashes only.");
|
|
367
|
+
process.exit(1);
|
|
368
|
+
}
|
|
369
|
+
const { status, json, body } = await _httpJSON("GET", `${baseUrl}/api/agent/whoami/${name}`, token);
|
|
370
|
+
if (status === "404") {
|
|
371
|
+
console.error(`agent '${name}' not found in your namespace.`);
|
|
372
|
+
process.exit(1);
|
|
373
|
+
}
|
|
374
|
+
if (status !== "200") {
|
|
375
|
+
console.error(`✗ HTTP ${status}: ${(json && json.error) || body}`);
|
|
376
|
+
process.exit(1);
|
|
377
|
+
}
|
|
378
|
+
const tag = json.is_global ? " (global)" : "";
|
|
379
|
+
console.log(`${json.agent}${tag}: ${json.whoami || "(empty)"}`);
|
|
380
|
+
if (json.status || json.last_seen) {
|
|
381
|
+
console.log(`status: ${json.status || "?"} last_seen: ${json.last_seen || "?"}`);
|
|
382
|
+
}
|
|
383
|
+
process.exit(0);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const { status, json, body } = await _httpJSON("GET", `${baseUrl}/api/agent/agents`, token);
|
|
387
|
+
if (status !== "200") {
|
|
388
|
+
console.error(`✗ HTTP ${status}: ${(json && json.error) || body}`);
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
391
|
+
if (!json.agents || json.agents.length === 0) {
|
|
392
|
+
console.log("(no agents in this namespace)");
|
|
393
|
+
process.exit(0);
|
|
394
|
+
}
|
|
395
|
+
for (const a of json.agents) {
|
|
396
|
+
const tag = a.is_global ? " (global)" : "";
|
|
397
|
+
const text = a.whoami || "(empty)";
|
|
398
|
+
console.log(`${a.agent}${tag}: ${text}`);
|
|
399
|
+
}
|
|
400
|
+
process.exit(0);
|
|
401
|
+
}
|
|
402
|
+
|
|
91
403
|
// ── subscribe ─────────────────────────────────────────────────
|
|
92
404
|
// Thin wrapper around scripts/subscribe.mjs so users see a clean
|
|
93
405
|
// "npx patchcord subscribe" in their Claude Code tool log instead
|
|
@@ -413,18 +725,32 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
413
725
|
globalCliInstalled = true;
|
|
414
726
|
} catch {
|
|
415
727
|
try {
|
|
416
|
-
const
|
|
417
|
-
mkdirSync(
|
|
418
|
-
cpSync(pluginRoot,
|
|
728
|
+
const fbStableDir = join(HOME, ".local", "share", "patchcord");
|
|
729
|
+
mkdirSync(fbStableDir, { recursive: true });
|
|
730
|
+
cpSync(pluginRoot, fbStableDir, { recursive: true, force: true });
|
|
419
731
|
const localBin = join(HOME, ".local", "bin");
|
|
420
732
|
mkdirSync(localBin, { recursive: true });
|
|
421
733
|
const wrapper = join(localBin, "patchcord");
|
|
422
|
-
writeFileSync(wrapper, `#!/bin/sh\nexec node "${join(
|
|
734
|
+
writeFileSync(wrapper, `#!/bin/sh\nexec node "${join(fbStableDir, "bin", "patchcord.mjs")}" "$@"\n`);
|
|
423
735
|
chmodSync(wrapper, 0o755);
|
|
424
736
|
globalCliInstalled = true;
|
|
425
737
|
} catch {}
|
|
426
738
|
}
|
|
427
739
|
|
|
740
|
+
// ── Stable copy for Claude Code plugin marketplace ──────────
|
|
741
|
+
// Claude Code's `plugin marketplace add` stores the literal directory
|
|
742
|
+
// path in its plugin manifest. If we point it at `pluginRoot` (the
|
|
743
|
+
// npx ephemeral cache), that directory gets wiped on the next `npx`
|
|
744
|
+
// invocation of any other package, and every Claude Code session
|
|
745
|
+
// afterward errors: "Plugin directory does not exist: ...".
|
|
746
|
+
// Maintain a stable mirror at ~/.local/share/patchcord/ and point
|
|
747
|
+
// the marketplace there instead.
|
|
748
|
+
const stablePluginDir = join(HOME, ".local", "share", "patchcord");
|
|
749
|
+
try {
|
|
750
|
+
mkdirSync(stablePluginDir, { recursive: true });
|
|
751
|
+
cpSync(pluginRoot, stablePluginDir, { recursive: true, force: true });
|
|
752
|
+
} catch {}
|
|
753
|
+
|
|
428
754
|
// ── Global setup (silent if nothing changed) ──
|
|
429
755
|
let globalChanges = [];
|
|
430
756
|
if (globalCliInstalled) globalChanges.push("Patchcord CLI installed globally");
|
|
@@ -438,10 +764,14 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
438
764
|
try { rmSync(npmCachePatchcord, { recursive: true, force: true }); } catch {}
|
|
439
765
|
}
|
|
440
766
|
|
|
441
|
-
// Always re-add marketplace (
|
|
442
|
-
//
|
|
443
|
-
//
|
|
444
|
-
|
|
767
|
+
// Always re-add marketplace (point at the STABLE mirror, not pluginRoot
|
|
768
|
+
// which is the npx ephemeral cache). Claude Code stores the literal
|
|
769
|
+
// path in its plugin manifest — pointing at npx cache breaks every
|
|
770
|
+
// future session as soon as that cache dir is swept.
|
|
771
|
+
// Remove any prior marketplace registration first so the path actually
|
|
772
|
+
// updates for users who got the buggy npx-cache path on an earlier install.
|
|
773
|
+
run(`claude plugin marketplace remove patchcord-marketplace`);
|
|
774
|
+
run(`claude plugin marketplace add "${stablePluginDir}"`);
|
|
445
775
|
const installed = run(`claude plugin list`)?.includes("patchcord");
|
|
446
776
|
wasPluginInstalled = !!installed;
|
|
447
777
|
if (installed) {
|
|
@@ -1276,8 +1606,12 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
1276
1606
|
mkdirSync(codexDir, { recursive: true });
|
|
1277
1607
|
const configPath = join(codexDir, "config.toml");
|
|
1278
1608
|
let existing = existsSync(configPath) ? readFileSync(configPath, "utf-8") : "";
|
|
1279
|
-
// Remove old patchcord config
|
|
1280
|
-
|
|
1609
|
+
// Remove old patchcord config blocks AND any sub-tables like
|
|
1610
|
+
// [mcp_servers.patchcord.http_headers]. The character class [\w.-]
|
|
1611
|
+
// covers patchcord-codex, patchcord.http_headers, etc. We consume
|
|
1612
|
+
// through to the next "[" so the entire block (including subsections)
|
|
1613
|
+
// is removed in one pass.
|
|
1614
|
+
existing = existing.replace(/\[mcp_servers\.patchcord[\w.-]*\][^\[]*/g, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
1281
1615
|
existing = existing.trimEnd() + `\n\n[mcp_servers.patchcord-codex]\nurl = "${serverUrl}/mcp"\nhttp_headers = { "Authorization" = "Bearer ${token}", "X-Patchcord-Machine" = "${hostname}" }\ntool_timeout_sec = 300\n`;
|
|
1282
1616
|
writeFileSync(configPath, existing);
|
|
1283
1617
|
// Clean up any PATCHCORD_TOKEN we previously wrote to .env
|
|
@@ -1605,5 +1939,5 @@ if (cmd === "skill") {
|
|
|
1605
1939
|
process.exit(0);
|
|
1606
1940
|
}
|
|
1607
1941
|
|
|
1608
|
-
console.error(`Unknown command: ${cmd}. Available: subscribe, --rename, --token, --
|
|
1942
|
+
console.error(`Unknown command: ${cmd}. Available: whoami, agents, subscribe, update, --rename, --token, --agent-type, --version, --help`);
|
|
1609
1943
|
process.exit(1);
|
package/package.json
CHANGED
package/skills/inbox/SKILL.md
CHANGED
|
@@ -112,6 +112,29 @@ Use the path from the sender's message.
|
|
|
112
112
|
|
|
113
113
|
Send the returned `path` to the other agent in your message so they can download it.
|
|
114
114
|
|
|
115
|
+
## Identity (`patchcord whoami` / `patchcord agents`)
|
|
116
|
+
|
|
117
|
+
`whoami` and `agents` are CLI commands, not MCP tools. They use the bearer token from the project's `.mcp.json` — same namespace scope, no extra setup. Cheap to call (input tokens only), don't bloat MCP.
|
|
118
|
+
|
|
119
|
+
- **Run `patchcord whoami` once per session.** Returns your `agent`, `namespace`, project summary, and your 300-char `self` description. Use it on first turn after `/clear` or a fresh session to orient.
|
|
120
|
+
- **Run `patchcord agents`** to see the full roster (every peer's whoami). One call, ~3KB, complete picture of the namespace.
|
|
121
|
+
- **Run `patchcord agents <name>`** when an unknown agent messages you and you want to know who they are before acting on their request.
|
|
122
|
+
|
|
123
|
+
### Updating your own whoami (gated)
|
|
124
|
+
|
|
125
|
+
300-char hard limit. Goes through a human-approval flow.
|
|
126
|
+
|
|
127
|
+
- `patchcord whoami --propose "<text>"` — submits a pending proposal. Returns a `proposal_id`. The server also creates a normal patchcord message to `human` showing the diff.
|
|
128
|
+
- Another agent (NOT you) or the human via dashboard must approve. Self-approval is blocked server-side — you can propose updates to your own whoami, but you cannot approve them.
|
|
129
|
+
- To approve someone else's proposal: `patchcord whoami --approve <proposal-id>`. To reject: `patchcord whoami --reject <id>`.
|
|
130
|
+
- The human sees your proposal in their inbox/dashboard. They will approve when ready. Do not retry — proposals are idempotent.
|
|
131
|
+
|
|
132
|
+
### Hard rules
|
|
133
|
+
|
|
134
|
+
- You may NEVER update another agent's whoami. The `--propose` flow only writes your own.
|
|
135
|
+
- Namespace scope is enforced server-side: `patchcord agents <name>` returns 404 if the name isn't in your namespace (global agents like claudeai/chatgpt are excepted on cloud).
|
|
136
|
+
- whoami text describes WHO you are and how you coordinate (e.g. "backend systems. sends every change to codex-backend for review"). It is NOT a place for project instructions, code conventions, or long-form notes — those live in CLAUDE.md and project docs.
|
|
137
|
+
|
|
115
138
|
## Threads
|
|
116
139
|
|
|
117
140
|
Named threads group related messages between a pair of agents. Use them for multi-turn tasks that need their own context (e.g. "auth-migration", "deploy-review").
|