patchcord 0.5.130 → 0.5.132

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "patchcord",
3
3
  "description": "Cross-machine agent messaging. Messages from other agents land in the inbox and wake the agent to reply.",
4
- "version": "0.5.130",
4
+ "version": "0.5.132",
5
5
  "author": {
6
6
  "name": "ppravdin"
7
7
  },
package/README.md CHANGED
@@ -23,6 +23,9 @@ npx patchcord@latest --token <token> --server https://patchcord.yourdomain.com
23
23
  - **Stop hook** — checks inbox between turns, notifies of pending messages
24
24
  - **Slash commands** — `/patchcord` and `/patchcord-wait` for Codex and Gemini CLI
25
25
  - **MCP config** — per-project or global config depending on tool
26
+ - **Cursor CLI** — bearer-only MCP endpoint, `patchcord:*` pre-allowed in
27
+ `~/.cursor/permissions.json`, and inbox/wait skills installed when
28
+ `cursor-agent` is detected
26
29
  - **Kimi CLI** — global MCP config plus skills in `~/.kimi/skills/`
27
30
 
28
31
  ## How it works
package/bin/patchcord.mjs CHANGED
@@ -74,72 +74,76 @@ if (
74
74
  cmd === "help" || cmd === "--help" || cmd === "-h" ||
75
75
  process.argv.slice(3).some((a) => a === "--help" || a === "-h")
76
76
  ) {
77
- console.log(`patchcord cross-agent messaging + team orchestration for AI coding agents
78
-
79
- Two layers: THIS AGENT (a per-project identity, by bearer token) and your ACCOUNT
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).
83
-
84
- SETUP (per agent / project)
85
- patchcord Set up this project's agent (browser)
86
- patchcord --token <t> [--server <url>] Headless / self-hosted setup
87
- patchcord --full Same + full statusline
88
- patchcord --rename <name> [--agent-type <t>] Rename this agent
89
-
90
- THIS AGENT identity + messaging
91
- patchcord whoami Who am I — this agent's identity + project
92
- patchcord whoami --propose "<text>" Set your self-description (two-shot confirm)
93
- patchcord agents [name] List agents in your namespace (or show one)
94
- patchcord subscribe [interval] Start the realtime listener (Kimi: bg poll)
95
- patchcord upload <file> [--mime <t>] [--as <name>] Share a file as an attachment
96
-
97
- ACCOUNT log in to the CLI (commands below log in on demand)
98
- patchcord login Authenticate the CLI (your account)
99
-
100
- ORCHESTRATOR set up the orchestrator agent (the shepherd) in this folder
77
+ console.log(`usage: patchcord [command] [options]
78
+
79
+ Cross-agent messaging + team orchestration for AI coding agents.
80
+
81
+ Two layers: THIS AGENT (a per-project identity, by bearer token) and your
82
+ ACCOUNT (user-level provisions agents, runs teams + schedules). \`patchcord
83
+ login\` authenticates the CLI once; account/team/schedule commands then use
84
+ that token and never open a browser on their own.
85
+
86
+ setup:
87
+ patchcord set up this project's agent (browser)
88
+ patchcord --token <t> [--server <url>] headless / self-hosted setup
89
+ patchcord --full same, with full statusline
90
+ patchcord --rename <name> [--agent-type <t>] rename this agent
91
+
92
+ agent:
93
+ patchcord whoami this agent's identity + project
94
+ patchcord whoami --propose "<text>" set self-description (two-shot confirm)
95
+ patchcord agents [name] list agents in your namespace, or show one
96
+ patchcord subscribe [interval] start the realtime listener
97
+ patchcord upload <file> [--mime <t>] [--as <name>] share a file as an attachment
98
+
99
+ account:
100
+ patchcord login authenticate the CLI (your account)
101
+
102
+ orchestrator:
101
103
  patchcord orchestrator [--namespace <ns>] [--tool <harness>] [--project <name>]
102
- Provision the orchestrator here (always a
103
- lead — see PROJECTS below); launch it to
104
- adopt this project or build a new team
105
-
106
- TEAM — the orchestrator's tools (run from the project root)
107
- patchcord provision <agent> --tool <X> --role <Y> --namespace <ns> [--dir <sub/>] \\
108
- [--project <name>] [--lead]
109
- Create a worker agent (identity + config).
110
- --lead marks it as a cross-namespace lead
111
- (--role lead does the same); --project
112
- assigns its namespace (see PROJECTS below)
104
+ provision the orchestrator here (always
105
+ a lead); launch it to adopt this project
106
+ or build a new team
107
+
108
+ team (run from the project root):
109
+ patchcord provision <agent> --tool <X> --role <Y> --namespace <ns>
110
+ [--dir <sub/>] [--project <name>] [--lead]
111
+ create a worker agent (identity + config)
113
112
  patchcord pull <agent> --tool <X> --namespace <ns> [--dir <sub/>]
114
- Place an EXISTING agent into a folder
115
- (same identity, new token; nothing overwritten)
113
+ move an existing agent into a folder
114
+ (same identity, new token)
116
115
  patchcord provision revoke <agent> --namespace <ns>
117
- patchcord team list [--namespace <ns>] Provisioned agents (server view, deduped)
118
- patchcord team status Reconcile folder identity tmux ↔ token
119
- patchcord team launch Launch each worker in mux
120
-
121
- PROJECTS — cloud-only. Group your OWN namespaces so their leads may cross-message
122
- (same user + same project + BOTH ends provisioned as leads → agent@namespace works;
123
- anything else is always denied, no exceptions). Namespace names are unique across
124
- ALL your projects you can't reuse a name in a different project.
125
- patchcord namespace set-project <namespace> <project>
126
- Assign a namespace to a project
127
- patchcord namespace set-project <namespace>
128
- Clear it (namespace becomes isolated again)
129
-
130
- SCHEDULES — timed / recurring messages (user-level)
131
- patchcord schedule create <name> --namespace <ns> --to <agent> --content "..." \\
132
- (--at <ISO> | --cron "<expr>" | --every <sec>) \\
116
+ revoke a worker agent
117
+ patchcord team list [--namespace <ns>] provisioned agents (server view)
118
+ patchcord team status reconcile folder identity ↔ tmux ↔ token
119
+ patchcord team launch launch each worker in mux
120
+
121
+ projects (cloud-only a free-text label grouping your own namespaces):
122
+ patchcord namespace set-project <namespace> <project> assign a project label
123
+ patchcord namespace set-project <namespace> clear it
124
+
125
+ cross-namespace links (cloud-only, no CLI — leads request/approve via MCP:
126
+ request_namespace_link / respond_namespace_link / list_my_namespace_links.
127
+ Both namespaces need a --lead agent; links are opt-in, not project-based.)
128
+
129
+ schedules:
130
+ patchcord schedule create <name> --namespace <ns> --to <agent> --content "..."
131
+ (--at <ISO> | --cron "<expr>" | --every <sec>)
133
132
  [--timezone <tz>] [--thread <slug>] [--max-runs N] [--expires <ISO>]
134
133
  patchcord schedule list [--namespace <ns>]
135
134
  patchcord schedule cancel|test|pause|resume <id>
136
135
 
137
- MISC
138
- patchcord update Update to the latest version
139
- patchcord --version Show installed version
140
- patchcord --help Show this help
136
+ misc:
137
+ patchcord update update to the latest version
138
+ patchcord --version show installed version
139
+ patchcord --help show this help
141
140
 
142
- First install: npx patchcord@latest`);
141
+ examples:
142
+ patchcord provision codex --tool codex --role maker --namespace myproj
143
+ patchcord schedule create standup --namespace myproj --to lead \\
144
+ --content "status?" --cron "0 9 * * *"
145
+ patchcord team status
146
+ `);
143
147
  process.exit(0);
144
148
  }
145
149
 
@@ -1004,6 +1008,19 @@ if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "
1004
1008
  o.mcp.patchcord = { type: "remote", url: `${baseUrl}/mcp`, headers: hdr };
1005
1009
  });
1006
1010
  }
1011
+ if (tool === "cursor") {
1012
+ // Cursor IDE + Cursor CLI (cursor-agent) share the same project config —
1013
+ // same file, same schema (see patchcord.dev/console/connect writer).
1014
+ const cdir = join(dir, ".cursor"); mkdirSync(cdir, { recursive: true });
1015
+ return writeJson(join(cdir, "mcp.json"), (o) => {
1016
+ o.mcpServers = o.mcpServers || {};
1017
+ o.mcpServers.patchcord = {
1018
+ type: "streamable-http",
1019
+ url: `${baseUrl}/mcp/bearer`,
1020
+ headers: hdr,
1021
+ };
1022
+ });
1023
+ }
1007
1024
  if (tool === "antigravity" || tool === "agy") {
1008
1025
  // Antigravity CLI: PROJECT-scoped config at .agents/mcp_config.json (one
1009
1026
  // project = one namespace = one agent). Remote HTTP uses `serverUrl`.
@@ -1071,11 +1088,13 @@ if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "
1071
1088
  const ns = flagVal("namespace");
1072
1089
  const subdir = flagVal("dir", arg);
1073
1090
  const project = flagVal("project", "");
1074
- // --lead marks this token as a project-scoped cross-namespace lead (server
1091
+ // --lead marks this token as eligible for cross-namespace lead chat (server
1075
1092
  // trusts ONLY this explicit, main-token-authenticated request — see
1076
- // patchcord-cloud's api_provision). `--role lead` also sets it server-side
1077
- // (role==="lead" is treated the same as is_lead:true), but `--lead` is the
1078
- // explicit, discoverable way to ask for it without overloading the
1093
+ // patchcord-cloud's api_provision). Reaching another namespace's lead also
1094
+ // requires an approved namespace_links pair (via request_namespace_link /
1095
+ // respond_namespace_link no CLI, agent-to-agent only). `--role lead` also
1096
+ // sets is_lead server-side (role==="lead" is treated the same), but `--lead`
1097
+ // is the explicit, discoverable way to ask for it without overloading the
1079
1098
  // free-text --role field.
1080
1099
  const lead = process.argv.includes("--lead");
1081
1100
  if (!ns) { console.error("--namespace <project-namespace> required"); process.exit(1); }
@@ -1100,12 +1119,12 @@ if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "
1100
1119
  writeFileSync(tj, JSON.stringify(man, null, 2) + "\n");
1101
1120
  }
1102
1121
  } catch {}
1103
- // --project assigns this namespace to a project — the cross-namespace lead
1104
- // chat boundary (namespaces in the same project may link leads; different
1105
- // projects, or no project, never can). Separate call: provisioning creates
1106
- // the agent identity, project assignment groups the NAMESPACE — safe to
1107
- // retry independently, and a namespace already in a project just gets
1108
- // reassigned (last call wins, server-side upsert).
1122
+ // --project assigns this namespace to a project — an organizational label
1123
+ // ONLY, does not affect cross-namespace messaging (that's namespace_links,
1124
+ // agent-to-agent, no CLI). Separate call: provisioning creates the agent
1125
+ // identity, project assignment groups the NAMESPACE — safe to retry
1126
+ // independently, and a namespace already in a project just gets reassigned
1127
+ // (last call wins, server-side upsert).
1109
1128
  if (project) {
1110
1129
  const { status: pStatus, json: pJson } = await accountCall("POST", "/api/namespace/set-project", { namespace_id: ns, project });
1111
1130
  if (pStatus !== "200") {
@@ -1119,22 +1138,22 @@ if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "
1119
1138
  }
1120
1139
 
1121
1140
  if (cmd === "namespace") {
1122
- // Cloud-only: group namespaces into a "project" — the boundary
1123
- // cross-namespace lead chat is gated on. Same user + same project + both
1124
- // ends are provisioned leads (--lead / --role lead) their leads may
1125
- // message each other with `agent@namespace`. Different project, or no
1126
- // project, or a non-lead agent → always denied, no exceptions.
1141
+ // Cloud-only: group namespaces into a "project" — a free-text organizational
1142
+ // label only. Does NOT gate cross-namespace lead chat that's decided by
1143
+ // per-pair namespace_links, approved lead-to-lead via the MCP tools
1144
+ // request_namespace_link / respond_namespace_link (no CLI for that; see
1145
+ // --help's CROSS-NAMESPACE LINKS section).
1127
1146
  const sub = process.argv[3];
1128
1147
  if (sub === "set-project") {
1129
1148
  const ns = process.argv[4];
1130
1149
  const project = process.argv[5];
1131
- if (!ns) { console.error("Usage: patchcord namespace set-project <namespace> [project-name]\n Omit project-name to clear (un-groups the namespace, reverting it to isolated)."); process.exit(1); }
1150
+ if (!ns) { console.error("Usage: patchcord namespace set-project <namespace> [project-name]\n Omit project-name to clear the label."); process.exit(1); }
1132
1151
  const { status, json } = await accountCall("POST", "/api/namespace/set-project", { namespace_id: ns, project: project || "" });
1133
1152
  if (status !== "200") { console.error(`set-project failed (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
1134
1153
  if (json.status === "cleared") {
1135
- console.log(`✓ ${ns} un-grouped — no project, isolated (no cross-namespace lead peers)`);
1154
+ console.log(`✓ ${ns} un-grouped — no project label`);
1136
1155
  } else {
1137
- console.log(`✓ ${ns} assigned to project "${json.project}"`);
1156
+ console.log(`✓ ${ns} assigned to project "${json.project}" (label only — does not affect cross-namespace messaging)`);
1138
1157
  }
1139
1158
  process.exit(0);
1140
1159
  }
@@ -1188,8 +1207,10 @@ if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "
1188
1207
  const project = flagVal("project", "");
1189
1208
  const hostname = run("hostname -s") || run("hostname") || "unknown";
1190
1209
  // is_lead: true always — the orchestrator IS the one identity per namespace
1191
- // meant to coordinate across teams. Harmless without --project: a lead with
1192
- // no project assignment has zero cross-namespace peers (fails closed).
1210
+ // meant to coordinate across teams. Harmless by itself: a lead only reaches
1211
+ // another namespace's lead after an approved namespace_links pair (agent-to-
1212
+ // agent via request_namespace_link/respond_namespace_link — fails closed
1213
+ // until both leads explicitly consent).
1193
1214
  const { status, json, m } = await accountCall("POST", "/api/provision", { namespace_id: ns, agent_id: "orchestrator", tool, role: "orchestrator", label: "orchestrator:self", is_lead: true });
1194
1215
  if (status !== "200" || !json?.token) { console.error(`could not set up orchestrator (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
1195
1216
  const base = String(json.url || `${m.baseUrl}/mcp`).replace(/\/mcp$/, "");
@@ -1722,9 +1743,14 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
1722
1743
  }
1723
1744
  }
1724
1745
 
1725
- // Cursor
1746
+ // Cursor. Cursor CLI (cursor-agent) can be installed without creating the
1747
+ // IDE's skills directory, so detect the executable as well as the directory.
1748
+ // The bearer-only endpoint prevents Cursor from incorrectly starting OAuth
1749
+ // discovery when the installer has supplied a static bearer header.
1726
1750
  const cursorSkillsRoot = join(HOME, ".cursor", "skills-cursor");
1727
- if (existsSync(cursorSkillsRoot)) {
1751
+ const hasCursorAgent = run("which cursor-agent");
1752
+ if (hasCursorAgent || existsSync(cursorSkillsRoot)) {
1753
+ mkdirSync(cursorSkillsRoot, { recursive: true });
1728
1754
  const cursorSkillDir = join(cursorSkillsRoot, "patchcord");
1729
1755
  const cursorWaitDir = join(cursorSkillsRoot, "patchcord-wait");
1730
1756
  let cursorChanged = false;
@@ -1739,6 +1765,27 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
1739
1765
  cursorChanged = true;
1740
1766
  }
1741
1767
  if (cursorChanged) globalChanges.push("Cursor skills installed");
1768
+
1769
+ if (hasCursorAgent) {
1770
+ const cursorPermissionsPath = join(HOME, ".cursor", "permissions.json");
1771
+ let allowlistChanged = false;
1772
+ const permissionsOk = updateJsonConfig(cursorPermissionsPath, (obj) => {
1773
+ if (obj.mcpAllowlist === undefined) obj.mcpAllowlist = [];
1774
+ if (!Array.isArray(obj.mcpAllowlist)) {
1775
+ console.log(`\n ${yellow}⚠${r} Skipped Cursor MCP allowlist — ${cursorPermissionsPath} has a non-array mcpAllowlist.`);
1776
+ return;
1777
+ }
1778
+ if (!obj.mcpAllowlist.includes("patchcord:*")) {
1779
+ obj.mcpAllowlist.push("patchcord:*");
1780
+ allowlistChanged = true;
1781
+ }
1782
+ });
1783
+ if (!permissionsOk) {
1784
+ globalChanges.push("✗ Cursor permissions error");
1785
+ } else if (allowlistChanged) {
1786
+ globalChanges.push("Cursor MCP allowlist configured");
1787
+ }
1788
+ }
1742
1789
  }
1743
1790
 
1744
1791
  // Windsurf
@@ -2074,8 +2121,8 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
2074
2121
  }
2075
2122
  }
2076
2123
 
2077
- if (!hasClaude && !existsSync(codexConfig) && !hasKimi) {
2078
- console.log(`${dim}No Claude Code, Codex CLI, or Kimi Code detected — skipping global setup.${r}`);
2124
+ if (!hasClaude && !existsSync(codexConfig) && !hasKimi && !hasCursorAgent && !existsSync(cursorSkillsRoot)) {
2125
+ console.log(`${dim}No Claude Code, Codex CLI, Cursor, or Kimi Code detected — skipping global setup.${r}`);
2079
2126
  console.log(`${dim} Kimi Code: https://kimi.moonshot.cn/download${r}`);
2080
2127
  }
2081
2128
 
@@ -2142,21 +2189,17 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
2142
2189
  || (flags.includes("--token") ? flags[flags.indexOf("--token") + 1] : "");
2143
2190
 
2144
2191
  if (tokenFlag) {
2145
- // --token bypass: need tool picker in terminal unless --tool=<slug>
2146
- // already pre-selected one for us.
2192
+ // --token bypass requires --tool=<slug> up frontno terminal picker.
2193
+ // The web connect page (patchcord.dev/console/connect/<platform>) is
2194
+ // where the tool is chosen; it hands back the full npx command with
2195
+ // --tool=<slug> already baked in. If someone lands here without one,
2196
+ // point them at the web flow instead of prompting.
2147
2197
  if (!choice) {
2148
- console.log(`\n${bold}Which tool are you setting up?${r}\n`);
2149
- console.log(` ${cyan}1.${r} Claude Code ${cyan}5.${r} Gemini CLI ${cyan}9.${r} OpenClaw`);
2150
- console.log(` ${cyan}2.${r} Codex CLI ${cyan}6.${r} VS Code ${cyan}10.${r} Antigravity CLI`);
2151
- console.log(` ${cyan}3.${r} Cursor ${cyan}7.${r} Zed ${cyan}11.${r} Cline`);
2152
- console.log(` ${cyan}4.${r} Windsurf ${cyan}8.${r} OpenCode ${cyan}12.${r} Kimi Code`);
2153
- console.log(` ${cyan}13.${r} Hermes\n`);
2154
- choice = (await ask(`${dim}Choose (1-13):${r} `)).trim();
2155
- if (!["1","2","3","4","5","6","7","8","9","10","11","12","13"].includes(choice)) {
2156
- console.error("Invalid choice.");
2157
- rl.close();
2158
- process.exit(1);
2159
- }
2198
+ console.error(`${bold}--tool=<slug> is required with --token.${r}`);
2199
+ console.error(`${dim}Pick your tool at https://patchcord.dev/console/connect — it gives you the exact command.${r}`);
2200
+ console.error(`${dim}Valid slugs: ${Object.keys(CLIENT_TYPE_MAP).join(", ")}${r}`);
2201
+ rl.close();
2202
+ process.exit(1);
2160
2203
  }
2161
2204
  token = tokenFlag.trim();
2162
2205
  if (!isSafeToken(token)) {
@@ -2506,7 +2549,8 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
2506
2549
  const cursorConfig = {
2507
2550
  mcpServers: {
2508
2551
  patchcord: {
2509
- url: `${serverUrl}/mcp`,
2552
+ type: "streamable-http",
2553
+ url: `${serverUrl}/mcp/bearer`,
2510
2554
  headers: {
2511
2555
  Authorization: `Bearer ${token}`,
2512
2556
  "X-Patchcord-Machine": hostname,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.5.130",
3
+ "version": "0.5.132",
4
4
  "description": "Cross-machine agent messaging for Claude Code and Codex",
5
5
  "scripts": {
6
6
  "version": "node scripts/sync-plugin-version.mjs && git add .claude-plugin/plugin.json"
@@ -184,7 +184,7 @@ Named threads group related messages between a pair of agents. Use them for mult
184
184
  - Never show raw JSON to the human - summarize naturally.
185
185
  - **Cross-namespace addressing (`agent@namespace`)**: the syntax always exists in `send_message`/`reply`, but what it actually reaches depends on who you are:
186
186
  - **Ordinary agents (the default):** `agent@namespace` only ever works for YOUR OWN namespace — same as a bare name. Targeting any other namespace is rejected. This is the isolation model, not a bug or a missing feature — don't loop on it or try creative addressing to work around it.
187
- - **Lead agents (cloud only — a small number of specially-provisioned identities per namespace, usually the orchestrator):** may reach a peer lead in a DIFFERENT namespace, but only if a human explicitly grouped both namespaces into the same "project" (a cloud-side setting you cannot set yourself from inside a session) different project, or no project, is always rejected, no exceptions. If you don't know whether you're a lead, you're not one — this isn't discoverable by trial and error; ask the human.
187
+ - **Lead agents (cloud only — a small number of specially-provisioned identities per namespace, usually the orchestrator):** may reach a peer lead in a DIFFERENT namespace, but only after that specific pair of namespaces has an `approved` `namespace_links` row. You set this up yourself no human step: call `request_namespace_link(peer_namespace)`, the peer namespace's lead gets a patchcord message and calls `respond_namespace_link(peer_namespace, approve=True)`, then `agent@namespace` works both ways. Check `list_my_namespace_links()` to see pending/approved/denied status. If you don't know whether you're a lead, you're not one — this isn't discoverable by trial and error; ask the human. (Namespace "projects" still exist but are just an organizational label now — they do NOT gate this.)
188
188
  - A rejection here is almost always correct behavior, not a server error — don't retry with variations of the name.
189
189
  - **Do not reply to acks.** "ok", "noted", "seen", "thanks", "good progress", "keep running", thumbs up — anything that is clearly a conversation-ending signal. Just read them and move on. If you must close the thread, use `reply(id, resolve=true)` with NO content. Never send a text reply to an ack.
190
190
  - **resolve=true with ack-only content is an anti-pattern.** `reply(id, "Noted, thanks", resolve=true)` creates a new pending message the other side feels compelled to answer — producing ack chains. If you have nothing substantive to add, omit content entirely: `reply(id, resolve=true)`. Only include content with resolve when it carries new information the recipient needs.