patchcord 0.6.34 → 0.6.35

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.6.34",
4
+ "version": "0.6.35",
5
5
  "author": {
6
6
  "name": "ppravdin"
7
7
  },
package/bin/patchcord.mjs CHANGED
@@ -222,14 +222,29 @@ team (run from the project root):
222
222
  patchcord team status reconcile folder ↔ identity ↔ tmux ↔ token
223
223
  patchcord team launch launch each worker in mux
224
224
 
225
- projects (cloud-only — a free-text label grouping your own namespaces):
226
- patchcord namespace set-project <namespace> <project> assign a project label
227
- patchcord namespace set-project <namespace> clear it
225
+ projects (cloud-only — groups your own namespaces into one initiative.
226
+ NOT just a label: the LEADS of Teams in one project can message each other
227
+ with no handshake. Any NON-lead still needs an approved link, below.):
228
+ patchcord project list projects + their Teams
229
+ patchcord project create <name> explicit; nothing else creates one
230
+ patchcord project rename <project-id> <new-name> label only; reach unchanged
231
+ patchcord project rm <project-id> Teams survive, un-grouped
232
+ patchcord namespace set-project <ns> <project> assign (project must exist)
233
+ patchcord namespace set-project <ns> <project> --create create it, then assign
234
+ patchcord namespace set-project <ns> un-group it
235
+ patchcord provision ... --project <name> [--create-project]
236
+ patchcord orchestrator ... --project <name> [--create-project]
237
+
238
+ A namespace in NO project has no cross-team peers at all — that is the
239
+ default and it is deliberate. Assignment never invents a project: an
240
+ unknown name is an error, because silently creating one on a typo made
241
+ mistyped projects invisible and unmanageable.
228
242
 
229
243
  cross-namespace links (cloud-only, no CLI — agent-to-agent via MCP:
230
244
  request_namespace_link(ns, agent) / respond_namespace_link(ns, agent, approve)
231
- / list_my_namespace_links. Any agent, no lead role — but each link is to ONE
232
- named agent, never the whole namespace; opt-in, not project-based.)
245
+ / list_my_namespace_links. Any agent, no lead role — each link is to ONE
246
+ named agent, never the whole namespace. This is the ONLY way to reach a
247
+ non-lead in another namespace, project or no project.)
233
248
 
234
249
  schedules:
235
250
  patchcord schedule create <name> --namespace <ns> --to <agent> --content "..."
@@ -1169,7 +1184,7 @@ if (cmd === "subscribe") {
1169
1184
  // account token (user-level, tied to NO agent) lives at ~/.patchcord/auth.json
1170
1185
  // (legacy: main.json / master.json) or $PATCHCORD_TOKEN (legacy: *_MAIN/MASTER).
1171
1186
  // `patchcord login` authenticates; everything else logs in on demand.
1172
- if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "provision" || cmd === "pull" || cmd === "team" || cmd === "schedule" || cmd === "namespace") {
1187
+ if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "provision" || cmd === "pull" || cmd === "team" || cmd === "schedule" || cmd === "namespace" || cmd === "project" || cmd === "projects") {
1173
1188
  const M = { cyan: "\x1b[36m", green: "\x1b[32m", dim: "\x1b[2m", rst: "\x1b[0m" };
1174
1189
  const AUTH_CONFIG = join(HOME, ".patchcord", "auth.json");
1175
1190
  const LEGACY_CONFIGS = [join(HOME, ".patchcord", "main.json"), join(HOME, ".patchcord", "master.json")];
@@ -1195,6 +1210,42 @@ if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "
1195
1210
  const forgetAuth = () => {
1196
1211
  for (const p of [AUTH_CONFIG, ...LEGACY_CONFIGS]) { try { unlinkSync(p); } catch {} }
1197
1212
  };
1213
+ // Assign a namespace to a project, shared by `provision --project` and
1214
+ // `orchestrator --project`.
1215
+ //
1216
+ // Assignment RESOLVES an existing project and NEVER creates one. The server
1217
+ // used to create a project for any name it did not recognise, so a typo
1218
+ // silently produced a new, invisible project — the defect that got an earlier
1219
+ // version of this feature abandoned. `--create-project` makes creation an
1220
+ // explicit act rather than a side effect of misspelling.
1221
+ //
1222
+ // warnOnly: a failure here must not fail the whole command. The agent
1223
+ // identity is already provisioned and the credential is already on disk;
1224
+ // exiting non-zero would make a working identity look like a failed one.
1225
+ const assignProject = async (ns, project, { warnOnly = false, label = "done" } = {}) => {
1226
+ if (process.argv.includes("--create-project")) {
1227
+ const { status: cStatus, json: cJson } = await accountCall("POST", "/api/projects", { name: project });
1228
+ if (cStatus === "201") console.log(` ${M.dim}created project "${project}"${M.rst}`);
1229
+ else if (cStatus !== "409") {
1230
+ console.error(` ${M.dim}warning: could not create project "${project}" (HTTP ${cStatus}): ${cJson?.error || ""}${M.rst}`);
1231
+ }
1232
+ }
1233
+ const { status, json } = await accountCall("POST", "/api/namespace/set-project", { namespace_id: ns, project });
1234
+ if (status === "200") {
1235
+ console.log(` ${M.dim}namespace ${ns} assigned to project "${project}" — its lead can now reach other leads in that project${M.rst}`);
1236
+ return true;
1237
+ }
1238
+ if (status === "404") {
1239
+ console.error(` ${M.dim}warning: ${label}, but there is no project named "${project}".${M.rst}`);
1240
+ console.error(` ${M.dim} create it: patchcord project create ${project}${M.rst}`);
1241
+ console.error(` ${M.dim} or re-run with --create-project${M.rst}`);
1242
+ } else {
1243
+ const retry = json?.retryable ? " (transient — retry shortly, your login is fine)" : "";
1244
+ console.error(` ${M.dim}warning: ${label}, but --project failed (HTTP ${status}): ${json?.error || ""}${retry}${M.rst}`);
1245
+ }
1246
+ if (!warnOnly) process.exit(1);
1247
+ return false;
1248
+ };
1198
1249
  const flagVal = (name, def = "") => {
1199
1250
  const eq = process.argv.find((a) => a.startsWith(`--${name}=`));
1200
1251
  if (eq) return eq.split("=").slice(1).join("=");
@@ -1504,45 +1555,144 @@ if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "
1504
1555
  writeFileSync(tj, JSON.stringify(man, null, 2) + "\n");
1505
1556
  }
1506
1557
  } catch {}
1507
- // --project assigns this namespace to a project an organizational label
1508
- // ONLY, does not affect cross-namespace messaging (that's namespace_links,
1509
- // agent-to-agent, no CLI). Separate call: provisioning creates the agent
1510
- // identity, project assignment groups the NAMESPACE safe to retry
1511
- // independently, and a namespace already in a project just gets reassigned
1512
- // (last call wins, server-side upsert).
1558
+ // --project assigns this namespace to a project: a grouping entity above
1559
+ // the namespace. It DOES affect cross-namespace messaging Teams in one
1560
+ // project get a standing LEAD-TO-LEAD channel with no handshake. Reaching a
1561
+ // NON-lead elsewhere still needs an approved namespace_links pair.
1562
+ //
1563
+ // Separate call: provisioning creates the agent identity, project assignment
1564
+ // groups the NAMESPACE — safe to retry independently. Assignment RESOLVES an
1565
+ // existing project and never creates one, so an unknown name is a 404, not a
1566
+ // new project; pass --create-project to create it first, deliberately.
1513
1567
  if (project) {
1514
- const { status: pStatus, json: pJson } = await accountCall("POST", "/api/namespace/set-project", { namespace_id: ns, project });
1515
- if (pStatus !== "200") {
1516
- console.error(` ${M.dim}warning: provisioned OK, but --project failed (HTTP ${pStatus}): ${pJson?.error || ""}${M.rst}`);
1517
- } else {
1518
- console.log(` ${M.dim}namespace ${ns} assigned to project "${project}"${M.rst}`);
1519
- }
1568
+ await assignProject(ns, project, { warnOnly: true, label: "provisioned OK" });
1520
1569
  }
1521
1570
  console.log(`✓ provisioned ${M.green}${ns}:${arg}${M.rst} [${tool}${role ? "/" + role : ""}${lead ? "/lead" : ""}] → ${dir}`);
1522
1571
  process.exit(0);
1523
1572
  }
1524
1573
 
1525
1574
  if (cmd === "namespace") {
1526
- // Cloud-only: group namespaces into a "project" — a free-text organizational
1527
- // label only. Does NOT gate cross-namespace lead chat that's decided by
1528
- // per-pair namespace_links, approved lead-to-lead via the MCP tools
1529
- // request_namespace_link / respond_namespace_link (no CLI for that; see
1530
- // --help's CROSS-NAMESPACE LINKS section).
1575
+ // Cloud-only: group namespaces into a PROJECT — a grouping entity above the
1576
+ // namespace. It is NOT merely a label: Teams in one project get a standing
1577
+ // LEAD-TO-LEAD message channel with no per-pair handshake. Reaching any
1578
+ // NON-lead in another namespace still requires an approved namespace_links
1579
+ // pair, agent-to-agent via the MCP tools request_namespace_link /
1580
+ // respond_namespace_link (no CLI for that; see --help's CROSS-NAMESPACE
1581
+ // LINKS section).
1531
1582
  const sub = process.argv[3];
1532
1583
  if (sub === "set-project") {
1533
1584
  const ns = process.argv[4];
1534
- const project = process.argv[5];
1535
- if (!ns) { console.error("Usage: patchcord namespace set-project <namespace> [project-name]\n Omit project-name to clear the label."); process.exit(1); }
1585
+ const project = (process.argv[5] || "").startsWith("-") ? "" : (process.argv[5] || "");
1586
+ const doCreate = process.argv.includes("--create");
1587
+ if (!ns) { console.error("Usage: patchcord namespace set-project <namespace> [project-name] [--create]\n Omit project-name to un-group the namespace."); process.exit(1); }
1588
+ if (doCreate && !project) { console.error("--create needs a project name."); process.exit(1); }
1589
+ // Assignment RESOLVES an existing project and never creates one. The
1590
+ // server used to create a project for any unrecognised name, which turned
1591
+ // every typo into a new invisible project; --create makes creation an
1592
+ // explicit act instead of a side effect of misspelling.
1593
+ if (doCreate) {
1594
+ const { status: cStatus, json: cJson } = await accountCall("POST", "/api/projects", { name: project });
1595
+ if (cStatus === "201") console.log(` ${M.dim}created project "${project}"${M.rst}`);
1596
+ else if (cStatus === "409") console.log(` ${M.dim}project "${project}" already exists — assigning to it${M.rst}`);
1597
+ else { console.error(`could not create project (HTTP ${cStatus}): ${cJson?.error || ""}`); process.exit(1); }
1598
+ }
1536
1599
  const { status, json } = await accountCall("POST", "/api/namespace/set-project", { namespace_id: ns, project: project || "" });
1537
- if (status !== "200") { console.error(`set-project failed (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
1600
+ if (status === "404") {
1601
+ console.error(`No project named "${project}".`);
1602
+ console.error(` Create it first: patchcord project create ${project}`);
1603
+ console.error(` Or in one step: patchcord namespace set-project ${ns} ${project} --create`);
1604
+ process.exit(1);
1605
+ }
1606
+ if (status !== "200") { console.error(`set-project failed (HTTP ${status}): ${json?.error || ""}${json?.retryable ? " (transient — retry)" : ""}`); process.exit(1); }
1538
1607
  if (json.status === "cleared") {
1539
- console.log(`✓ ${ns} un-grouped — no project label`);
1608
+ console.log(`✓ ${ns} un-grouped — belongs to no project, and its lead has no cross-team peers`);
1540
1609
  } else {
1541
- console.log(`✓ ${ns} assigned to project "${json.project}" (label only — does not affect cross-namespace messaging)`);
1610
+ console.log(`✓ ${ns} assigned to project "${json.project}"`);
1611
+ console.log(` ${M.dim}its LEAD can now message the leads of other Teams in this project;${M.rst}`);
1612
+ console.log(` ${M.dim}other agents still need an approved link (request_namespace_link)${M.rst}`);
1542
1613
  }
1543
1614
  process.exit(0);
1544
1615
  }
1545
- console.error("Usage: patchcord namespace set-project <namespace> [project-name]");
1616
+ console.error("Usage: patchcord namespace set-project <namespace> [project-name] [--create]");
1617
+ process.exit(1);
1618
+ }
1619
+
1620
+ // ── projects: the grouping entity above namespaces ──────────────────
1621
+ // Exists because the server deliberately does NOT create a project as a side
1622
+ // effect of assignment. Without a create/list path here, the only way to make
1623
+ // one was a raw API call — and without a LIST path, a mistyped project was
1624
+ // invisible, which is the exact defect that got an earlier version of this
1625
+ // feature abandoned.
1626
+ if (cmd === "project" || cmd === "projects") {
1627
+ const sub = process.argv[3] || "list";
1628
+ const fail = (status, json, what) => {
1629
+ const retry = json?.retryable ? " (transient — retry shortly, your login is fine)" : "";
1630
+ console.error(`${what} failed (HTTP ${status}): ${json?.error || ""}${retry}`);
1631
+ process.exit(1);
1632
+ };
1633
+
1634
+ if (sub === "list" || sub === "ls") {
1635
+ const { status, json } = await accountCall("GET", "/api/projects");
1636
+ if (status !== "200") fail(status, json, "project list");
1637
+ const projects = json?.projects || [];
1638
+ if (!projects.length) {
1639
+ console.log("No projects yet.");
1640
+ console.log(` ${M.dim}patchcord project create <name>${M.rst}`);
1641
+ process.exit(0);
1642
+ }
1643
+ for (const p of projects) {
1644
+ const members = (p.namespaces || []);
1645
+ console.log(`${M.green}${p.name}${M.rst} ${M.dim}${p.id}${M.rst}`);
1646
+ if (members.length) for (const ns of members) console.log(` · ${ns}`);
1647
+ else console.log(` ${M.dim}(no Teams assigned)${M.rst}`);
1648
+ }
1649
+ process.exit(0);
1650
+ }
1651
+
1652
+ if (sub === "create" || sub === "add") {
1653
+ const name = process.argv[4];
1654
+ if (!name) { console.error("Usage: patchcord project create <name>"); process.exit(1); }
1655
+ const { status, json } = await accountCall("POST", "/api/projects", { name });
1656
+ if (status === "409") { console.error(`A project named "${name}" already exists.`); process.exit(1); }
1657
+ if (status !== "201") fail(status, json, "project create");
1658
+ console.log(`✓ created project "${json.name}" ${M.dim}${json.id}${M.rst}`);
1659
+ console.log(` ${M.dim}patchcord namespace set-project <namespace> ${json.name}${M.rst}`);
1660
+ process.exit(0);
1661
+ }
1662
+
1663
+ if (sub === "rename" || sub === "mv") {
1664
+ const id = process.argv[4];
1665
+ const name = process.argv[5];
1666
+ if (!id || !name) { console.error("Usage: patchcord project rename <project-id> <new-name>\n Get the id from `patchcord project list`."); process.exit(1); }
1667
+ const { status, json } = await accountCall("PATCH", `/api/projects/${encodeURIComponent(id)}`, { name });
1668
+ if (status === "404") { console.error(`No project with id ${id}.`); process.exit(1); }
1669
+ if (status === "409") { console.error(`A project named "${name}" already exists.`); process.exit(1); }
1670
+ if (status !== "200") fail(status, json, "project rename");
1671
+ console.log(`✓ renamed to "${json.name}" ${M.dim}(membership and reach unchanged — this is only the label)${M.rst}`);
1672
+ process.exit(0);
1673
+ }
1674
+
1675
+ if (sub === "rm" || sub === "delete") {
1676
+ const id = process.argv[4];
1677
+ if (!id) { console.error("Usage: patchcord project rm <project-id>\n Get the id from `patchcord project list`."); process.exit(1); }
1678
+ const { status, json } = await accountCall("DELETE", `/api/projects/${encodeURIComponent(id)}`);
1679
+ if (status === "404") { console.error(`No project with id ${id}.`); process.exit(1); }
1680
+ if (status !== "200") fail(status, json, "project rm");
1681
+ const ungrouped = json?.ungrouped_namespaces || [];
1682
+ console.log(`✓ deleted project ${M.dim}${id}${M.rst}`);
1683
+ if (ungrouped.length) {
1684
+ // Deleting a project withdraws the lead-to-lead channel between exactly
1685
+ // these Teams. Fail-closed — reach is only ever removed here, never
1686
+ // widened — but it must be stated rather than discovered.
1687
+ console.log(` ${ungrouped.length} Team(s) un-grouped; their leads no longer reach each other:`);
1688
+ for (const ns of ungrouped) console.log(` · ${ns}`);
1689
+ } else {
1690
+ console.log(` ${M.dim}no Teams were assigned to it${M.rst}`);
1691
+ }
1692
+ process.exit(0);
1693
+ }
1694
+
1695
+ console.error("Usage: patchcord project <list|create|rename|rm>");
1546
1696
  process.exit(1);
1547
1697
  }
1548
1698
 
@@ -1595,21 +1745,17 @@ if (cmd === "login" || cmd === "orchestrator" || cmd === "teamlead" || cmd === "
1595
1745
  const project = flagVal("project", "");
1596
1746
  const hostname = run("hostname -s") || run("hostname") || "unknown";
1597
1747
  // is_lead: true always — the orchestrator IS the one identity per namespace
1598
- // meant to coordinate across teams. Harmless by itself: a lead only reaches
1599
- // another namespace's lead after an approved namespace_links pair (agent-to-
1600
- // agent via request_namespace_link/respond_namespace_link — fails closed
1601
- // until both leads explicitly consent).
1748
+ // meant to coordinate across teams. Not harmless by itself any more: a lead
1749
+ // reaches another namespace's lead EITHER after an approved namespace_links
1750
+ // pair (agent-to-agent via request_namespace_link/respond_namespace_link) OR
1751
+ // when both namespaces sit in the same PROJECT, which needs no handshake.
1752
+ // So --project below is a reachability decision, not just a label.
1602
1753
  const { status, json, m } = await accountCall("POST", "/api/provision", { namespace_id: ns, agent_id: "orchestrator", tool, role: "orchestrator", label: "orchestrator:self", is_lead: true });
1603
1754
  if (status !== "200" || !json?.token) { console.error(`could not set up orchestrator (HTTP ${status}): ${json?.error || ""}`); process.exit(1); }
1604
1755
  const base = String(json.url || `${m.baseUrl}/mcp`).replace(/\/mcp(\/bearer)?$/, "");
1605
1756
  writeWorkerConfig(tool, root, base, json.token, hostname);
1606
1757
  if (project) {
1607
- const { status: pStatus, json: pJson } = await accountCall("POST", "/api/namespace/set-project", { namespace_id: ns, project });
1608
- if (pStatus !== "200") {
1609
- console.error(` ${M.dim}warning: orchestrator set up OK, but --project failed (HTTP ${pStatus}): ${pJson?.error || ""}${M.rst}`);
1610
- } else {
1611
- console.log(` ${M.dim}namespace ${ns} assigned to project "${project}"${M.rst}`);
1612
- }
1758
+ await assignProject(ns, project, { warnOnly: true, label: "orchestrator set up OK" });
1613
1759
  }
1614
1760
  mkdirSync(join(root, ".patchcord"), { recursive: true });
1615
1761
  const tj = join(root, ".patchcord", "team.json");
@@ -4034,5 +4180,5 @@ if (cmd === "skill") {
4034
4180
  process.exit(0);
4035
4181
  }
4036
4182
 
4037
- console.error(`Unknown command: ${cmd}. Available: install, whoami, agents, upload, subscribe, update, login, provision, pull, orchestrator, team, schedule, --rename, --token, --agent-type, --version, --help`);
4183
+ console.error(`Unknown command: ${cmd}. Available: install, whoami, agents, upload, subscribe, update, login, provision, pull, orchestrator, team, project, namespace, schedule, --rename, --token, --agent-type, --version, --help`);
4038
4184
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.6.34",
3
+ "version": "0.6.35",
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"