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/.claude-plugin/plugin.json +9 -2
- package/CHANGELOG.md +43 -329
- package/README.md +36 -19
- package/dist/channel/index.js +3 -1
- package/dist/channel/index.js.map +1 -1
- package/dist/cli/index.js +442 -40
- package/dist/cli/index.js.map +1 -1
- package/dist/client/assets/CoworkSettings-CXODT6KV.js +1 -0
- package/dist/client/assets/core-DhEqZVGG.js +1 -0
- package/dist/client/assets/index-C6rbXHNq.css +1 -0
- package/dist/client/assets/index-q_8NVSM3.js +228 -0
- package/dist/client/assets/webview-BQBJMQvJ.js +1 -0
- package/dist/client/index.html +47 -1
- package/dist/monitor/index.js +33 -32
- package/dist/monitor/index.js.map +1 -1
- package/dist/server/index.js +3615 -3102
- package/dist/server/index.js.map +1 -1
- package/package.json +6 -7
- package/skills/tandem/SKILL.md +2 -2
- package/dist/client/assets/index-B1Cd5UGT.js +0 -349
- package/dist/client/assets/webview-0tvvWtyc.js +0 -1
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
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
150
|
-
...existing,
|
|
151
|
-
|
|
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/
|
|
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/
|
|
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/
|
|
1507
|
+
// src/shared/auth/token-file.ts
|
|
1110
1508
|
import envPaths from "env-paths";
|
|
1111
|
-
import
|
|
1112
|
-
import
|
|
1509
|
+
import fs2 from "fs";
|
|
1510
|
+
import path3 from "path";
|
|
1113
1511
|
function getTokenFilePath() {
|
|
1114
|
-
return
|
|
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
|
|
1517
|
+
const content = await fs2.promises.readFile(filePath, "utf8");
|
|
1120
1518
|
if (process.platform !== "win32") {
|
|
1121
1519
|
try {
|
|
1122
|
-
const stat = await
|
|
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
|
|
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
|
|
1138
|
-
"src/
|
|
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
|
|
1151
|
-
import
|
|
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 =
|
|
1176
|
-
const tmpPath =
|
|
1573
|
+
const dir = path4.dirname(tokenPath);
|
|
1574
|
+
const tmpPath = path4.join(dir, `.auth-token-tmp-${randomBytes(4).toString("hex")}`);
|
|
1177
1575
|
try {
|
|
1178
|
-
await
|
|
1179
|
-
await
|
|
1576
|
+
await fsPromises2.writeFile(tmpPath, newToken, { encoding: "utf8", mode: 384 });
|
|
1577
|
+
await fsPromises2.rename(tmpPath, tokenPath);
|
|
1180
1578
|
} catch (err) {
|
|
1181
|
-
await
|
|
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
|
-
|
|
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.
|
|
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
|
|
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] === "
|
|
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"),
|