tandem-editor 0.7.0 → 0.8.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({
@@ -51,15 +397,29 @@ __export(setup_exports, {
51
397
  validateChannelShimPrereq: () => validateChannelShimPrereq
52
398
  });
53
399
  import { randomUUID } from "crypto";
54
- import { existsSync, readFileSync as readFileSync2 } from "fs";
400
+ import { existsSync, readdirSync, readFileSync as readFileSync2 } from "fs";
55
401
  import { copyFile, mkdir, rename, unlink, writeFile } from "fs/promises";
56
402
  import { homedir } from "os";
57
403
  import { basename, dirname as dirname2, join, resolve as resolve2 } from "path";
58
404
  import { fileURLToPath as fileURLToPath2 } from "url";
59
405
  function buildMcpEntries(channelPath, opts = {}) {
60
- const tandemEntry = { type: "http", url: `${MCP_URL}/mcp` };
61
- if (opts.token) {
62
- tandemEntry.headers = { Authorization: `Bearer ${opts.token}` };
406
+ const isDesktop = opts.targetKind === "claude-desktop";
407
+ let tandemEntry;
408
+ if (isDesktop) {
409
+ const env = { TANDEM_URL: MCP_URL };
410
+ if (opts.token) {
411
+ env.TANDEM_AUTH_TOKEN = opts.token;
412
+ }
413
+ tandemEntry = {
414
+ command: "npx",
415
+ args: ["-y", "tandem-editor", "mcp-stdio"],
416
+ env
417
+ };
418
+ } else {
419
+ tandemEntry = { type: "http", url: `${MCP_URL}/mcp` };
420
+ if (opts.token) {
421
+ tandemEntry.headers = { Authorization: `Bearer ${opts.token}` };
422
+ }
63
423
  }
64
424
  const entries = { tandem: tandemEntry };
65
425
  if (opts.withChannelShim) {
@@ -81,7 +441,7 @@ function detectTargets(opts = {}) {
81
441
  const claudeCodeConfig = join(home, ".claude.json");
82
442
  const claudeCodeDir = join(home, ".claude");
83
443
  if (opts.force || existsSync(claudeCodeConfig) || existsSync(claudeCodeDir)) {
84
- targets.push({ label: "Claude Code", configPath: claudeCodeConfig });
444
+ targets.push({ label: "Claude Code", configPath: claudeCodeConfig, kind: "claude-code" });
85
445
  }
86
446
  let desktopConfig = null;
87
447
  if (process.platform === "win32") {
@@ -99,7 +459,33 @@ function detectTargets(opts = {}) {
99
459
  desktopConfig = join(home, ".config", "claude", "claude_desktop_config.json");
100
460
  }
101
461
  if (desktopConfig && (opts.force || existsSync(desktopConfig))) {
102
- targets.push({ label: "Claude Desktop", configPath: desktopConfig });
462
+ targets.push({ label: "Claude Desktop", configPath: desktopConfig, kind: "claude-desktop" });
463
+ }
464
+ if (process.platform === "win32") {
465
+ const localAppData = opts.localAppDataOverride ?? process.env.LOCALAPPDATA ?? join(home, "AppData", "Local");
466
+ const packagesDir = join(localAppData, "Packages");
467
+ try {
468
+ const entries = readdirSync(packagesDir);
469
+ for (const pkg of entries.filter((n) => n.startsWith("Claude_"))) {
470
+ const msixConfig = join(
471
+ packagesDir,
472
+ pkg,
473
+ "LocalCache",
474
+ "Roaming",
475
+ "Claude",
476
+ "claude_desktop_config.json"
477
+ );
478
+ if (opts.force || existsSync(msixConfig)) {
479
+ const suffix = entries.filter((n) => n.startsWith("Claude_")).length > 1 ? ` (${pkg.slice(0, 12)}\u2026)` : "";
480
+ targets.push({
481
+ label: `Claude Desktop MSIX${suffix}`,
482
+ configPath: msixConfig,
483
+ kind: "claude-desktop"
484
+ });
485
+ }
486
+ }
487
+ } catch {
488
+ }
103
489
  }
104
490
  return targets;
105
491
  }
@@ -146,13 +532,19 @@ async function applyConfig(configPath, entries) {
146
532
  throw err;
147
533
  }
148
534
  }
149
- const updated = {
150
- ...existing,
151
- mcpServers: {
152
- ...existing.mcpServers ?? {},
153
- ...entries
154
- }
535
+ const merged = {
536
+ ...existing.mcpServers ?? {},
537
+ ...entries
155
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 };
156
548
  await mkdir(dirname2(configPath), { recursive: true });
157
549
  await atomicWrite(JSON.stringify(updated, null, 2) + "\n", configPath);
158
550
  }
@@ -167,13 +559,14 @@ function validateChannelShimPrereq(channelPath) {
167
559
  }
168
560
  async function applyConfigWithToken(token, opts = {}) {
169
561
  const targets = detectTargets({ force: opts.force });
170
- const entries = buildMcpEntries(CHANNEL_DIST, {
171
- withChannelShim: opts.withChannelShim,
172
- token: token ?? void 0
173
- });
174
562
  let updated = 0;
175
563
  const errors = [];
176
564
  for (const t of targets) {
565
+ const entries = buildMcpEntries(CHANNEL_DIST, {
566
+ withChannelShim: opts.withChannelShim,
567
+ token: token ?? void 0,
568
+ targetKind: t.kind
569
+ });
177
570
  try {
178
571
  await applyConfig(t.configPath, entries);
179
572
  updated++;
@@ -204,9 +597,12 @@ Run 'npm run build' first, or drop --with-channel-shim to use the plugin monitor
204
597
  console.error(` Found: ${t.label} (${t.configPath})`);
205
598
  }
206
599
  console.error("\nWriting MCP configuration...");
207
- const entries = buildMcpEntries(CHANNEL_DIST, { withChannelShim: opts.withChannelShim });
208
600
  let failures = 0;
209
601
  for (const t of targets) {
602
+ const entries = buildMcpEntries(CHANNEL_DIST, {
603
+ withChannelShim: opts.withChannelShim,
604
+ targetKind: t.kind
605
+ });
210
606
  try {
211
607
  await applyConfig(t.configPath, entries);
212
608
  console.error(` \x1B[32m\u2713\x1B[0m ${t.label}`);
@@ -634,7 +1030,7 @@ var init_utils = __esm({
634
1030
  }
635
1031
  });
636
1032
 
637
- // src/server/events/types.ts
1033
+ // src/shared/events/types.ts
638
1034
  function parseTandemEvent(raw) {
639
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") {
640
1036
  return null;
@@ -684,6 +1080,7 @@ function formatEventContent(event) {
684
1080
  }
685
1081
  default: {
686
1082
  const _exhaustive = event;
1083
+ void _exhaustive;
687
1084
  return `Unknown event${doc}`;
688
1085
  }
689
1086
  }
@@ -713,6 +1110,7 @@ function formatEventMeta(event) {
713
1110
  break;
714
1111
  default: {
715
1112
  const _exhaustive = event;
1113
+ void _exhaustive;
716
1114
  break;
717
1115
  }
718
1116
  }
@@ -720,7 +1118,7 @@ function formatEventMeta(event) {
720
1118
  }
721
1119
  var VALID_EVENT_TYPES;
722
1120
  var init_types = __esm({
723
- "src/server/events/types.ts"() {
1121
+ "src/shared/events/types.ts"() {
724
1122
  "use strict";
725
1123
  init_utils();
726
1124
  VALID_EVENT_TYPES = /* @__PURE__ */ new Set([
@@ -900,9 +1298,9 @@ var AWARENESS_DEBOUNCE_MS, MODE_CACHE_TTL_MS, cachedMode, cachedModeAt;
900
1298
  var init_event_bridge = __esm({
901
1299
  "src/channel/event-bridge.ts"() {
902
1300
  "use strict";
903
- init_types();
904
1301
  init_cli_runtime();
905
1302
  init_constants();
1303
+ init_types();
906
1304
  AWARENESS_DEBOUNCE_MS = 500;
907
1305
  MODE_CACHE_TTL_MS = 2e3;
908
1306
  cachedMode = "tandem";
@@ -1106,23 +1504,23 @@ var init_channel = __esm({
1106
1504
  }
1107
1505
  });
1108
1506
 
1109
- // src/server/auth/token-store.ts
1507
+ // src/shared/auth/token-file.ts
1110
1508
  import envPaths from "env-paths";
1111
- import fs from "fs";
1112
- import path from "path";
1509
+ import fs2 from "fs";
1510
+ import path3 from "path";
1113
1511
  function getTokenFilePath() {
1114
- return path.join(envPaths("tandem", { suffix: "" }).data, TOKEN_FILE_NAME);
1512
+ return path3.join(envPaths("tandem", { suffix: "" }).data, TOKEN_FILE_NAME);
1115
1513
  }
1116
1514
  async function readTokenFromFile() {
1117
1515
  const filePath = getTokenFilePath();
1118
1516
  try {
1119
- const content = await fs.promises.readFile(filePath, "utf8");
1517
+ const content = await fs2.promises.readFile(filePath, "utf8");
1120
1518
  if (process.platform !== "win32") {
1121
1519
  try {
1122
- const stat = await fs.promises.stat(filePath);
1520
+ const stat = await fs2.promises.stat(filePath);
1123
1521
  if ((stat.mode & 63) !== 0) {
1124
1522
  console.error("[tandem] auth token file has insecure permissions; attempting chmod 0600");
1125
- await fs.promises.chmod(filePath, 384);
1523
+ await fs2.promises.chmod(filePath, 384);
1126
1524
  }
1127
1525
  } catch {
1128
1526
  }
@@ -1134,8 +1532,8 @@ async function readTokenFromFile() {
1134
1532
  throw err;
1135
1533
  }
1136
1534
  }
1137
- var init_token_store = __esm({
1138
- "src/server/auth/token-store.ts"() {
1535
+ var init_token_file = __esm({
1536
+ "src/shared/auth/token-file.ts"() {
1139
1537
  "use strict";
1140
1538
  init_constants();
1141
1539
  }
@@ -1147,8 +1545,8 @@ __export(rotate_token_exports, {
1147
1545
  rotateToken: () => rotateToken
1148
1546
  });
1149
1547
  import { createHash, randomBytes } from "crypto";
1150
- import { promises as fsPromises } from "fs";
1151
- import path2 from "path";
1548
+ import { promises as fsPromises2 } from "fs";
1549
+ import path4 from "path";
1152
1550
  function fingerprint(token) {
1153
1551
  return createHash("sha256").update(token, "utf8").digest("hex").slice(0, 8);
1154
1552
  }
@@ -1172,13 +1570,13 @@ async function rotateToken() {
1172
1570
  }
1173
1571
  const newToken = generateToken();
1174
1572
  const tokenPath = getTokenFilePath();
1175
- const dir = path2.dirname(tokenPath);
1176
- const tmpPath = path2.join(dir, `.auth-token-tmp-${randomBytes(4).toString("hex")}`);
1573
+ const dir = path4.dirname(tokenPath);
1574
+ const tmpPath = path4.join(dir, `.auth-token-tmp-${randomBytes(4).toString("hex")}`);
1177
1575
  try {
1178
- await fsPromises.writeFile(tmpPath, newToken, { encoding: "utf8", mode: 384 });
1179
- await fsPromises.rename(tmpPath, tokenPath);
1576
+ await fsPromises2.writeFile(tmpPath, newToken, { encoding: "utf8", mode: 384 });
1577
+ await fsPromises2.rename(tmpPath, tokenPath);
1180
1578
  } catch (err) {
1181
- await fsPromises.unlink(tmpPath).catch(() => {
1579
+ await fsPromises2.unlink(tmpPath).catch(() => {
1182
1580
  });
1183
1581
  throw err;
1184
1582
  }
@@ -1257,7 +1655,7 @@ async function rotateToken() {
1257
1655
  var init_rotate_token = __esm({
1258
1656
  "src/cli/rotate-token.ts"() {
1259
1657
  "use strict";
1260
- init_token_store();
1658
+ init_token_file();
1261
1659
  init_constants();
1262
1660
  init_setup();
1263
1661
  }
@@ -1320,7 +1718,7 @@ process.once("unhandledRejection", (reason) => {
1320
1718
  `);
1321
1719
  process.exit(1);
1322
1720
  });
1323
- var version = true ? "0.7.0" : "0.0.0-dev";
1721
+ var version = true ? "0.8.0" : "0.0.0-dev";
1324
1722
  var args = process.argv.slice(2);
1325
1723
  var isStdioMode = args[0] === "mcp-stdio" || args[0] === "channel";
1326
1724
  if (!isStdioMode) {
@@ -1330,7 +1728,7 @@ if (args.includes("--help") || args.includes("-h")) {
1330
1728
  console.log(`tandem v${version}
1331
1729
 
1332
1730
  Usage:
1333
- tandem Start Tandem server and open the browser
1731
+ tandem Start Tandem server and open the editor
1334
1732
  tandem setup Register MCP tools with Claude Code / Claude Desktop
1335
1733
  tandem setup --force Register to default paths regardless of detection
1336
1734
  tandem setup --with-channel-shim Also register the stdio channel shim (legacy opt-in)
@@ -1350,7 +1748,11 @@ if (args.includes("--version") || args.includes("-v")) {
1350
1748
  process.exit(0);
1351
1749
  }
1352
1750
  try {
1353
- if (args[0] === "setup") {
1751
+ if (args[0] === "--uninstall-scrub") {
1752
+ const { runUninstallScrub: runUninstallScrub2 } = await Promise.resolve().then(() => (init_uninstall_scrub(), uninstall_scrub_exports));
1753
+ const exitCode = await runUninstallScrub2();
1754
+ process.exit(exitCode);
1755
+ } else if (args[0] === "setup") {
1354
1756
  const { runSetup: runSetup2 } = await Promise.resolve().then(() => (init_setup(), setup_exports));
1355
1757
  await runSetup2({
1356
1758
  force: args.includes("--force"),