tandem-editor 0.7.1 → 0.9.0

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.
package/dist/cli/index.js CHANGED
@@ -9,6 +9,352 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
+ // src/cli/win-path-guard.ts
13
+ import { promises as fs } from "fs";
14
+ import path from "path";
15
+ async function assertSafeWorkspacePath(candidate, realLocalAppData, logger) {
16
+ const warn = (msg) => logger?.warn(`[path-guard] ${msg}`);
17
+ if (await hasSymlinkInChain(candidate, warn)) {
18
+ warn(`symlink/reparse point in chain: ${candidate}`);
19
+ return null;
20
+ }
21
+ let real;
22
+ try {
23
+ real = await fs.realpath(candidate);
24
+ } catch (err) {
25
+ warn(`realpath failed for ${candidate}: ${err.message}`);
26
+ return null;
27
+ }
28
+ if (isUncPath(real)) {
29
+ warn(`UNC path rejected: ${real}`);
30
+ return null;
31
+ }
32
+ if (!isComponentWiseChild(real, realLocalAppData)) {
33
+ warn(`path outside %LOCALAPPDATA%: ${real}`);
34
+ return null;
35
+ }
36
+ return real;
37
+ }
38
+ async function hasSymlinkInChain(p, warn) {
39
+ let current = path.resolve(p);
40
+ const visited = /* @__PURE__ */ new Set();
41
+ while (true) {
42
+ if (visited.has(current)) break;
43
+ visited.add(current);
44
+ try {
45
+ const stat = await fs.lstat(current);
46
+ if (stat.isSymbolicLink()) {
47
+ return true;
48
+ }
49
+ } catch (err) {
50
+ warn(`lstat failed for ${current}: ${err.message}`);
51
+ return true;
52
+ }
53
+ const parent = path.dirname(current);
54
+ if (parent === current) break;
55
+ current = parent;
56
+ }
57
+ return false;
58
+ }
59
+ function isUncPath(p) {
60
+ if (p.startsWith("\\\\?\\UNC\\") || p.startsWith("//?/UNC/")) return true;
61
+ if (p.startsWith("\\\\") && !p.startsWith("\\\\?\\") || p.startsWith("//") && !p.startsWith("//?/"))
62
+ return true;
63
+ return false;
64
+ }
65
+ function isComponentWiseChild(child, root) {
66
+ const normalize = (p) => p.replace(/[\\/]+/g, path.sep).replace(/[/\\]$/, "");
67
+ const rootNorm = normalize(root);
68
+ const childNorm = normalize(child);
69
+ const rootParts = rootNorm.split(path.sep);
70
+ const childParts = childNorm.split(path.sep);
71
+ if (childParts.length <= rootParts.length) return false;
72
+ for (let i = 0; i < rootParts.length; i++) {
73
+ if (rootParts[i].toLowerCase() !== childParts[i].toLowerCase()) return false;
74
+ }
75
+ return true;
76
+ }
77
+ var init_win_path_guard = __esm({
78
+ "src/cli/win-path-guard.ts"() {
79
+ "use strict";
80
+ }
81
+ });
82
+
83
+ // src/cli/uninstall-scrub.ts
84
+ var uninstall_scrub_exports = {};
85
+ __export(uninstall_scrub_exports, {
86
+ findCoworkWorkspaces: () => findCoworkWorkspaces,
87
+ removeCoworkSettings: () => removeCoworkSettings,
88
+ removeInstalledPlugins: () => removeInstalledPlugins,
89
+ removeKnownMarketplaces: () => removeKnownMarketplaces,
90
+ rewriteJson: () => rewriteJson,
91
+ runUninstallScrub: () => runUninstallScrub
92
+ });
93
+ import { execFile } from "child_process";
94
+ import { promises as fsPromises } from "fs";
95
+ import path2 from "path";
96
+ import { promisify } from "util";
97
+ async function openLogger() {
98
+ const localAppData = process.env.LOCALAPPDATA;
99
+ if (!localAppData) {
100
+ const write2 = (level, msg) => {
101
+ process.stderr.write(`[tandem uninstall-scrub ${level}] ${msg}
102
+ `);
103
+ };
104
+ return {
105
+ info: (m) => write2("info", m),
106
+ warn: (m) => write2("warn", m),
107
+ error: (m) => write2("error", m),
108
+ close: async () => {
109
+ }
110
+ };
111
+ }
112
+ const logDir = path2.join(localAppData, "tandem", "Logs");
113
+ await fsPromises.mkdir(logDir, { recursive: true }).catch(() => {
114
+ });
115
+ const logPath = path2.join(logDir, "uninstall.log");
116
+ const stream = await fsPromises.open(logPath, "a").catch(() => null);
117
+ const write = (level, msg) => {
118
+ const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [${level}] ${msg}
119
+ `;
120
+ process.stderr.write(line);
121
+ if (stream) {
122
+ stream.write(line).catch(() => {
123
+ });
124
+ }
125
+ };
126
+ return {
127
+ info: (m) => write("info", m),
128
+ warn: (m) => write("warn", m),
129
+ error: (m) => write("error", m),
130
+ close: async () => {
131
+ if (stream) await stream.close().catch(() => {
132
+ });
133
+ }
134
+ };
135
+ }
136
+ async function findCoworkWorkspaces(logger) {
137
+ const localAppData = process.env.LOCALAPPDATA;
138
+ if (!localAppData) {
139
+ logger.info("%LOCALAPPDATA% not set \u2014 skipping workspace scan");
140
+ return [];
141
+ }
142
+ let realLad;
143
+ try {
144
+ realLad = await fsPromises.realpath(localAppData);
145
+ } catch {
146
+ realLad = localAppData;
147
+ }
148
+ const packagesDir = path2.join(localAppData, "Packages");
149
+ let packageEntries;
150
+ try {
151
+ packageEntries = await fsPromises.readdir(packagesDir);
152
+ } catch (err) {
153
+ logger.info(`cannot read Packages dir: ${err.message}`);
154
+ return [];
155
+ }
156
+ const claudePackages = packageEntries.filter((name) => name.startsWith("Claude_"));
157
+ if (claudePackages.length === 0) {
158
+ logger.info("no Claude_* package directories found");
159
+ return [];
160
+ }
161
+ const workspaces = [];
162
+ for (const pkg of claudePackages) {
163
+ const sessionsRoot = path2.join(
164
+ packagesDir,
165
+ pkg,
166
+ "LocalCache",
167
+ "Roaming",
168
+ "Claude",
169
+ "local-agent-mode-sessions"
170
+ );
171
+ let wsEntries;
172
+ try {
173
+ wsEntries = await fsPromises.readdir(sessionsRoot);
174
+ } catch (err) {
175
+ logger.warn(`cannot read sessions root ${sessionsRoot}: ${err.message}`);
176
+ continue;
177
+ }
178
+ for (const ws of wsEntries) {
179
+ const wsPath = path2.join(sessionsRoot, ws);
180
+ let vmEntries;
181
+ try {
182
+ vmEntries = await fsPromises.readdir(wsPath);
183
+ } catch (err) {
184
+ logger.warn(`cannot read workspace dir ${wsPath}: ${err.message}`);
185
+ continue;
186
+ }
187
+ for (const vm of vmEntries) {
188
+ const vmPath = path2.join(wsPath, vm);
189
+ try {
190
+ const stat = await fsPromises.stat(vmPath);
191
+ if (!stat.isDirectory()) continue;
192
+ const safePath = await assertSafeWorkspacePath(vmPath, realLad, logger);
193
+ if (safePath !== null) {
194
+ workspaces.push(safePath);
195
+ }
196
+ } catch (err) {
197
+ logger.warn(`cannot stat ${vmPath}: ${err.message}`);
198
+ }
199
+ }
200
+ }
201
+ }
202
+ logger.info(`found ${workspaces.length} workspace(s)`);
203
+ return workspaces;
204
+ }
205
+ async function rewriteJson(filePath, mutate, logger) {
206
+ let content;
207
+ try {
208
+ content = await fsPromises.readFile(filePath, "utf8");
209
+ } catch (err) {
210
+ if (err.code === "ENOENT") {
211
+ return false;
212
+ }
213
+ logger.warn(`cannot read ${filePath}: ${err.message}`);
214
+ return false;
215
+ }
216
+ let parsed;
217
+ try {
218
+ parsed = JSON.parse(content);
219
+ } catch (err) {
220
+ logger.warn(`invalid JSON in ${filePath}: ${err.message}`);
221
+ return false;
222
+ }
223
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
224
+ logger.warn(`${filePath} is not a JSON object \u2014 skipping`);
225
+ return false;
226
+ }
227
+ const changed = mutate(parsed);
228
+ if (!changed) {
229
+ return false;
230
+ }
231
+ const dir = path2.dirname(filePath);
232
+ const tmpName = `.tandem-scrub-tmp-${Math.random().toString(36).slice(2, 10)}`;
233
+ const tmpPath = path2.join(dir, tmpName);
234
+ try {
235
+ await fsPromises.writeFile(tmpPath, JSON.stringify(parsed, null, 2), "utf8");
236
+ await fsPromises.rename(tmpPath, filePath);
237
+ } catch (err) {
238
+ await fsPromises.unlink(tmpPath).catch(() => {
239
+ });
240
+ throw err;
241
+ }
242
+ return true;
243
+ }
244
+ function removeInstalledPlugins(obj) {
245
+ let changed = false;
246
+ for (const key of ["mcpServers", "servers"]) {
247
+ const servers = obj[key];
248
+ if (typeof servers === "object" && servers !== null && !Array.isArray(servers)) {
249
+ const map = servers;
250
+ if (TANDEM_PLUGIN_ID in map) {
251
+ delete map[TANDEM_PLUGIN_ID];
252
+ changed = true;
253
+ }
254
+ }
255
+ }
256
+ return changed;
257
+ }
258
+ function removeKnownMarketplaces(obj) {
259
+ const mp = obj.marketplaces;
260
+ if (typeof mp === "object" && mp !== null && !Array.isArray(mp)) {
261
+ const map = mp;
262
+ if (TANDEM_PLUGIN_ID in map) {
263
+ delete map[TANDEM_PLUGIN_ID];
264
+ return true;
265
+ }
266
+ }
267
+ return false;
268
+ }
269
+ function removeCoworkSettings(obj) {
270
+ const enabled = obj.enabledPlugins;
271
+ if (Array.isArray(enabled)) {
272
+ const before = enabled.length;
273
+ obj.enabledPlugins = enabled.filter((v) => v !== TANDEM_ENABLED_KEY);
274
+ return obj.enabledPlugins.length < before;
275
+ }
276
+ if (typeof enabled === "object" && enabled !== null) {
277
+ const map = enabled;
278
+ if (TANDEM_ENABLED_KEY in map) {
279
+ delete map[TANDEM_ENABLED_KEY];
280
+ return true;
281
+ }
282
+ }
283
+ return false;
284
+ }
285
+ async function deleteFirewallRule(name, logger) {
286
+ try {
287
+ await execFileAsync("netsh", ["advfirewall", "firewall", "delete", "rule", `name=${name}`]);
288
+ logger.info(`deleted firewall rule: ${name}`);
289
+ } catch (err) {
290
+ const e = err;
291
+ const stdoutStr = e.stdout ?? "";
292
+ if (stdoutStr.includes("No rules match")) {
293
+ logger.info(`no firewall rule to delete: ${name}`);
294
+ return;
295
+ }
296
+ logger.warn(
297
+ `failed to delete firewall rule ${name}: ${e.message ?? String(err)} (stdout: ${stdoutStr.trim().slice(0, 200)})`
298
+ );
299
+ }
300
+ }
301
+ async function runUninstallScrub() {
302
+ const logger = await openLogger();
303
+ logger.info("Tandem uninstall scrub starting");
304
+ if (process.platform !== "win32") {
305
+ logger.info(`platform ${process.platform} is not win32 \u2014 skipping Cowork scrub`);
306
+ await logger.close();
307
+ return 0;
308
+ }
309
+ let failures = 0;
310
+ try {
311
+ const workspaces = await findCoworkWorkspaces(logger);
312
+ for (const ws of workspaces) {
313
+ const pluginsDir = path2.join(ws, "cowork_plugins");
314
+ try {
315
+ await rewriteJson(
316
+ path2.join(pluginsDir, "installed_plugins.json"),
317
+ removeInstalledPlugins,
318
+ logger
319
+ );
320
+ await rewriteJson(
321
+ path2.join(pluginsDir, "known_marketplaces.json"),
322
+ removeKnownMarketplaces,
323
+ logger
324
+ );
325
+ await rewriteJson(
326
+ path2.join(pluginsDir, "cowork_settings.json"),
327
+ removeCoworkSettings,
328
+ logger
329
+ );
330
+ } catch (err) {
331
+ logger.error(`scrub failed for ${ws}: ${err.message}`);
332
+ failures++;
333
+ }
334
+ }
335
+ await deleteFirewallRule(FIREWALL_ALLOW_RULE, logger);
336
+ await deleteFirewallRule(FIREWALL_DENY_RULE, logger);
337
+ logger.info(`scrub complete: ${workspaces.length} workspace(s), ${failures} failure(s)`);
338
+ } catch (err) {
339
+ logger.error(`scrub fatal error: ${err.message}`);
340
+ failures++;
341
+ }
342
+ await logger.close();
343
+ return failures > 0 ? 1 : 0;
344
+ }
345
+ var execFileAsync, TANDEM_PLUGIN_ID, TANDEM_ENABLED_KEY, FIREWALL_ALLOW_RULE, FIREWALL_DENY_RULE;
346
+ var init_uninstall_scrub = __esm({
347
+ "src/cli/uninstall-scrub.ts"() {
348
+ "use strict";
349
+ init_win_path_guard();
350
+ execFileAsync = promisify(execFile);
351
+ TANDEM_PLUGIN_ID = "tandem";
352
+ TANDEM_ENABLED_KEY = "tandem@tandem";
353
+ FIREWALL_ALLOW_RULE = "Tandem Cowork";
354
+ FIREWALL_DENY_RULE = "Tandem Cowork \u2014 Deny (elevation refused)";
355
+ }
356
+ });
357
+
12
358
  // src/shared/constants.ts
13
359
  var DEFAULT_MCP_PORT, MAX_FILE_SIZE, MAX_WS_PAYLOAD, IDLE_TIMEOUT, SESSION_MAX_AGE, CHANNEL_MAX_RETRIES, CHANNEL_RETRY_DELAY_MS, TOKEN_FILE_NAME;
14
360
  var init_constants = __esm({
@@ -186,13 +532,19 @@ async function applyConfig(configPath, entries) {
186
532
  throw err;
187
533
  }
188
534
  }
189
- const updated = {
190
- ...existing,
191
- mcpServers: {
192
- ...existing.mcpServers ?? {},
193
- ...entries
194
- }
535
+ const merged = {
536
+ ...existing.mcpServers ?? {},
537
+ ...entries
195
538
  };
539
+ if (!entries["tandem-channel"]) {
540
+ if (merged["tandem-channel"]) {
541
+ console.error(
542
+ ` Warning: removed stale tandem-channel entry from ${configPath} (legacy Tauri install artifact)`
543
+ );
544
+ }
545
+ delete merged["tandem-channel"];
546
+ }
547
+ const updated = { ...existing, mcpServers: merged };
196
548
  await mkdir(dirname2(configPath), { recursive: true });
197
549
  await atomicWrite(JSON.stringify(updated, null, 2) + "\n", configPath);
198
550
  }
@@ -678,7 +1030,7 @@ var init_utils = __esm({
678
1030
  }
679
1031
  });
680
1032
 
681
- // src/server/events/types.ts
1033
+ // src/shared/events/types.ts
682
1034
  function parseTandemEvent(raw) {
683
1035
  if (typeof raw !== "object" || raw === null || !("id" in raw) || typeof raw.id !== "string" || !("type" in raw) || !VALID_EVENT_TYPES.has(raw.type) || !("timestamp" in raw) || typeof raw.timestamp !== "number" || !("payload" in raw) || typeof raw.payload !== "object") {
684
1036
  return null;
@@ -728,6 +1080,7 @@ function formatEventContent(event) {
728
1080
  }
729
1081
  default: {
730
1082
  const _exhaustive = event;
1083
+ void _exhaustive;
731
1084
  return `Unknown event${doc}`;
732
1085
  }
733
1086
  }
@@ -757,6 +1110,7 @@ function formatEventMeta(event) {
757
1110
  break;
758
1111
  default: {
759
1112
  const _exhaustive = event;
1113
+ void _exhaustive;
760
1114
  break;
761
1115
  }
762
1116
  }
@@ -764,7 +1118,7 @@ function formatEventMeta(event) {
764
1118
  }
765
1119
  var VALID_EVENT_TYPES;
766
1120
  var init_types = __esm({
767
- "src/server/events/types.ts"() {
1121
+ "src/shared/events/types.ts"() {
768
1122
  "use strict";
769
1123
  init_utils();
770
1124
  VALID_EVENT_TYPES = /* @__PURE__ */ new Set([
@@ -841,7 +1195,11 @@ async function connectAndStream(mcp, tandemUrl, lastEventId, onEventId) {
841
1195
  status: "idle",
842
1196
  active: false
843
1197
  })
844
- }).catch(() => {
1198
+ }).catch((err) => {
1199
+ console.error(
1200
+ "[Channel] clearAwareness failed (non-fatal):",
1201
+ err instanceof Error ? err.message : err
1202
+ );
845
1203
  });
846
1204
  }
847
1205
  function flushAwareness() {
@@ -887,11 +1245,21 @@ async function connectAndStream(mcp, tandemUrl, lastEventId, onEventId) {
887
1245
  try {
888
1246
  event = parseTandemEvent(JSON.parse(data));
889
1247
  } catch {
890
- console.error("[Channel] Malformed SSE event data (skipping):", data.slice(0, 200));
1248
+ console.error(
1249
+ "[Channel] Malformed SSE event data (skipping), eventId=%s:",
1250
+ eventId,
1251
+ data.slice(0, 200)
1252
+ );
1253
+ if (eventId) onEventId(eventId);
891
1254
  continue;
892
1255
  }
893
1256
  if (!event) {
894
- console.error("[Channel] Received invalid SSE event, skipping");
1257
+ console.error(
1258
+ "[Channel] Invalid SSE event structure (skipping), eventId=%s:",
1259
+ eventId,
1260
+ data.slice(0, 200)
1261
+ );
1262
+ if (eventId) onEventId(eventId);
895
1263
  continue;
896
1264
  }
897
1265
  if (event.type !== "chat:message") {
@@ -902,7 +1270,6 @@ async function connectAndStream(mcp, tandemUrl, lastEventId, onEventId) {
902
1270
  continue;
903
1271
  }
904
1272
  }
905
- if (eventId) onEventId(eventId);
906
1273
  try {
907
1274
  await mcp.notification({
908
1275
  method: "notifications/claude/channel",
@@ -915,6 +1282,7 @@ async function connectAndStream(mcp, tandemUrl, lastEventId, onEventId) {
915
1282
  console.error("[Channel] MCP notification failed (transport broken?):", err);
916
1283
  throw err;
917
1284
  }
1285
+ if (eventId) onEventId(eventId);
918
1286
  scheduleAwareness(event);
919
1287
  }
920
1288
  }
@@ -944,9 +1312,9 @@ var AWARENESS_DEBOUNCE_MS, MODE_CACHE_TTL_MS, cachedMode, cachedModeAt;
944
1312
  var init_event_bridge = __esm({
945
1313
  "src/channel/event-bridge.ts"() {
946
1314
  "use strict";
947
- init_types();
948
1315
  init_cli_runtime();
949
1316
  init_constants();
1317
+ init_types();
950
1318
  AWARENESS_DEBOUNCE_MS = 500;
951
1319
  MODE_CACHE_TTL_MS = 2e3;
952
1320
  cachedMode = "tandem";
@@ -1150,23 +1518,23 @@ var init_channel = __esm({
1150
1518
  }
1151
1519
  });
1152
1520
 
1153
- // src/server/auth/token-store.ts
1521
+ // src/shared/auth/token-file.ts
1154
1522
  import envPaths from "env-paths";
1155
- import fs from "fs";
1156
- import path from "path";
1523
+ import fs2 from "fs";
1524
+ import path3 from "path";
1157
1525
  function getTokenFilePath() {
1158
- return path.join(envPaths("tandem", { suffix: "" }).data, TOKEN_FILE_NAME);
1526
+ return path3.join(envPaths("tandem", { suffix: "" }).data, TOKEN_FILE_NAME);
1159
1527
  }
1160
1528
  async function readTokenFromFile() {
1161
1529
  const filePath = getTokenFilePath();
1162
1530
  try {
1163
- const content = await fs.promises.readFile(filePath, "utf8");
1531
+ const content = await fs2.promises.readFile(filePath, "utf8");
1164
1532
  if (process.platform !== "win32") {
1165
1533
  try {
1166
- const stat = await fs.promises.stat(filePath);
1534
+ const stat = await fs2.promises.stat(filePath);
1167
1535
  if ((stat.mode & 63) !== 0) {
1168
1536
  console.error("[tandem] auth token file has insecure permissions; attempting chmod 0600");
1169
- await fs.promises.chmod(filePath, 384);
1537
+ await fs2.promises.chmod(filePath, 384);
1170
1538
  }
1171
1539
  } catch {
1172
1540
  }
@@ -1178,8 +1546,8 @@ async function readTokenFromFile() {
1178
1546
  throw err;
1179
1547
  }
1180
1548
  }
1181
- var init_token_store = __esm({
1182
- "src/server/auth/token-store.ts"() {
1549
+ var init_token_file = __esm({
1550
+ "src/shared/auth/token-file.ts"() {
1183
1551
  "use strict";
1184
1552
  init_constants();
1185
1553
  }
@@ -1191,8 +1559,8 @@ __export(rotate_token_exports, {
1191
1559
  rotateToken: () => rotateToken
1192
1560
  });
1193
1561
  import { createHash, randomBytes } from "crypto";
1194
- import { promises as fsPromises } from "fs";
1195
- import path2 from "path";
1562
+ import { promises as fsPromises2 } from "fs";
1563
+ import path4 from "path";
1196
1564
  function fingerprint(token) {
1197
1565
  return createHash("sha256").update(token, "utf8").digest("hex").slice(0, 8);
1198
1566
  }
@@ -1216,13 +1584,13 @@ async function rotateToken() {
1216
1584
  }
1217
1585
  const newToken = generateToken();
1218
1586
  const tokenPath = getTokenFilePath();
1219
- const dir = path2.dirname(tokenPath);
1220
- const tmpPath = path2.join(dir, `.auth-token-tmp-${randomBytes(4).toString("hex")}`);
1587
+ const dir = path4.dirname(tokenPath);
1588
+ const tmpPath = path4.join(dir, `.auth-token-tmp-${randomBytes(4).toString("hex")}`);
1221
1589
  try {
1222
- await fsPromises.writeFile(tmpPath, newToken, { encoding: "utf8", mode: 384 });
1223
- await fsPromises.rename(tmpPath, tokenPath);
1590
+ await fsPromises2.writeFile(tmpPath, newToken, { encoding: "utf8", mode: 384 });
1591
+ await fsPromises2.rename(tmpPath, tokenPath);
1224
1592
  } catch (err) {
1225
- await fsPromises.unlink(tmpPath).catch(() => {
1593
+ await fsPromises2.unlink(tmpPath).catch(() => {
1226
1594
  });
1227
1595
  throw err;
1228
1596
  }
@@ -1301,7 +1669,7 @@ async function rotateToken() {
1301
1669
  var init_rotate_token = __esm({
1302
1670
  "src/cli/rotate-token.ts"() {
1303
1671
  "use strict";
1304
- init_token_store();
1672
+ init_token_file();
1305
1673
  init_constants();
1306
1674
  init_setup();
1307
1675
  }
@@ -1364,7 +1732,7 @@ process.once("unhandledRejection", (reason) => {
1364
1732
  `);
1365
1733
  process.exit(1);
1366
1734
  });
1367
- var version = true ? "0.7.1" : "0.0.0-dev";
1735
+ var version = true ? "0.9.0" : "0.0.0-dev";
1368
1736
  var args = process.argv.slice(2);
1369
1737
  var isStdioMode = args[0] === "mcp-stdio" || args[0] === "channel";
1370
1738
  if (!isStdioMode) {
@@ -1374,7 +1742,7 @@ if (args.includes("--help") || args.includes("-h")) {
1374
1742
  console.log(`tandem v${version}
1375
1743
 
1376
1744
  Usage:
1377
- tandem Start Tandem server and open the browser
1745
+ tandem Start Tandem server and open the editor
1378
1746
  tandem setup Register MCP tools with Claude Code / Claude Desktop
1379
1747
  tandem setup --force Register to default paths regardless of detection
1380
1748
  tandem setup --with-channel-shim Also register the stdio channel shim (legacy opt-in)
@@ -1394,7 +1762,11 @@ if (args.includes("--version") || args.includes("-v")) {
1394
1762
  process.exit(0);
1395
1763
  }
1396
1764
  try {
1397
- if (args[0] === "setup") {
1765
+ if (args[0] === "--uninstall-scrub") {
1766
+ const { runUninstallScrub: runUninstallScrub2 } = await Promise.resolve().then(() => (init_uninstall_scrub(), uninstall_scrub_exports));
1767
+ const exitCode = await runUninstallScrub2();
1768
+ process.exit(exitCode);
1769
+ } else if (args[0] === "setup") {
1398
1770
  const { runSetup: runSetup2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
1399
1771
  await runSetup2({
1400
1772
  force: args.includes("--force"),