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/.claude-plugin/plugin.json +9 -2
- package/CHANGELOG.md +75 -313
- package/README.md +37 -28
- package/dist/channel/index.js +21 -5
- package/dist/channel/index.js.map +1 -1
- package/dist/cli/index.js +405 -33
- package/dist/cli/index.js.map +1 -1
- package/dist/client/assets/CoworkSettings-C9Dd4D9z.js +1 -0
- package/dist/client/assets/core-DhEqZVGG.js +1 -0
- package/dist/client/assets/index-Bz3WFCWw.css +1 -0
- package/dist/client/assets/index-CN_6DqdC.js +228 -0
- package/dist/client/assets/webview-BQBJMQvJ.js +1 -0
- package/dist/client/index.html +47 -1
- package/dist/monitor/index.js +34 -33
- package/dist/monitor/index.js.map +1 -1
- package/dist/server/index.js +3941 -3394
- package/dist/server/index.js.map +1 -1
- package/package.json +6 -7
- package/skills/tandem/SKILL.md +9 -9
- package/dist/client/assets/index-DLTxaDBk.js +0 -351
- 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({
|
|
@@ -186,13 +532,19 @@ async function applyConfig(configPath, entries) {
|
|
|
186
532
|
throw err;
|
|
187
533
|
}
|
|
188
534
|
}
|
|
189
|
-
const
|
|
190
|
-
...existing,
|
|
191
|
-
|
|
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/
|
|
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/
|
|
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(
|
|
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(
|
|
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/
|
|
1521
|
+
// src/shared/auth/token-file.ts
|
|
1154
1522
|
import envPaths from "env-paths";
|
|
1155
|
-
import
|
|
1156
|
-
import
|
|
1523
|
+
import fs2 from "fs";
|
|
1524
|
+
import path3 from "path";
|
|
1157
1525
|
function getTokenFilePath() {
|
|
1158
|
-
return
|
|
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
|
|
1531
|
+
const content = await fs2.promises.readFile(filePath, "utf8");
|
|
1164
1532
|
if (process.platform !== "win32") {
|
|
1165
1533
|
try {
|
|
1166
|
-
const stat = await
|
|
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
|
|
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
|
|
1182
|
-
"src/
|
|
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
|
|
1195
|
-
import
|
|
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 =
|
|
1220
|
-
const tmpPath =
|
|
1587
|
+
const dir = path4.dirname(tokenPath);
|
|
1588
|
+
const tmpPath = path4.join(dir, `.auth-token-tmp-${randomBytes(4).toString("hex")}`);
|
|
1221
1589
|
try {
|
|
1222
|
-
await
|
|
1223
|
-
await
|
|
1590
|
+
await fsPromises2.writeFile(tmpPath, newToken, { encoding: "utf8", mode: 384 });
|
|
1591
|
+
await fsPromises2.rename(tmpPath, tokenPath);
|
|
1224
1592
|
} catch (err) {
|
|
1225
|
-
await
|
|
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
|
-
|
|
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.
|
|
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
|
|
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] === "
|
|
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"),
|