patchcord 0.5.105 → 0.5.107

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 +112 -118
  2. package/package.json +1 -1
package/bin/patchcord.mjs CHANGED
@@ -67,22 +67,48 @@ function detectFolder(dir) {
67
67
 
68
68
 
69
69
  if (cmd === "help" || cmd === "--help" || cmd === "-h") {
70
- console.log(`patchcord — agent messaging for AI coding agents
71
-
72
- Usage:
73
- patchcord Setup via browser (patchcord.dev)
74
- patchcord --token <token> [--server <url>] Headless / self-hosted setup
75
- patchcord --full Same + full statusline
76
- patchcord --rename <new-name> [--agent-type <type>] Rename this agent
77
- patchcord whoami Show your identity + project
78
- patchcord whoami --propose "<text>" Update your whoami (two-shot: first call asks you to show human, second call applies)
79
- patchcord agents List every agent (with whoami)
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)
82
- patchcord subscribe [interval] Start the realtime listener (Kimi: background poll, default 30s)
83
- patchcord update Update to the latest version
84
- patchcord --version Show installed version
85
- patchcord --help Show this help
70
+ console.log(`patchcord — cross-agent messaging + team orchestration for AI coding agents
71
+
72
+ Two layers: THIS AGENT (a per-project identity, by bearer token) and your ACCOUNT
73
+ (user-level; provisions agents, runs teams + schedules). Account/team/schedule
74
+ commands authenticate the CLI in your browser automatically on first use.
75
+
76
+ SETUP (per agent / project)
77
+ patchcord Set up this project's agent (browser)
78
+ patchcord --token <t> [--server <url>] Headless / self-hosted setup
79
+ patchcord --full Same + full statusline
80
+ patchcord --rename <name> [--agent-type <t>] Rename this agent
81
+
82
+ THIS AGENT identity + messaging
83
+ patchcord whoami Who am I this agent's identity + project
84
+ patchcord whoami --propose "<text>" Set your self-description (two-shot confirm)
85
+ patchcord agents [name] List agents in your namespace (or show one)
86
+ patchcord subscribe [interval] Start the realtime listener (Kimi: bg poll)
87
+ patchcord upload <file> [--mime <t>] [--as <name>] Share a file as an attachment
88
+
89
+ ACCOUNT — user-level (auto-auths the CLI in your browser on first use)
90
+ patchcord main Log in / re-authenticate the CLI
91
+ patchcord provision <agent> --tool <X> --role <Y> --namespace <ns> [--dir <sub/>]
92
+ Create a worker agent (identity + config)
93
+ patchcord provision revoke <agent> --namespace <ns>
94
+
95
+ TEAM — the main agent's tools (run from the project root)
96
+ patchcord team init <project> [--namespace <ns>] [--tool <harness>]
97
+ patchcord team list [--namespace <ns>] Provisioned agents (server view, deduped)
98
+ patchcord team status Reconcile folder ↔ identity ↔ tmux ↔ token
99
+ patchcord team launch Launch each worker in mux
100
+
101
+ SCHEDULES — timed / recurring messages (user-level)
102
+ patchcord schedule create <name> --namespace <ns> --to <agent> --content "..." \\
103
+ (--at <ISO> | --cron "<expr>" | --every <sec>) \\
104
+ [--timezone <tz>] [--thread <slug>] [--max-runs N] [--expires <ISO>]
105
+ patchcord schedule list [--namespace <ns>]
106
+ patchcord schedule cancel|test|pause|resume <id>
107
+
108
+ MISC
109
+ patchcord update Update to the latest version
110
+ patchcord --version Show installed version
111
+ patchcord --help Show this help
86
112
 
87
113
  First install: npx patchcord@latest`);
88
114
  process.exit(0);
@@ -797,7 +823,7 @@ if (cmd === "subscribe") {
797
823
  // (docs/main-agent.md). The "main" is the managing agent of a team; its main
798
824
  // token is a user-scoped provisioning credential stored at ~/.patchcord/main.json
799
825
  // (legacy: master.json) or $PATCHCORD_MAIN_TOKEN (legacy: $PATCHCORD_MASTER_TOKEN).
800
- if (cmd === "main" || cmd === "master" || cmd === "provision" || cmd === "team" || cmd === "schedule") {
826
+ if (cmd === "main" || cmd === "provision" || cmd === "team" || cmd === "schedule") {
801
827
  const M = { cyan: "\x1b[36m", green: "\x1b[32m", dim: "\x1b[2m", rst: "\x1b[0m" };
802
828
  const MAIN_CONFIG = join(HOME, ".patchcord", "main.json");
803
829
  const LEGACY_CONFIG = join(HOME, ".patchcord", "master.json"); // pre-rename
@@ -821,11 +847,45 @@ if (cmd === "main" || cmd === "master" || cmd === "provision" || cmd === "team"
821
847
  if (i >= 0 && process.argv[i + 1] && !process.argv[i + 1].startsWith("-")) return process.argv[i + 1];
822
848
  return def;
823
849
  };
824
- const requireMain = () => {
825
- const m = readMain();
826
- if (!m) { console.error("No main token. Run: patchcord main connect"); process.exit(1); }
827
- return m;
850
+ // Browser auth flow saves + returns the account token.
851
+ const connectMain = async () => {
852
+ const harness = flagVal("harness", flagVal("tool", ""));
853
+ const create = run(`curl -sf --max-time 10 -X POST "${DEFAULT_API}/api/main/session" -H "Content-Type: application/json" -d '{"harness":"${harness}"}'`);
854
+ let sessionId = "";
855
+ try { sessionId = (JSON.parse(create).session_id) || ""; } catch {}
856
+ if (!sessionId) { console.error("Could not start auth session."); process.exit(1); }
857
+ const url = `https://patchcord.dev/master?session=${sessionId}`;
858
+ console.log(`\n Authenticate the patchcord CLI in your browser:\n ${M.cyan}${url}${M.rst}\n`);
859
+ console.log(` ${M.dim}Waiting...${M.rst}`);
860
+ const http = await import("http");
861
+ const https = await import("https");
862
+ const token = await new Promise((resolve) => {
863
+ const lib = DEFAULT_API.startsWith("https") ? https : http;
864
+ const req = lib.get(`${DEFAULT_API}/api/connect/session/${sessionId}/wait`, { headers: { Accept: "text/event-stream" } }, (res) => {
865
+ let buf = "";
866
+ res.on("data", (c) => {
867
+ buf += c.toString();
868
+ for (const line of buf.split("\n")) {
869
+ if (line.startsWith("data: ")) {
870
+ try { const d = JSON.parse(line.slice(6)); if (d.token) { resolve(d.token); try { req.destroy(); } catch {} return; } } catch {}
871
+ }
872
+ }
873
+ });
874
+ res.on("end", () => resolve(null));
875
+ res.on("error", () => resolve(null));
876
+ });
877
+ req.on("error", () => resolve(null));
878
+ setTimeout(() => { try { req.destroy(); } catch {}; resolve(null); }, 900000);
879
+ });
880
+ if (!token) { console.error("\n Authentication timed out or failed."); process.exit(1); }
881
+ mkdirSync(dirname(MAIN_CONFIG), { recursive: true });
882
+ writeFileSync(MAIN_CONFIG, JSON.stringify({ token, baseUrl: DEFAULT_API }, null, 2) + "\n");
883
+ console.log(` ${M.green}✓${M.rst} CLI authenticated ${M.dim}(${MAIN_CONFIG})${M.rst}\n`);
884
+ return { token, baseUrl: DEFAULT_API };
828
885
  };
886
+ // Auth on demand: any command needing the account token authenticates the
887
+ // CLI in the browser if it isn't logged in yet, then proceeds.
888
+ const requireMain = async () => readMain() || await connectMain();
829
889
  const writeWorkerConfig = (tool, dir, baseUrl, token, hostname) => {
830
890
  mkdirSync(dir, { recursive: true });
831
891
  const hdr = { Authorization: `Bearer ${token}`, "X-Patchcord-Machine": hostname };
@@ -865,56 +925,15 @@ if (cmd === "main" || cmd === "master" || cmd === "provision" || cmd === "team"
865
925
  });
866
926
  };
867
927
 
868
- if (cmd === "main" || cmd === "master") {
869
- const sub = process.argv[3];
870
- if (sub === "connect") {
871
- const harness = flagVal("harness", flagVal("tool", ""));
872
- const create = run(`curl -sf --max-time 10 -X POST "${DEFAULT_API}/api/main/session" -H "Content-Type: application/json" -d '{"harness":"${harness}"}'`);
873
- let sessionId = "";
874
- try { sessionId = (JSON.parse(create).session_id) || ""; } catch {}
875
- if (!sessionId) { console.error("Could not start main-auth session."); process.exit(1); }
876
- const url = `https://patchcord.dev/master?session=${sessionId}`;
877
- console.log(`\n Authorize the main agent in your browser:\n ${M.cyan}${url}${M.rst}\n`);
878
- console.log(` ${M.dim}Waiting for authorization...${M.rst}`);
879
- const http = await import("http");
880
- const https = await import("https");
881
- const token = await new Promise((resolve) => {
882
- const lib = DEFAULT_API.startsWith("https") ? https : http;
883
- const req = lib.get(`${DEFAULT_API}/api/connect/session/${sessionId}/wait`, { headers: { Accept: "text/event-stream" } }, (res) => {
884
- let buf = "";
885
- res.on("data", (c) => {
886
- buf += c.toString();
887
- for (const line of buf.split("\n")) {
888
- if (line.startsWith("data: ")) {
889
- try { const d = JSON.parse(line.slice(6)); if (d.token) { resolve(d.token); try { req.destroy(); } catch {} return; } } catch {}
890
- }
891
- }
892
- });
893
- res.on("end", () => resolve(null));
894
- res.on("error", () => resolve(null));
895
- });
896
- req.on("error", () => resolve(null));
897
- setTimeout(() => { try { req.destroy(); } catch {}; resolve(null); }, 900000);
898
- });
899
- if (!token) { console.error("\n Authorization timed out or failed."); process.exit(1); }
900
- mkdirSync(dirname(MAIN_CONFIG), { recursive: true });
901
- writeFileSync(MAIN_CONFIG, JSON.stringify({ token, baseUrl: DEFAULT_API }, null, 2) + "\n");
902
- console.log(`\n ${M.green}✓${M.rst} Main token saved: ${M.dim}${MAIN_CONFIG}${M.rst}`);
903
- process.exit(0);
904
- }
905
- if (sub === "whoami") {
906
- const m = requireMain();
907
- const { status, json } = await _httpJSON("GET", `${m.baseUrl}/api/main/whoami`, m.token);
908
- if (status !== "200" || !json) { console.error(`main whoami failed (HTTP ${status})`); process.exit(1); }
909
- console.log(`main · user ${json.user_id} · ${json.agents}/${json.quota} agents`);
910
- process.exit(0);
911
- }
912
- console.error("Usage: patchcord main <connect|whoami>");
913
- process.exit(1);
928
+ if (cmd === "main") {
929
+ // Explicit (re)authentication. Other commands auth on demand via
930
+ // requireMain(), so this is only needed to log in ahead of time or switch.
931
+ await connectMain();
932
+ process.exit(0);
914
933
  }
915
934
 
916
935
  if (cmd === "provision") {
917
- const m = requireMain();
936
+ const m = await requireMain();
918
937
  const arg = process.argv[3];
919
938
  if (!arg || arg.startsWith("-")) { console.error("Usage: patchcord provision <agent> --tool X --role Y --namespace ns [--dir sub/]"); process.exit(1); }
920
939
  if (arg === "revoke") {
@@ -968,20 +987,16 @@ if (cmd === "main" || cmd === "master" || cmd === "provision" || cmd === "team"
968
987
  // write its config at the project root, so the main — running here —
969
988
  // resolves to ns:main and SEES every worker it provisions into ns.
970
989
  // Without this the main has no identity and whoami grabs a stale global.
971
- const m = readMain();
972
- if (m) {
973
- const hostname = run("hostname -s") || run("hostname") || "unknown";
974
- const { status, json } = await _httpJSON("POST", `${m.baseUrl}/api/provision`, m.token, { namespace_id: ns, agent_id: "main", tool: mainTool, role: "main", label: "main:self" });
975
- if (status === "200" && json?.token) {
976
- const base = String(json.url || `${m.baseUrl}/mcp`).replace(/\/mcp$/, "");
977
- writeWorkerConfig(mainTool, root, base, json.token, hostname);
978
- manifest.main = { agent: "main", tool: mainTool };
979
- console.log(`✓ main identity: ${M.green}${ns}:main${M.rst} [${mainTool}] — config written at ${root}`);
980
- } else {
981
- console.log(`⚠ could not provision main identity (HTTP ${status}: ${json?.error || ""}) — scaffolding only`);
982
- }
990
+ const m = await requireMain();
991
+ const hostname = run("hostname -s") || run("hostname") || "unknown";
992
+ const { status, json } = await _httpJSON("POST", `${m.baseUrl}/api/provision`, m.token, { namespace_id: ns, agent_id: "main", tool: mainTool, role: "main", label: "main:self" });
993
+ if (status === "200" && json?.token) {
994
+ const base = String(json.url || `${m.baseUrl}/mcp`).replace(/\/mcp$/, "");
995
+ writeWorkerConfig(mainTool, root, base, json.token, hostname);
996
+ manifest.main = { agent: "main", tool: mainTool };
997
+ console.log(`✓ main identity: ${M.green}${ns}:main${M.rst} [${mainTool}] — config written at ${root}`);
983
998
  } else {
984
- console.log(`⚠ no main token run 'patchcord main connect' first, then re-run team init`);
999
+ console.log(`⚠ could not provision main identity (HTTP ${status}: ${json?.error || ""}) scaffolding only`);
985
1000
  }
986
1001
  writeFileSync(join(root, ".patchcord", "team.json"), JSON.stringify(manifest, null, 2) + "\n");
987
1002
  writeFileSync(join(root, "AGENTS.md"), `# ${project} — Patchcord team\n\nNamespace: ${ns}\nMain identity: ${ns}:main (you, when running patchcord from this folder)\nProvision workers: patchcord provision <agent> --tool X --role Y --namespace ${ns} --dir <agent>/\n`);
@@ -990,12 +1005,23 @@ if (cmd === "main" || cmd === "master" || cmd === "provision" || cmd === "team"
990
1005
  process.exit(0);
991
1006
  }
992
1007
  if (sub === "list") {
993
- const m = requireMain();
1008
+ const m = await requireMain();
994
1009
  const { status, json } = await _httpJSON("GET", `${m.baseUrl}/api/provision/list`, m.token);
995
1010
  if (status !== "200") { console.error(`list failed (HTTP ${status})`); process.exit(1); }
996
- const agents = json?.agents || [];
997
- for (const a of agents) console.log(` ${a.namespace_id}:${a.agent_id} ${M.dim}${a.label || ""}${M.rst}`);
998
- if (!agents.length) console.log(" (no agents)");
1011
+ // Dedup by namespace:agent the server returns one row per token, so a
1012
+ // re-provisioned agent shows up many times. Optional --namespace filter.
1013
+ const filterNs = flagVal("namespace");
1014
+ const seen = new Set();
1015
+ let n = 0;
1016
+ for (const a of (json?.agents || [])) {
1017
+ if (filterNs && a.namespace_id !== filterNs) continue;
1018
+ const key = `${a.namespace_id}:${a.agent_id}`;
1019
+ if (seen.has(key)) continue;
1020
+ seen.add(key);
1021
+ console.log(` ${key} ${M.dim}${a.label || ""}${M.rst}`);
1022
+ n++;
1023
+ }
1024
+ if (!n) console.log(" (no agents)");
999
1025
  process.exit(0);
1000
1026
  }
1001
1027
  if (sub === "status") {
@@ -1138,7 +1164,7 @@ if (cmd === "main" || cmd === "master" || cmd === "provision" || cmd === "team"
1138
1164
  if (cmd === "schedule") {
1139
1165
  // User-level scheduled / recurring messages (server Plan 041). Authed by
1140
1166
  // the main token — the user can manage schedules in any namespace they own.
1141
- const m = requireMain();
1167
+ const m = await requireMain();
1142
1168
  const sub = process.argv[3];
1143
1169
  const BASE = `${m.baseUrl}/api/dashboard/scheduled`;
1144
1170
  const when = (s) => s.cron_expr ? `cron ${s.cron_expr}` : s.interval_sec ? `every ${s.interval_sec}s` : s.fire_at ? `once @ ${s.fire_at}` : s.schedule_kind;
@@ -2768,38 +2794,6 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
2768
2794
  }
2769
2795
 
2770
2796
  // ── channel: spawn the channel MCP server (used by .mcp.json) ──
2771
- if (cmd === "channel") {
2772
- const channelScript = join(pluginRoot, "channel", "server.ts");
2773
- if (!existsSync(channelScript)) {
2774
- console.error("Channel server not found. Reinstall patchcord.");
2775
- process.exit(1);
2776
- }
2777
- // Prefer bun, fall back to node (tsx)
2778
- const hasBun = run("which bun");
2779
- if (hasBun) {
2780
- const { spawnSync } = await import("child_process");
2781
- // Install deps if needed
2782
- const channelDir = join(pluginRoot, "channel");
2783
- if (!existsSync(join(channelDir, "node_modules"))) {
2784
- spawnSync("bun", ["install", "--no-summary"], { cwd: channelDir, stdio: "inherit" });
2785
- }
2786
- const result = spawnSync("bun", ["run", channelScript], { stdio: "inherit", env: process.env });
2787
- process.exit(result.status ?? 1);
2788
- } else {
2789
- console.error("Channel plugin requires bun. Install from https://bun.sh");
2790
- process.exit(1);
2791
- }
2792
- }
2793
-
2794
- // ── back-compat: init → install + agent ───────────────────────
2795
- if (cmd === "init") {
2796
- console.log(`"patchcord init" is now two commands:
2797
-
2798
- patchcord install One-time global setup (once)
2799
- patchcord agent Set up MCP for this project (per project)`);
2800
- process.exit(0);
2801
- }
2802
-
2803
2797
  // ── skill: custom skill from web console ─────────────────────
2804
2798
  if (cmd === "skill") {
2805
2799
  const sub = process.argv[3];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.5.105",
3
+ "version": "0.5.107",
4
4
  "description": "Cross-machine agent messaging for Claude Code and Codex",
5
5
  "author": "ppravdin",
6
6
  "license": "MIT",