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.
- package/bin/patchcord.mjs +112 -118
- 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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
patchcord
|
|
78
|
-
patchcord
|
|
79
|
-
patchcord
|
|
80
|
-
patchcord
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
patchcord
|
|
84
|
-
patchcord --
|
|
85
|
-
patchcord
|
|
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 === "
|
|
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
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
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"
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
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 =
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
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(`⚠
|
|
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
|
-
|
|
997
|
-
|
|
998
|
-
|
|
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];
|