patchcord 0.5.106 → 0.5.108

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 +151 -109
  2. package/package.json +1 -1
package/bin/patchcord.mjs CHANGED
@@ -67,22 +67,52 @@ 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 — log in to the CLI (commands below log in on demand)
90
+ patchcord login Authenticate the CLI (your account)
91
+
92
+ TEAMLEAD — set up the team-lead agent (the shepherd) in this folder
93
+ patchcord teamlead [--namespace <ns>] [--tool <harness>]
94
+ Provision the teamlead here; launch it
95
+ to adopt this project or build a new team
96
+
97
+ TEAM — the teamlead's tools (run from the project root)
98
+ patchcord provision <agent> --tool <X> --role <Y> --namespace <ns> [--dir <sub/>]
99
+ Create a worker agent (identity + config)
100
+ patchcord provision revoke <agent> --namespace <ns>
101
+ patchcord team list [--namespace <ns>] Provisioned agents (server view, deduped)
102
+ patchcord team status Reconcile folder ↔ identity ↔ tmux ↔ token
103
+ patchcord team launch Launch each worker in mux
104
+
105
+ SCHEDULES — timed / recurring messages (user-level)
106
+ patchcord schedule create <name> --namespace <ns> --to <agent> --content "..." \\
107
+ (--at <ISO> | --cron "<expr>" | --every <sec>) \\
108
+ [--timezone <tz>] [--thread <slug>] [--max-runs N] [--expires <ISO>]
109
+ patchcord schedule list [--namespace <ns>]
110
+ patchcord schedule cancel|test|pause|resume <id>
111
+
112
+ MISC
113
+ patchcord update Update to the latest version
114
+ patchcord --version Show installed version
115
+ patchcord --help Show this help
86
116
 
87
117
  First install: npx patchcord@latest`);
88
118
  process.exit(0);
@@ -793,20 +823,21 @@ if (cmd === "subscribe") {
793
823
  // works the same as the space-form (--token foo). The internal flag parsing below
794
824
  // supports both. Non-flag commands (channel, init, skill, help, plugin-path) have
795
825
  // their own branches above and below.
796
- // ── Main agent: provisioning + team orchestration ──────────────────────────
797
- // (docs/main-agent.md). The "main" is the managing agent of a team; its main
798
- // token is a user-scoped provisioning credential stored at ~/.patchcord/main.json
799
- // (legacy: master.json) or $PATCHCORD_MAIN_TOKEN (legacy: $PATCHCORD_MASTER_TOKEN).
800
- if (cmd === "main" || cmd === "provision" || cmd === "team" || cmd === "schedule") {
826
+ // ── CLI account auth + teamlead/team/provisioning/schedule ─────────────────
827
+ // These commands act on the user's ACCOUNT, so they require CLI login. The
828
+ // account token (user-level, tied to NO agent) lives at ~/.patchcord/auth.json
829
+ // (legacy: main.json / master.json) or $PATCHCORD_TOKEN (legacy: *_MAIN/MASTER).
830
+ // `patchcord login` authenticates; everything else logs in on demand.
831
+ if (cmd === "login" || cmd === "teamlead" || cmd === "provision" || cmd === "team" || cmd === "schedule") {
801
832
  const M = { cyan: "\x1b[36m", green: "\x1b[32m", dim: "\x1b[2m", rst: "\x1b[0m" };
802
- const MAIN_CONFIG = join(HOME, ".patchcord", "main.json");
803
- const LEGACY_CONFIG = join(HOME, ".patchcord", "master.json"); // pre-rename
833
+ const AUTH_CONFIG = join(HOME, ".patchcord", "auth.json");
834
+ const LEGACY_CONFIGS = [join(HOME, ".patchcord", "main.json"), join(HOME, ".patchcord", "master.json")];
804
835
  const DEFAULT_API = process.env.PATCHCORD_BASE_URL || "https://api.patchcord.dev";
805
836
 
806
- const readMain = () => {
807
- const envTok = process.env.PATCHCORD_MAIN_TOKEN || process.env.PATCHCORD_MASTER_TOKEN;
837
+ const readAuth = () => {
838
+ const envTok = process.env.PATCHCORD_TOKEN || process.env.PATCHCORD_MAIN_TOKEN || process.env.PATCHCORD_MASTER_TOKEN;
808
839
  if (envTok) return { token: envTok, baseUrl: DEFAULT_API };
809
- for (const p of [MAIN_CONFIG, LEGACY_CONFIG]) {
840
+ for (const p of [AUTH_CONFIG, ...LEGACY_CONFIGS]) {
810
841
  try {
811
842
  const m = JSON.parse(readFileSync(p, "utf-8"));
812
843
  if (m && m.token) return { token: m.token, baseUrl: m.baseUrl || DEFAULT_API };
@@ -821,11 +852,44 @@ if (cmd === "main" || cmd === "provision" || cmd === "team" || cmd === "schedule
821
852
  if (i >= 0 && process.argv[i + 1] && !process.argv[i + 1].startsWith("-")) return process.argv[i + 1];
822
853
  return def;
823
854
  };
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;
855
+ // Browser login saves + returns the account token. Authenticates the CLI;
856
+ // not related to any agent.
857
+ const doLogin = async () => {
858
+ const create = run(`curl -sf --max-time 10 -X POST "${DEFAULT_API}/api/main/session" -H "Content-Type: application/json" -d '{}'`);
859
+ let sessionId = "";
860
+ try { sessionId = (JSON.parse(create).session_id) || ""; } catch {}
861
+ if (!sessionId) { console.error("Could not start login session."); process.exit(1); }
862
+ const url = `https://patchcord.dev/master?session=${sessionId}`;
863
+ console.log(`\n Log in to patchcord in your browser:\n ${M.cyan}${url}${M.rst}\n`);
864
+ console.log(` ${M.dim}Waiting...${M.rst}`);
865
+ const http = await import("http");
866
+ const https = await import("https");
867
+ const token = await new Promise((resolve) => {
868
+ const lib = DEFAULT_API.startsWith("https") ? https : http;
869
+ const req = lib.get(`${DEFAULT_API}/api/connect/session/${sessionId}/wait`, { headers: { Accept: "text/event-stream" } }, (res) => {
870
+ let buf = "";
871
+ res.on("data", (c) => {
872
+ buf += c.toString();
873
+ for (const line of buf.split("\n")) {
874
+ if (line.startsWith("data: ")) {
875
+ try { const d = JSON.parse(line.slice(6)); if (d.token) { resolve(d.token); try { req.destroy(); } catch {} return; } } catch {}
876
+ }
877
+ }
878
+ });
879
+ res.on("end", () => resolve(null));
880
+ res.on("error", () => resolve(null));
881
+ });
882
+ req.on("error", () => resolve(null));
883
+ setTimeout(() => { try { req.destroy(); } catch {}; resolve(null); }, 900000);
884
+ });
885
+ if (!token) { console.error("\n Login timed out or failed."); process.exit(1); }
886
+ mkdirSync(dirname(AUTH_CONFIG), { recursive: true });
887
+ writeFileSync(AUTH_CONFIG, JSON.stringify({ token, baseUrl: DEFAULT_API }, null, 2) + "\n");
888
+ console.log(` ${M.green}✓${M.rst} Logged in ${M.dim}(${AUTH_CONFIG})${M.rst}\n`);
889
+ return { token, baseUrl: DEFAULT_API };
828
890
  };
891
+ // Auth on demand: any command needing the account logs in if not already.
892
+ const requireAuth = async () => readAuth() || await doLogin();
829
893
  const writeWorkerConfig = (tool, dir, baseUrl, token, hostname) => {
830
894
  mkdirSync(dir, { recursive: true });
831
895
  const hdr = { Authorization: `Bearer ${token}`, "X-Patchcord-Machine": hostname };
@@ -865,49 +929,15 @@ if (cmd === "main" || cmd === "provision" || cmd === "team" || cmd === "schedule
865
929
  });
866
930
  };
867
931
 
868
- if (cmd === "main") {
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
- console.error("Usage: patchcord main connect");
906
- process.exit(1);
932
+ if (cmd === "login") {
933
+ // Explicit CLI login. Other commands log in on demand via requireAuth(),
934
+ // so this is only needed to authenticate ahead of time or switch accounts.
935
+ await doLogin();
936
+ process.exit(0);
907
937
  }
908
938
 
909
939
  if (cmd === "provision") {
910
- const m = requireMain();
940
+ const m = await requireAuth();
911
941
  const arg = process.argv[3];
912
942
  if (!arg || arg.startsWith("-")) { console.error("Usage: patchcord provision <agent> --tool X --role Y --namespace ns [--dir sub/]"); process.exit(1); }
913
943
  if (arg === "revoke") {
@@ -947,43 +977,55 @@ if (cmd === "main" || cmd === "provision" || cmd === "team" || cmd === "schedule
947
977
  process.exit(0);
948
978
  }
949
979
 
980
+ if (cmd === "teamlead") {
981
+ // Set up the TEAMLEAD agent in THIS folder. The teamlead is a distinct kind
982
+ // of agent — it shepherds a team: provisions, connects, and manages the
983
+ // other agents. It is NOT a normal agent: it gets its own identity, its own
984
+ // onboarding instruction, and on launch either ADOPTS this existing project
985
+ // or CREATES a new team by interviewing the user.
986
+ const m = await requireAuth();
987
+ const root = process.cwd();
988
+ const ns = (flagVal("namespace", basename(root)) || "team").replace(/[^a-z0-9-]/gi, "-").toLowerCase();
989
+ const tool = flagVal("tool", "claude_code");
990
+ const hostname = run("hostname -s") || run("hostname") || "unknown";
991
+ const { status, json } = await _httpJSON("POST", `${m.baseUrl}/api/provision`, m.token, { namespace_id: ns, agent_id: "teamlead", tool, role: "teamlead", label: "teamlead:self" });
992
+ if (status !== "200" || !json?.token) { console.error(`could not set up teamlead (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
993
+ const base = String(json.url || `${m.baseUrl}/mcp`).replace(/\/mcp$/, "");
994
+ writeWorkerConfig(tool, root, base, json.token, hostname);
995
+ mkdirSync(join(root, ".patchcord"), { recursive: true });
996
+ const tj = join(root, ".patchcord", "team.json");
997
+ let manifest = { project: basename(root), namespace: ns, teamlead: { agent: "teamlead", tool }, agents: [] };
998
+ try { if (existsSync(tj)) manifest = { ...JSON.parse(readFileSync(tj, "utf-8")), namespace: ns, teamlead: { agent: "teamlead", tool } }; } catch {}
999
+ writeFileSync(tj, JSON.stringify(manifest, null, 2) + "\n");
1000
+ // The teamlead's onboarding instruction (read on launch).
1001
+ writeFileSync(join(root, "TEAMLEAD.md"), `# You are the TEAMLEAD (${ns}:teamlead)
1002
+
1003
+ You shepherd a team of patchcord agents in this folder. You are NOT a worker —
1004
+ you design the team, provision its agents, launch them, and manage them.
1005
+
1006
+ ## On start, pick ONE
1007
+ 1. **Adopt this project.** If a team already exists here (\`.patchcord/team.json\`
1008
+ lists workers, or agents are already set up), take it into management:
1009
+ \`patchcord team status\` to reconcile folder ↔ identity ↔ tmux ↔ token, then run it.
1010
+ 2. **Create a new team.** Otherwise interview the user — what are we building,
1011
+ which roles/harnesses, how many — confirm the plan, then build it.
1012
+
1013
+ ## Build / run
1014
+ - Provision each worker: \`patchcord provision <agent> --tool <claude_code|codex|opencode|kimi> --role <role> --namespace ${ns} --dir <agent>/\`
1015
+ - Launch: \`patchcord team launch\` (mux), verify with \`patchcord team status\`.
1016
+ - Coordinate over patchcord (inbox / send_message); manage at the meta level.
1017
+ - Your own identity here is ${ns}:teamlead — \`patchcord whoami\` confirms it.
1018
+ `);
1019
+ console.log(`\n ${M.green}✓${M.rst} Teamlead ready: ${M.green}${ns}:teamlead${M.rst} [${tool}] in ${root}`);
1020
+ console.log(` ${M.dim}Launch it here — it adopts this project or creates a new team, asking you:${M.rst}`);
1021
+ console.log(` mux new ${tool === "claude_code" ? "claude" : (tool === "kimi-code" ? "kimi" : tool)} --dir .`);
1022
+ process.exit(0);
1023
+ }
1024
+
950
1025
  if (cmd === "team") {
951
1026
  const sub = process.argv[3];
952
- if (sub === "init") {
953
- const project = process.argv[4];
954
- if (!project) { console.error("Usage: patchcord team init <project> [--namespace ns] [--tool <main-harness>]"); process.exit(1); }
955
- const ns = flagVal("namespace", project.replace(/[^a-z0-9-]/gi, "-").toLowerCase());
956
- const mainTool = flagVal("tool", "claude_code");
957
- const root = join(process.cwd(), project);
958
- mkdirSync(join(root, ".patchcord"), { recursive: true });
959
- const manifest = { project, namespace: ns, pattern: "architect-workers-reviewer", main: null, agents: [] };
960
- // Provision the main's OWN messaging identity IN the team namespace and
961
- // write its config at the project root, so the main — running here —
962
- // resolves to ns:main and SEES every worker it provisions into ns.
963
- // Without this the main has no identity and whoami grabs a stale global.
964
- const m = readMain();
965
- if (m) {
966
- const hostname = run("hostname -s") || run("hostname") || "unknown";
967
- 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" });
968
- if (status === "200" && json?.token) {
969
- const base = String(json.url || `${m.baseUrl}/mcp`).replace(/\/mcp$/, "");
970
- writeWorkerConfig(mainTool, root, base, json.token, hostname);
971
- manifest.main = { agent: "main", tool: mainTool };
972
- console.log(`✓ main identity: ${M.green}${ns}:main${M.rst} [${mainTool}] — config written at ${root}`);
973
- } else {
974
- console.log(`⚠ could not provision main identity (HTTP ${status}: ${json?.error || ""}) — scaffolding only`);
975
- }
976
- } else {
977
- console.log(`⚠ no main token — run 'patchcord main connect' first, then re-run team init`);
978
- }
979
- writeFileSync(join(root, ".patchcord", "team.json"), JSON.stringify(manifest, null, 2) + "\n");
980
- 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`);
981
- console.log(`✓ team scaffolded: ${root} (namespace ${ns})`);
982
- console.log(` ${M.dim}cd ${project} — your identity there is ${ns}:main; you'll see every worker you provision into ${ns}.${M.rst}`);
983
- process.exit(0);
984
- }
985
1027
  if (sub === "list") {
986
- const m = requireMain();
1028
+ const m = await requireAuth();
987
1029
  const { status, json } = await _httpJSON("GET", `${m.baseUrl}/api/provision/list`, m.token);
988
1030
  if (status !== "200") { console.error(`list failed (HTTP ${status})`); process.exit(1); }
989
1031
  // Dedup by namespace:agent — the server returns one row per token, so a
@@ -1051,10 +1093,10 @@ if (cmd === "main" || cmd === "provision" || cmd === "team" || cmd === "schedule
1051
1093
  panes.push({ session, window, path: real(path), cmd, claimed: false });
1052
1094
  }
1053
1095
 
1054
- // Build the roster: main first, then workers.
1096
+ // Build the roster: teamlead first, then workers.
1055
1097
  const roster = [];
1056
- const mainEntry = manifest.main || manifest.master; // master = pre-rename
1057
- if (mainEntry) roster.push({ ...mainEntry, role: "main", dir: ".", _main: true });
1098
+ const leadEntry = manifest.teamlead || manifest.main || manifest.master; // pre-rename names
1099
+ if (leadEntry) roster.push({ ...leadEntry, role: "teamlead", dir: ".", _lead: true });
1058
1100
  for (const a of (manifest.agents || [])) roster.push(a);
1059
1101
 
1060
1102
  const rows = [];
@@ -1135,14 +1177,14 @@ if (cmd === "main" || cmd === "provision" || cmd === "team" || cmd === "schedule
1135
1177
  }
1136
1178
  process.exit(0);
1137
1179
  }
1138
- console.error("Usage: patchcord team <init|list|launch|status>");
1180
+ console.error("Usage: patchcord team <list|launch|status>");
1139
1181
  process.exit(1);
1140
1182
  }
1141
1183
 
1142
1184
  if (cmd === "schedule") {
1143
1185
  // User-level scheduled / recurring messages (server Plan 041). Authed by
1144
1186
  // the main token — the user can manage schedules in any namespace they own.
1145
- const m = requireMain();
1187
+ const m = await requireAuth();
1146
1188
  const sub = process.argv[3];
1147
1189
  const BASE = `${m.baseUrl}/api/dashboard/scheduled`;
1148
1190
  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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.5.106",
3
+ "version": "0.5.108",
4
4
  "description": "Cross-machine agent messaging for Claude Code and Codex",
5
5
  "author": "ppravdin",
6
6
  "license": "MIT",