patchcord 0.5.116 → 0.5.118

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
@@ -66,12 +66,20 @@ function detectFolder(dir) {
66
66
  }
67
67
 
68
68
 
69
- if (cmd === "help" || cmd === "--help" || cmd === "-h") {
69
+ // A trailing --help/-h on ANY subcommand must show help and exit never run the
70
+ // command. (patchcord orchestrator --help used to PROVISION an orchestrator;
71
+ // patchcord login --help hung on the browser login. Side-effecting --help is a
72
+ // footgun.) We short-circuit here, before any account/provision/login dispatch.
73
+ if (
74
+ cmd === "help" || cmd === "--help" || cmd === "-h" ||
75
+ process.argv.slice(3).some((a) => a === "--help" || a === "-h")
76
+ ) {
70
77
  console.log(`patchcord — cross-agent messaging + team orchestration for AI coding agents
71
78
 
72
79
  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.
80
+ (user-level; provisions agents, runs teams + schedules). Run \`patchcord login\`
81
+ once to authenticate the CLI; account/team/schedule commands then use that token
82
+ (they never open a browser on their own).
75
83
 
76
84
  SETUP (per agent / project)
77
85
  patchcord Set up this project's agent (browser)
@@ -89,12 +97,12 @@ THIS AGENT — identity + messaging
89
97
  ACCOUNT — log in to the CLI (commands below log in on demand)
90
98
  patchcord login Authenticate the CLI (your account)
91
99
 
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
100
+ ORCHESTRATOR — set up the orchestrator agent (the shepherd) in this folder
101
+ patchcord orchestrator [--namespace <ns>] [--tool <harness>]
102
+ Provision the orchestrator here; launch it
95
103
  to adopt this project or build a new team
96
104
 
97
- TEAM — the teamlead's tools (run from the project root)
105
+ TEAM — the orchestrator's tools (run from the project root)
98
106
  patchcord provision <agent> --tool <X> --role <Y> --namespace <ns> [--dir <sub/>]
99
107
  Create a worker agent (identity + config)
100
108
  patchcord pull <agent> --tool <X> --namespace <ns> [--dir <sub/>]
@@ -341,8 +349,12 @@ async function _resolveBearer(options = {}) {
341
349
  );
342
350
  })();
343
351
 
344
- const kimiGlobalReader = () => readJsonAt(join(HOME, ".kimi", "mcp.json"), ["mcpServers", "patchcord"], "kimi");
345
- const kimiCodeGlobalReader = () => readJsonAt(join(process.env.KIMI_CODE_HOME || join(HOME, ".kimi-code"), "mcp.json"), ["mcpServers", "patchcord"], "kimi");
352
+ // NO global kimi/kimi-code reader. Kimi is a PER-PROJECT identity
353
+ // (.kimi-code/mcp.json in the worktree); a global ~/.kimi(-code)/mcp.json would
354
+ // make a leftover token hijack every kimi session everywhere. A kimi outside a
355
+ // patchcord worktree is simply not an agent. (The tools below are global-ONLY
356
+ // by nature — Windsurf/Gemini/Zed/etc. store MCP config globally — so they keep
357
+ // their global readers.)
346
358
  const defaultGlobalCandidates = [
347
359
  () => readJsonAt(join(HOME, ".codeium", "windsurf", "mcp_config.json"), ["mcpServers", "patchcord"], "windsurf"),
348
360
  () => readJsonAt(join(HOME, ".gemini", "settings.json"), ["mcpServers", "patchcord"], "gemini"),
@@ -352,9 +364,7 @@ async function _resolveBearer(options = {}) {
352
364
  () => readHermesShape(join(HOME, ".hermes", "config.yaml")),
353
365
  ...clinePaths.map((p) => () => readJsonAt(p, ["mcpServers", "patchcord"], "cline")),
354
366
  ];
355
- const globalCandidates = preferKimi
356
- ? [kimiGlobalReader, kimiCodeGlobalReader, ...defaultGlobalCandidates]
357
- : [...defaultGlobalCandidates, kimiGlobalReader, kimiCodeGlobalReader];
367
+ const globalCandidates = defaultGlobalCandidates;
358
368
  for (const r of globalCandidates) {
359
369
  const found = r();
360
370
  if (found) { found.scope = "global"; return found; }
@@ -827,12 +837,12 @@ if (cmd === "subscribe") {
827
837
  // works the same as the space-form (--token foo). The internal flag parsing below
828
838
  // supports both. Non-flag commands (channel, init, skill, help, plugin-path) have
829
839
  // their own branches above and below.
830
- // ── CLI account auth + teamlead/team/provisioning/schedule ─────────────────
840
+ // ── CLI account auth + orchestrator/team/provisioning/schedule ─────────────
831
841
  // These commands act on the user's ACCOUNT, so they require CLI login. The
832
842
  // account token (user-level, tied to NO agent) lives at ~/.patchcord/auth.json
833
843
  // (legacy: main.json / master.json) or $PATCHCORD_TOKEN (legacy: *_MAIN/MASTER).
834
844
  // `patchcord login` authenticates; everything else logs in on demand.
835
- if (cmd === "login" || cmd === "teamlead" || cmd === "provision" || cmd === "pull" || cmd === "team" || cmd === "schedule") {
845
+ if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "provision" || cmd === "pull" || cmd === "team" || cmd === "schedule") {
836
846
  const M = { cyan: "\x1b[36m", green: "\x1b[32m", dim: "\x1b[2m", rst: "\x1b[0m" };
837
847
  const AUTH_CONFIG = join(HOME, ".patchcord", "auth.json");
838
848
  const LEGACY_CONFIGS = [join(HOME, ".patchcord", "main.json"), join(HOME, ".patchcord", "master.json")];
@@ -902,7 +912,19 @@ if (cmd === "login" || cmd === "teamlead" || cmd === "provision" || cmd === "pul
902
912
  return { token, baseUrl: DEFAULT_API };
903
913
  };
904
914
  // Auth on demand: any command needing the account logs in if not already.
905
- const requireAuth = async () => readAuth() || await doLogin();
915
+ const requireAuth = async () => {
916
+ const a = readAuth();
917
+ if (a) return a;
918
+ // No account token. NEVER auto-drop into the interactive browser login here:
919
+ // doLogin() prints a URL and BLOCKS up to 15 minutes for a browser that never
920
+ // comes in a headless/agent context (the orchestrator misread that hang as
921
+ // "provision produced no output"). TTY detection is unreliable (a pty/agent
922
+ // shell can report isTTY=true), so we fail fast unconditionally. Only the
923
+ // explicit `patchcord login` command opens a browser.
924
+ console.error("No patchcord account token — no pcm- token in ~/.patchcord/auth.json or $PATCHCORD_TOKEN.");
925
+ console.error("Run `patchcord login` once (interactive), or set $PATCHCORD_TOKEN. Cannot continue.");
926
+ process.exit(1);
927
+ };
906
928
  // Account-authed request with stale-token recovery. If the server rejects the
907
929
  // stored token (401/403), forget it, re-log-in once, and retry. Returns the
908
930
  // _httpJSON result plus the auth used (for callers that need m.baseUrl).
@@ -910,9 +932,11 @@ if (cmd === "login" || cmd === "teamlead" || cmd === "provision" || cmd === "pul
910
932
  let m = await requireAuth();
911
933
  let r = await _httpJSON(method, `${m.baseUrl}${path}`, m.token, body);
912
934
  if (r.status === "401" || r.status === "403") {
935
+ // Stale/invalid account token. Forget it and fail fast — do NOT auto-relogin
936
+ // (would hang headless, see requireAuth). The user re-runs `patchcord login`.
913
937
  forgetAuth();
914
- m = await doLogin();
915
- r = await _httpJSON(method, `${m.baseUrl}${path}`, m.token, body);
938
+ console.error(`patchcord account token rejected (HTTP ${r.status}). Run \`patchcord login\` again. Cannot continue.`);
939
+ process.exit(1);
916
940
  }
917
941
  return { ...r, m };
918
942
  };
@@ -957,6 +981,31 @@ if (cmd === "login" || cmd === "teamlead" || cmd === "provision" || cmd === "pul
957
981
  o.mcpServers.patchcord = { serverUrl: `${baseUrl}/mcp`, headers: hdr };
958
982
  });
959
983
  }
984
+ if (tool === "hermes") {
985
+ // Hermes reads MCP servers ONLY from its GLOBAL ~/.hermes/config.yaml
986
+ // (mcp_servers key) — it ignores a project-local .mcp.json. So unlike the
987
+ // other tools we cannot write into `dir`; we upsert the global config,
988
+ // mirroring the `npx patchcord` installer's Hermes path. NOTE: global =
989
+ // one patchcord identity per machine, so provisioning a second hermes
990
+ // worker in another namespace overwrites the first's token. Single-hermes
991
+ // teams only until Hermes gains project-scoped MCP config.
992
+ const hermesPath = join(HOME, ".hermes", "config.yaml");
993
+ mkdirSync(dirname(hermesPath), { recursive: true });
994
+ let existingYaml = "";
995
+ try { existingYaml = existsSync(hermesPath) ? readFileSync(hermesPath, "utf-8") : ""; } catch {}
996
+ writeFileSync(hermesPath, upsertHermesConfig(existingYaml, `${baseUrl}/mcp`, token));
997
+ // Install the Hermes patchcord skills (inbox/subscribe/wait) so the worker
998
+ // knows the messaging flow, same as the installer does.
999
+ try {
1000
+ const hermesSkillsSrc = join(pluginRoot, "per-project-skills", "hermes");
1001
+ if (existsSync(hermesSkillsSrc)) {
1002
+ const hermesSkillsDest = join(HOME, ".hermes", "skills", "integrations");
1003
+ mkdirSync(hermesSkillsDest, { recursive: true });
1004
+ cpSync(hermesSkillsSrc, hermesSkillsDest, { recursive: true });
1005
+ }
1006
+ } catch {}
1007
+ return hermesPath;
1008
+ }
960
1009
  // default: claude_code. type:"http" is REQUIRED — without it Claude Code
961
1010
  // defaults to stdio transport and rejects the entry ("command: expected
962
1011
  // string, received undefined").
@@ -1046,27 +1095,34 @@ if (cmd === "login" || cmd === "teamlead" || cmd === "provision" || cmd === "pul
1046
1095
  process.exit(0);
1047
1096
  }
1048
1097
 
1049
- if (cmd === "teamlead") {
1050
- // Set up the TEAMLEAD agent in THIS folder. The teamlead is a distinct kind
1051
- // of agent — it shepherds a team: provisions, connects, and manages the
1052
- // other agents. It is NOT a normal agent: it gets its own identity, its own
1053
- // onboarding instruction, and on launch either ADOPTS this existing project
1054
- // or CREATES a new team by interviewing the user.
1098
+ if (cmd === "orchestrator" || cmd === "teamlead") {
1099
+ // Set up the ORCHESTRATOR agent in THIS folder. The orchestrator is a
1100
+ // distinct kind of agent — it shepherds a team: provisions, connects, and
1101
+ // manages the other agents. It is NOT a normal agent: it gets its own
1102
+ // identity, its own onboarding instruction, and on launch either ADOPTS
1103
+ // this existing project or CREATES a new team by interviewing the user.
1104
+ // (`teamlead` is kept as a back-compat alias for the command name.)
1055
1105
  const root = process.cwd();
1056
1106
  const ns = (flagVal("namespace", basename(root)) || "team").replace(/[^a-z0-9-]/gi, "-").toLowerCase();
1057
1107
  const tool = flagVal("tool", "claude_code");
1058
1108
  const hostname = run("hostname -s") || run("hostname") || "unknown";
1059
- const { status, json, m } = await accountCall("POST", "/api/provision", { namespace_id: ns, agent_id: "teamlead", tool, role: "teamlead", label: "teamlead:self" });
1060
- if (status !== "200" || !json?.token) { console.error(`could not set up teamlead (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
1109
+ const { status, json, m } = await accountCall("POST", "/api/provision", { namespace_id: ns, agent_id: "orchestrator", tool, role: "orchestrator", label: "orchestrator:self" });
1110
+ if (status !== "200" || !json?.token) { console.error(`could not set up orchestrator (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
1061
1111
  const base = String(json.url || `${m.baseUrl}/mcp`).replace(/\/mcp$/, "");
1062
1112
  writeWorkerConfig(tool, root, base, json.token, hostname);
1063
1113
  mkdirSync(join(root, ".patchcord"), { recursive: true });
1064
1114
  const tj = join(root, ".patchcord", "team.json");
1065
- let manifest = { project: basename(root), namespace: ns, teamlead: { agent: "teamlead", tool }, agents: [] };
1066
- try { if (existsSync(tj)) manifest = { ...JSON.parse(readFileSync(tj, "utf-8")), namespace: ns, teamlead: { agent: "teamlead", tool } }; } catch {}
1115
+ let manifest = { project: basename(root), namespace: ns, orchestrator: { agent: "orchestrator", tool }, agents: [] };
1116
+ try {
1117
+ if (existsSync(tj)) {
1118
+ const prev = JSON.parse(readFileSync(tj, "utf-8"));
1119
+ delete prev.teamlead; delete prev.main; delete prev.master; // drop pre-rename keys
1120
+ manifest = { ...prev, namespace: ns, orchestrator: { agent: "orchestrator", tool } };
1121
+ }
1122
+ } catch {}
1067
1123
  writeFileSync(tj, JSON.stringify(manifest, null, 2) + "\n");
1068
- // The teamlead's onboarding instruction (read on launch).
1069
- writeFileSync(join(root, "TEAMLEAD.md"), `# You are the TEAMLEAD (${ns}:teamlead)
1124
+ // The orchestrator's onboarding instruction (read on launch).
1125
+ writeFileSync(join(root, "ORCHESTRATOR.md"), `# You are the ORCHESTRATOR (${ns}:orchestrator)
1070
1126
 
1071
1127
  You shepherd a team of patchcord agents in this folder. You are NOT a worker —
1072
1128
  you design the team, provision its agents, launch them, and manage them.
@@ -1079,14 +1135,16 @@ you design the team, provision its agents, launch them, and manage them.
1079
1135
  which roles/harnesses, how many — confirm the plan, then build it.
1080
1136
 
1081
1137
  ## Build / run
1082
- - Provision each worker: \`patchcord provision <agent> --tool <claude_code|codex|opencode|kimi> --role <role> --namespace ${ns} --dir <agent>/\`
1138
+ - Provision each worker: \`patchcord provision <agent> --tool <claude_code|codex|opencode|kimi|agy> --role <role> --namespace ${ns} --dir <agent>/\`
1139
+ - Pull an existing agent into a folder: \`patchcord pull <agent> --tool <X> --namespace ${ns} --dir <agent>/\`
1083
1140
  - Launch: \`patchcord team launch\` (mux), verify with \`patchcord team status\`.
1084
1141
  - Coordinate over patchcord (inbox / send_message); manage at the meta level.
1085
- - Your own identity here is ${ns}:teamlead — \`patchcord whoami\` confirms it.
1142
+ - Your own identity here is ${ns}:orchestrator — \`patchcord whoami\` confirms it.
1086
1143
  `);
1087
- console.log(`\n ${M.green}✓${M.rst} Teamlead ready: ${M.green}${ns}:teamlead${M.rst} [${tool}] in ${root}`);
1144
+ try { if (existsSync(join(root, "TEAMLEAD.md"))) unlinkSync(join(root, "TEAMLEAD.md")); } catch {} // remove pre-rename onboarding file
1145
+ console.log(`\n ${M.green}✓${M.rst} Orchestrator ready: ${M.green}${ns}:orchestrator${M.rst} [${tool}] in ${root}`);
1088
1146
  console.log(` ${M.dim}Launch it here — it adopts this project or creates a new team, asking you:${M.rst}`);
1089
- console.log(` mux new ${tool === "claude_code" ? "claude" : (tool === "kimi-code" ? "kimi" : tool)} --dir .`);
1147
+ console.log(` mux new ${tool === "claude_code" ? "claude" : (tool === "kimi-code" ? "kimi" : (tool === "antigravity" || tool === "agy") ? "agy" : tool)} --dir .`);
1090
1148
  process.exit(0);
1091
1149
  }
1092
1150
 
@@ -1160,10 +1218,10 @@ you design the team, provision its agents, launch them, and manage them.
1160
1218
  panes.push({ session, window, path: real(path), cmd, claimed: false });
1161
1219
  }
1162
1220
 
1163
- // Build the roster: teamlead first, then workers.
1221
+ // Build the roster: orchestrator first, then workers.
1164
1222
  const roster = [];
1165
- const leadEntry = manifest.teamlead || manifest.main || manifest.master; // pre-rename names
1166
- if (leadEntry) roster.push({ ...leadEntry, role: "teamlead", dir: ".", _lead: true });
1223
+ const leadEntry = manifest.orchestrator || manifest.teamlead || manifest.main || manifest.master; // pre-rename names
1224
+ if (leadEntry) roster.push({ ...leadEntry, role: "orchestrator", dir: ".", _lead: true });
1167
1225
  for (const a of (manifest.agents || [])) roster.push(a);
1168
1226
 
1169
1227
  const rows = [];
@@ -3013,5 +3071,5 @@ if (cmd === "skill") {
3013
3071
  process.exit(0);
3014
3072
  }
3015
3073
 
3016
- console.error(`Unknown command: ${cmd}. Available: install, whoami, agents, upload, subscribe, update, login, provision, pull, teamlead, team, schedule, --rename, --token, --agent-type, --version, --help`);
3074
+ console.error(`Unknown command: ${cmd}. Available: install, whoami, agents, upload, subscribe, update, login, provision, pull, orchestrator, team, schedule, --rename, --token, --agent-type, --version, --help`);
3017
3075
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.5.116",
3
+ "version": "0.5.118",
4
4
  "description": "Cross-machine agent messaging for Claude Code and Codex",
5
5
  "author": "ppravdin",
6
6
  "license": "MIT",
@@ -18,23 +18,28 @@ INPUT=$(cat)
18
18
  STOP_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false' 2>/dev/null || echo "false")
19
19
  [ "$STOP_ACTIVE" = "true" ] && exit 0
20
20
 
21
- # Resolve MCP config: per-project first, then global
21
+ # Resolve MCP config: PROJECT-scoped ONLY (walk up from cwd for .kimi/mcp.json).
22
+ # NO global ~/.kimi/mcp.json fallback — a Kimi launched outside a patchcord
23
+ # worktree is not that agent and must not be nagged. No MCP authed in the cwd
24
+ # tree => no inbox check. (This Stop hook is registered globally in
25
+ # ~/.kimi/config.toml, so the project-scoped token is the ONLY thing that should
26
+ # make it act.)
22
27
  KIMI_MCP=""
23
28
  CWD=$(echo "$INPUT" | jq -r '.cwd // empty' 2>/dev/null || echo "")
24
29
 
25
30
  if [ -n "$CWD" ]; then
26
31
  dir="$CWD"
27
32
  while [ "$dir" != "/" ]; do
28
- if [ -f "$dir/.kimi/mcp.json" ]; then
29
- KIMI_MCP="$dir/.kimi/mcp.json"
33
+ if [ -f "$dir/.kimi-code/mcp.json" ]; then
34
+ KIMI_MCP="$dir/.kimi-code/mcp.json"
30
35
  break
31
36
  fi
32
37
  dir=$(dirname "$dir")
33
38
  done
34
39
  fi
35
40
 
36
- # Fallback to global config
37
- [ -z "$KIMI_MCP" ] && KIMI_MCP="${HOME}/.kimi/mcp.json"
41
+ # No project-scoped .kimi-code/mcp.json in the cwd tree => not a patchcord agent here.
42
+ [ -z "$KIMI_MCP" ] && exit 0
38
43
 
39
44
  TOKEN=""
40
45
  URL=""
@@ -14,25 +14,26 @@ set -euo pipefail
14
14
 
15
15
  command -v jq >/dev/null 2>&1 || { echo "jq required" >&2; exit 1; }
16
16
 
17
- # Resolve MCP config: per-project first, then global
17
+ # Resolve MCP config: PROJECT-scoped ONLY (walk up from cwd for .kimi-code/mcp.json).
18
+ # Kimi Code (the current product) uses .kimi-code/ — the legacy .kimi/ path is
19
+ # deprecated and MUST NOT be used; it also mismatched where `patchcord provision`
20
+ # writes the config (.kimi-code/mcp.json), so the listener never found it.
21
+ # NO global fallback: auto-launched by the (global) SessionStart hook on EVERY
22
+ # Kimi session, so a global fallback would start a listener for some leftover
23
+ # identity in every folder. No project config in the cwd tree => no patchcord
24
+ # agent here => exit quietly (0, not 1: must not spam stderr in non-patchcord dirs).
18
25
  KIMI_MCP=""
19
26
  dir="$PWD"
20
27
  while [ "$dir" != "/" ]; do
21
- if [ -f "$dir/.kimi/mcp.json" ]; then
22
- KIMI_MCP="$dir/.kimi/mcp.json"
28
+ if [ -f "$dir/.kimi-code/mcp.json" ]; then
29
+ KIMI_MCP="$dir/.kimi-code/mcp.json"
23
30
  break
24
31
  fi
25
32
  dir=$(dirname "$dir")
26
33
  done
27
34
 
28
- # Fallback to global config
29
- if [ -z "$KIMI_MCP" ]; then
30
- KIMI_MCP="${HOME}/.kimi/mcp.json"
31
- fi
32
-
33
- if [ ! -f "$KIMI_MCP" ]; then
34
- echo "Kimi MCP config not found at $KIMI_MCP or any parent — run npx patchcord@latest first" >&2
35
- exit 1
35
+ if [ -z "$KIMI_MCP" ] || [ ! -f "$KIMI_MCP" ]; then
36
+ exit 0
36
37
  fi
37
38
 
38
39
  TOKEN=$(jq -r '.mcpServers.patchcord.headers.Authorization // empty' "$KIMI_MCP" 2>/dev/null | sed 's/^Bearer //i' || true)