patchcord 0.5.56 → 0.5.58

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
@@ -75,11 +75,10 @@ Usage:
75
75
  patchcord --full Same + full statusline
76
76
  patchcord --rename <new-name> [--agent-type <type>] Rename this agent
77
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
78
+ patchcord whoami --propose "<text>" Update your whoami (two-shot: first call asks you to show human, second call applies)
81
79
  patchcord agents List every agent (with whoami)
82
80
  patchcord agents <name> Show one agent's whoami
81
+ patchcord upload <file> [--mime <type>] [--as <name>] Upload a file as a patchcord attachment (prints the storage path)
83
82
  patchcord subscribe Start the realtime listener
84
83
  patchcord update Update to the latest version
85
84
  patchcord --version Show installed version
@@ -280,7 +279,8 @@ if (cmd === "whoami") {
280
279
  process.exit(1);
281
280
  }
282
281
 
283
- // --propose "<text>"
282
+ // --propose "<text>" — two-shot gate. First call returns "show_human",
283
+ // second call with identical text returns "applied".
284
284
  const proposeIdx = args.indexOf("--propose");
285
285
  if (proposeIdx !== -1) {
286
286
  const text = (args[proposeIdx + 1] || "").trim();
@@ -288,8 +288,9 @@ if (cmd === "whoami") {
288
288
  console.error("Usage: patchcord whoami --propose \"<text>\"");
289
289
  process.exit(1);
290
290
  }
291
+ // Client-side length check — don't even round-trip if over limit.
291
292
  if (text.length > 300) {
292
- console.error(`Whoami text exceeds 300 characters (got ${text.length}).`);
293
+ console.error(`whoami is ${text.length} characters; limit is 300. trim and retry.`);
293
294
  process.exit(1);
294
295
  }
295
296
  const { status, json, body } = await _httpJSON("POST", `${baseUrl}/api/agent/whoami/propose`, token, { text });
@@ -297,35 +298,25 @@ if (cmd === "whoami") {
297
298
  console.error(`✗ HTTP ${status}: ${(json && json.error) || body}`);
298
299
  process.exit(1);
299
300
  }
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);
301
+ if (json.status === "applied") {
302
+ console.log(`✓ whoami applied.`);
303
+ console.log(`self: ${json.whoami}`);
304
+ process.exit(0);
316
305
  }
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);
306
+ if (json.status === "unchanged") {
307
+ console.log(`unchanged — current whoami already matches.`);
308
+ console.log(`self: ${json.whoami}`);
309
+ process.exit(0);
326
310
  }
327
- console.log(`✓ ${json.status}: ${json.agent} (proposal ${json.proposal_id})`);
328
- process.exit(0);
311
+ if (json.status === "show_human") {
312
+ console.log(`gate: show the diff to the human, get explicit yes, then run the same command again to apply.`);
313
+ console.log();
314
+ console.log(`current: ${json.current || "(empty)"}`);
315
+ console.log(`proposed: ${json.proposed}`);
316
+ process.exit(0);
317
+ }
318
+ console.log(`unexpected response: ${body}`);
319
+ process.exit(1);
329
320
  }
330
321
 
331
322
  // plain whoami (no flags)
@@ -400,6 +391,66 @@ if (cmd === "agents") {
400
391
  process.exit(0);
401
392
  }
402
393
 
394
+ // ── upload <file> [--mime <type>] [--as <filename>] ───────────
395
+ // Inline-upload an attachment to patchcord storage. Wraps what would
396
+ // otherwise be a base64-and-curl dance. Prints the storage path on
397
+ // success so the user can pipe it into send_message.
398
+ if (cmd === "upload") {
399
+ const args = process.argv.slice(3);
400
+ const filePath = (args[0] || "").trim();
401
+ if (!filePath) {
402
+ console.error("Usage: patchcord upload <file> [--mime <type>] [--as <filename>]");
403
+ process.exit(1);
404
+ }
405
+ if (!existsSync(filePath)) {
406
+ console.error(`file not found: ${filePath}`);
407
+ process.exit(1);
408
+ }
409
+ const mimeIdx = args.indexOf("--mime");
410
+ const mime = mimeIdx !== -1 ? (args[mimeIdx + 1] || "").trim() : "";
411
+ const asIdx = args.indexOf("--as");
412
+ const renameTo = asIdx !== -1 ? (args[asIdx + 1] || "").trim() : "";
413
+
414
+ const found = await _resolveBearer();
415
+ if (!found) {
416
+ console.error("No patchcord config found in current directory or any parent. Run `npx patchcord@latest` from a project directory first.");
417
+ process.exit(1);
418
+ }
419
+ const { token, baseUrl } = found;
420
+ if (!isSafeToken(token) || !isSafeUrl(baseUrl)) {
421
+ console.error(`Invalid patchcord URL or token in config.`);
422
+ process.exit(1);
423
+ }
424
+
425
+ const { readFileSync, statSync } = await import("fs");
426
+ const stats = statSync(filePath);
427
+ if (stats.size === 0) {
428
+ console.error(`file is empty: ${filePath}`);
429
+ process.exit(1);
430
+ }
431
+ if (stats.size > 25 * 1024 * 1024) {
432
+ console.error(`file is ${(stats.size / 1024 / 1024).toFixed(1)}MB; max is 25MB for inline upload.`);
433
+ process.exit(1);
434
+ }
435
+
436
+ const data = readFileSync(filePath);
437
+ const content_b64 = data.toString("base64");
438
+ const filename = renameTo || basename(filePath);
439
+
440
+ const body = { filename, content_b64 };
441
+ if (mime) body.mime_type = mime;
442
+
443
+ const { status, json, body: respBody } = await _httpJSON(
444
+ "POST", `${baseUrl}/api/agent/attachment/upload`, token, body
445
+ );
446
+ if (status !== "200") {
447
+ console.error(`✗ HTTP ${status}: ${(json && json.error) || respBody}`);
448
+ process.exit(1);
449
+ }
450
+ console.log(json.path);
451
+ process.exit(0);
452
+ }
453
+
403
454
  // ── subscribe ─────────────────────────────────────────────────
404
455
  // Thin wrapper around scripts/subscribe.mjs so users see a clean
405
456
  // "npx patchcord subscribe" in their Claude Code tool log instead
@@ -1939,5 +1990,5 @@ if (cmd === "skill") {
1939
1990
  process.exit(0);
1940
1991
  }
1941
1992
 
1942
- console.error(`Unknown command: ${cmd}. Available: whoami, agents, subscribe, update, --rename, --token, --agent-type, --version, --help`);
1993
+ console.error(`Unknown command: ${cmd}. Available: whoami, agents, upload, subscribe, update, --rename, --token, --agent-type, --version, --help`);
1943
1994
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.5.56",
3
+ "version": "0.5.58",
4
4
  "description": "Cross-machine agent messaging for Claude Code and Codex",
5
5
  "author": "ppravdin",
6
6
  "license": "MIT",
@@ -120,14 +120,23 @@ Send the returned `path` to the other agent in your message so they can download
120
120
  - **Run `patchcord agents`** to see the full roster (every peer's whoami). One call, ~3KB, complete picture of the namespace.
121
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
122
 
123
- ### Updating your own whoami (gated)
123
+ ### Updating your own whoami
124
124
 
125
- 300-char hard limit. Goes through a human-approval flow.
125
+ 300-char hard limit (CLI enforces client-side).
126
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.
127
+ Server responds with one of three statuses:
128
+
129
+ - **applied** done. Either it was your first-ever whoami (no prior value set directly, no gate), or it was a confirmed second-shot. Print and move on.
130
+ - **unchanged** proposed text matches current. No-op.
131
+ - **show_human** — current value exists and the proposed text differs. Server printed `current:` and `proposed:`. You MUST:
132
+ 1. Show the diff to the human in conversation
133
+ 2. Ask them to confirm
134
+ 3. Wait for explicit "yes"
135
+ 4. Run the **exact same** `patchcord whoami --propose "<text>"` command again. Server will then return `applied`.
136
+
137
+ Pending state expires after 10 minutes. If the human says no, do not call again. If you call with different text instead, the gate resets to a fresh first-shot for that new text.
138
+
139
+ Never call `--propose` a second time with the same text without showing the human between calls.
131
140
 
132
141
  ### Hard rules
133
142