patchcord 0.5.108 → 0.5.110

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 +38 -20
  2. package/package.json +1 -1
package/bin/patchcord.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { existsSync, mkdirSync, cpSync, readdirSync, readFileSync, writeFileSync } from "fs";
3
+ import { existsSync, mkdirSync, cpSync, readdirSync, readFileSync, writeFileSync, unlinkSync } from "fs";
4
4
  import { join, dirname, basename } from "path";
5
5
  import { fileURLToPath } from "url";
6
6
  import { execSync } from "child_process";
@@ -834,17 +834,26 @@ if (cmd === "login" || cmd === "teamlead" || cmd === "provision" || cmd === "tea
834
834
  const LEGACY_CONFIGS = [join(HOME, ".patchcord", "main.json"), join(HOME, ".patchcord", "master.json")];
835
835
  const DEFAULT_API = process.env.PATCHCORD_BASE_URL || "https://api.patchcord.dev";
836
836
 
837
+ // Account tokens are always `pcm-` prefixed. Anything else (e.g. a stale
838
+ // worker token `pct-...` or a legacy bearer in $PATCHCORD_TOKEN) is NOT an
839
+ // account credential — ignore it so auth-on-demand falls through to login
840
+ // instead of feeding junk to /api/* and hard-failing with 401.
841
+ const isAccountToken = (t) => typeof t === "string" && t.startsWith("pcm-");
837
842
  const readAuth = () => {
838
843
  const envTok = process.env.PATCHCORD_TOKEN || process.env.PATCHCORD_MAIN_TOKEN || process.env.PATCHCORD_MASTER_TOKEN;
839
- if (envTok) return { token: envTok, baseUrl: DEFAULT_API };
844
+ if (envTok && isAccountToken(envTok)) return { token: envTok, baseUrl: DEFAULT_API };
840
845
  for (const p of [AUTH_CONFIG, ...LEGACY_CONFIGS]) {
841
846
  try {
842
847
  const m = JSON.parse(readFileSync(p, "utf-8"));
843
- if (m && m.token) return { token: m.token, baseUrl: m.baseUrl || DEFAULT_API };
848
+ if (m && isAccountToken(m.token)) return { token: m.token, baseUrl: m.baseUrl || DEFAULT_API };
844
849
  } catch {}
845
850
  }
846
851
  return null;
847
852
  };
853
+ // Drop a stale stored account token so the next requireAuth re-logs-in.
854
+ const forgetAuth = () => {
855
+ for (const p of [AUTH_CONFIG, ...LEGACY_CONFIGS]) { try { unlinkSync(p); } catch {} }
856
+ };
848
857
  const flagVal = (name, def = "") => {
849
858
  const eq = process.argv.find((a) => a.startsWith(`--${name}=`));
850
859
  if (eq) return eq.split("=").slice(1).join("=");
@@ -855,11 +864,11 @@ if (cmd === "login" || cmd === "teamlead" || cmd === "provision" || cmd === "tea
855
864
  // Browser login → saves + returns the account token. Authenticates the CLI;
856
865
  // not related to any agent.
857
866
  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 '{}'`);
867
+ const create = run(`curl -sf --max-time 10 -X POST "${DEFAULT_API}/api/login/session" -H "Content-Type: application/json" -d '{}'`);
859
868
  let sessionId = "";
860
869
  try { sessionId = (JSON.parse(create).session_id) || ""; } catch {}
861
870
  if (!sessionId) { console.error("Could not start login session."); process.exit(1); }
862
- const url = `https://patchcord.dev/master?session=${sessionId}`;
871
+ const url = `https://patchcord.dev/connect/cli?session=${sessionId}`;
863
872
  console.log(`\n Log in to patchcord in your browser:\n ${M.cyan}${url}${M.rst}\n`);
864
873
  console.log(` ${M.dim}Waiting...${M.rst}`);
865
874
  const http = await import("http");
@@ -890,6 +899,19 @@ if (cmd === "login" || cmd === "teamlead" || cmd === "provision" || cmd === "tea
890
899
  };
891
900
  // Auth on demand: any command needing the account logs in if not already.
892
901
  const requireAuth = async () => readAuth() || await doLogin();
902
+ // Account-authed request with stale-token recovery. If the server rejects the
903
+ // stored token (401/403), forget it, re-log-in once, and retry. Returns the
904
+ // _httpJSON result plus the auth used (for callers that need m.baseUrl).
905
+ const accountCall = async (method, path, body) => {
906
+ let m = await requireAuth();
907
+ let r = await _httpJSON(method, `${m.baseUrl}${path}`, m.token, body);
908
+ if (r.status === "401" || r.status === "403") {
909
+ forgetAuth();
910
+ m = await doLogin();
911
+ r = await _httpJSON(method, `${m.baseUrl}${path}`, m.token, body);
912
+ }
913
+ return { ...r, m };
914
+ };
893
915
  const writeWorkerConfig = (tool, dir, baseUrl, token, hostname) => {
894
916
  mkdirSync(dir, { recursive: true });
895
917
  const hdr = { Authorization: `Bearer ${token}`, "X-Patchcord-Machine": hostname };
@@ -937,14 +959,13 @@ if (cmd === "login" || cmd === "teamlead" || cmd === "provision" || cmd === "tea
937
959
  }
938
960
 
939
961
  if (cmd === "provision") {
940
- const m = await requireAuth();
941
962
  const arg = process.argv[3];
942
963
  if (!arg || arg.startsWith("-")) { console.error("Usage: patchcord provision <agent> --tool X --role Y --namespace ns [--dir sub/]"); process.exit(1); }
943
964
  if (arg === "revoke") {
944
965
  const ra = process.argv[4];
945
966
  const ns = flagVal("namespace");
946
967
  if (!ra || !ns) { console.error("Usage: patchcord provision revoke <agent> --namespace ns"); process.exit(1); }
947
- const { status, json } = await _httpJSON("POST", `${m.baseUrl}/api/provision/revoke`, m.token, { namespace_id: ns, agent_id: ra });
968
+ const { status, json } = await accountCall("POST", "/api/provision/revoke", { namespace_id: ns, agent_id: ra });
948
969
  if (status !== "200") { console.error(`revoke failed (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
949
970
  console.log(`✓ revoked ${ns}:${ra} (${json.count} token(s))`);
950
971
  process.exit(0);
@@ -954,7 +975,7 @@ if (cmd === "login" || cmd === "teamlead" || cmd === "provision" || cmd === "tea
954
975
  const ns = flagVal("namespace");
955
976
  const subdir = flagVal("dir", arg);
956
977
  if (!ns) { console.error("--namespace <project-namespace> required"); process.exit(1); }
957
- const { status, json } = await _httpJSON("POST", `${m.baseUrl}/api/provision`, m.token, { namespace_id: ns, agent_id: arg, tool, role, label: `main:${tool}` });
978
+ const { status, json, m } = await accountCall("POST", "/api/provision", { namespace_id: ns, agent_id: arg, tool, role, label: `main:${tool}` });
958
979
  if (status !== "200" || !json?.token) { console.error(`provision failed (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
959
980
  const base = String(json.url || `${m.baseUrl}/mcp`).replace(/\/mcp$/, "");
960
981
  const dir = join(process.cwd(), subdir);
@@ -983,12 +1004,11 @@ if (cmd === "login" || cmd === "teamlead" || cmd === "provision" || cmd === "tea
983
1004
  // other agents. It is NOT a normal agent: it gets its own identity, its own
984
1005
  // onboarding instruction, and on launch either ADOPTS this existing project
985
1006
  // or CREATES a new team by interviewing the user.
986
- const m = await requireAuth();
987
1007
  const root = process.cwd();
988
1008
  const ns = (flagVal("namespace", basename(root)) || "team").replace(/[^a-z0-9-]/gi, "-").toLowerCase();
989
1009
  const tool = flagVal("tool", "claude_code");
990
1010
  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" });
1011
+ const { status, json, m } = await accountCall("POST", "/api/provision", { namespace_id: ns, agent_id: "teamlead", tool, role: "teamlead", label: "teamlead:self" });
992
1012
  if (status !== "200" || !json?.token) { console.error(`could not set up teamlead (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
993
1013
  const base = String(json.url || `${m.baseUrl}/mcp`).replace(/\/mcp$/, "");
994
1014
  writeWorkerConfig(tool, root, base, json.token, hostname);
@@ -1025,8 +1045,7 @@ you design the team, provision its agents, launch them, and manage them.
1025
1045
  if (cmd === "team") {
1026
1046
  const sub = process.argv[3];
1027
1047
  if (sub === "list") {
1028
- const m = await requireAuth();
1029
- const { status, json } = await _httpJSON("GET", `${m.baseUrl}/api/provision/list`, m.token);
1048
+ const { status, json } = await accountCall("GET", "/api/provision/list");
1030
1049
  if (status !== "200") { console.error(`list failed (HTTP ${status})`); process.exit(1); }
1031
1050
  // Dedup by namespace:agent — the server returns one row per token, so a
1032
1051
  // re-provisioned agent shows up many times. Optional --namespace filter.
@@ -1184,16 +1203,15 @@ you design the team, provision its agents, launch them, and manage them.
1184
1203
  if (cmd === "schedule") {
1185
1204
  // User-level scheduled / recurring messages (server Plan 041). Authed by
1186
1205
  // the main token — the user can manage schedules in any namespace they own.
1187
- const m = await requireAuth();
1188
1206
  const sub = process.argv[3];
1189
- const BASE = `${m.baseUrl}/api/dashboard/scheduled`;
1207
+ const BASE = `/api/dashboard/scheduled`;
1190
1208
  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;
1191
1209
  const fmt = (s) => `${s.id} ${M.green}${s.namespace_id}:${s.to_agent}${M.rst} ${s.active ? "" : M.dim + "[paused] " + M.rst}${s.name} ${M.dim}${when(s)}${s.next_fire_at ? ` → ${s.next_fire_at}` : ""}${M.rst}`;
1192
1210
 
1193
1211
  if (sub === "list") {
1194
1212
  const ns = flagVal("namespace");
1195
1213
  const url = ns ? `${BASE}?namespace=${encodeURIComponent(ns)}` : BASE;
1196
- const { status, json } = await _httpJSON("GET", url, m.token);
1214
+ const { status, json } = await accountCall("GET", url);
1197
1215
  if (status !== "200") { console.error(`list failed (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
1198
1216
  const items = json?.schedules || [];
1199
1217
  for (const s of items) console.log(" " + fmt(s));
@@ -1215,7 +1233,7 @@ you design the team, provision its agents, launch them, and manage them.
1215
1233
  const thread = flagVal("thread"); if (thread) body.thread_slug = thread;
1216
1234
  const maxRuns = flagVal("max-runs"); if (maxRuns) body.max_runs = parseInt(maxRuns, 10);
1217
1235
  const expires = flagVal("expires"); if (expires) body.expires_at = expires;
1218
- const { status, json } = await _httpJSON("POST", BASE, m.token, body);
1236
+ const { status, json } = await accountCall("POST", BASE, body);
1219
1237
  if (status !== "201" && status !== "200") { console.error(`create failed (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
1220
1238
  console.log(`✓ scheduled ${M.green}${ns}:${to}${M.rst} [${schedule_kind}] ${M.dim}${json?.schedule?.id || ""}${M.rst}`);
1221
1239
  process.exit(0);
@@ -1223,7 +1241,7 @@ you design the team, provision its agents, launch them, and manage them.
1223
1241
  if (sub === "cancel" || sub === "delete" || sub === "rm") {
1224
1242
  const id = process.argv[4];
1225
1243
  if (!id) { console.error("Usage: patchcord schedule cancel <id>"); process.exit(1); }
1226
- const { status, json } = await _httpJSON("DELETE", `${BASE}/${id}`, m.token);
1244
+ const { status, json } = await accountCall("DELETE", `${BASE}/${id}`);
1227
1245
  if (status !== "200") { console.error(`cancel failed (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
1228
1246
  console.log(`✓ cancelled ${id}`);
1229
1247
  process.exit(0);
@@ -1231,7 +1249,7 @@ you design the team, provision its agents, launch them, and manage them.
1231
1249
  if (sub === "test" || sub === "fire") {
1232
1250
  const id = process.argv[4];
1233
1251
  if (!id) { console.error("Usage: patchcord schedule test <id>"); process.exit(1); }
1234
- const { status, json } = await _httpJSON("POST", `${BASE}/${id}/test`, m.token, {});
1252
+ const { status, json } = await accountCall("POST", `${BASE}/${id}/test`, {});
1235
1253
  if (status !== "200") { console.error(`test failed (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
1236
1254
  console.log(`✓ fired ${id} once (does not consume max-runs)`);
1237
1255
  process.exit(0);
@@ -1239,7 +1257,7 @@ you design the team, provision its agents, launch them, and manage them.
1239
1257
  if (sub === "pause" || sub === "resume") {
1240
1258
  const id = process.argv[4];
1241
1259
  if (!id) { console.error(`Usage: patchcord schedule ${sub} <id>`); process.exit(1); }
1242
- const { status, json } = await _httpJSON("PATCH", `${BASE}/${id}`, m.token, { active: sub === "resume" });
1260
+ const { status, json } = await accountCall("PATCH", `${BASE}/${id}`, { active: sub === "resume" });
1243
1261
  if (status !== "200") { console.error(`${sub} failed (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
1244
1262
  console.log(`✓ ${sub === "resume" ? "resumed" : "paused"} ${id}`);
1245
1263
  process.exit(0);
@@ -2897,5 +2915,5 @@ if (cmd === "skill") {
2897
2915
  process.exit(0);
2898
2916
  }
2899
2917
 
2900
- console.error(`Unknown command: ${cmd}. Available: whoami, agents, upload, subscribe, update, --rename, --token, --agent-type, --version, --help`);
2918
+ console.error(`Unknown command: ${cmd}. Available: install, whoami, agents, upload, subscribe, update, login, provision, teamlead, team, schedule, --rename, --token, --agent-type, --version, --help`);
2901
2919
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.5.108",
3
+ "version": "0.5.110",
4
4
  "description": "Cross-machine agent messaging for Claude Code and Codex",
5
5
  "author": "ppravdin",
6
6
  "license": "MIT",