@super-hands/connect 0.1.6 → 0.1.9

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 (3) hide show
  1. package/README.md +27 -2
  2. package/client.mjs +306 -23
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -20,9 +20,34 @@ variable too.
20
20
  left untouched. Restart Cursor and enable the server under Settings → MCP.
21
21
  - **Claude Code** — registers the server through Claude Code's own CLI
22
22
  (`claude mcp add`, user scope). A new `claude` session connects on start.
23
+ - **Codex** — appends the `superhands` block to `~/.codex/config.toml`, and
24
+ only when no such block is there. Both the app and the CLI read that file.
25
+ - **The skill** — writes `skills/superhands/SKILL.md` under each connected
26
+ client's own directory. It routes UI work through the team's guidance and
27
+ carries no credential.
23
28
 
24
- With no flags it connects every client it finds; `--cursor` or `--claude`
25
- narrow it to one.
29
+ With no flags it connects every client it finds; `--cursor`, `--claude` or
30
+ `--codex` narrow it to one.
31
+
32
+ ## Removing it
33
+
34
+ ```
35
+ npx -y @super-hands/connect@latest uninstall
36
+ ```
37
+
38
+ The exact inverse, in this order: it reads every Superhands credential on the
39
+ machine out of the configs, hands each one back to the deployment that issued
40
+ it, and only then removes the entries and the skill files. The other way round
41
+ would delete the only copy of the credential needed to tell the server
42
+ anything.
43
+
44
+ It removes only what this tool wrote — one key out of `~/.cursor/mcp.json`,
45
+ its own block out of `~/.codex/config.toml`, and the Claude Code entry through
46
+ `claude mcp remove`. Everything else in those files stays. Running it twice, or
47
+ on a machine that was never connected, does nothing and says so; a machine that
48
+ cannot reach the internet is still cleaned locally, with a warning that the
49
+ credential is still live. The same `--cursor` / `--claude` / `--codex` flags
50
+ remove one client and leave the rest.
26
51
 
27
52
  ## What it does not do
28
53
 
package/client.mjs CHANGED
@@ -11,7 +11,7 @@
11
11
 
12
12
  // lib/connect-client-entry.ts
13
13
  import { execFileSync, spawnSync } from "node:child_process";
14
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
14
+ import { existsSync, mkdirSync, readFileSync, rmSync, rmdirSync, writeFileSync } from "node:fs";
15
15
  import { homedir } from "node:os";
16
16
  import { dirname, join } from "node:path";
17
17
 
@@ -28,6 +28,9 @@ var MCP_SERVER_KEY = "superhands";
28
28
  function mcpAuthorizationHeader(token) {
29
29
  return `Bearer ${token}`;
30
30
  }
31
+ function mcpRevokeEndpoint(mcpEndpoint) {
32
+ return `${mcpEndpoint.replace(/\/+$/, "")}/oauth/revoke`;
33
+ }
31
34
  function codexConfigBlock(args) {
32
35
  return [
33
36
  `[mcp_servers.${MCP_SERVER_KEY}]`,
@@ -37,7 +40,7 @@ function codexConfigBlock(args) {
37
40
  }
38
41
 
39
42
  // lib/connect-skill.ts
40
- var CONNECT_SKILL_VERSION = 2;
43
+ var CONNECT_SKILL_VERSION = 4;
41
44
  var CONNECT_SKILL_DIR = MCP_SERVER_KEY;
42
45
  var CONNECT_SKILL_FILENAME = "SKILL.md";
43
46
  var VERSION_MARKER = `[//]: # (superhands-skill-version: ${CONNECT_SKILL_VERSION})`;
@@ -60,8 +63,10 @@ Before you build, change, or restyle any UI:
60
63
 
61
64
  1. Call the \`superhands_get_guidance\` MCP tool with \`intent\` set to what
62
65
  you have been asked to build, in plain language \u2014 for example "a sign-in
63
- page with email and password and an error state". The decisions that govern
64
- that work come back in full, ahead of the rest.
66
+ page with email and password and an error state", and
67
+ \`skill_version: ${CONNECT_SKILL_VERSION}\` \u2014 the version of this file, so
68
+ the server can tell you when this file is out of date. The decisions that
69
+ govern that work come back in full, ahead of the rest.
65
70
  2. Build to what comes back. Where a decision names a case it deliberately
66
71
  does not govern, your own judgment resumes there.
67
72
  3. When you have finished \u2014 not part-way through \u2014 send the files you wrote or
@@ -78,6 +83,23 @@ mention that the team writes its guidance on the Superhands Guidance page.
78
83
  If the \`${MCP_SERVER_KEY}\` MCP server is not reachable in this session, say
79
84
  so rather than guessing at the team's decisions \u2014 it is configured on this
80
85
  machine, so a missing connection is worth telling the person about.
86
+
87
+ ## Removing Superhands from this machine
88
+
89
+ If the person asks to uninstall, remove, or disconnect Superhands, run:
90
+
91
+ \`\`\`
92
+ npx -y ${CONNECT_CLIENT_SPEC} uninstall
93
+ \`\`\`
94
+
95
+ and tell them what it printed. It hands every Superhands credential on this
96
+ machine back to the server, then takes the MCP entries and this file out of
97
+ each client it finds. Add \`--claude\`, \`--cursor\` or \`--codex\` to remove
98
+ one client and leave the rest.
99
+
100
+ Do not do this by hand. Editing an MCP config to take one server out is how
101
+ every other server in that file gets lost, and deleting the files without the
102
+ command leaves credentials live on the server with nothing left to end them.
81
103
  `;
82
104
  function installedSkillVersion(contents) {
83
105
  const match = contents.match(/superhands-skill-version:\s*(\d+)/);
@@ -94,6 +116,11 @@ function stop(message) {
94
116
  `);
95
117
  process.exit(1);
96
118
  }
119
+ function bearerToken(value) {
120
+ if (typeof value !== "string") return null;
121
+ const match = /^Bearer\s+(\S+)$/i.exec(value.trim());
122
+ return match ? match[1] : null;
123
+ }
97
124
  function cursorServerEntry(args) {
98
125
  return {
99
126
  url: args.endpoint,
@@ -122,6 +149,59 @@ function mergedCursorConfig(existing, args) {
122
149
  return { text: `${JSON.stringify(config, null, 2)}
123
150
  `, replaced };
124
151
  }
152
+ function cursorConfigWithoutServer(existing) {
153
+ if (existing === null || existing.trim() === "") return { text: existing ?? "", removed: false };
154
+ let parsed;
155
+ try {
156
+ parsed = JSON.parse(existing);
157
+ } catch {
158
+ throw new Error("not valid JSON");
159
+ }
160
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
161
+ throw new Error("not a JSON object");
162
+ }
163
+ const config = parsed;
164
+ const serversRaw = config.mcpServers;
165
+ if (typeof serversRaw !== "object" || serversRaw === null || Array.isArray(serversRaw)) {
166
+ return { text: existing, removed: false };
167
+ }
168
+ const servers = serversRaw;
169
+ if (!(MCP_SERVER_KEY in servers)) return { text: existing, removed: false };
170
+ delete servers[MCP_SERVER_KEY];
171
+ return { text: `${JSON.stringify(config, null, 2)}
172
+ `, removed: true };
173
+ }
174
+ function cursorConnection(existing) {
175
+ if (!existing || existing.trim() === "") return null;
176
+ let parsed;
177
+ try {
178
+ parsed = JSON.parse(existing);
179
+ } catch {
180
+ return null;
181
+ }
182
+ const entry = parsed?.mcpServers?.[MCP_SERVER_KEY];
183
+ if (!entry || typeof entry.url !== "string") return null;
184
+ const token = bearerToken(entry.headers?.Authorization);
185
+ return token ? { endpoint: entry.url, token } : null;
186
+ }
187
+ var CODEX_OWN_BLOCK = `\\[mcp_servers\\.${MCP_SERVER_KEY}\\]\\nurl = "([^"\\n]*)"\\n(?:http_headers = \\{ "Authorization" = "([^"\\n]*)" \\}|bearer_token = "([^"\\n]*)")`;
188
+ function codexConfigWithoutServer(existing) {
189
+ const block = new RegExp(`${CODEX_OWN_BLOCK}\\n?`);
190
+ if (block.test(existing)) {
191
+ return { text: existing.replace(block, ""), removed: true, foreign: false };
192
+ }
193
+ return {
194
+ text: existing,
195
+ removed: false,
196
+ foreign: existing.includes(`[mcp_servers.${MCP_SERVER_KEY}]`)
197
+ };
198
+ }
199
+ function codexConnection(existing) {
200
+ const match = new RegExp(CODEX_OWN_BLOCK).exec(existing);
201
+ if (!match) return null;
202
+ const token = match[2] ? bearerToken(match[2]) : match[3] ?? null;
203
+ return token ? { endpoint: match[1], token } : null;
204
+ }
125
205
  function appendedCodexConfig(existing, args) {
126
206
  const staleOwnBlock = new RegExp(
127
207
  `\\[mcp_servers\\.${MCP_SERVER_KEY}\\]\\nurl = "[^"\\n]*"\\nbearer_token = "[^"\\n]*"`
@@ -142,19 +222,92 @@ function appendedCodexConfig(existing, args) {
142
222
  }
143
223
  function writeSkill(clientDir) {
144
224
  const skillPath = join(clientDir, "skills", CONNECT_SKILL_DIR, CONNECT_SKILL_FILENAME);
225
+ let existing = "";
226
+ try {
227
+ existing = readFileSync(skillPath, "utf8");
228
+ } catch {
229
+ existing = "";
230
+ }
231
+ const existingVersion = existing === "" ? 0 : installedSkillVersion(existing);
232
+ if (existingVersion >= CONNECT_SKILL_VERSION) return existingVersion;
145
233
  try {
146
- let existing = "";
147
- try {
148
- existing = readFileSync(skillPath, "utf8");
149
- } catch {
150
- existing = "";
151
- }
152
- if (existing !== "" && installedSkillVersion(existing) >= CONNECT_SKILL_VERSION) return;
153
234
  mkdirSync(dirname(skillPath), { recursive: true });
154
235
  writeFileSync(skillPath, CONNECT_SKILL_CONTENT);
155
236
  say(` Wrote the ${CONNECT_SKILL_DIR} skill to ${skillPath} \u2014 it routes UI work through the team's guidance.`);
237
+ return CONNECT_SKILL_VERSION;
156
238
  } catch {
157
239
  say(` Could not write the ${CONNECT_SKILL_DIR} skill at ${skillPath}. The connection works without it.`);
240
+ return existingVersion;
241
+ }
242
+ }
243
+ function connectReportUrl(endpoint) {
244
+ return `${endpoint.replace(/\/+$/, "")}/connect`;
245
+ }
246
+ async function reportSkillVersion(args) {
247
+ try {
248
+ await fetch(connectReportUrl(args.endpoint), {
249
+ method: "POST",
250
+ headers: {
251
+ Authorization: mcpAuthorizationHeader(args.token),
252
+ "content-type": "application/json"
253
+ },
254
+ body: JSON.stringify({ skill_version: args.version }),
255
+ signal: AbortSignal.timeout(5e3)
256
+ });
257
+ } catch {
258
+ }
259
+ }
260
+ function removeSkill(clientDir) {
261
+ const skillPath = join(clientDir, "skills", CONNECT_SKILL_DIR, CONNECT_SKILL_FILENAME);
262
+ if (!existsSync(skillPath)) return false;
263
+ try {
264
+ rmSync(skillPath);
265
+ try {
266
+ rmdirSync(dirname(skillPath));
267
+ } catch {
268
+ }
269
+ say(` Removed the ${CONNECT_SKILL_DIR} skill at ${skillPath}.`);
270
+ return true;
271
+ } catch {
272
+ say(` Could not remove the ${CONNECT_SKILL_DIR} skill at ${skillPath} \u2014 delete it by hand.`);
273
+ return false;
274
+ }
275
+ }
276
+ async function revokeToken(connection) {
277
+ try {
278
+ const response = await fetch(mcpRevokeEndpoint(connection.endpoint), {
279
+ method: "POST",
280
+ headers: { "content-type": "application/x-www-form-urlencoded" },
281
+ body: new URLSearchParams({ token: connection.token }).toString(),
282
+ // Bounded, because an unreachable host is the expected failure here and
283
+ // a hung uninstall is worse than one that says it could not revoke.
284
+ signal: AbortSignal.timeout(15e3)
285
+ });
286
+ return response.ok;
287
+ } catch {
288
+ return false;
289
+ }
290
+ }
291
+ function claudeConnectionFrom(output) {
292
+ const url = /^\s*URL:\s*(\S+)\s*$/m.exec(output);
293
+ const authorization = /^\s*Authorization:\s*(Bearer\s+\S+)\s*$/m.exec(output);
294
+ if (!url) return null;
295
+ const token = bearerToken(authorization?.[1]);
296
+ return token ? { endpoint: url[1], token } : null;
297
+ }
298
+ function claudeSaysNothingToRemove(output) {
299
+ return new RegExp(`No MCP server named\\s+"?${MCP_SERVER_KEY}"?`, "i").test(output);
300
+ }
301
+ function claudeConnection() {
302
+ try {
303
+ return claudeConnectionFrom(
304
+ execFileSync("claude", ["mcp", "get", MCP_SERVER_KEY], {
305
+ encoding: "utf8",
306
+ stdio: ["ignore", "pipe", "ignore"]
307
+ })
308
+ );
309
+ } catch {
310
+ return null;
158
311
  }
159
312
  }
160
313
  function hasCli(bin) {
@@ -176,7 +329,24 @@ function openCursor() {
176
329
  }
177
330
  return false;
178
331
  }
332
+ function chosenClients(flags) {
333
+ const cursor = flags.has("--cursor");
334
+ const claude = flags.has("--claude");
335
+ const codex = flags.has("--codex");
336
+ const explicit = cursor || claude || codex;
337
+ return explicit ? { cursor, claude, codex, explicit } : { cursor: true, claude: true, codex: true, explicit };
338
+ }
179
339
  async function main() {
340
+ const argv = process.argv.slice(2);
341
+ const flags = new Set(argv.filter((arg) => arg.startsWith("--")));
342
+ const command = argv.find((arg) => !arg.startsWith("--")) ?? "install";
343
+ if (command === "uninstall") return uninstall(flags);
344
+ if (command !== "install") {
345
+ stop(`unknown command "${command}". This tool takes "install" (the default) or "uninstall".`);
346
+ }
347
+ return install(flags);
348
+ }
349
+ async function install(flags) {
180
350
  const token = process.env[CONNECT_TOKEN_ENV]?.trim();
181
351
  const endpoint = process.env[CONNECT_URL_ENV]?.trim() || CONNECT_DEFAULT_MCP_URL;
182
352
  if (!token) {
@@ -184,15 +354,12 @@ async function main() {
184
354
  `this command needs ${CONNECT_TOKEN_ENV} set on the same line. Copy the whole command from Superhands setup and run it unchanged.`
185
355
  );
186
356
  }
187
- const flags = new Set(process.argv.slice(2));
188
- const onlyCursor = flags.has("--cursor");
189
- const onlyClaude = flags.has("--claude");
190
- const onlyCodex = flags.has("--codex");
191
- const autodetect = !onlyCursor && !onlyClaude && !onlyCodex;
357
+ const wanted = chosenClients(flags);
192
358
  let connected = 0;
193
359
  let attempted = 0;
360
+ const skillVersions = [];
194
361
  const cursorDir = join(homedir(), ".cursor");
195
- if (onlyCursor || autodetect && existsSync(cursorDir)) {
362
+ if (wanted.cursor && (wanted.explicit || existsSync(cursorDir))) {
196
363
  attempted += 1;
197
364
  const configPath = join(cursorDir, "mcp.json");
198
365
  let existing = null;
@@ -222,7 +389,7 @@ async function main() {
222
389
  } else {
223
390
  say(` Open Cursor (restart it if it was running), then enable ${MCP_SERVER_KEY} under Settings \u2192 MCP.`);
224
391
  }
225
- writeSkill(cursorDir);
392
+ skillVersions.push(writeSkill(cursorDir));
226
393
  connected += 1;
227
394
  } catch {
228
395
  say(`Cursor \u2014 ${configPath} could not be written, so it was left as it was.`);
@@ -236,7 +403,7 @@ async function main() {
236
403
  }
237
404
  }
238
405
  }
239
- if (onlyClaude || autodetect && hasCli("claude")) {
406
+ if (wanted.claude && (wanted.explicit || hasCli("claude"))) {
240
407
  attempted += 1;
241
408
  try {
242
409
  try {
@@ -263,14 +430,14 @@ async function main() {
263
430
  );
264
431
  say(`Claude Code \u2014 added the ${MCP_SERVER_KEY} server (user scope).`);
265
432
  say(" Open a new claude session and it connects on start.");
266
- writeSkill(join(homedir(), ".claude"));
433
+ skillVersions.push(writeSkill(join(homedir(), ".claude")));
267
434
  connected += 1;
268
435
  } catch {
269
436
  say("Claude Code \u2014 `claude mcp add` failed. Run it by hand from the Superhands MCP page.");
270
437
  }
271
438
  }
272
439
  const codexDir = join(homedir(), ".codex");
273
- if (onlyCodex || autodetect && (existsSync(codexDir) || hasCli("codex"))) {
440
+ if (wanted.codex && (wanted.explicit || existsSync(codexDir) || hasCli("codex"))) {
274
441
  attempted += 1;
275
442
  const configPath = join(codexDir, "config.toml");
276
443
  let existing = "";
@@ -283,7 +450,7 @@ async function main() {
283
450
  if (result.alreadyPresent) {
284
451
  say(`Codex \u2014 ${configPath} already names a ${MCP_SERVER_KEY} server, so it was left as it is.`);
285
452
  say(" If that connection is stale, update the http_headers Authorization value there by hand.");
286
- writeSkill(codexDir);
453
+ skillVersions.push(writeSkill(codexDir));
287
454
  connected += 1;
288
455
  } else {
289
456
  try {
@@ -293,7 +460,7 @@ async function main() {
293
460
  result.repaired ? `Codex \u2014 replaced the ${MCP_SERVER_KEY} entry in ${configPath}: its old bearer_token spelling makes current Codex reject the whole config.` : `Codex \u2014 added the ${MCP_SERVER_KEY} server to ${configPath}.`
294
461
  );
295
462
  say(" The app and the CLI both read this config \u2014 open either and it connects when a session starts.");
296
- writeSkill(codexDir);
463
+ skillVersions.push(writeSkill(codexDir));
297
464
  connected += 1;
298
465
  } catch {
299
466
  say(`Codex \u2014 ${configPath} could not be written, so it was left as it was.`);
@@ -310,8 +477,124 @@ async function main() {
310
477
  attempted > 0 ? "nothing was connected. Fix the issue above, then run this command again." : `no supported client was found on this machine. Add it to yours by hand: the server URL is ${endpoint}, sent with the header "Authorization: ${mcpAuthorizationHeader(token)}".`
311
478
  );
312
479
  }
480
+ const installed = skillVersions.length > 0 ? Math.min(...skillVersions) : 0;
481
+ if (installed > 0) {
482
+ await reportSkillVersion({ endpoint, token, version: installed });
483
+ }
313
484
  say("Done. Superhands notices the moment an agent connects \u2014 setup ticks by itself.");
314
485
  }
486
+ async function uninstall(flags) {
487
+ const wanted = chosenClients(flags);
488
+ const cursorDir = join(homedir(), ".cursor");
489
+ const codexDir = join(homedir(), ".codex");
490
+ const cursorConfigPath = join(cursorDir, "mcp.json");
491
+ const codexConfigPath = join(codexDir, "config.toml");
492
+ const found = [];
493
+ const claudeAvailable = wanted.claude && hasCli("claude");
494
+ let cursorText = null;
495
+ if (wanted.cursor) {
496
+ try {
497
+ cursorText = readFileSync(cursorConfigPath, "utf8");
498
+ } catch {
499
+ cursorText = null;
500
+ }
501
+ const connection = cursorConnection(cursorText);
502
+ if (connection) found.push(connection);
503
+ }
504
+ if (claudeAvailable) {
505
+ const connection = claudeConnection();
506
+ if (connection) found.push(connection);
507
+ }
508
+ let codexText = "";
509
+ if (wanted.codex) {
510
+ try {
511
+ codexText = readFileSync(codexConfigPath, "utf8");
512
+ } catch {
513
+ codexText = "";
514
+ }
515
+ const connection = codexConnection(codexText);
516
+ if (connection) found.push(connection);
517
+ }
518
+ const unique = new Map(found.map((c) => [`${c.endpoint}\0${c.token}`, c]));
519
+ let revoked = 0;
520
+ let unreachable = 0;
521
+ for (const connection of unique.values()) {
522
+ if (await revokeToken(connection)) revoked += 1;
523
+ else unreachable += 1;
524
+ }
525
+ if (revoked > 0) {
526
+ say(
527
+ `Ended ${revoked} credential${revoked === 1 ? "" : "s"} on the server \u2014 those agents now show as disconnected in Superhands.`
528
+ );
529
+ }
530
+ if (unreachable > 0) {
531
+ say(
532
+ `Could not reach Superhands to end ${unreachable} credential${unreachable === 1 ? "" : "s"} \u2014 cleaning this machine anyway. Press Disconnect on the Superhands Agents page to finish it.`
533
+ );
534
+ }
535
+ let removed = 0;
536
+ if (wanted.cursor) {
537
+ let stripped = null;
538
+ try {
539
+ stripped = cursorConfigWithoutServer(cursorText);
540
+ } catch {
541
+ say(`Cursor \u2014 ${cursorConfigPath} is not valid JSON, so it was left untouched.`);
542
+ say(` Remove the "${MCP_SERVER_KEY}" entry under "mcpServers" by hand.`);
543
+ }
544
+ if (stripped?.removed) {
545
+ try {
546
+ writeFileSync(cursorConfigPath, stripped.text);
547
+ say(`Cursor \u2014 removed the ${MCP_SERVER_KEY} server from ${cursorConfigPath}.`);
548
+ removed += 1;
549
+ } catch {
550
+ say(`Cursor \u2014 ${cursorConfigPath} could not be written, so it was left as it was.`);
551
+ say(
552
+ ` A sandboxed agent usually cannot write outside its workspace. Run this same command in a regular terminal, or delete the "${MCP_SERVER_KEY}" entry under "mcpServers" in that file yourself.`
553
+ );
554
+ }
555
+ }
556
+ if (removeSkill(cursorDir)) removed += 1;
557
+ }
558
+ if (claudeAvailable) {
559
+ const result = spawnSync("claude", ["mcp", "remove", "--scope", "user", MCP_SERVER_KEY], {
560
+ encoding: "utf8"
561
+ });
562
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
563
+ if (!result.error && result.status === 0) {
564
+ say(`Claude Code \u2014 removed the ${MCP_SERVER_KEY} server (user scope).`);
565
+ removed += 1;
566
+ } else if (!claudeSaysNothingToRemove(output)) {
567
+ say(
568
+ `Claude Code \u2014 \`claude mcp remove --scope user ${MCP_SERVER_KEY}\` failed. Run it by hand.`
569
+ );
570
+ }
571
+ if (removeSkill(join(homedir(), ".claude"))) removed += 1;
572
+ }
573
+ if (wanted.codex) {
574
+ const stripped = codexConfigWithoutServer(codexText);
575
+ if (stripped.removed) {
576
+ try {
577
+ writeFileSync(codexConfigPath, stripped.text);
578
+ say(`Codex \u2014 removed the ${MCP_SERVER_KEY} server from ${codexConfigPath}.`);
579
+ removed += 1;
580
+ } catch {
581
+ say(`Codex \u2014 ${codexConfigPath} could not be written, so it was left as it was.`);
582
+ say(
583
+ ` A sandboxed agent usually cannot write outside its workspace. Run this same command in a regular terminal, or delete the [mcp_servers.${MCP_SERVER_KEY}] block from that file yourself.`
584
+ );
585
+ }
586
+ } else if (stripped.foreign) {
587
+ say(`Codex \u2014 ${codexConfigPath} names a ${MCP_SERVER_KEY} server this tool did not write.`);
588
+ say(` Delete its [mcp_servers.${MCP_SERVER_KEY}] block by hand.`);
589
+ }
590
+ if (removeSkill(codexDir)) removed += 1;
591
+ }
592
+ if (removed === 0 && unique.size === 0) {
593
+ say("Nothing to remove \u2014 Superhands is not installed for these clients on this machine.");
594
+ return;
595
+ }
596
+ say("Done. Restart any client that was open to drop the connection it already loaded.");
597
+ }
315
598
 
316
599
  // connect-client.ts
317
600
  await main();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@super-hands/connect",
3
- "version": "0.1.6",
4
- "description": "Connect the coding agents on this machine to your team's Superhands MCP server. Writes each client's own config; reads no repository, uploads nothing.",
3
+ "version": "0.1.9",
4
+ "description": "Connect the coding agents on this machine to your team's Superhands MCP server, and take it back off again with `uninstall`. Writes each client's own config; reads no repository, uploads nothing.",
5
5
  "bin": {
6
6
  "superhands-connect": "client.mjs"
7
7
  },