patchcord 0.5.53 → 0.5.55
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 +321 -5
- package/package.json +1 -1
- package/skills/inbox/SKILL.md +23 -0
package/bin/patchcord.mjs
CHANGED
|
@@ -73,7 +73,13 @@ Usage:
|
|
|
73
73
|
patchcord Setup via browser (patchcord.dev)
|
|
74
74
|
patchcord --token <token> [--server <url>] Headless / self-hosted setup
|
|
75
75
|
patchcord --full Same + full statusline
|
|
76
|
-
patchcord --rename <new-name> [--
|
|
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
|
|
@@ -119,7 +431,7 @@ if (cmd === "subscribe") {
|
|
|
119
431
|
const newName = (process.argv[renameIdx + 1] || "").trim().toLowerCase();
|
|
120
432
|
const expectIdx = process.argv.indexOf("--expect-token");
|
|
121
433
|
const expectPrefix = expectIdx !== -1 ? (process.argv[expectIdx + 1] || "").trim() : "";
|
|
122
|
-
const toolIdx = process.argv.indexOf("--tool");
|
|
434
|
+
const toolIdx = process.argv.indexOf("--tool") !== -1 ? process.argv.indexOf("--tool") : process.argv.indexOf("--agent-type");
|
|
123
435
|
const tool = toolIdx !== -1 ? (process.argv[toolIdx + 1] || "").trim().toLowerCase() : "";
|
|
124
436
|
|
|
125
437
|
if (!newName || !/^[a-z][a-z0-9-]{1,49}$/.test(newName)) {
|
|
@@ -1276,8 +1588,12 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
1276
1588
|
mkdirSync(codexDir, { recursive: true });
|
|
1277
1589
|
const configPath = join(codexDir, "config.toml");
|
|
1278
1590
|
let existing = existsSync(configPath) ? readFileSync(configPath, "utf-8") : "";
|
|
1279
|
-
// Remove old patchcord config
|
|
1280
|
-
|
|
1591
|
+
// Remove old patchcord config blocks AND any sub-tables like
|
|
1592
|
+
// [mcp_servers.patchcord.http_headers]. The character class [\w.-]
|
|
1593
|
+
// covers patchcord-codex, patchcord.http_headers, etc. We consume
|
|
1594
|
+
// through to the next "[" so the entire block (including subsections)
|
|
1595
|
+
// is removed in one pass.
|
|
1596
|
+
existing = existing.replace(/\[mcp_servers\.patchcord[\w.-]*\][^\[]*/g, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
1281
1597
|
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
1598
|
writeFileSync(configPath, existing);
|
|
1283
1599
|
// Clean up any PATCHCORD_TOKEN we previously wrote to .env
|
|
@@ -1605,5 +1921,5 @@ if (cmd === "skill") {
|
|
|
1605
1921
|
process.exit(0);
|
|
1606
1922
|
}
|
|
1607
1923
|
|
|
1608
|
-
console.error(`Unknown command: ${cmd}. Available: subscribe, --rename, --token, --
|
|
1924
|
+
console.error(`Unknown command: ${cmd}. Available: whoami, agents, subscribe, update, --rename, --token, --agent-type, --version, --help`);
|
|
1609
1925
|
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").
|