tandem-editor 0.2.12 → 0.3.1
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/CHANGELOG.md +31 -0
- package/README.md +62 -13
- package/dist/channel/index.js +45 -2
- package/dist/channel/index.js.map +1 -1
- package/dist/cli/index.js +15 -14
- package/dist/cli/index.js.map +1 -1
- package/dist/client/assets/index-CcE9UvS8.js +308 -0
- package/dist/client/index.html +1 -1
- package/dist/server/index.js +497 -468
- package/dist/server/index.js.map +1 -1
- package/package.json +3 -1
- package/dist/client/assets/index-R-RaIO5I.js +0 -297
package/dist/server/index.js
CHANGED
|
@@ -9,7 +9,7 @@ var __export = (target, all) => {
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
// src/shared/constants.ts
|
|
12
|
-
var DEFAULT_WS_PORT, DEFAULT_MCP_PORT, SUPPORTED_EXTENSIONS, MAX_FILE_SIZE, MAX_WS_PAYLOAD, IDLE_TIMEOUT, SESSION_MAX_AGE,
|
|
12
|
+
var DEFAULT_WS_PORT, DEFAULT_MCP_PORT, SUPPORTED_EXTENSIONS, MAX_FILE_SIZE, MAX_WS_PAYLOAD, IDLE_TIMEOUT, SESSION_MAX_AGE, TANDEM_MODE_DEFAULT, SELECTION_DWELL_DEFAULT_MS, SELECTION_DWELL_MIN_MS, SELECTION_DWELL_MAX_MS, CHARS_PER_PAGE, LARGE_FILE_PAGE_THRESHOLD, VERY_LARGE_FILE_PAGE_THRESHOLD, CTRL_ROOM, Y_MAP_ANNOTATIONS, Y_MAP_AWARENESS, Y_MAP_USER_AWARENESS, Y_MAP_MODE, Y_MAP_DWELL_MS, Y_MAP_CHAT, Y_MAP_DOCUMENT_META, Y_MAP_SAVED_AT_VERSION, NOTIFICATION_BUFFER_SIZE, TUTORIAL_ANNOTATION_PREFIX, CHANNEL_EVENT_BUFFER_SIZE, CHANNEL_EVENT_BUFFER_AGE_MS, CHANNEL_SSE_KEEPALIVE_MS;
|
|
13
13
|
var init_constants = __esm({
|
|
14
14
|
"src/shared/constants.ts"() {
|
|
15
15
|
"use strict";
|
|
@@ -20,7 +20,10 @@ var init_constants = __esm({
|
|
|
20
20
|
MAX_WS_PAYLOAD = 10 * 1024 * 1024;
|
|
21
21
|
IDLE_TIMEOUT = 30 * 60 * 1e3;
|
|
22
22
|
SESSION_MAX_AGE = 30 * 24 * 60 * 60 * 1e3;
|
|
23
|
-
|
|
23
|
+
TANDEM_MODE_DEFAULT = "tandem";
|
|
24
|
+
SELECTION_DWELL_DEFAULT_MS = 1e3;
|
|
25
|
+
SELECTION_DWELL_MIN_MS = 500;
|
|
26
|
+
SELECTION_DWELL_MAX_MS = 3e3;
|
|
24
27
|
CHARS_PER_PAGE = 3e3;
|
|
25
28
|
LARGE_FILE_PAGE_THRESHOLD = 50;
|
|
26
29
|
VERY_LARGE_FILE_PAGE_THRESHOLD = 100;
|
|
@@ -28,6 +31,8 @@ var init_constants = __esm({
|
|
|
28
31
|
Y_MAP_ANNOTATIONS = "annotations";
|
|
29
32
|
Y_MAP_AWARENESS = "awareness";
|
|
30
33
|
Y_MAP_USER_AWARENESS = "userAwareness";
|
|
34
|
+
Y_MAP_MODE = "mode";
|
|
35
|
+
Y_MAP_DWELL_MS = "selectionDwellMs";
|
|
31
36
|
Y_MAP_CHAT = "chat";
|
|
32
37
|
Y_MAP_DOCUMENT_META = "documentMeta";
|
|
33
38
|
Y_MAP_SAVED_AT_VERSION = "savedAtVersion";
|
|
@@ -39,106 +44,79 @@ var init_constants = __esm({
|
|
|
39
44
|
}
|
|
40
45
|
});
|
|
41
46
|
|
|
42
|
-
// src/server/
|
|
43
|
-
import
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
function getOrCreateDocument(name) {
|
|
56
|
-
let doc = documents.get(name);
|
|
57
|
-
if (!doc) {
|
|
58
|
-
doc = new Y.Doc();
|
|
59
|
-
documents.set(name, doc);
|
|
60
|
-
}
|
|
61
|
-
return doc;
|
|
62
|
-
}
|
|
63
|
-
async function startHocuspocus(port) {
|
|
64
|
-
hocuspocusInstance = new Hocuspocus({
|
|
65
|
-
port,
|
|
66
|
-
address: "127.0.0.1",
|
|
67
|
-
quiet: true,
|
|
68
|
-
// stdout is the MCP wire — suppress the startup banner
|
|
69
|
-
async onConnect({ request, documentName }) {
|
|
70
|
-
const origin = request?.headers?.origin;
|
|
71
|
-
if (!origin) {
|
|
72
|
-
console.error("[Hocuspocus] Rejected connection: missing Origin header");
|
|
73
|
-
throw new Error("Connection rejected: missing origin header");
|
|
74
|
-
}
|
|
75
|
-
const url = new URL(origin);
|
|
76
|
-
if (url.hostname !== "localhost" && url.hostname !== "127.0.0.1") {
|
|
77
|
-
console.error(`[Hocuspocus] Rejected connection from origin: ${origin}`);
|
|
78
|
-
throw new Error("Connection rejected: invalid origin");
|
|
79
|
-
}
|
|
80
|
-
console.error(`[Hocuspocus] Client connected to: ${documentName}`);
|
|
81
|
-
},
|
|
82
|
-
async onDisconnect({ documentName }) {
|
|
83
|
-
console.error(`[Hocuspocus] Client disconnected from: ${documentName}`);
|
|
84
|
-
},
|
|
85
|
-
async onLoadDocument({ document, documentName }) {
|
|
86
|
-
console.error(`[Hocuspocus] Loading document: ${documentName}`);
|
|
87
|
-
const existing = documents.get(documentName);
|
|
88
|
-
if (existing && existing !== document) {
|
|
89
|
-
const update = Y.encodeStateAsUpdate(existing);
|
|
90
|
-
Y.applyUpdate(document, update);
|
|
91
|
-
existing.destroy();
|
|
92
|
-
console.error(`[Hocuspocus] Merged pre-existing content into document: ${documentName}`);
|
|
93
|
-
}
|
|
94
|
-
documents.set(documentName, document);
|
|
95
|
-
if (onDocSwapped) {
|
|
96
|
-
onDocSwapped(documentName, document);
|
|
97
|
-
} else {
|
|
98
|
-
console.error(
|
|
99
|
-
`[Tandem] WARN: onDocSwapped callback not registered during doc load for ${documentName}. Server-side observers will NOT be attached. Call setDocLifecycleCallbacks() before starting Hocuspocus.`
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
return document;
|
|
103
|
-
},
|
|
104
|
-
async afterUnloadDocument({ documentName }) {
|
|
105
|
-
if (shouldKeepDocument?.(documentName)) {
|
|
106
|
-
console.error(`[Hocuspocus] Kept document in map (MCP still tracking): ${documentName}`);
|
|
47
|
+
// src/server/file-watcher.ts
|
|
48
|
+
import fs from "fs";
|
|
49
|
+
function watchFile(filePath, onChanged) {
|
|
50
|
+
if (watched.has(filePath)) return;
|
|
51
|
+
let watcher;
|
|
52
|
+
try {
|
|
53
|
+
watcher = fs.watch(filePath, (eventType) => {
|
|
54
|
+
if (eventType !== "change") return;
|
|
55
|
+
const entry = watched.get(filePath);
|
|
56
|
+
if (!entry) return;
|
|
57
|
+
if (entry.suppressed) {
|
|
58
|
+
entry.suppressed = false;
|
|
107
59
|
return;
|
|
108
60
|
}
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
documents.delete(documentName);
|
|
112
|
-
console.error(`[Hocuspocus] Unloaded document from map: ${documentName}`);
|
|
61
|
+
if (entry.timer !== null) {
|
|
62
|
+
clearTimeout(entry.timer);
|
|
113
63
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
console.error(`[Tandem] Hocuspocus httpServer error: ${err.message}`);
|
|
64
|
+
entry.timer = setTimeout(() => {
|
|
65
|
+
entry.timer = null;
|
|
66
|
+
onChanged(filePath).catch((err) => {
|
|
67
|
+
console.error(`[FileWatcher] onChanged callback failed for ${filePath}:`, err);
|
|
68
|
+
});
|
|
69
|
+
}, 500);
|
|
121
70
|
});
|
|
71
|
+
} catch (err) {
|
|
72
|
+
console.error(`[FileWatcher] Failed to watch ${filePath}:`, err);
|
|
73
|
+
return;
|
|
122
74
|
}
|
|
123
|
-
|
|
75
|
+
watcher.on("error", (err) => {
|
|
76
|
+
console.error(`[FileWatcher] Watcher error for ${filePath}:`, err);
|
|
77
|
+
unwatchFile(filePath);
|
|
78
|
+
});
|
|
79
|
+
watched.set(filePath, { watcher, timer: null, suppressed: false });
|
|
80
|
+
console.error(`[FileWatcher] Watching ${filePath}`);
|
|
124
81
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
82
|
+
function suppressNextChange(filePath) {
|
|
83
|
+
const entry = watched.get(filePath);
|
|
84
|
+
if (entry) {
|
|
85
|
+
entry.suppressed = true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function unwatchFile(filePath) {
|
|
89
|
+
const entry = watched.get(filePath);
|
|
90
|
+
if (!entry) return;
|
|
91
|
+
if (entry.timer !== null) {
|
|
92
|
+
clearTimeout(entry.timer);
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
entry.watcher.close();
|
|
96
|
+
} catch (err) {
|
|
97
|
+
console.error(`[FileWatcher] watcher.close() failed for ${filePath}:`, err);
|
|
98
|
+
}
|
|
99
|
+
watched.delete(filePath);
|
|
100
|
+
console.error(`[FileWatcher] Unwatched ${filePath}`);
|
|
101
|
+
}
|
|
102
|
+
function unwatchAll() {
|
|
103
|
+
for (const filePath of [...watched.keys()]) {
|
|
104
|
+
unwatchFile(filePath);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
var watched;
|
|
108
|
+
var init_file_watcher = __esm({
|
|
109
|
+
"src/server/file-watcher.ts"() {
|
|
128
110
|
"use strict";
|
|
129
|
-
|
|
130
|
-
documents = /* @__PURE__ */ new Map();
|
|
131
|
-
shouldKeepDocument = null;
|
|
132
|
-
onDocSwapped = null;
|
|
133
|
-
onDocUnloaded = null;
|
|
111
|
+
watched = /* @__PURE__ */ new Map();
|
|
134
112
|
}
|
|
135
113
|
});
|
|
136
114
|
|
|
137
115
|
// src/server/platform.ts
|
|
138
116
|
import { execSync } from "child_process";
|
|
117
|
+
import envPaths from "env-paths";
|
|
139
118
|
import net from "net";
|
|
140
119
|
import path from "path";
|
|
141
|
-
import envPaths from "env-paths";
|
|
142
120
|
function freePort(port) {
|
|
143
121
|
try {
|
|
144
122
|
if (process.platform === "win32") {
|
|
@@ -231,15 +209,15 @@ var init_platform = __esm({
|
|
|
231
209
|
});
|
|
232
210
|
|
|
233
211
|
// src/server/session/manager.ts
|
|
234
|
-
import
|
|
212
|
+
import fs2 from "fs/promises";
|
|
235
213
|
import path2 from "path";
|
|
236
|
-
import * as
|
|
214
|
+
import * as Y from "yjs";
|
|
237
215
|
async function atomicWrite(sessionPath, content) {
|
|
238
216
|
const tmpPath = `${sessionPath}.tmp`;
|
|
239
|
-
await
|
|
217
|
+
await fs2.writeFile(tmpPath, content, "utf-8");
|
|
240
218
|
for (let attempt = 0; attempt < RENAME_MAX_RETRIES; attempt++) {
|
|
241
219
|
try {
|
|
242
|
-
await
|
|
220
|
+
await fs2.rename(tmpPath, sessionPath);
|
|
243
221
|
return;
|
|
244
222
|
} catch (err) {
|
|
245
223
|
const code = err.code;
|
|
@@ -247,7 +225,7 @@ async function atomicWrite(sessionPath, content) {
|
|
|
247
225
|
await new Promise((r) => setTimeout(r, RENAME_RETRY_BASE_MS * 2 ** attempt));
|
|
248
226
|
continue;
|
|
249
227
|
}
|
|
250
|
-
await
|
|
228
|
+
await fs2.unlink(tmpPath).catch(() => {
|
|
251
229
|
});
|
|
252
230
|
throw err;
|
|
253
231
|
}
|
|
@@ -261,12 +239,12 @@ async function saveSession(filePath, format, doc) {
|
|
|
261
239
|
let sourceFileMtime = 0;
|
|
262
240
|
if (!filePath.startsWith("upload://")) {
|
|
263
241
|
try {
|
|
264
|
-
const stat = await
|
|
242
|
+
const stat = await fs2.stat(filePath);
|
|
265
243
|
sourceFileMtime = stat.mtimeMs;
|
|
266
244
|
} catch {
|
|
267
245
|
}
|
|
268
246
|
}
|
|
269
|
-
const state =
|
|
247
|
+
const state = Y.encodeStateAsUpdate(doc);
|
|
270
248
|
const ydocState = Buffer.from(state).toString("base64");
|
|
271
249
|
const data = {
|
|
272
250
|
filePath,
|
|
@@ -276,7 +254,7 @@ async function saveSession(filePath, format, doc) {
|
|
|
276
254
|
lastAccessed: Date.now()
|
|
277
255
|
};
|
|
278
256
|
if (!sessionDirReady) {
|
|
279
|
-
await
|
|
257
|
+
await fs2.mkdir(SESSION_DIR, { recursive: true });
|
|
280
258
|
sessionDirReady = true;
|
|
281
259
|
}
|
|
282
260
|
const sessionPath = path2.join(SESSION_DIR, `${key}.json`);
|
|
@@ -286,14 +264,14 @@ async function loadSession(filePath) {
|
|
|
286
264
|
const key = sessionKey(filePath);
|
|
287
265
|
const sessionPath = path2.join(SESSION_DIR, `${key}.json`);
|
|
288
266
|
try {
|
|
289
|
-
const content = await
|
|
267
|
+
const content = await fs2.readFile(sessionPath, "utf-8");
|
|
290
268
|
return JSON.parse(content);
|
|
291
269
|
} catch (err) {
|
|
292
270
|
const code = err.code;
|
|
293
271
|
if (code === "ENOENT") return null;
|
|
294
272
|
if (err instanceof SyntaxError) {
|
|
295
273
|
console.error(`[Tandem] Corrupted session file ${sessionPath}, removing:`, err.message);
|
|
296
|
-
await
|
|
274
|
+
await fs2.unlink(sessionPath).catch((unlinkErr) => {
|
|
297
275
|
console.error(`[Tandem] Failed to remove corrupted session ${sessionPath}:`, unlinkErr);
|
|
298
276
|
});
|
|
299
277
|
return null;
|
|
@@ -304,12 +282,12 @@ async function loadSession(filePath) {
|
|
|
304
282
|
}
|
|
305
283
|
function restoreYDoc(doc, session) {
|
|
306
284
|
const state = Buffer.from(session.ydocState, "base64");
|
|
307
|
-
|
|
285
|
+
Y.applyUpdate(doc, new Uint8Array(state));
|
|
308
286
|
}
|
|
309
287
|
async function sourceFileChanged(session) {
|
|
310
288
|
if (session.filePath.startsWith("upload://")) return false;
|
|
311
289
|
try {
|
|
312
|
-
const stat = await
|
|
290
|
+
const stat = await fs2.stat(session.filePath);
|
|
313
291
|
return stat.mtimeMs !== session.sourceFileMtime;
|
|
314
292
|
} catch {
|
|
315
293
|
return true;
|
|
@@ -319,7 +297,7 @@ async function deleteSession(filePath) {
|
|
|
319
297
|
const key = sessionKey(filePath);
|
|
320
298
|
const sessionPath = path2.join(SESSION_DIR, `${key}.json`);
|
|
321
299
|
try {
|
|
322
|
-
await
|
|
300
|
+
await fs2.unlink(sessionPath);
|
|
323
301
|
} catch (err) {
|
|
324
302
|
const code = err.code;
|
|
325
303
|
if (code !== "ENOENT") {
|
|
@@ -329,7 +307,7 @@ async function deleteSession(filePath) {
|
|
|
329
307
|
}
|
|
330
308
|
async function saveCtrlSession(doc) {
|
|
331
309
|
if (!sessionDirReady) {
|
|
332
|
-
await
|
|
310
|
+
await fs2.mkdir(SESSION_DIR, { recursive: true });
|
|
333
311
|
sessionDirReady = true;
|
|
334
312
|
}
|
|
335
313
|
const chatMap = doc.getMap(Y_MAP_CHAT);
|
|
@@ -347,7 +325,7 @@ async function saveCtrlSession(doc) {
|
|
|
347
325
|
}
|
|
348
326
|
}, MCP_ORIGIN);
|
|
349
327
|
}
|
|
350
|
-
const state =
|
|
328
|
+
const state = Y.encodeStateAsUpdate(doc);
|
|
351
329
|
const ydocState = Buffer.from(state).toString("base64");
|
|
352
330
|
const data = { ydocState, lastAccessed: Date.now() };
|
|
353
331
|
const sessionPath = path2.join(SESSION_DIR, `${CTRL_SESSION_KEY}.json`);
|
|
@@ -356,7 +334,7 @@ async function saveCtrlSession(doc) {
|
|
|
356
334
|
async function loadCtrlSession() {
|
|
357
335
|
const sessionPath = path2.join(SESSION_DIR, `${CTRL_SESSION_KEY}.json`);
|
|
358
336
|
try {
|
|
359
|
-
const content = await
|
|
337
|
+
const content = await fs2.readFile(sessionPath, "utf-8");
|
|
360
338
|
const data = JSON.parse(content);
|
|
361
339
|
return data.ydocState ?? null;
|
|
362
340
|
} catch (err) {
|
|
@@ -364,7 +342,7 @@ async function loadCtrlSession() {
|
|
|
364
342
|
if (code === "ENOENT") return null;
|
|
365
343
|
if (err instanceof SyntaxError) {
|
|
366
344
|
console.error(`[Tandem] Corrupted ctrl session ${sessionPath}, removing:`, err.message);
|
|
367
|
-
await
|
|
345
|
+
await fs2.unlink(sessionPath).catch((unlinkErr) => {
|
|
368
346
|
console.error(
|
|
369
347
|
`[Tandem] Failed to remove corrupted ctrl session ${sessionPath}:`,
|
|
370
348
|
unlinkErr
|
|
@@ -378,18 +356,18 @@ async function loadCtrlSession() {
|
|
|
378
356
|
}
|
|
379
357
|
function restoreCtrlDoc(doc, base64State) {
|
|
380
358
|
const state = Buffer.from(base64State, "base64");
|
|
381
|
-
|
|
359
|
+
Y.applyUpdate(doc, new Uint8Array(state));
|
|
382
360
|
}
|
|
383
361
|
async function listSessionFilePaths() {
|
|
384
362
|
try {
|
|
385
|
-
await
|
|
386
|
-
const files = await
|
|
363
|
+
await fs2.mkdir(SESSION_DIR, { recursive: true });
|
|
364
|
+
const files = await fs2.readdir(SESSION_DIR);
|
|
387
365
|
const results = [];
|
|
388
366
|
for (const file of files) {
|
|
389
367
|
if (!file.endsWith(".json")) continue;
|
|
390
368
|
if (file === `${encodeURIComponent(CTRL_ROOM)}.json`) continue;
|
|
391
369
|
try {
|
|
392
|
-
const raw = await
|
|
370
|
+
const raw = await fs2.readFile(path2.join(SESSION_DIR, file), "utf-8");
|
|
393
371
|
const data = JSON.parse(raw);
|
|
394
372
|
if (!data.filePath || data.filePath.startsWith("upload://")) continue;
|
|
395
373
|
results.push({ filePath: data.filePath, lastAccessed: data.lastAccessed ?? 0 });
|
|
@@ -408,7 +386,7 @@ async function cleanupSessions() {
|
|
|
408
386
|
let cleaned = 0;
|
|
409
387
|
let files;
|
|
410
388
|
try {
|
|
411
|
-
files = await
|
|
389
|
+
files = await fs2.readdir(SESSION_DIR);
|
|
412
390
|
} catch (err) {
|
|
413
391
|
if (err.code === "ENOENT") return 0;
|
|
414
392
|
console.error("[Tandem] Failed to read session directory:", err);
|
|
@@ -418,9 +396,9 @@ async function cleanupSessions() {
|
|
|
418
396
|
for (const file of files) {
|
|
419
397
|
try {
|
|
420
398
|
const filePath = path2.join(SESSION_DIR, file);
|
|
421
|
-
const stat = await
|
|
399
|
+
const stat = await fs2.stat(filePath);
|
|
422
400
|
if (now - stat.mtimeMs > SESSION_MAX_AGE) {
|
|
423
|
-
await
|
|
401
|
+
await fs2.unlink(filePath);
|
|
424
402
|
cleaned++;
|
|
425
403
|
}
|
|
426
404
|
} catch (err) {
|
|
@@ -454,9 +432,9 @@ var AUTO_SAVE_INTERVAL, RENAME_MAX_RETRIES, RENAME_RETRY_BASE_MS, sessionDirRead
|
|
|
454
432
|
var init_manager = __esm({
|
|
455
433
|
"src/server/session/manager.ts"() {
|
|
456
434
|
"use strict";
|
|
457
|
-
init_platform();
|
|
458
435
|
init_constants();
|
|
459
436
|
init_queue();
|
|
437
|
+
init_platform();
|
|
460
438
|
AUTO_SAVE_INTERVAL = 60 * 1e3;
|
|
461
439
|
RENAME_MAX_RETRIES = 3;
|
|
462
440
|
RENAME_RETRY_BASE_MS = 50;
|
|
@@ -467,71 +445,136 @@ var init_manager = __esm({
|
|
|
467
445
|
}
|
|
468
446
|
});
|
|
469
447
|
|
|
470
|
-
// src/server/
|
|
471
|
-
import
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
448
|
+
// src/server/yjs/provider.ts
|
|
449
|
+
import { Hocuspocus } from "@hocuspocus/server";
|
|
450
|
+
import * as Y2 from "yjs";
|
|
451
|
+
function setDocLifecycleCallbacks(swapped, unloaded) {
|
|
452
|
+
onDocSwapped = swapped;
|
|
453
|
+
onDocUnloaded = unloaded;
|
|
454
|
+
}
|
|
455
|
+
function setShouldKeepDocument(fn) {
|
|
456
|
+
shouldKeepDocument = fn;
|
|
457
|
+
}
|
|
458
|
+
function getDocument(name) {
|
|
459
|
+
return documents.get(name);
|
|
460
|
+
}
|
|
461
|
+
function getOrCreateDocument(name) {
|
|
462
|
+
let doc = documents.get(name);
|
|
463
|
+
if (!doc) {
|
|
464
|
+
doc = new Y2.Doc();
|
|
465
|
+
documents.set(name, doc);
|
|
466
|
+
}
|
|
467
|
+
return doc;
|
|
468
|
+
}
|
|
469
|
+
async function startHocuspocus(port) {
|
|
470
|
+
hocuspocusInstance = new Hocuspocus({
|
|
471
|
+
port,
|
|
472
|
+
address: "127.0.0.1",
|
|
473
|
+
quiet: true,
|
|
474
|
+
// stdout is the MCP wire — suppress the startup banner
|
|
475
|
+
async onConnect({ request, documentName }) {
|
|
476
|
+
const origin = request?.headers?.origin;
|
|
477
|
+
if (!origin) {
|
|
478
|
+
console.error("[Hocuspocus] Rejected connection: missing Origin header");
|
|
479
|
+
throw new Error("Connection rejected: missing origin header");
|
|
480
|
+
}
|
|
481
|
+
const url = new URL(origin);
|
|
482
|
+
if (url.hostname !== "localhost" && url.hostname !== "127.0.0.1") {
|
|
483
|
+
console.error(`[Hocuspocus] Rejected connection from origin: ${origin}`);
|
|
484
|
+
throw new Error("Connection rejected: invalid origin");
|
|
485
|
+
}
|
|
486
|
+
console.error(`[Hocuspocus] Client connected to: ${documentName}`);
|
|
487
|
+
},
|
|
488
|
+
async onDisconnect({ documentName }) {
|
|
489
|
+
console.error(`[Hocuspocus] Client disconnected from: ${documentName}`);
|
|
490
|
+
},
|
|
491
|
+
async onLoadDocument({ document, documentName }) {
|
|
492
|
+
console.error(`[Hocuspocus] Loading document: ${documentName}`);
|
|
493
|
+
const existing = documents.get(documentName);
|
|
494
|
+
if (existing && existing !== document) {
|
|
495
|
+
const update = Y2.encodeStateAsUpdate(existing);
|
|
496
|
+
Y2.applyUpdate(document, update);
|
|
497
|
+
existing.destroy();
|
|
498
|
+
console.error(`[Hocuspocus] Merged pre-existing content into document: ${documentName}`);
|
|
499
|
+
}
|
|
500
|
+
documents.set(documentName, document);
|
|
501
|
+
if (onDocSwapped) {
|
|
502
|
+
onDocSwapped(documentName, document);
|
|
503
|
+
} else {
|
|
504
|
+
console.error(
|
|
505
|
+
`[Tandem] WARN: onDocSwapped callback not registered during doc load for ${documentName}. Server-side observers will NOT be attached. Call setDocLifecycleCallbacks() before starting Hocuspocus.`
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
return document;
|
|
509
|
+
},
|
|
510
|
+
async afterUnloadDocument({ documentName }) {
|
|
511
|
+
if (shouldKeepDocument?.(documentName)) {
|
|
512
|
+
console.error(`[Hocuspocus] Kept document in map (MCP still tracking): ${documentName}`);
|
|
482
513
|
return;
|
|
483
514
|
}
|
|
484
|
-
if (
|
|
485
|
-
|
|
515
|
+
if (documents.has(documentName)) {
|
|
516
|
+
onDocUnloaded?.(documentName);
|
|
517
|
+
documents.delete(documentName);
|
|
518
|
+
console.error(`[Hocuspocus] Unloaded document from map: ${documentName}`);
|
|
486
519
|
}
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
await hocuspocusInstance.listen();
|
|
523
|
+
const internal = hocuspocusInstance.server?.httpServer;
|
|
524
|
+
if (internal) {
|
|
525
|
+
internal.on("error", (err) => {
|
|
526
|
+
console.error(`[Tandem] Hocuspocus httpServer error: ${err.message}`);
|
|
493
527
|
});
|
|
494
|
-
} catch (err) {
|
|
495
|
-
console.error(`[FileWatcher] Failed to watch ${filePath}:`, err);
|
|
496
|
-
return;
|
|
497
528
|
}
|
|
498
|
-
|
|
499
|
-
console.error(`[FileWatcher] Watcher error for ${filePath}:`, err);
|
|
500
|
-
unwatchFile(filePath);
|
|
501
|
-
});
|
|
502
|
-
watched.set(filePath, { watcher, timer: null, suppressed: false });
|
|
503
|
-
console.error(`[FileWatcher] Watching ${filePath}`);
|
|
529
|
+
return hocuspocusInstance;
|
|
504
530
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
531
|
+
var hocuspocusInstance, documents, shouldKeepDocument, onDocSwapped, onDocUnloaded;
|
|
532
|
+
var init_provider = __esm({
|
|
533
|
+
"src/server/yjs/provider.ts"() {
|
|
534
|
+
"use strict";
|
|
535
|
+
hocuspocusInstance = null;
|
|
536
|
+
documents = /* @__PURE__ */ new Map();
|
|
537
|
+
shouldKeepDocument = null;
|
|
538
|
+
onDocSwapped = null;
|
|
539
|
+
onDocUnloaded = null;
|
|
509
540
|
}
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
// src/shared/utils.ts
|
|
544
|
+
function generateId(prefix) {
|
|
545
|
+
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
510
546
|
}
|
|
511
|
-
function
|
|
512
|
-
|
|
513
|
-
if (!entry) return;
|
|
514
|
-
if (entry.timer !== null) {
|
|
515
|
-
clearTimeout(entry.timer);
|
|
516
|
-
}
|
|
517
|
-
try {
|
|
518
|
-
entry.watcher.close();
|
|
519
|
-
} catch (err) {
|
|
520
|
-
console.error(`[FileWatcher] watcher.close() failed for ${filePath}:`, err);
|
|
521
|
-
}
|
|
522
|
-
watched.delete(filePath);
|
|
523
|
-
console.error(`[FileWatcher] Unwatched ${filePath}`);
|
|
547
|
+
function generateAnnotationId() {
|
|
548
|
+
return generateId("ann");
|
|
524
549
|
}
|
|
525
|
-
function
|
|
526
|
-
|
|
527
|
-
|
|
550
|
+
function generateMessageId() {
|
|
551
|
+
return generateId("msg");
|
|
552
|
+
}
|
|
553
|
+
function generateEventId() {
|
|
554
|
+
return generateId("evt");
|
|
555
|
+
}
|
|
556
|
+
function generateNotificationId() {
|
|
557
|
+
return generateId("ntf");
|
|
558
|
+
}
|
|
559
|
+
var init_utils = __esm({
|
|
560
|
+
"src/shared/utils.ts"() {
|
|
561
|
+
"use strict";
|
|
528
562
|
}
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// src/shared/offsets.ts
|
|
566
|
+
function headingPrefixLength(level) {
|
|
567
|
+
if (!level) return 0;
|
|
568
|
+
return level + 1;
|
|
529
569
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
570
|
+
function headingPrefix(level) {
|
|
571
|
+
return "#".repeat(level) + " ";
|
|
572
|
+
}
|
|
573
|
+
var FLAT_SEPARATOR;
|
|
574
|
+
var init_offsets = __esm({
|
|
575
|
+
"src/shared/offsets.ts"() {
|
|
533
576
|
"use strict";
|
|
534
|
-
|
|
577
|
+
FLAT_SEPARATOR = "\n";
|
|
535
578
|
}
|
|
536
579
|
});
|
|
537
580
|
|
|
@@ -853,10 +896,10 @@ var init_mdast_ydoc = __esm({
|
|
|
853
896
|
});
|
|
854
897
|
|
|
855
898
|
// src/server/file-io/markdown.ts
|
|
856
|
-
import { unified } from "unified";
|
|
857
|
-
import remarkParse from "remark-parse";
|
|
858
899
|
import remarkGfm from "remark-gfm";
|
|
900
|
+
import remarkParse from "remark-parse";
|
|
859
901
|
import remarkStringify from "remark-stringify";
|
|
902
|
+
import { unified } from "unified";
|
|
860
903
|
function loadMarkdown(doc, markdown) {
|
|
861
904
|
const tree = parser.parse(markdown);
|
|
862
905
|
mdastToYDoc(doc, tree);
|
|
@@ -881,22 +924,6 @@ var init_markdown = __esm({
|
|
|
881
924
|
}
|
|
882
925
|
});
|
|
883
926
|
|
|
884
|
-
// src/shared/offsets.ts
|
|
885
|
-
function headingPrefixLength(level) {
|
|
886
|
-
if (!level) return 0;
|
|
887
|
-
return level + 1;
|
|
888
|
-
}
|
|
889
|
-
function headingPrefix(level) {
|
|
890
|
-
return "#".repeat(level) + " ";
|
|
891
|
-
}
|
|
892
|
-
var FLAT_SEPARATOR;
|
|
893
|
-
var init_offsets = __esm({
|
|
894
|
-
"src/shared/offsets.ts"() {
|
|
895
|
-
"use strict";
|
|
896
|
-
FLAT_SEPARATOR = "\n";
|
|
897
|
-
}
|
|
898
|
-
});
|
|
899
|
-
|
|
900
927
|
// src/server/mcp/document-model.ts
|
|
901
928
|
import path3 from "path";
|
|
902
929
|
import * as Y4 from "yjs";
|
|
@@ -1426,6 +1453,42 @@ var init_types = __esm({
|
|
|
1426
1453
|
}
|
|
1427
1454
|
});
|
|
1428
1455
|
|
|
1456
|
+
// src/shared/types.ts
|
|
1457
|
+
import { z } from "zod";
|
|
1458
|
+
var AnnotationTypeSchema, AnnotationStatusSchema, HighlightColorSchema, SeveritySchema, TandemModeSchema, AuthorSchema, AnnotationActionSchema, ExportFormatSchema, DocumentFormatSchema, ToolErrorCodeSchema;
|
|
1459
|
+
var init_types2 = __esm({
|
|
1460
|
+
"src/shared/types.ts"() {
|
|
1461
|
+
"use strict";
|
|
1462
|
+
init_types();
|
|
1463
|
+
AnnotationTypeSchema = z.enum([
|
|
1464
|
+
"highlight",
|
|
1465
|
+
"comment",
|
|
1466
|
+
"suggestion",
|
|
1467
|
+
"overlay",
|
|
1468
|
+
"question",
|
|
1469
|
+
"flag"
|
|
1470
|
+
]);
|
|
1471
|
+
AnnotationStatusSchema = z.enum(["pending", "accepted", "dismissed"]);
|
|
1472
|
+
HighlightColorSchema = z.enum(["yellow", "red", "green", "blue", "purple"]);
|
|
1473
|
+
SeveritySchema = z.enum(["info", "warning", "error", "success"]);
|
|
1474
|
+
TandemModeSchema = z.enum(["solo", "tandem"]);
|
|
1475
|
+
AuthorSchema = z.enum(["user", "claude", "import"]);
|
|
1476
|
+
AnnotationActionSchema = z.enum(["accept", "dismiss"]);
|
|
1477
|
+
ExportFormatSchema = z.enum(["markdown", "json"]);
|
|
1478
|
+
DocumentFormatSchema = z.enum(["md", "txt", "html", "docx"]);
|
|
1479
|
+
ToolErrorCodeSchema = z.enum([
|
|
1480
|
+
"RANGE_GONE",
|
|
1481
|
+
"RANGE_MOVED",
|
|
1482
|
+
"FILE_LOCKED",
|
|
1483
|
+
"FILE_NOT_FOUND",
|
|
1484
|
+
"NO_DOCUMENT",
|
|
1485
|
+
"INVALID_RANGE",
|
|
1486
|
+
"FORMAT_ERROR",
|
|
1487
|
+
"PERMISSION_DENIED"
|
|
1488
|
+
]);
|
|
1489
|
+
}
|
|
1490
|
+
});
|
|
1491
|
+
|
|
1429
1492
|
// src/shared/positions/index.ts
|
|
1430
1493
|
var init_positions = __esm({
|
|
1431
1494
|
"src/shared/positions/index.ts"() {
|
|
@@ -1632,49 +1695,12 @@ function refreshAllRanges(annotations, ydoc, map) {
|
|
|
1632
1695
|
var init_positions2 = __esm({
|
|
1633
1696
|
"src/server/positions.ts"() {
|
|
1634
1697
|
"use strict";
|
|
1635
|
-
init_queue();
|
|
1636
1698
|
init_positions();
|
|
1699
|
+
init_queue();
|
|
1637
1700
|
init_document_model();
|
|
1638
1701
|
}
|
|
1639
1702
|
});
|
|
1640
1703
|
|
|
1641
|
-
// src/shared/types.ts
|
|
1642
|
-
import { z } from "zod";
|
|
1643
|
-
var AnnotationTypeSchema, AnnotationStatusSchema, AnnotationPrioritySchema, InterruptionModeSchema, HighlightColorSchema, SeveritySchema, AuthorSchema, AnnotationActionSchema, ExportFormatSchema, DocumentFormatSchema, ToolErrorCodeSchema;
|
|
1644
|
-
var init_types2 = __esm({
|
|
1645
|
-
"src/shared/types.ts"() {
|
|
1646
|
-
"use strict";
|
|
1647
|
-
init_types();
|
|
1648
|
-
AnnotationTypeSchema = z.enum([
|
|
1649
|
-
"highlight",
|
|
1650
|
-
"comment",
|
|
1651
|
-
"suggestion",
|
|
1652
|
-
"overlay",
|
|
1653
|
-
"question",
|
|
1654
|
-
"flag"
|
|
1655
|
-
]);
|
|
1656
|
-
AnnotationStatusSchema = z.enum(["pending", "accepted", "dismissed"]);
|
|
1657
|
-
AnnotationPrioritySchema = z.enum(["normal", "urgent"]);
|
|
1658
|
-
InterruptionModeSchema = z.enum(["all", "urgent-only", "paused"]);
|
|
1659
|
-
HighlightColorSchema = z.enum(["yellow", "red", "green", "blue", "purple"]);
|
|
1660
|
-
SeveritySchema = z.enum(["info", "warning", "error", "success"]);
|
|
1661
|
-
AuthorSchema = z.enum(["user", "claude", "import"]);
|
|
1662
|
-
AnnotationActionSchema = z.enum(["accept", "dismiss"]);
|
|
1663
|
-
ExportFormatSchema = z.enum(["markdown", "json"]);
|
|
1664
|
-
DocumentFormatSchema = z.enum(["md", "txt", "html", "docx"]);
|
|
1665
|
-
ToolErrorCodeSchema = z.enum([
|
|
1666
|
-
"RANGE_GONE",
|
|
1667
|
-
"RANGE_MOVED",
|
|
1668
|
-
"FILE_LOCKED",
|
|
1669
|
-
"FILE_NOT_FOUND",
|
|
1670
|
-
"NO_DOCUMENT",
|
|
1671
|
-
"INVALID_RANGE",
|
|
1672
|
-
"FORMAT_ERROR",
|
|
1673
|
-
"PERMISSION_DENIED"
|
|
1674
|
-
]);
|
|
1675
|
-
}
|
|
1676
|
-
});
|
|
1677
|
-
|
|
1678
1704
|
// src/server/file-io/docx-walker.ts
|
|
1679
1705
|
import { parseDocument as parseDocument2 } from "htmlparser2";
|
|
1680
1706
|
function isElement2(node) {
|
|
@@ -1805,8 +1831,8 @@ var init_docx_walker = __esm({
|
|
|
1805
1831
|
});
|
|
1806
1832
|
|
|
1807
1833
|
// src/server/file-io/docx-comments.ts
|
|
1808
|
-
import JSZip from "jszip";
|
|
1809
1834
|
import { parseDocument as parseDocument3 } from "htmlparser2";
|
|
1835
|
+
import JSZip from "jszip";
|
|
1810
1836
|
async function extractDocxComments(buffer3) {
|
|
1811
1837
|
const zip = await JSZip.loadAsync(buffer3);
|
|
1812
1838
|
const commentsXml = await zip.file("word/comments.xml")?.async("text");
|
|
@@ -1914,9 +1940,9 @@ var init_docx_comments = __esm({
|
|
|
1914
1940
|
"src/server/file-io/docx-comments.ts"() {
|
|
1915
1941
|
"use strict";
|
|
1916
1942
|
init_constants();
|
|
1917
|
-
init_positions2();
|
|
1918
1943
|
init_types2();
|
|
1919
1944
|
init_queue();
|
|
1945
|
+
init_positions2();
|
|
1920
1946
|
init_docx_walker();
|
|
1921
1947
|
}
|
|
1922
1948
|
});
|
|
@@ -2239,9 +2265,9 @@ var init_dist2 = __esm({
|
|
|
2239
2265
|
});
|
|
2240
2266
|
|
|
2241
2267
|
// src/server/file-io/docx-apply.ts
|
|
2242
|
-
import JSZip2 from "jszip";
|
|
2243
|
-
import { parseDocument as parseDocument4 } from "htmlparser2";
|
|
2244
2268
|
import render from "dom-serializer";
|
|
2269
|
+
import { parseDocument as parseDocument4 } from "htmlparser2";
|
|
2270
|
+
import JSZip2 from "jszip";
|
|
2245
2271
|
function buildOffsetMap(xml, targetOffsets) {
|
|
2246
2272
|
const entries = /* @__PURE__ */ new Map();
|
|
2247
2273
|
const commentParagraphIds = /* @__PURE__ */ new Map();
|
|
@@ -2797,10 +2823,10 @@ var markdownAdapter, plaintextAdapter, docxAdapter, adapters;
|
|
|
2797
2823
|
var init_file_io = __esm({
|
|
2798
2824
|
"src/server/file-io/index.ts"() {
|
|
2799
2825
|
"use strict";
|
|
2800
|
-
|
|
2826
|
+
init_document_model();
|
|
2801
2827
|
init_docx();
|
|
2802
2828
|
init_docx_comments();
|
|
2803
|
-
|
|
2829
|
+
init_markdown();
|
|
2804
2830
|
init_docx_apply();
|
|
2805
2831
|
markdownAdapter = {
|
|
2806
2832
|
canSave: true,
|
|
@@ -2881,28 +2907,6 @@ var init_notifications = __esm({
|
|
|
2881
2907
|
}
|
|
2882
2908
|
});
|
|
2883
2909
|
|
|
2884
|
-
// src/shared/utils.ts
|
|
2885
|
-
function generateId(prefix) {
|
|
2886
|
-
return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
2887
|
-
}
|
|
2888
|
-
function generateAnnotationId() {
|
|
2889
|
-
return generateId("ann");
|
|
2890
|
-
}
|
|
2891
|
-
function generateMessageId() {
|
|
2892
|
-
return generateId("msg");
|
|
2893
|
-
}
|
|
2894
|
-
function generateEventId() {
|
|
2895
|
-
return generateId("evt");
|
|
2896
|
-
}
|
|
2897
|
-
function generateNotificationId() {
|
|
2898
|
-
return generateId("ntf");
|
|
2899
|
-
}
|
|
2900
|
-
var init_utils = __esm({
|
|
2901
|
-
"src/shared/utils.ts"() {
|
|
2902
|
-
"use strict";
|
|
2903
|
-
}
|
|
2904
|
-
});
|
|
2905
|
-
|
|
2906
2910
|
// src/server/mcp/file-opener.ts
|
|
2907
2911
|
var file_opener_exports = {};
|
|
2908
2912
|
__export(file_opener_exports, {
|
|
@@ -2910,10 +2914,10 @@ __export(file_opener_exports, {
|
|
|
2910
2914
|
openFileByPath: () => openFileByPath,
|
|
2911
2915
|
openFileFromContent: () => openFileFromContent
|
|
2912
2916
|
});
|
|
2913
|
-
import
|
|
2917
|
+
import { randomUUID } from "crypto";
|
|
2914
2918
|
import fsSync from "fs";
|
|
2919
|
+
import fs4 from "fs/promises";
|
|
2915
2920
|
import path5 from "path";
|
|
2916
|
-
import { randomUUID } from "crypto";
|
|
2917
2921
|
async function openFileByPath(filePath, options) {
|
|
2918
2922
|
let resolved = path5.resolve(filePath);
|
|
2919
2923
|
try {
|
|
@@ -3262,19 +3266,19 @@ var reloadInProgress;
|
|
|
3262
3266
|
var init_file_opener = __esm({
|
|
3263
3267
|
"src/server/mcp/file-opener.ts"() {
|
|
3264
3268
|
"use strict";
|
|
3265
|
-
init_provider();
|
|
3266
3269
|
init_constants();
|
|
3270
|
+
init_utils();
|
|
3267
3271
|
init_queue();
|
|
3272
|
+
init_docx();
|
|
3273
|
+
init_docx_comments();
|
|
3274
|
+
init_docx_html();
|
|
3268
3275
|
init_file_io();
|
|
3276
|
+
init_markdown();
|
|
3269
3277
|
init_file_watcher();
|
|
3270
|
-
init_positions2();
|
|
3271
3278
|
init_notifications();
|
|
3272
|
-
|
|
3273
|
-
init_markdown();
|
|
3274
|
-
init_docx();
|
|
3275
|
-
init_docx_html();
|
|
3276
|
-
init_docx_comments();
|
|
3279
|
+
init_positions2();
|
|
3277
3280
|
init_manager();
|
|
3281
|
+
init_provider();
|
|
3278
3282
|
init_document_model();
|
|
3279
3283
|
init_document_service();
|
|
3280
3284
|
reloadInProgress = /* @__PURE__ */ new Set();
|
|
@@ -3282,8 +3286,8 @@ var init_file_opener = __esm({
|
|
|
3282
3286
|
});
|
|
3283
3287
|
|
|
3284
3288
|
// src/server/mcp/document-service.ts
|
|
3285
|
-
import path6 from "path";
|
|
3286
3289
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
3290
|
+
import path6 from "path";
|
|
3287
3291
|
function getOpenDocs() {
|
|
3288
3292
|
return openDocs;
|
|
3289
3293
|
}
|
|
@@ -3443,11 +3447,11 @@ var openDocs, activeDocId;
|
|
|
3443
3447
|
var init_document_service = __esm({
|
|
3444
3448
|
"src/server/mcp/document-service.ts"() {
|
|
3445
3449
|
"use strict";
|
|
3446
|
-
init_provider();
|
|
3447
|
-
init_manager();
|
|
3448
3450
|
init_constants();
|
|
3449
3451
|
init_queue();
|
|
3450
3452
|
init_file_watcher();
|
|
3453
|
+
init_manager();
|
|
3454
|
+
init_provider();
|
|
3451
3455
|
openDocs = /* @__PURE__ */ new Map();
|
|
3452
3456
|
setShouldKeepDocument((name) => openDocs.has(name) || name === CTRL_ROOM);
|
|
3453
3457
|
activeDocId = null;
|
|
@@ -3463,6 +3467,19 @@ var init_types3 = __esm({
|
|
|
3463
3467
|
});
|
|
3464
3468
|
|
|
3465
3469
|
// src/server/events/queue.ts
|
|
3470
|
+
function getDwellMs() {
|
|
3471
|
+
const ctrlDoc = getOrCreateDocument(CTRL_ROOM);
|
|
3472
|
+
const awareness = ctrlDoc.getMap(Y_MAP_USER_AWARENESS);
|
|
3473
|
+
const val = awareness.get(Y_MAP_DWELL_MS);
|
|
3474
|
+
if (val === void 0) return SELECTION_DWELL_DEFAULT_MS;
|
|
3475
|
+
if (typeof val === "number" && val >= SELECTION_DWELL_MIN_MS && val <= SELECTION_DWELL_MAX_MS) {
|
|
3476
|
+
return val;
|
|
3477
|
+
}
|
|
3478
|
+
console.warn(
|
|
3479
|
+
`[EventQueue] Invalid dwell time in CTRL_ROOM awareness (type=${typeof val}, value=${String(val)}); using default ${SELECTION_DWELL_DEFAULT_MS}ms`
|
|
3480
|
+
);
|
|
3481
|
+
return SELECTION_DWELL_DEFAULT_MS;
|
|
3482
|
+
}
|
|
3466
3483
|
function getTrackableId(event) {
|
|
3467
3484
|
switch (event.type) {
|
|
3468
3485
|
case "annotation:created":
|
|
@@ -3569,26 +3586,37 @@ function attachObservers(docName, doc) {
|
|
|
3569
3586
|
annotationsMap.observe(annotationsObs);
|
|
3570
3587
|
cleanups.push(() => annotationsMap.unobserve(annotationsObs));
|
|
3571
3588
|
const userAwareness = doc.getMap(Y_MAP_USER_AWARENESS);
|
|
3589
|
+
let selectionDwellTimer = null;
|
|
3572
3590
|
const awarenessObs = (event, txn) => {
|
|
3573
3591
|
if (txn.origin === MCP_ORIGIN) return;
|
|
3574
3592
|
if (event.keysChanged.has("selection")) {
|
|
3575
3593
|
const selection = userAwareness.get("selection");
|
|
3594
|
+
if (selectionDwellTimer) {
|
|
3595
|
+
clearTimeout(selectionDwellTimer);
|
|
3596
|
+
selectionDwellTimer = null;
|
|
3597
|
+
}
|
|
3576
3598
|
if (!selection || selection.from === selection.to) return;
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3599
|
+
selectionDwellTimer = setTimeout(() => {
|
|
3600
|
+
selectionDwellTimer = null;
|
|
3601
|
+
pushEvent({
|
|
3602
|
+
id: generateEventId(),
|
|
3603
|
+
type: "selection:changed",
|
|
3604
|
+
timestamp: Date.now(),
|
|
3605
|
+
documentId: docName,
|
|
3606
|
+
payload: {
|
|
3607
|
+
from: selection.from,
|
|
3608
|
+
to: selection.to,
|
|
3609
|
+
selectedText: selection.selectedText ?? ""
|
|
3610
|
+
}
|
|
3611
|
+
});
|
|
3612
|
+
}, getDwellMs());
|
|
3588
3613
|
}
|
|
3589
3614
|
};
|
|
3590
3615
|
userAwareness.observe(awarenessObs);
|
|
3591
|
-
cleanups.push(() =>
|
|
3616
|
+
cleanups.push(() => {
|
|
3617
|
+
userAwareness.unobserve(awarenessObs);
|
|
3618
|
+
if (selectionDwellTimer) clearTimeout(selectionDwellTimer);
|
|
3619
|
+
});
|
|
3592
3620
|
docObservers.set(docName, cleanups);
|
|
3593
3621
|
console.error(`[EventQueue] Attached observers for document: ${docName}`);
|
|
3594
3622
|
}
|
|
@@ -3788,115 +3816,62 @@ var init_launcher = __esm({
|
|
|
3788
3816
|
init_constants();
|
|
3789
3817
|
claudeProcess = null;
|
|
3790
3818
|
TANDEM_SYSTEM_PROMPT = [
|
|
3791
|
-
"You are Claude, connected to Tandem \u2014 a collaborative document editor.",
|
|
3792
|
-
"You will receive real-time push notifications via the tandem-channel when users",
|
|
3793
|
-
"create annotations, send chat messages, accept/dismiss your suggestions, or switch documents.",
|
|
3794
|
-
"Use your tandem MCP tools (tandem_getTextContent, tandem_comment, tandem_highlight,",
|
|
3795
|
-
"tandem_suggest, tandem_edit, etc.) to review and annotate documents.",
|
|
3796
|
-
"Start by calling tandem_checkInbox to see what needs attention."
|
|
3797
|
-
].join(" ");
|
|
3798
|
-
}
|
|
3799
|
-
});
|
|
3800
|
-
|
|
3801
|
-
// src/server/index.ts
|
|
3802
|
-
|
|
3803
|
-
import
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
import { dirname, join } from "path";
|
|
3814
|
-
import { fileURLToPath } from "url";
|
|
3815
|
-
import { createRequire } from "module";
|
|
3816
|
-
|
|
3817
|
-
// src/server/open-browser.ts
|
|
3818
|
-
import { execFile } from "child_process";
|
|
3819
|
-
function openBrowser(url) {
|
|
3820
|
-
let command;
|
|
3821
|
-
let args;
|
|
3822
|
-
if (process.platform === "win32") {
|
|
3823
|
-
command = "cmd";
|
|
3824
|
-
args = ["/c", "start", "", url];
|
|
3825
|
-
} else if (process.platform === "darwin") {
|
|
3826
|
-
command = "open";
|
|
3827
|
-
args = [url];
|
|
3828
|
-
} else {
|
|
3829
|
-
command = "xdg-open";
|
|
3830
|
-
args = [url];
|
|
3831
|
-
}
|
|
3832
|
-
execFile(command, args, (err) => {
|
|
3833
|
-
if (err) {
|
|
3834
|
-
console.error("[Tandem] Could not open browser automatically.");
|
|
3835
|
-
console.error(`[Tandem] Open this URL manually: ${url}`);
|
|
3836
|
-
}
|
|
3837
|
-
});
|
|
3838
|
-
}
|
|
3839
|
-
|
|
3840
|
-
// src/server/mcp/annotations.ts
|
|
3841
|
-
init_constants();
|
|
3842
|
-
init_queue();
|
|
3843
|
-
init_provider();
|
|
3844
|
-
import { z as z3 } from "zod";
|
|
3845
|
-
|
|
3846
|
-
// src/server/mcp/document.ts
|
|
3847
|
-
init_provider();
|
|
3848
|
-
import { z as z2 } from "zod";
|
|
3849
|
-
import * as Y8 from "yjs";
|
|
3850
|
-
|
|
3851
|
-
// src/server/mcp/response.ts
|
|
3852
|
-
function mcpSuccess(data) {
|
|
3853
|
-
return {
|
|
3854
|
-
content: [{ type: "text", text: JSON.stringify({ error: false, data }) }]
|
|
3855
|
-
};
|
|
3856
|
-
}
|
|
3857
|
-
function mcpError(code, message, details) {
|
|
3858
|
-
return {
|
|
3859
|
-
content: [
|
|
3860
|
-
{
|
|
3861
|
-
type: "text",
|
|
3862
|
-
text: JSON.stringify({ error: true, code, message, ...details && { details } })
|
|
3863
|
-
}
|
|
3864
|
-
]
|
|
3865
|
-
};
|
|
3866
|
-
}
|
|
3867
|
-
function noDocumentError() {
|
|
3868
|
-
return mcpError("NO_DOCUMENT", "No document is open. Call tandem_open first.");
|
|
3869
|
-
}
|
|
3870
|
-
function getErrorMessage(err) {
|
|
3871
|
-
return err instanceof Error ? err.message : String(err);
|
|
3872
|
-
}
|
|
3873
|
-
function withErrorBoundary(toolName, handler) {
|
|
3874
|
-
return async (args) => {
|
|
3875
|
-
try {
|
|
3876
|
-
return await handler(args);
|
|
3877
|
-
} catch (err) {
|
|
3878
|
-
console.error(`[Tandem] Tool ${toolName} threw:`, err);
|
|
3879
|
-
return mcpError("INTERNAL_ERROR", `${toolName} failed: ${getErrorMessage(err)}`);
|
|
3819
|
+
"You are Claude, connected to Tandem \u2014 a collaborative document editor.",
|
|
3820
|
+
"You will receive real-time push notifications via the tandem-channel when users",
|
|
3821
|
+
"create annotations, send chat messages, accept/dismiss your suggestions, or switch documents.",
|
|
3822
|
+
"Use your tandem MCP tools (tandem_getTextContent, tandem_comment, tandem_highlight,",
|
|
3823
|
+
"tandem_suggest, tandem_edit, etc.) to review and annotate documents.",
|
|
3824
|
+
"Start by calling tandem_checkInbox to see what needs attention."
|
|
3825
|
+
].join(" ");
|
|
3826
|
+
}
|
|
3827
|
+
});
|
|
3828
|
+
|
|
3829
|
+
// src/server/index.ts
|
|
3830
|
+
init_constants();
|
|
3831
|
+
import path10 from "path";
|
|
3832
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3833
|
+
|
|
3834
|
+
// src/server/error-filter.ts
|
|
3835
|
+
function isKnownHocuspocusError(err) {
|
|
3836
|
+
if (!(err instanceof Error)) return false;
|
|
3837
|
+
if ("code" in err) {
|
|
3838
|
+
const code = err.code;
|
|
3839
|
+
if (typeof code === "string" && code.startsWith("WS_ERR_")) {
|
|
3840
|
+
return true;
|
|
3880
3841
|
}
|
|
3881
|
-
}
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3842
|
+
}
|
|
3843
|
+
const msg = err.message;
|
|
3844
|
+
if (msg.startsWith("WebSocket is not open")) return true;
|
|
3845
|
+
if (msg.includes("Unexpected end of array") || msg.includes("Integer out of Range")) return true;
|
|
3846
|
+
if (msg.startsWith("Received a message with an unknown type:")) return true;
|
|
3847
|
+
return false;
|
|
3885
3848
|
}
|
|
3886
3849
|
|
|
3850
|
+
// src/server/index.ts
|
|
3851
|
+
init_queue();
|
|
3852
|
+
init_file_watcher();
|
|
3853
|
+
|
|
3887
3854
|
// src/server/mcp/document.ts
|
|
3888
|
-
|
|
3889
|
-
init_utils();
|
|
3855
|
+
init_constants();
|
|
3890
3856
|
init_offsets();
|
|
3857
|
+
init_types2();
|
|
3858
|
+
init_utils();
|
|
3859
|
+
init_queue();
|
|
3891
3860
|
init_file_io();
|
|
3892
3861
|
init_file_watcher();
|
|
3862
|
+
init_notifications();
|
|
3863
|
+
init_positions2();
|
|
3864
|
+
init_manager();
|
|
3865
|
+
init_provider();
|
|
3866
|
+
import * as Y8 from "yjs";
|
|
3867
|
+
import { z as z2 } from "zod";
|
|
3893
3868
|
|
|
3894
3869
|
// src/server/mcp/convert.ts
|
|
3870
|
+
init_file_io();
|
|
3895
3871
|
init_provider();
|
|
3896
3872
|
init_document_model();
|
|
3897
|
-
init_file_io();
|
|
3898
|
-
init_file_opener();
|
|
3899
3873
|
init_document_service();
|
|
3874
|
+
init_file_opener();
|
|
3900
3875
|
import fs5 from "fs/promises";
|
|
3901
3876
|
import path7 from "path";
|
|
3902
3877
|
async function findAvailablePath(basePath) {
|
|
@@ -3990,16 +3965,49 @@ async function convertToMarkdown(documentId, outputPath) {
|
|
|
3990
3965
|
}
|
|
3991
3966
|
|
|
3992
3967
|
// src/server/mcp/document.ts
|
|
3993
|
-
init_manager();
|
|
3994
|
-
init_file_opener();
|
|
3995
|
-
init_constants();
|
|
3996
|
-
init_types2();
|
|
3997
|
-
init_queue();
|
|
3998
3968
|
init_document_model();
|
|
3999
|
-
init_positions2();
|
|
4000
3969
|
init_document_service();
|
|
4001
|
-
|
|
3970
|
+
init_file_opener();
|
|
3971
|
+
|
|
3972
|
+
// src/server/mcp/response.ts
|
|
3973
|
+
function mcpSuccess(data) {
|
|
3974
|
+
return {
|
|
3975
|
+
content: [{ type: "text", text: JSON.stringify({ error: false, data }) }]
|
|
3976
|
+
};
|
|
3977
|
+
}
|
|
3978
|
+
function mcpError(code, message, details) {
|
|
3979
|
+
return {
|
|
3980
|
+
content: [
|
|
3981
|
+
{
|
|
3982
|
+
type: "text",
|
|
3983
|
+
text: JSON.stringify({ error: true, code, message, ...details && { details } })
|
|
3984
|
+
}
|
|
3985
|
+
]
|
|
3986
|
+
};
|
|
3987
|
+
}
|
|
3988
|
+
function noDocumentError() {
|
|
3989
|
+
return mcpError("NO_DOCUMENT", "No document is open. Call tandem_open first.");
|
|
3990
|
+
}
|
|
3991
|
+
function getErrorMessage(err) {
|
|
3992
|
+
return err instanceof Error ? err.message : String(err);
|
|
3993
|
+
}
|
|
3994
|
+
function withErrorBoundary(toolName, handler) {
|
|
3995
|
+
return async (args) => {
|
|
3996
|
+
try {
|
|
3997
|
+
return await handler(args);
|
|
3998
|
+
} catch (err) {
|
|
3999
|
+
console.error(`[Tandem] Tool ${toolName} threw:`, err);
|
|
4000
|
+
return mcpError("INTERNAL_ERROR", `${toolName} failed: ${getErrorMessage(err)}`);
|
|
4001
|
+
}
|
|
4002
|
+
};
|
|
4003
|
+
}
|
|
4004
|
+
function escapeRegex(str) {
|
|
4005
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4006
|
+
}
|
|
4007
|
+
|
|
4008
|
+
// src/server/mcp/document.ts
|
|
4002
4009
|
init_positions2();
|
|
4010
|
+
init_document_model();
|
|
4003
4011
|
init_document_service();
|
|
4004
4012
|
init_file_opener();
|
|
4005
4013
|
function getOutline(fragment) {
|
|
@@ -4298,15 +4306,12 @@ function registerDocumentTools(server) {
|
|
|
4298
4306
|
withErrorBoundary("tandem_status", async () => {
|
|
4299
4307
|
const activeId = getActiveDocId();
|
|
4300
4308
|
const active = activeId ? openDocs2.get(activeId) : null;
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
const awareness = doc.getMap(Y_MAP_USER_AWARENESS);
|
|
4305
|
-
interruptionMode = awareness.get("interruptionMode") ?? INTERRUPTION_MODE_DEFAULT;
|
|
4306
|
-
}
|
|
4309
|
+
const ctrlDoc = getOrCreateDocument(CTRL_ROOM);
|
|
4310
|
+
const ctrlAwareness = ctrlDoc.getMap(Y_MAP_USER_AWARENESS);
|
|
4311
|
+
const mode = TandemModeSchema.catch(TANDEM_MODE_DEFAULT).parse(ctrlAwareness.get(Y_MAP_MODE));
|
|
4307
4312
|
return mcpSuccess({
|
|
4308
4313
|
running: true,
|
|
4309
|
-
|
|
4314
|
+
mode,
|
|
4310
4315
|
activeDocument: active ? { documentId: active.id, filePath: active.filePath, format: active.format } : null,
|
|
4311
4316
|
openDocuments: Array.from(openDocs2.values()).map((d) => ({
|
|
4312
4317
|
documentId: d.id,
|
|
@@ -4398,12 +4403,56 @@ function registerDocumentTools(server) {
|
|
|
4398
4403
|
);
|
|
4399
4404
|
}
|
|
4400
4405
|
|
|
4406
|
+
// src/server/index.ts
|
|
4407
|
+
init_document_model();
|
|
4408
|
+
init_document_service();
|
|
4409
|
+
init_file_opener();
|
|
4410
|
+
|
|
4411
|
+
// src/server/mcp/server.ts
|
|
4412
|
+
import { existsSync } from "fs";
|
|
4413
|
+
import { dirname, join } from "path";
|
|
4414
|
+
import { fileURLToPath } from "url";
|
|
4415
|
+
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
|
4416
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4417
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4418
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
4419
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
4420
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
4421
|
+
import { createRequire } from "module";
|
|
4422
|
+
|
|
4423
|
+
// src/server/open-browser.ts
|
|
4424
|
+
import { execFile } from "child_process";
|
|
4425
|
+
function openBrowser(url) {
|
|
4426
|
+
let command;
|
|
4427
|
+
let args;
|
|
4428
|
+
if (process.platform === "win32") {
|
|
4429
|
+
command = "cmd";
|
|
4430
|
+
args = ["/c", "start", "", url];
|
|
4431
|
+
} else if (process.platform === "darwin") {
|
|
4432
|
+
command = "open";
|
|
4433
|
+
args = [url];
|
|
4434
|
+
} else {
|
|
4435
|
+
command = "xdg-open";
|
|
4436
|
+
args = [url];
|
|
4437
|
+
}
|
|
4438
|
+
execFile(command, args, (err) => {
|
|
4439
|
+
if (err) {
|
|
4440
|
+
console.error("[Tandem] Could not open browser automatically.");
|
|
4441
|
+
console.error(`[Tandem] Open this URL manually: ${url}`);
|
|
4442
|
+
}
|
|
4443
|
+
});
|
|
4444
|
+
}
|
|
4445
|
+
|
|
4401
4446
|
// src/server/mcp/annotations.ts
|
|
4402
|
-
|
|
4447
|
+
init_constants();
|
|
4403
4448
|
init_types2();
|
|
4404
|
-
init_positions2();
|
|
4405
|
-
init_notifications();
|
|
4406
4449
|
init_utils();
|
|
4450
|
+
init_queue();
|
|
4451
|
+
init_docx();
|
|
4452
|
+
init_notifications();
|
|
4453
|
+
init_positions2();
|
|
4454
|
+
init_provider();
|
|
4455
|
+
import { z as z3 } from "zod";
|
|
4407
4456
|
init_positions2();
|
|
4408
4457
|
function getDocAndAnnotations(documentId) {
|
|
4409
4458
|
const doc = getCurrentDoc(documentId);
|
|
@@ -4467,6 +4516,15 @@ function createAnnotation(map, ydoc, type, anchored, content, extras) {
|
|
|
4467
4516
|
...extras
|
|
4468
4517
|
};
|
|
4469
4518
|
ydoc.transact(() => map.set(id, annotation), MCP_ORIGIN);
|
|
4519
|
+
const snippet = annotation.textSnapshot ? `: "${annotation.textSnapshot.slice(0, 60)}${annotation.textSnapshot.length > 60 ? "\u2026" : ""}"` : "";
|
|
4520
|
+
pushNotification({
|
|
4521
|
+
id: generateNotificationId(),
|
|
4522
|
+
type: "review-pending",
|
|
4523
|
+
severity: "info",
|
|
4524
|
+
message: `New ${type[0].toUpperCase() + type.slice(1)}${snippet}`,
|
|
4525
|
+
dedupKey: `review-pending:${type}`,
|
|
4526
|
+
timestamp: Date.now()
|
|
4527
|
+
});
|
|
4470
4528
|
return id;
|
|
4471
4529
|
}
|
|
4472
4530
|
function collectAnnotations(map) {
|
|
@@ -4491,16 +4549,13 @@ function registerAnnotationTools(server) {
|
|
|
4491
4549
|
color: HighlightColorSchema.describe("Highlight color"),
|
|
4492
4550
|
note: z3.string().optional().describe("Optional note for the highlight"),
|
|
4493
4551
|
documentId: z3.string().optional().describe("Target document ID (defaults to active document)"),
|
|
4494
|
-
priority: AnnotationPrioritySchema.optional().describe(
|
|
4495
|
-
"Annotation priority. Set to 'urgent' for critical issues that should be visible even when the user has interruption mode set to urgent-only. Flags and questions are implicitly urgent."
|
|
4496
|
-
),
|
|
4497
4552
|
textSnapshot: z3.string().optional().describe(
|
|
4498
4553
|
"Expected text at [from, to] \u2014 returns RANGE_MOVED with relocated range on mismatch, or RANGE_GONE if text was deleted"
|
|
4499
4554
|
)
|
|
4500
4555
|
},
|
|
4501
4556
|
withErrorBoundary(
|
|
4502
4557
|
"tandem_highlight",
|
|
4503
|
-
async ({ from: rawFrom, to: rawTo, color, note, documentId,
|
|
4558
|
+
async ({ from: rawFrom, to: rawTo, color, note, documentId, textSnapshot }) => {
|
|
4504
4559
|
const da = getDocAndAnnotations(documentId);
|
|
4505
4560
|
if (!da) return noDocumentError();
|
|
4506
4561
|
const from = toFlatOffset(rawFrom);
|
|
@@ -4513,7 +4568,6 @@ function registerAnnotationTools(server) {
|
|
|
4513
4568
|
const snap = captureSnapshot(da.ydoc, result.range.from, result.range.to);
|
|
4514
4569
|
const id = createAnnotation(da.map, da.ydoc, "highlight", result, note || "", {
|
|
4515
4570
|
color,
|
|
4516
|
-
...priority ? { priority } : {},
|
|
4517
4571
|
textSnapshot: snap
|
|
4518
4572
|
});
|
|
4519
4573
|
return mcpSuccess({ annotationId: id });
|
|
@@ -4528,16 +4582,13 @@ function registerAnnotationTools(server) {
|
|
|
4528
4582
|
to: z3.number().describe("End position"),
|
|
4529
4583
|
text: z3.string().describe("Comment text"),
|
|
4530
4584
|
documentId: z3.string().optional().describe("Target document ID (defaults to active document)"),
|
|
4531
|
-
priority: AnnotationPrioritySchema.optional().describe(
|
|
4532
|
-
"Annotation priority. Set to 'urgent' for critical issues that should be visible even when the user has interruption mode set to urgent-only. Flags and questions are implicitly urgent."
|
|
4533
|
-
),
|
|
4534
4585
|
textSnapshot: z3.string().optional().describe(
|
|
4535
4586
|
"Expected text at [from, to] \u2014 returns RANGE_MOVED with relocated range on mismatch, or RANGE_GONE if text was deleted"
|
|
4536
4587
|
)
|
|
4537
4588
|
},
|
|
4538
4589
|
withErrorBoundary(
|
|
4539
4590
|
"tandem_comment",
|
|
4540
|
-
async ({ from: rawFrom, to: rawTo, text, documentId,
|
|
4591
|
+
async ({ from: rawFrom, to: rawTo, text, documentId, textSnapshot }) => {
|
|
4541
4592
|
const da = getDocAndAnnotations(documentId);
|
|
4542
4593
|
if (!da) return noDocumentError();
|
|
4543
4594
|
const from = toFlatOffset(rawFrom);
|
|
@@ -4549,7 +4600,6 @@ function registerAnnotationTools(server) {
|
|
|
4549
4600
|
}
|
|
4550
4601
|
const snap = captureSnapshot(da.ydoc, result.range.from, result.range.to);
|
|
4551
4602
|
const id = createAnnotation(da.map, da.ydoc, "comment", result, text, {
|
|
4552
|
-
...priority ? { priority } : {},
|
|
4553
4603
|
textSnapshot: snap
|
|
4554
4604
|
});
|
|
4555
4605
|
return mcpSuccess({ annotationId: id });
|
|
@@ -4565,16 +4615,13 @@ function registerAnnotationTools(server) {
|
|
|
4565
4615
|
newText: z3.string().describe("Suggested replacement text"),
|
|
4566
4616
|
reason: z3.string().optional().describe("Reason for the suggestion"),
|
|
4567
4617
|
documentId: z3.string().optional().describe("Target document ID (defaults to active document)"),
|
|
4568
|
-
priority: AnnotationPrioritySchema.optional().describe(
|
|
4569
|
-
"Annotation priority. Set to 'urgent' for critical issues that should be visible even when the user has interruption mode set to urgent-only. Flags and questions are implicitly urgent."
|
|
4570
|
-
),
|
|
4571
4618
|
textSnapshot: z3.string().optional().describe(
|
|
4572
4619
|
"Expected text at [from, to] \u2014 returns RANGE_MOVED with relocated range on mismatch, or RANGE_GONE if text was deleted"
|
|
4573
4620
|
)
|
|
4574
4621
|
},
|
|
4575
4622
|
withErrorBoundary(
|
|
4576
4623
|
"tandem_suggest",
|
|
4577
|
-
async ({ from: rawFrom, to: rawTo, newText, reason, documentId,
|
|
4624
|
+
async ({ from: rawFrom, to: rawTo, newText, reason, documentId, textSnapshot }) => {
|
|
4578
4625
|
const da = getDocAndAnnotations(documentId);
|
|
4579
4626
|
if (!da) return noDocumentError();
|
|
4580
4627
|
const from = toFlatOffset(rawFrom);
|
|
@@ -4591,7 +4638,7 @@ function registerAnnotationTools(server) {
|
|
|
4591
4638
|
"suggestion",
|
|
4592
4639
|
result,
|
|
4593
4640
|
JSON.stringify({ newText, reason: reason || "" }),
|
|
4594
|
-
{
|
|
4641
|
+
{ textSnapshot: snap }
|
|
4595
4642
|
);
|
|
4596
4643
|
return mcpSuccess({ annotationId: id });
|
|
4597
4644
|
}
|
|
@@ -4605,16 +4652,13 @@ function registerAnnotationTools(server) {
|
|
|
4605
4652
|
to: z3.number().describe("End position"),
|
|
4606
4653
|
note: z3.string().optional().describe("Reason for flagging"),
|
|
4607
4654
|
documentId: z3.string().optional().describe("Target document ID (defaults to active document)"),
|
|
4608
|
-
priority: AnnotationPrioritySchema.optional().describe(
|
|
4609
|
-
"Annotation priority. Set to 'urgent' for critical issues that should be visible even when the user has interruption mode set to urgent-only. Flags and questions are implicitly urgent."
|
|
4610
|
-
),
|
|
4611
4655
|
textSnapshot: z3.string().optional().describe(
|
|
4612
4656
|
"Expected text at [from, to] \u2014 returns RANGE_MOVED with relocated range on mismatch, or RANGE_GONE if text was deleted"
|
|
4613
4657
|
)
|
|
4614
4658
|
},
|
|
4615
4659
|
withErrorBoundary(
|
|
4616
4660
|
"tandem_flag",
|
|
4617
|
-
async ({ from: rawFrom, to: rawTo, note, documentId,
|
|
4661
|
+
async ({ from: rawFrom, to: rawTo, note, documentId, textSnapshot }) => {
|
|
4618
4662
|
const da = getDocAndAnnotations(documentId);
|
|
4619
4663
|
if (!da) return noDocumentError();
|
|
4620
4664
|
const from = toFlatOffset(rawFrom);
|
|
@@ -4626,7 +4670,6 @@ function registerAnnotationTools(server) {
|
|
|
4626
4670
|
}
|
|
4627
4671
|
const snap = captureSnapshot(da.ydoc, result.range.from, result.range.to);
|
|
4628
4672
|
const id = createAnnotation(da.map, da.ydoc, "flag", result, note || "", {
|
|
4629
|
-
...priority ? { priority } : {},
|
|
4630
4673
|
textSnapshot: snap
|
|
4631
4674
|
});
|
|
4632
4675
|
return mcpSuccess({ annotationId: id });
|
|
@@ -4781,20 +4824,21 @@ function registerAnnotationTools(server) {
|
|
|
4781
4824
|
|
|
4782
4825
|
// src/server/mcp/api-routes.ts
|
|
4783
4826
|
init_constants();
|
|
4827
|
+
init_types2();
|
|
4828
|
+
init_notifications();
|
|
4829
|
+
init_provider();
|
|
4784
4830
|
init_document_model();
|
|
4785
|
-
init_file_opener();
|
|
4786
4831
|
init_document_service();
|
|
4787
|
-
init_notifications();
|
|
4788
4832
|
|
|
4789
4833
|
// src/server/mcp/docx-apply.ts
|
|
4790
|
-
init_document_service();
|
|
4791
4834
|
init_constants();
|
|
4835
|
+
init_file_io();
|
|
4792
4836
|
init_positions2();
|
|
4793
4837
|
init_document_model();
|
|
4794
|
-
|
|
4795
|
-
import { z as z4 } from "zod";
|
|
4838
|
+
init_document_service();
|
|
4796
4839
|
import fs6 from "fs/promises";
|
|
4797
4840
|
import path8 from "path";
|
|
4841
|
+
import { z as z4 } from "zod";
|
|
4798
4842
|
async function applyChangesCore(documentId, author, backupPath) {
|
|
4799
4843
|
const r = requireDocument(documentId);
|
|
4800
4844
|
if (!r) throw Object.assign(new Error("No document is open."), { code: "NO_DOCUMENT" });
|
|
@@ -4968,6 +5012,7 @@ function registerApplyTools(server) {
|
|
|
4968
5012
|
}
|
|
4969
5013
|
|
|
4970
5014
|
// src/server/mcp/api-routes.ts
|
|
5015
|
+
init_file_opener();
|
|
4971
5016
|
function isHostAllowed(host) {
|
|
4972
5017
|
const reqHost = (host ?? "").split(":")[0];
|
|
4973
5018
|
return reqHost === "localhost" || reqHost === "127.0.0.1";
|
|
@@ -5169,6 +5214,12 @@ function registerApiRoutes(app, largeBody) {
|
|
|
5169
5214
|
sendApiError(res, err);
|
|
5170
5215
|
}
|
|
5171
5216
|
});
|
|
5217
|
+
app.get("/api/mode", apiMiddleware, (_req, res) => {
|
|
5218
|
+
const ctrlDoc = getOrCreateDocument(CTRL_ROOM);
|
|
5219
|
+
const awareness = ctrlDoc.getMap(Y_MAP_USER_AWARENESS);
|
|
5220
|
+
const mode = TandemModeSchema.catch(TANDEM_MODE_DEFAULT).parse(awareness.get(Y_MAP_MODE));
|
|
5221
|
+
res.json({ mode });
|
|
5222
|
+
});
|
|
5172
5223
|
app.options("/api/apply-changes", apiMiddleware);
|
|
5173
5224
|
app.post("/api/apply-changes", apiMiddleware, largeBody, async (req, res) => {
|
|
5174
5225
|
const { documentId, author, backupPath } = req.body ?? {};
|
|
@@ -5198,11 +5249,12 @@ function registerApiRoutes(app, largeBody) {
|
|
|
5198
5249
|
}
|
|
5199
5250
|
|
|
5200
5251
|
// src/server/mcp/awareness.ts
|
|
5201
|
-
init_provider();
|
|
5202
|
-
import { z as z5 } from "zod";
|
|
5203
|
-
init_utils();
|
|
5204
5252
|
init_constants();
|
|
5253
|
+
init_types2();
|
|
5254
|
+
init_utils();
|
|
5205
5255
|
init_queue();
|
|
5256
|
+
init_provider();
|
|
5257
|
+
import { z as z5 } from "zod";
|
|
5206
5258
|
var surfacedIds = /* @__PURE__ */ new Set();
|
|
5207
5259
|
function registerAwarenessTools(server) {
|
|
5208
5260
|
server.tool(
|
|
@@ -5307,7 +5359,8 @@ function registerAwarenessTools(server) {
|
|
|
5307
5359
|
const userAwareness = doc.getMap(Y_MAP_USER_AWARENESS);
|
|
5308
5360
|
const selection = userAwareness.get("selection");
|
|
5309
5361
|
const activity = userAwareness.get("activity");
|
|
5310
|
-
const
|
|
5362
|
+
const ctrlAwareness = ctrlDoc.getMap(Y_MAP_USER_AWARENESS);
|
|
5363
|
+
const mode = TandemModeSchema.catch(TANDEM_MODE_DEFAULT).parse(ctrlAwareness.get(Y_MAP_MODE));
|
|
5311
5364
|
const hasSelection = selection && selection.from !== selection.to;
|
|
5312
5365
|
const selectedText = hasSelection ? safeSlice2(fullText, selection.from, selection.to) : null;
|
|
5313
5366
|
const parts = [];
|
|
@@ -5335,7 +5388,7 @@ function registerAwarenessTools(server) {
|
|
|
5335
5388
|
return mcpSuccess({
|
|
5336
5389
|
summary,
|
|
5337
5390
|
hasNew,
|
|
5338
|
-
|
|
5391
|
+
mode,
|
|
5339
5392
|
userActions,
|
|
5340
5393
|
userResponses,
|
|
5341
5394
|
chatMessages,
|
|
@@ -5875,60 +5928,11 @@ async function startMcpServerHttp(port, host = "127.0.0.1") {
|
|
|
5875
5928
|
});
|
|
5876
5929
|
}
|
|
5877
5930
|
|
|
5878
|
-
// src/server/index.ts
|
|
5879
|
-
init_provider();
|
|
5880
|
-
init_constants();
|
|
5881
|
-
init_manager();
|
|
5882
|
-
init_platform();
|
|
5883
|
-
|
|
5884
|
-
// src/server/version-check.ts
|
|
5885
|
-
import fs7 from "fs/promises";
|
|
5886
|
-
import path9 from "path";
|
|
5887
|
-
async function checkVersionChange(currentVersion, versionFilePath) {
|
|
5888
|
-
let storedVersion = null;
|
|
5889
|
-
try {
|
|
5890
|
-
storedVersion = (await fs7.readFile(versionFilePath, "utf-8")).trim();
|
|
5891
|
-
} catch (err) {
|
|
5892
|
-
if (err.code !== "ENOENT") {
|
|
5893
|
-
console.error("[Tandem] Failed to read last-seen-version:", err);
|
|
5894
|
-
}
|
|
5895
|
-
}
|
|
5896
|
-
const result = storedVersion === null ? "first-install" : storedVersion === currentVersion ? "current" : "upgraded";
|
|
5897
|
-
if (result !== "current") {
|
|
5898
|
-
await fs7.mkdir(path9.dirname(versionFilePath), { recursive: true });
|
|
5899
|
-
await fs7.writeFile(versionFilePath, currentVersion, "utf-8");
|
|
5900
|
-
}
|
|
5901
|
-
return result;
|
|
5902
|
-
}
|
|
5903
|
-
|
|
5904
|
-
// src/server/error-filter.ts
|
|
5905
|
-
function isKnownHocuspocusError(err) {
|
|
5906
|
-
if (!(err instanceof Error)) return false;
|
|
5907
|
-
if ("code" in err) {
|
|
5908
|
-
const code = err.code;
|
|
5909
|
-
if (typeof code === "string" && code.startsWith("WS_ERR_")) {
|
|
5910
|
-
return true;
|
|
5911
|
-
}
|
|
5912
|
-
}
|
|
5913
|
-
const msg = err.message;
|
|
5914
|
-
if (msg.startsWith("WebSocket is not open")) return true;
|
|
5915
|
-
if (msg.includes("Unexpected end of array") || msg.includes("Integer out of Range")) return true;
|
|
5916
|
-
if (msg.startsWith("Received a message with an unknown type:")) return true;
|
|
5917
|
-
return false;
|
|
5918
|
-
}
|
|
5919
|
-
|
|
5920
|
-
// src/server/index.ts
|
|
5921
|
-
init_queue();
|
|
5922
|
-
init_document_service();
|
|
5923
|
-
init_file_watcher();
|
|
5924
|
-
init_file_opener();
|
|
5925
|
-
init_document_model();
|
|
5926
|
-
|
|
5927
5931
|
// src/server/mcp/tutorial-annotations.ts
|
|
5928
5932
|
init_constants();
|
|
5933
|
+
init_types2();
|
|
5929
5934
|
init_queue();
|
|
5930
5935
|
init_positions2();
|
|
5931
|
-
init_types2();
|
|
5932
5936
|
init_document_model();
|
|
5933
5937
|
var TUTORIAL_ANNOTATIONS = [
|
|
5934
5938
|
{
|
|
@@ -6004,6 +6008,31 @@ function injectTutorialAnnotations(doc) {
|
|
|
6004
6008
|
}
|
|
6005
6009
|
|
|
6006
6010
|
// src/server/index.ts
|
|
6011
|
+
init_platform();
|
|
6012
|
+
init_manager();
|
|
6013
|
+
|
|
6014
|
+
// src/server/version-check.ts
|
|
6015
|
+
import fs7 from "fs/promises";
|
|
6016
|
+
import path9 from "path";
|
|
6017
|
+
async function checkVersionChange(currentVersion, versionFilePath) {
|
|
6018
|
+
let storedVersion = null;
|
|
6019
|
+
try {
|
|
6020
|
+
storedVersion = (await fs7.readFile(versionFilePath, "utf-8")).trim();
|
|
6021
|
+
} catch (err) {
|
|
6022
|
+
if (err.code !== "ENOENT") {
|
|
6023
|
+
console.error("[Tandem] Failed to read last-seen-version:", err);
|
|
6024
|
+
}
|
|
6025
|
+
}
|
|
6026
|
+
const result = storedVersion === null ? "first-install" : storedVersion === currentVersion ? "current" : "upgraded";
|
|
6027
|
+
if (result !== "current") {
|
|
6028
|
+
await fs7.mkdir(path9.dirname(versionFilePath), { recursive: true });
|
|
6029
|
+
await fs7.writeFile(versionFilePath, currentVersion, "utf-8");
|
|
6030
|
+
}
|
|
6031
|
+
return result;
|
|
6032
|
+
}
|
|
6033
|
+
|
|
6034
|
+
// src/server/index.ts
|
|
6035
|
+
init_provider();
|
|
6007
6036
|
var isProduction = process.env.TANDEM_OPEN_BROWSER === "1";
|
|
6008
6037
|
var SUPPRESSED_PATTERNS = [/^\[mammoth\]/, /Invalid access/i, /^\s*add yjs type/i];
|
|
6009
6038
|
var originalStderrWrite = process.stderr.write.bind(process.stderr);
|