patchcord 0.5.129 → 0.5.131
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/.claude-plugin/plugin.json +1 -1
- package/bin/patchcord.mjs +151 -64
- package/package.json +1 -1
- package/skills/inbox/SKILL.md +4 -1
package/bin/patchcord.mjs
CHANGED
|
@@ -74,58 +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
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
patchcord
|
|
88
|
-
patchcord --
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
patchcord
|
|
94
|
-
patchcord
|
|
95
|
-
patchcord
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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:
|
|
103
|
+
patchcord orchestrator [--namespace <ns>] [--tool <harness>] [--project <name>]
|
|
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)
|
|
108
112
|
patchcord pull <agent> --tool <X> --namespace <ns> [--dir <sub/>]
|
|
109
|
-
|
|
110
|
-
(same identity, new token
|
|
113
|
+
move an existing agent into a folder
|
|
114
|
+
(same identity, new token)
|
|
111
115
|
patchcord provision revoke <agent> --namespace <ns>
|
|
112
|
-
|
|
113
|
-
patchcord team
|
|
114
|
-
patchcord team
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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>)
|
|
119
132
|
[--timezone <tz>] [--thread <slug>] [--max-runs N] [--expires <ISO>]
|
|
120
133
|
patchcord schedule list [--namespace <ns>]
|
|
121
134
|
patchcord schedule cancel|test|pause|resume <id>
|
|
122
135
|
|
|
123
|
-
|
|
124
|
-
patchcord update
|
|
125
|
-
patchcord --version
|
|
126
|
-
patchcord --help
|
|
136
|
+
misc:
|
|
137
|
+
patchcord update update to the latest version
|
|
138
|
+
patchcord --version show installed version
|
|
139
|
+
patchcord --help show this help
|
|
127
140
|
|
|
128
|
-
|
|
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
|
+
`);
|
|
129
147
|
process.exit(0);
|
|
130
148
|
}
|
|
131
149
|
|
|
@@ -845,7 +863,7 @@ if (cmd === "subscribe") {
|
|
|
845
863
|
// account token (user-level, tied to NO agent) lives at ~/.patchcord/auth.json
|
|
846
864
|
// (legacy: main.json / master.json) or $PATCHCORD_TOKEN (legacy: *_MAIN/MASTER).
|
|
847
865
|
// `patchcord login` authenticates; everything else logs in on demand.
|
|
848
|
-
if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "provision" || cmd === "pull" || cmd === "team" || cmd === "schedule") {
|
|
866
|
+
if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "provision" || cmd === "pull" || cmd === "team" || cmd === "schedule" || cmd === "namespace") {
|
|
849
867
|
const M = { cyan: "\x1b[36m", green: "\x1b[32m", dim: "\x1b[2m", rst: "\x1b[0m" };
|
|
850
868
|
const AUTH_CONFIG = join(HOME, ".patchcord", "auth.json");
|
|
851
869
|
const LEGACY_CONFIGS = [join(HOME, ".patchcord", "main.json"), join(HOME, ".patchcord", "master.json")];
|
|
@@ -990,6 +1008,15 @@ if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "
|
|
|
990
1008
|
o.mcp.patchcord = { type: "remote", url: `${baseUrl}/mcp`, headers: hdr };
|
|
991
1009
|
});
|
|
992
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 = { url: `${baseUrl}/mcp`, headers: hdr };
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
993
1020
|
if (tool === "antigravity" || tool === "agy") {
|
|
994
1021
|
// Antigravity CLI: PROJECT-scoped config at .agents/mcp_config.json (one
|
|
995
1022
|
// project = one namespace = one agent). Remote HTTP uses `serverUrl`.
|
|
@@ -1042,7 +1069,7 @@ if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "
|
|
|
1042
1069
|
|
|
1043
1070
|
if (cmd === "provision") {
|
|
1044
1071
|
const arg = process.argv[3];
|
|
1045
|
-
if (!arg || arg.startsWith("-")) { console.error("Usage: patchcord provision <agent> --tool X --role Y --namespace ns [--dir sub/]"); process.exit(1); }
|
|
1072
|
+
if (!arg || arg.startsWith("-")) { console.error("Usage: patchcord provision <agent> --tool X --role Y --namespace ns [--dir sub/] [--project name] [--lead]"); process.exit(1); }
|
|
1046
1073
|
if (arg === "revoke") {
|
|
1047
1074
|
const ra = process.argv[4];
|
|
1048
1075
|
const ns = flagVal("namespace");
|
|
@@ -1056,8 +1083,20 @@ if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "
|
|
|
1056
1083
|
const role = flagVal("role", "");
|
|
1057
1084
|
const ns = flagVal("namespace");
|
|
1058
1085
|
const subdir = flagVal("dir", arg);
|
|
1086
|
+
const project = flagVal("project", "");
|
|
1087
|
+
// --lead marks this token as eligible for cross-namespace lead chat (server
|
|
1088
|
+
// trusts ONLY this explicit, main-token-authenticated request — see
|
|
1089
|
+
// patchcord-cloud's api_provision). Reaching another namespace's lead also
|
|
1090
|
+
// requires an approved namespace_links pair (via request_namespace_link /
|
|
1091
|
+
// respond_namespace_link — no CLI, agent-to-agent only). `--role lead` also
|
|
1092
|
+
// sets is_lead server-side (role==="lead" is treated the same), but `--lead`
|
|
1093
|
+
// is the explicit, discoverable way to ask for it without overloading the
|
|
1094
|
+
// free-text --role field.
|
|
1095
|
+
const lead = process.argv.includes("--lead");
|
|
1059
1096
|
if (!ns) { console.error("--namespace <project-namespace> required"); process.exit(1); }
|
|
1060
|
-
const
|
|
1097
|
+
const provisionBody = { namespace_id: ns, agent_id: arg, tool, role, label: `main:${tool}` };
|
|
1098
|
+
if (lead) provisionBody.is_lead = true;
|
|
1099
|
+
const { status, json, m } = await accountCall("POST", "/api/provision", provisionBody);
|
|
1061
1100
|
if (status !== "200" || !json?.token) { console.error(`provision failed (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
|
|
1062
1101
|
const base = String(json.url || `${m.baseUrl}/mcp`).replace(/\/mcp$/, "");
|
|
1063
1102
|
const dir = join(process.cwd(), subdir);
|
|
@@ -1076,10 +1115,48 @@ if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "
|
|
|
1076
1115
|
writeFileSync(tj, JSON.stringify(man, null, 2) + "\n");
|
|
1077
1116
|
}
|
|
1078
1117
|
} catch {}
|
|
1079
|
-
|
|
1118
|
+
// --project assigns this namespace to a project — an organizational label
|
|
1119
|
+
// ONLY, does not affect cross-namespace messaging (that's namespace_links,
|
|
1120
|
+
// agent-to-agent, no CLI). Separate call: provisioning creates the agent
|
|
1121
|
+
// identity, project assignment groups the NAMESPACE — safe to retry
|
|
1122
|
+
// independently, and a namespace already in a project just gets reassigned
|
|
1123
|
+
// (last call wins, server-side upsert).
|
|
1124
|
+
if (project) {
|
|
1125
|
+
const { status: pStatus, json: pJson } = await accountCall("POST", "/api/namespace/set-project", { namespace_id: ns, project });
|
|
1126
|
+
if (pStatus !== "200") {
|
|
1127
|
+
console.error(` ${M.dim}warning: provisioned OK, but --project failed (HTTP ${pStatus}): ${pJson?.error || ""}${M.rst}`);
|
|
1128
|
+
} else {
|
|
1129
|
+
console.log(` ${M.dim}namespace ${ns} assigned to project "${project}"${M.rst}`);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
console.log(`✓ provisioned ${M.green}${ns}:${arg}${M.rst} [${tool}${role ? "/" + role : ""}${lead ? "/lead" : ""}] → ${dir}`);
|
|
1080
1133
|
process.exit(0);
|
|
1081
1134
|
}
|
|
1082
1135
|
|
|
1136
|
+
if (cmd === "namespace") {
|
|
1137
|
+
// Cloud-only: group namespaces into a "project" — a free-text organizational
|
|
1138
|
+
// label only. Does NOT gate cross-namespace lead chat — that's decided by
|
|
1139
|
+
// per-pair namespace_links, approved lead-to-lead via the MCP tools
|
|
1140
|
+
// request_namespace_link / respond_namespace_link (no CLI for that; see
|
|
1141
|
+
// --help's CROSS-NAMESPACE LINKS section).
|
|
1142
|
+
const sub = process.argv[3];
|
|
1143
|
+
if (sub === "set-project") {
|
|
1144
|
+
const ns = process.argv[4];
|
|
1145
|
+
const project = process.argv[5];
|
|
1146
|
+
if (!ns) { console.error("Usage: patchcord namespace set-project <namespace> [project-name]\n Omit project-name to clear the label."); process.exit(1); }
|
|
1147
|
+
const { status, json } = await accountCall("POST", "/api/namespace/set-project", { namespace_id: ns, project: project || "" });
|
|
1148
|
+
if (status !== "200") { console.error(`set-project failed (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
|
|
1149
|
+
if (json.status === "cleared") {
|
|
1150
|
+
console.log(`✓ ${ns} un-grouped — no project label`);
|
|
1151
|
+
} else {
|
|
1152
|
+
console.log(`✓ ${ns} assigned to project "${json.project}" (label only — does not affect cross-namespace messaging)`);
|
|
1153
|
+
}
|
|
1154
|
+
process.exit(0);
|
|
1155
|
+
}
|
|
1156
|
+
console.error("Usage: patchcord namespace set-project <namespace> [project-name]");
|
|
1157
|
+
process.exit(1);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1083
1160
|
if (cmd === "pull") {
|
|
1084
1161
|
// Inverse of provision: place an EXISTING agent identity into a folder.
|
|
1085
1162
|
// Mints another token bound to the same agent_id@namespace (its original
|
|
@@ -1123,11 +1200,25 @@ if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "
|
|
|
1123
1200
|
const root = process.cwd();
|
|
1124
1201
|
const ns = (flagVal("namespace", basename(root)) || "team").replace(/[^a-z0-9-]/gi, "-").toLowerCase();
|
|
1125
1202
|
const tool = flagVal("tool", "claude_code");
|
|
1203
|
+
const project = flagVal("project", "");
|
|
1126
1204
|
const hostname = run("hostname -s") || run("hostname") || "unknown";
|
|
1127
|
-
|
|
1205
|
+
// is_lead: true always — the orchestrator IS the one identity per namespace
|
|
1206
|
+
// meant to coordinate across teams. Harmless by itself: a lead only reaches
|
|
1207
|
+
// another namespace's lead after an approved namespace_links pair (agent-to-
|
|
1208
|
+
// agent via request_namespace_link/respond_namespace_link — fails closed
|
|
1209
|
+
// until both leads explicitly consent).
|
|
1210
|
+
const { status, json, m } = await accountCall("POST", "/api/provision", { namespace_id: ns, agent_id: "orchestrator", tool, role: "orchestrator", label: "orchestrator:self", is_lead: true });
|
|
1128
1211
|
if (status !== "200" || !json?.token) { console.error(`could not set up orchestrator (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
|
|
1129
1212
|
const base = String(json.url || `${m.baseUrl}/mcp`).replace(/\/mcp$/, "");
|
|
1130
1213
|
writeWorkerConfig(tool, root, base, json.token, hostname);
|
|
1214
|
+
if (project) {
|
|
1215
|
+
const { status: pStatus, json: pJson } = await accountCall("POST", "/api/namespace/set-project", { namespace_id: ns, project });
|
|
1216
|
+
if (pStatus !== "200") {
|
|
1217
|
+
console.error(` ${M.dim}warning: orchestrator set up OK, but --project failed (HTTP ${pStatus}): ${pJson?.error || ""}${M.rst}`);
|
|
1218
|
+
} else {
|
|
1219
|
+
console.log(` ${M.dim}namespace ${ns} assigned to project "${project}"${M.rst}`);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1131
1222
|
mkdirSync(join(root, ".patchcord"), { recursive: true });
|
|
1132
1223
|
const tj = join(root, ".patchcord", "team.json");
|
|
1133
1224
|
let manifest = { project: basename(root), namespace: ns, orchestrator: { agent: "orchestrator", tool }, agents: [] };
|
|
@@ -2068,21 +2159,17 @@ if (!cmd || cmd === "install" || cmd === "agent" || cmd?.startsWith("--")) {
|
|
|
2068
2159
|
|| (flags.includes("--token") ? flags[flags.indexOf("--token") + 1] : "");
|
|
2069
2160
|
|
|
2070
2161
|
if (tokenFlag) {
|
|
2071
|
-
// --token bypass
|
|
2072
|
-
//
|
|
2162
|
+
// --token bypass requires --tool=<slug> up front — no terminal picker.
|
|
2163
|
+
// The web connect page (patchcord.dev/console/connect/<platform>) is
|
|
2164
|
+
// where the tool is chosen; it hands back the full npx command with
|
|
2165
|
+
// --tool=<slug> already baked in. If someone lands here without one,
|
|
2166
|
+
// point them at the web flow instead of prompting.
|
|
2073
2167
|
if (!choice) {
|
|
2074
|
-
console.
|
|
2075
|
-
console.
|
|
2076
|
-
console.
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
console.log(` ${cyan}13.${r} Hermes\n`);
|
|
2080
|
-
choice = (await ask(`${dim}Choose (1-13):${r} `)).trim();
|
|
2081
|
-
if (!["1","2","3","4","5","6","7","8","9","10","11","12","13"].includes(choice)) {
|
|
2082
|
-
console.error("Invalid choice.");
|
|
2083
|
-
rl.close();
|
|
2084
|
-
process.exit(1);
|
|
2085
|
-
}
|
|
2168
|
+
console.error(`${bold}--tool=<slug> is required with --token.${r}`);
|
|
2169
|
+
console.error(`${dim}Pick your tool at https://patchcord.dev/console/connect — it gives you the exact command.${r}`);
|
|
2170
|
+
console.error(`${dim}Valid slugs: ${Object.keys(CLIENT_TYPE_MAP).join(", ")}${r}`);
|
|
2171
|
+
rl.close();
|
|
2172
|
+
process.exit(1);
|
|
2086
2173
|
}
|
|
2087
2174
|
token = tokenFlag.trim();
|
|
2088
2175
|
if (!isSafeToken(token)) {
|
package/package.json
CHANGED
package/skills/inbox/SKILL.md
CHANGED
|
@@ -182,7 +182,10 @@ Named threads group related messages between a pair of agents. Use them for mult
|
|
|
182
182
|
- Never ask "should I do this?" - just do it. User can undo in 3 seconds.
|
|
183
183
|
- Never ask "want me to wait?" - check presence and wait or don't based on that.
|
|
184
184
|
- Never show raw JSON to the human - summarize naturally.
|
|
185
|
-
- Cross-namespace
|
|
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
|
+
- **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 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
|
+
- A rejection here is almost always correct behavior, not a server error — don't retry with variations of the name.
|
|
186
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.
|
|
187
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.
|
|
188
191
|
- **When you receive an ack**, close it silently: `reply(id, resolve=true)`. No content. This stops the chain.
|