patchcord 0.5.109 → 0.5.111

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 +40 -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("=");
@@ -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 };
@@ -922,10 +944,12 @@ if (cmd === "login" || cmd === "teamlead" || cmd === "provision" || cmd === "tea
922
944
  o.mcp.patchcord = { type: "remote", url: `${baseUrl}/mcp`, headers: hdr };
923
945
  });
924
946
  }
925
- // default: claude_code
947
+ // default: claude_code. type:"http" is REQUIRED — without it Claude Code
948
+ // defaults to stdio transport and rejects the entry ("command: expected
949
+ // string, received undefined").
926
950
  return writeJson(join(dir, ".mcp.json"), (o) => {
927
951
  o.mcpServers = o.mcpServers || {};
928
- o.mcpServers.patchcord = { url: `${baseUrl}/mcp`, headers: hdr };
952
+ o.mcpServers.patchcord = { type: "http", url: `${baseUrl}/mcp`, headers: hdr };
929
953
  });
930
954
  };
931
955
 
@@ -937,14 +961,13 @@ if (cmd === "login" || cmd === "teamlead" || cmd === "provision" || cmd === "tea
937
961
  }
938
962
 
939
963
  if (cmd === "provision") {
940
- const m = await requireAuth();
941
964
  const arg = process.argv[3];
942
965
  if (!arg || arg.startsWith("-")) { console.error("Usage: patchcord provision <agent> --tool X --role Y --namespace ns [--dir sub/]"); process.exit(1); }
943
966
  if (arg === "revoke") {
944
967
  const ra = process.argv[4];
945
968
  const ns = flagVal("namespace");
946
969
  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 });
970
+ const { status, json } = await accountCall("POST", "/api/provision/revoke", { namespace_id: ns, agent_id: ra });
948
971
  if (status !== "200") { console.error(`revoke failed (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
949
972
  console.log(`✓ revoked ${ns}:${ra} (${json.count} token(s))`);
950
973
  process.exit(0);
@@ -954,7 +977,7 @@ if (cmd === "login" || cmd === "teamlead" || cmd === "provision" || cmd === "tea
954
977
  const ns = flagVal("namespace");
955
978
  const subdir = flagVal("dir", arg);
956
979
  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}` });
980
+ const { status, json, m } = await accountCall("POST", "/api/provision", { namespace_id: ns, agent_id: arg, tool, role, label: `main:${tool}` });
958
981
  if (status !== "200" || !json?.token) { console.error(`provision failed (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
959
982
  const base = String(json.url || `${m.baseUrl}/mcp`).replace(/\/mcp$/, "");
960
983
  const dir = join(process.cwd(), subdir);
@@ -983,12 +1006,11 @@ if (cmd === "login" || cmd === "teamlead" || cmd === "provision" || cmd === "tea
983
1006
  // other agents. It is NOT a normal agent: it gets its own identity, its own
984
1007
  // onboarding instruction, and on launch either ADOPTS this existing project
985
1008
  // or CREATES a new team by interviewing the user.
986
- const m = await requireAuth();
987
1009
  const root = process.cwd();
988
1010
  const ns = (flagVal("namespace", basename(root)) || "team").replace(/[^a-z0-9-]/gi, "-").toLowerCase();
989
1011
  const tool = flagVal("tool", "claude_code");
990
1012
  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" });
1013
+ const { status, json, m } = await accountCall("POST", "/api/provision", { namespace_id: ns, agent_id: "teamlead", tool, role: "teamlead", label: "teamlead:self" });
992
1014
  if (status !== "200" || !json?.token) { console.error(`could not set up teamlead (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
993
1015
  const base = String(json.url || `${m.baseUrl}/mcp`).replace(/\/mcp$/, "");
994
1016
  writeWorkerConfig(tool, root, base, json.token, hostname);
@@ -1025,8 +1047,7 @@ you design the team, provision its agents, launch them, and manage them.
1025
1047
  if (cmd === "team") {
1026
1048
  const sub = process.argv[3];
1027
1049
  if (sub === "list") {
1028
- const m = await requireAuth();
1029
- const { status, json } = await _httpJSON("GET", `${m.baseUrl}/api/provision/list`, m.token);
1050
+ const { status, json } = await accountCall("GET", "/api/provision/list");
1030
1051
  if (status !== "200") { console.error(`list failed (HTTP ${status})`); process.exit(1); }
1031
1052
  // Dedup by namespace:agent — the server returns one row per token, so a
1032
1053
  // re-provisioned agent shows up many times. Optional --namespace filter.
@@ -1184,16 +1205,15 @@ you design the team, provision its agents, launch them, and manage them.
1184
1205
  if (cmd === "schedule") {
1185
1206
  // User-level scheduled / recurring messages (server Plan 041). Authed by
1186
1207
  // the main token — the user can manage schedules in any namespace they own.
1187
- const m = await requireAuth();
1188
1208
  const sub = process.argv[3];
1189
- const BASE = `${m.baseUrl}/api/dashboard/scheduled`;
1209
+ const BASE = `/api/dashboard/scheduled`;
1190
1210
  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
1211
  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
1212
 
1193
1213
  if (sub === "list") {
1194
1214
  const ns = flagVal("namespace");
1195
1215
  const url = ns ? `${BASE}?namespace=${encodeURIComponent(ns)}` : BASE;
1196
- const { status, json } = await _httpJSON("GET", url, m.token);
1216
+ const { status, json } = await accountCall("GET", url);
1197
1217
  if (status !== "200") { console.error(`list failed (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
1198
1218
  const items = json?.schedules || [];
1199
1219
  for (const s of items) console.log(" " + fmt(s));
@@ -1215,7 +1235,7 @@ you design the team, provision its agents, launch them, and manage them.
1215
1235
  const thread = flagVal("thread"); if (thread) body.thread_slug = thread;
1216
1236
  const maxRuns = flagVal("max-runs"); if (maxRuns) body.max_runs = parseInt(maxRuns, 10);
1217
1237
  const expires = flagVal("expires"); if (expires) body.expires_at = expires;
1218
- const { status, json } = await _httpJSON("POST", BASE, m.token, body);
1238
+ const { status, json } = await accountCall("POST", BASE, body);
1219
1239
  if (status !== "201" && status !== "200") { console.error(`create failed (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
1220
1240
  console.log(`✓ scheduled ${M.green}${ns}:${to}${M.rst} [${schedule_kind}] ${M.dim}${json?.schedule?.id || ""}${M.rst}`);
1221
1241
  process.exit(0);
@@ -1223,7 +1243,7 @@ you design the team, provision its agents, launch them, and manage them.
1223
1243
  if (sub === "cancel" || sub === "delete" || sub === "rm") {
1224
1244
  const id = process.argv[4];
1225
1245
  if (!id) { console.error("Usage: patchcord schedule cancel <id>"); process.exit(1); }
1226
- const { status, json } = await _httpJSON("DELETE", `${BASE}/${id}`, m.token);
1246
+ const { status, json } = await accountCall("DELETE", `${BASE}/${id}`);
1227
1247
  if (status !== "200") { console.error(`cancel failed (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
1228
1248
  console.log(`✓ cancelled ${id}`);
1229
1249
  process.exit(0);
@@ -1231,7 +1251,7 @@ you design the team, provision its agents, launch them, and manage them.
1231
1251
  if (sub === "test" || sub === "fire") {
1232
1252
  const id = process.argv[4];
1233
1253
  if (!id) { console.error("Usage: patchcord schedule test <id>"); process.exit(1); }
1234
- const { status, json } = await _httpJSON("POST", `${BASE}/${id}/test`, m.token, {});
1254
+ const { status, json } = await accountCall("POST", `${BASE}/${id}/test`, {});
1235
1255
  if (status !== "200") { console.error(`test failed (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
1236
1256
  console.log(`✓ fired ${id} once (does not consume max-runs)`);
1237
1257
  process.exit(0);
@@ -1239,7 +1259,7 @@ you design the team, provision its agents, launch them, and manage them.
1239
1259
  if (sub === "pause" || sub === "resume") {
1240
1260
  const id = process.argv[4];
1241
1261
  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" });
1262
+ const { status, json } = await accountCall("PATCH", `${BASE}/${id}`, { active: sub === "resume" });
1243
1263
  if (status !== "200") { console.error(`${sub} failed (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
1244
1264
  console.log(`✓ ${sub === "resume" ? "resumed" : "paused"} ${id}`);
1245
1265
  process.exit(0);
@@ -2897,5 +2917,5 @@ if (cmd === "skill") {
2897
2917
  process.exit(0);
2898
2918
  }
2899
2919
 
2900
- console.error(`Unknown command: ${cmd}. Available: whoami, agents, upload, subscribe, update, --rename, --token, --agent-type, --version, --help`);
2920
+ console.error(`Unknown command: ${cmd}. Available: install, whoami, agents, upload, subscribe, update, login, provision, teamlead, team, schedule, --rename, --token, --agent-type, --version, --help`);
2901
2921
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.5.109",
3
+ "version": "0.5.111",
4
4
  "description": "Cross-machine agent messaging for Claude Code and Codex",
5
5
  "author": "ppravdin",
6
6
  "license": "MIT",