worktrust 0.6.1 → 0.6.3

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/package.json +1 -1
  2. package/setup-mcp.mjs +32 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "worktrust",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "Couple this computer to WorkTrust: approve a code in your browser, and every AI app here reports what you built. Metadata only, no dependencies.",
5
5
  "type": "module",
6
6
  "bin": {
package/setup-mcp.mjs CHANGED
@@ -43,6 +43,23 @@ function fromClaude() {
43
43
  } catch { return null; }
44
44
  }
45
45
  const discovered = fromClaude();
46
+ /**
47
+ * EVERY DIRECT WORKTRUST ENTRY CLAUDE CODE HOLDS, BY NAME (2026-10-04). A coupling made by hand may
48
+ * carry any name ("worktrust-imac-home") and a key in its header; `npx worktrust` once read it as
49
+ * "already coupled" and left Claude Code on that unbound key. With the bridge, each such entry is
50
+ * taken out and the bridge put in; on removal, they all go. Only entries whose address is a
51
+ * WorkTrust door are touched, user scope here and local scope in the folder that holds it.
52
+ */
53
+ function directClaudeEntries() {
54
+ try {
55
+ const config = JSON.parse(readFileSync(join(homedir(), ".claude.json"), "utf8"));
56
+ const door = (server) => typeof server?.url === "string" && /\/api\/mcp\b/.test(server.url) && /worktrust/i.test(new URL(server.url).host);
57
+ const entries = Object.entries(config.mcpServers ?? {}).filter(([, server]) => door(server)).map(([name]) => ({ name, scope: "user", dir: null }));
58
+ for (const [dir, project] of Object.entries(config.projects ?? {})) for (const [name, server] of Object.entries(project?.mcpServers ?? {})) if (door(server)) entries.push({ name, scope: "local", dir });
59
+ const bridged = Object.values(config.mcpServers ?? {}).some((server) => Array.isArray(server?.args) && server.args.includes(BRIDGE));
60
+ return { entries, bridged };
61
+ } catch { return { entries: [], bridged: false }; }
62
+ }
46
63
  const URL_ = value("url") ?? process.env.WORKTRUST_MCP_URL ?? discovered?.url;
47
64
  const TOKEN = value("token") ?? process.env.WORKTRUST_MCP_TOKEN ?? discovered?.token;
48
65
  if (!URL_ || (!TOKEN && !BRIDGE)) { console.error("no coupling: pass --url and --token (from Sources → Claude (MCP) → Add coupling), or couple Claude Code first"); process.exit(1); }
@@ -75,7 +92,15 @@ for (const client of CLIENTS) {
75
92
  // it when the command is not. User scope, so the coupling holds in every folder on this machine.
76
93
  const claudeArgs = REMOVE ? ["mcp", "remove", "-s", "user", NAME] : BRIDGE ? ["mcp", "add", "-s", "user", NAME, "--", NODE, BRIDGE, "mcp"] : ["mcp", "add", "--transport", "http", "-s", "user", NAME, URL_, "-H", `Authorization: Bearer ${TOKEN}`];
77
94
  const shown = REMOVE ? `claude mcp remove -s user ${NAME}` : BRIDGE ? `claude mcp add -s user ${NAME} -- "${NODE}" "${BRIDGE}" mcp` : `claude mcp add --transport http -s user ${NAME} "${URL_}" -H "Authorization: Bearer <token>"`;
78
- if (!REMOVE && discovered) { console.log(` ✓ ${client.label}: already coupled`); standing += 1; continue; }
95
+ const direct = directClaudeEntries();
96
+ if (!REMOVE && (BRIDGE ? direct.bridged && direct.entries.length === 0 : discovered)) { console.log(` ✓ ${client.label}: already coupled`); standing += 1; continue; }
97
+ // A direct entry under any name is replaced by the bridge, and every one goes on removal.
98
+ if (BRIDGE || REMOVE) for (const entry of direct.entries.filter((one) => !(REMOVE && one.name === NAME && one.scope === "user"))) {
99
+ const verb = `claude mcp remove -s ${entry.scope} ${entry.name}`;
100
+ if (!WRITE) { console.log(` ↻ ${client.label}: the direct entry "${entry.name}" (${entry.scope}) is replaced`); continue; }
101
+ const out = claudeOnPath() ? spawnSync("claude", ["mcp", "remove", "-s", entry.scope, entry.name], { encoding: "utf8", shell: platform() === "win32", ...(entry.dir ? { cwd: entry.dir } : {}) }) : null;
102
+ console.log(out?.status === 0 ? ` – ${client.label}: removed the direct entry "${entry.name}" (${entry.scope})` : ` ! ${client.label}: could not remove "${entry.name}"; run it yourself${entry.dir ? ` in ${entry.dir}` : ""}: ${verb}`);
103
+ }
79
104
  if (WRITE && claudeOnPath()) {
80
105
  const run = spawnSync("claude", claudeArgs, { encoding: "utf8", shell: platform() === "win32" });
81
106
  if (run.status === 0) { console.log(` + ${client.label}: ${REMOVE ? "removed" : "coupled"} through \`claude mcp ${REMOVE ? "remove" : "add"}\` (user scope)`); touched += 1; continue; }
@@ -86,7 +111,8 @@ for (const client of CLIENTS) {
86
111
  continue;
87
112
  }
88
113
  const text = existsSync(client.file) ? readFileSync(client.file, "utf8") : "";
89
- const already = text.includes(URL_) || (BRIDGE !== undefined && text.includes(JSON.stringify(BRIDGE).slice(1, -1))) || (REMOVE && /worktrust\.mjs/.test(text));
114
+ // With the bridge, coupled means the bridge is in AND no entry with a key in it is left beside it.
115
+ const already = REMOVE ? text.includes(URL_) || /worktrust\.mjs/.test(text) : BRIDGE !== undefined ? text.includes(JSON.stringify(BRIDGE).slice(1, -1)) && !text.includes(URL_) : text.includes(URL_);
90
116
  // THE WAY OUT, ON THE MACHINE. Uncoupling in the app ends the TOKEN; the config that names the
91
117
  // door stays behind on every client here, so the next person to read this file believes the
92
118
  // machine is coupled. `--remove` takes the entry out; the account side is a separate act and
@@ -107,7 +133,10 @@ for (const client of CLIENTS) {
107
133
  } else if (client.shape === "toml") {
108
134
  const text = readFileSync(client.file, "utf8");
109
135
  // The block and nothing after it: from its own header to the next one, or the end.
110
- const stripped = text.replace(new RegExp(`\\n?\\[mcp_servers\\.${NAME}\\][^\\[]*`, "g"), "\n");
136
+ // Every block that names the door, under any name (a hand-made one too, 2026-10-04), or ours.
137
+ // A block runs to the next table header at the START of a line, never to the next "[" — the
138
+ // bridge's own `args = ["…", "mcp"]` holds one, and cutting there left half a block behind.
139
+ const stripped = text.replace(/\n?\[mcp_servers\.("?)([A-Za-z0-9_.-]+)\1\][\s\S]*?(?=\n\[|$)/g, (block, _q, name) => (name === NAME || block.includes(URL_) || /worktrust\.mjs/.test(block) ? "" : block));
111
140
  console.log(` − ${client.label}: ${WRITE ? "removed from" : "would remove from"} ${client.file}`);
112
141
  if (WRITE) writeFileSync(client.file, stripped);
113
142
  }