tandem-editor 0.3.0 → 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.
@@ -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, TANDEM_MODE_DEFAULT, SELECTION_DWELL_DEFAULT_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_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;
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";
@@ -22,6 +22,8 @@ var init_constants = __esm({
22
22
  SESSION_MAX_AGE = 30 * 24 * 60 * 60 * 1e3;
23
23
  TANDEM_MODE_DEFAULT = "tandem";
24
24
  SELECTION_DWELL_DEFAULT_MS = 1e3;
25
+ SELECTION_DWELL_MIN_MS = 500;
26
+ SELECTION_DWELL_MAX_MS = 3e3;
25
27
  CHARS_PER_PAGE = 3e3;
26
28
  LARGE_FILE_PAGE_THRESHOLD = 50;
27
29
  VERY_LARGE_FILE_PAGE_THRESHOLD = 100;
@@ -30,6 +32,7 @@ var init_constants = __esm({
30
32
  Y_MAP_AWARENESS = "awareness";
31
33
  Y_MAP_USER_AWARENESS = "userAwareness";
32
34
  Y_MAP_MODE = "mode";
35
+ Y_MAP_DWELL_MS = "selectionDwellMs";
33
36
  Y_MAP_CHAT = "chat";
34
37
  Y_MAP_DOCUMENT_META = "documentMeta";
35
38
  Y_MAP_SAVED_AT_VERSION = "savedAtVersion";
@@ -41,106 +44,79 @@ var init_constants = __esm({
41
44
  }
42
45
  });
43
46
 
44
- // src/server/yjs/provider.ts
45
- import { Hocuspocus } from "@hocuspocus/server";
46
- import * as Y from "yjs";
47
- function setDocLifecycleCallbacks(swapped, unloaded) {
48
- onDocSwapped = swapped;
49
- onDocUnloaded = unloaded;
50
- }
51
- function setShouldKeepDocument(fn) {
52
- shouldKeepDocument = fn;
53
- }
54
- function getDocument(name) {
55
- return documents.get(name);
56
- }
57
- function getOrCreateDocument(name) {
58
- let doc = documents.get(name);
59
- if (!doc) {
60
- doc = new Y.Doc();
61
- documents.set(name, doc);
62
- }
63
- return doc;
64
- }
65
- async function startHocuspocus(port) {
66
- hocuspocusInstance = new Hocuspocus({
67
- port,
68
- address: "127.0.0.1",
69
- quiet: true,
70
- // stdout is the MCP wire — suppress the startup banner
71
- async onConnect({ request, documentName }) {
72
- const origin = request?.headers?.origin;
73
- if (!origin) {
74
- console.error("[Hocuspocus] Rejected connection: missing Origin header");
75
- throw new Error("Connection rejected: missing origin header");
76
- }
77
- const url = new URL(origin);
78
- if (url.hostname !== "localhost" && url.hostname !== "127.0.0.1") {
79
- console.error(`[Hocuspocus] Rejected connection from origin: ${origin}`);
80
- throw new Error("Connection rejected: invalid origin");
81
- }
82
- console.error(`[Hocuspocus] Client connected to: ${documentName}`);
83
- },
84
- async onDisconnect({ documentName }) {
85
- console.error(`[Hocuspocus] Client disconnected from: ${documentName}`);
86
- },
87
- async onLoadDocument({ document, documentName }) {
88
- console.error(`[Hocuspocus] Loading document: ${documentName}`);
89
- const existing = documents.get(documentName);
90
- if (existing && existing !== document) {
91
- const update = Y.encodeStateAsUpdate(existing);
92
- Y.applyUpdate(document, update);
93
- existing.destroy();
94
- console.error(`[Hocuspocus] Merged pre-existing content into document: ${documentName}`);
95
- }
96
- documents.set(documentName, document);
97
- if (onDocSwapped) {
98
- onDocSwapped(documentName, document);
99
- } else {
100
- console.error(
101
- `[Tandem] WARN: onDocSwapped callback not registered during doc load for ${documentName}. Server-side observers will NOT be attached. Call setDocLifecycleCallbacks() before starting Hocuspocus.`
102
- );
103
- }
104
- return document;
105
- },
106
- async afterUnloadDocument({ documentName }) {
107
- if (shouldKeepDocument?.(documentName)) {
108
- 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;
109
59
  return;
110
60
  }
111
- if (documents.has(documentName)) {
112
- onDocUnloaded?.(documentName);
113
- documents.delete(documentName);
114
- console.error(`[Hocuspocus] Unloaded document from map: ${documentName}`);
61
+ if (entry.timer !== null) {
62
+ clearTimeout(entry.timer);
115
63
  }
116
- }
117
- });
118
- await hocuspocusInstance.listen();
119
- const internal = hocuspocusInstance.server?.httpServer;
120
- if (internal) {
121
- internal.on("error", (err) => {
122
- 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);
123
70
  });
71
+ } catch (err) {
72
+ console.error(`[FileWatcher] Failed to watch ${filePath}:`, err);
73
+ return;
124
74
  }
125
- return hocuspocusInstance;
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}`);
126
81
  }
127
- var hocuspocusInstance, documents, shouldKeepDocument, onDocSwapped, onDocUnloaded;
128
- var init_provider = __esm({
129
- "src/server/yjs/provider.ts"() {
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"() {
130
110
  "use strict";
131
- hocuspocusInstance = null;
132
- documents = /* @__PURE__ */ new Map();
133
- shouldKeepDocument = null;
134
- onDocSwapped = null;
135
- onDocUnloaded = null;
111
+ watched = /* @__PURE__ */ new Map();
136
112
  }
137
113
  });
138
114
 
139
115
  // src/server/platform.ts
140
116
  import { execSync } from "child_process";
117
+ import envPaths from "env-paths";
141
118
  import net from "net";
142
119
  import path from "path";
143
- import envPaths from "env-paths";
144
120
  function freePort(port) {
145
121
  try {
146
122
  if (process.platform === "win32") {
@@ -233,15 +209,15 @@ var init_platform = __esm({
233
209
  });
234
210
 
235
211
  // src/server/session/manager.ts
236
- import fs from "fs/promises";
212
+ import fs2 from "fs/promises";
237
213
  import path2 from "path";
238
- import * as Y2 from "yjs";
214
+ import * as Y from "yjs";
239
215
  async function atomicWrite(sessionPath, content) {
240
216
  const tmpPath = `${sessionPath}.tmp`;
241
- await fs.writeFile(tmpPath, content, "utf-8");
217
+ await fs2.writeFile(tmpPath, content, "utf-8");
242
218
  for (let attempt = 0; attempt < RENAME_MAX_RETRIES; attempt++) {
243
219
  try {
244
- await fs.rename(tmpPath, sessionPath);
220
+ await fs2.rename(tmpPath, sessionPath);
245
221
  return;
246
222
  } catch (err) {
247
223
  const code = err.code;
@@ -249,7 +225,7 @@ async function atomicWrite(sessionPath, content) {
249
225
  await new Promise((r) => setTimeout(r, RENAME_RETRY_BASE_MS * 2 ** attempt));
250
226
  continue;
251
227
  }
252
- await fs.unlink(tmpPath).catch(() => {
228
+ await fs2.unlink(tmpPath).catch(() => {
253
229
  });
254
230
  throw err;
255
231
  }
@@ -263,12 +239,12 @@ async function saveSession(filePath, format, doc) {
263
239
  let sourceFileMtime = 0;
264
240
  if (!filePath.startsWith("upload://")) {
265
241
  try {
266
- const stat = await fs.stat(filePath);
242
+ const stat = await fs2.stat(filePath);
267
243
  sourceFileMtime = stat.mtimeMs;
268
244
  } catch {
269
245
  }
270
246
  }
271
- const state = Y2.encodeStateAsUpdate(doc);
247
+ const state = Y.encodeStateAsUpdate(doc);
272
248
  const ydocState = Buffer.from(state).toString("base64");
273
249
  const data = {
274
250
  filePath,
@@ -278,7 +254,7 @@ async function saveSession(filePath, format, doc) {
278
254
  lastAccessed: Date.now()
279
255
  };
280
256
  if (!sessionDirReady) {
281
- await fs.mkdir(SESSION_DIR, { recursive: true });
257
+ await fs2.mkdir(SESSION_DIR, { recursive: true });
282
258
  sessionDirReady = true;
283
259
  }
284
260
  const sessionPath = path2.join(SESSION_DIR, `${key}.json`);
@@ -288,14 +264,14 @@ async function loadSession(filePath) {
288
264
  const key = sessionKey(filePath);
289
265
  const sessionPath = path2.join(SESSION_DIR, `${key}.json`);
290
266
  try {
291
- const content = await fs.readFile(sessionPath, "utf-8");
267
+ const content = await fs2.readFile(sessionPath, "utf-8");
292
268
  return JSON.parse(content);
293
269
  } catch (err) {
294
270
  const code = err.code;
295
271
  if (code === "ENOENT") return null;
296
272
  if (err instanceof SyntaxError) {
297
273
  console.error(`[Tandem] Corrupted session file ${sessionPath}, removing:`, err.message);
298
- await fs.unlink(sessionPath).catch((unlinkErr) => {
274
+ await fs2.unlink(sessionPath).catch((unlinkErr) => {
299
275
  console.error(`[Tandem] Failed to remove corrupted session ${sessionPath}:`, unlinkErr);
300
276
  });
301
277
  return null;
@@ -306,12 +282,12 @@ async function loadSession(filePath) {
306
282
  }
307
283
  function restoreYDoc(doc, session) {
308
284
  const state = Buffer.from(session.ydocState, "base64");
309
- Y2.applyUpdate(doc, new Uint8Array(state));
285
+ Y.applyUpdate(doc, new Uint8Array(state));
310
286
  }
311
287
  async function sourceFileChanged(session) {
312
288
  if (session.filePath.startsWith("upload://")) return false;
313
289
  try {
314
- const stat = await fs.stat(session.filePath);
290
+ const stat = await fs2.stat(session.filePath);
315
291
  return stat.mtimeMs !== session.sourceFileMtime;
316
292
  } catch {
317
293
  return true;
@@ -321,7 +297,7 @@ async function deleteSession(filePath) {
321
297
  const key = sessionKey(filePath);
322
298
  const sessionPath = path2.join(SESSION_DIR, `${key}.json`);
323
299
  try {
324
- await fs.unlink(sessionPath);
300
+ await fs2.unlink(sessionPath);
325
301
  } catch (err) {
326
302
  const code = err.code;
327
303
  if (code !== "ENOENT") {
@@ -331,7 +307,7 @@ async function deleteSession(filePath) {
331
307
  }
332
308
  async function saveCtrlSession(doc) {
333
309
  if (!sessionDirReady) {
334
- await fs.mkdir(SESSION_DIR, { recursive: true });
310
+ await fs2.mkdir(SESSION_DIR, { recursive: true });
335
311
  sessionDirReady = true;
336
312
  }
337
313
  const chatMap = doc.getMap(Y_MAP_CHAT);
@@ -349,7 +325,7 @@ async function saveCtrlSession(doc) {
349
325
  }
350
326
  }, MCP_ORIGIN);
351
327
  }
352
- const state = Y2.encodeStateAsUpdate(doc);
328
+ const state = Y.encodeStateAsUpdate(doc);
353
329
  const ydocState = Buffer.from(state).toString("base64");
354
330
  const data = { ydocState, lastAccessed: Date.now() };
355
331
  const sessionPath = path2.join(SESSION_DIR, `${CTRL_SESSION_KEY}.json`);
@@ -358,7 +334,7 @@ async function saveCtrlSession(doc) {
358
334
  async function loadCtrlSession() {
359
335
  const sessionPath = path2.join(SESSION_DIR, `${CTRL_SESSION_KEY}.json`);
360
336
  try {
361
- const content = await fs.readFile(sessionPath, "utf-8");
337
+ const content = await fs2.readFile(sessionPath, "utf-8");
362
338
  const data = JSON.parse(content);
363
339
  return data.ydocState ?? null;
364
340
  } catch (err) {
@@ -366,7 +342,7 @@ async function loadCtrlSession() {
366
342
  if (code === "ENOENT") return null;
367
343
  if (err instanceof SyntaxError) {
368
344
  console.error(`[Tandem] Corrupted ctrl session ${sessionPath}, removing:`, err.message);
369
- await fs.unlink(sessionPath).catch((unlinkErr) => {
345
+ await fs2.unlink(sessionPath).catch((unlinkErr) => {
370
346
  console.error(
371
347
  `[Tandem] Failed to remove corrupted ctrl session ${sessionPath}:`,
372
348
  unlinkErr
@@ -380,18 +356,18 @@ async function loadCtrlSession() {
380
356
  }
381
357
  function restoreCtrlDoc(doc, base64State) {
382
358
  const state = Buffer.from(base64State, "base64");
383
- Y2.applyUpdate(doc, new Uint8Array(state));
359
+ Y.applyUpdate(doc, new Uint8Array(state));
384
360
  }
385
361
  async function listSessionFilePaths() {
386
362
  try {
387
- await fs.mkdir(SESSION_DIR, { recursive: true });
388
- const files = await fs.readdir(SESSION_DIR);
363
+ await fs2.mkdir(SESSION_DIR, { recursive: true });
364
+ const files = await fs2.readdir(SESSION_DIR);
389
365
  const results = [];
390
366
  for (const file of files) {
391
367
  if (!file.endsWith(".json")) continue;
392
368
  if (file === `${encodeURIComponent(CTRL_ROOM)}.json`) continue;
393
369
  try {
394
- const raw = await fs.readFile(path2.join(SESSION_DIR, file), "utf-8");
370
+ const raw = await fs2.readFile(path2.join(SESSION_DIR, file), "utf-8");
395
371
  const data = JSON.parse(raw);
396
372
  if (!data.filePath || data.filePath.startsWith("upload://")) continue;
397
373
  results.push({ filePath: data.filePath, lastAccessed: data.lastAccessed ?? 0 });
@@ -410,7 +386,7 @@ async function cleanupSessions() {
410
386
  let cleaned = 0;
411
387
  let files;
412
388
  try {
413
- files = await fs.readdir(SESSION_DIR);
389
+ files = await fs2.readdir(SESSION_DIR);
414
390
  } catch (err) {
415
391
  if (err.code === "ENOENT") return 0;
416
392
  console.error("[Tandem] Failed to read session directory:", err);
@@ -420,9 +396,9 @@ async function cleanupSessions() {
420
396
  for (const file of files) {
421
397
  try {
422
398
  const filePath = path2.join(SESSION_DIR, file);
423
- const stat = await fs.stat(filePath);
399
+ const stat = await fs2.stat(filePath);
424
400
  if (now - stat.mtimeMs > SESSION_MAX_AGE) {
425
- await fs.unlink(filePath);
401
+ await fs2.unlink(filePath);
426
402
  cleaned++;
427
403
  }
428
404
  } catch (err) {
@@ -456,9 +432,9 @@ var AUTO_SAVE_INTERVAL, RENAME_MAX_RETRIES, RENAME_RETRY_BASE_MS, sessionDirRead
456
432
  var init_manager = __esm({
457
433
  "src/server/session/manager.ts"() {
458
434
  "use strict";
459
- init_platform();
460
435
  init_constants();
461
436
  init_queue();
437
+ init_platform();
462
438
  AUTO_SAVE_INTERVAL = 60 * 1e3;
463
439
  RENAME_MAX_RETRIES = 3;
464
440
  RENAME_RETRY_BASE_MS = 50;
@@ -469,71 +445,136 @@ var init_manager = __esm({
469
445
  }
470
446
  });
471
447
 
472
- // src/server/file-watcher.ts
473
- import fs2 from "fs";
474
- function watchFile(filePath, onChanged) {
475
- if (watched.has(filePath)) return;
476
- let watcher;
477
- try {
478
- watcher = fs2.watch(filePath, (eventType) => {
479
- if (eventType !== "change") return;
480
- const entry = watched.get(filePath);
481
- if (!entry) return;
482
- if (entry.suppressed) {
483
- entry.suppressed = false;
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}`);
484
513
  return;
485
514
  }
486
- if (entry.timer !== null) {
487
- clearTimeout(entry.timer);
515
+ if (documents.has(documentName)) {
516
+ onDocUnloaded?.(documentName);
517
+ documents.delete(documentName);
518
+ console.error(`[Hocuspocus] Unloaded document from map: ${documentName}`);
488
519
  }
489
- entry.timer = setTimeout(() => {
490
- entry.timer = null;
491
- onChanged(filePath).catch((err) => {
492
- console.error(`[FileWatcher] onChanged callback failed for ${filePath}:`, err);
493
- });
494
- }, 500);
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}`);
495
527
  });
496
- } catch (err) {
497
- console.error(`[FileWatcher] Failed to watch ${filePath}:`, err);
498
- return;
499
528
  }
500
- watcher.on("error", (err) => {
501
- console.error(`[FileWatcher] Watcher error for ${filePath}:`, err);
502
- unwatchFile(filePath);
503
- });
504
- watched.set(filePath, { watcher, timer: null, suppressed: false });
505
- console.error(`[FileWatcher] Watching ${filePath}`);
529
+ return hocuspocusInstance;
506
530
  }
507
- function suppressNextChange(filePath) {
508
- const entry = watched.get(filePath);
509
- if (entry) {
510
- entry.suppressed = true;
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;
511
540
  }
541
+ });
542
+
543
+ // src/shared/utils.ts
544
+ function generateId(prefix) {
545
+ return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
512
546
  }
513
- function unwatchFile(filePath) {
514
- const entry = watched.get(filePath);
515
- if (!entry) return;
516
- if (entry.timer !== null) {
517
- clearTimeout(entry.timer);
518
- }
519
- try {
520
- entry.watcher.close();
521
- } catch (err) {
522
- console.error(`[FileWatcher] watcher.close() failed for ${filePath}:`, err);
523
- }
524
- watched.delete(filePath);
525
- console.error(`[FileWatcher] Unwatched ${filePath}`);
547
+ function generateAnnotationId() {
548
+ return generateId("ann");
526
549
  }
527
- function unwatchAll() {
528
- for (const filePath of [...watched.keys()]) {
529
- unwatchFile(filePath);
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";
530
562
  }
563
+ });
564
+
565
+ // src/shared/offsets.ts
566
+ function headingPrefixLength(level) {
567
+ if (!level) return 0;
568
+ return level + 1;
531
569
  }
532
- var watched;
533
- var init_file_watcher = __esm({
534
- "src/server/file-watcher.ts"() {
570
+ function headingPrefix(level) {
571
+ return "#".repeat(level) + " ";
572
+ }
573
+ var FLAT_SEPARATOR;
574
+ var init_offsets = __esm({
575
+ "src/shared/offsets.ts"() {
535
576
  "use strict";
536
- watched = /* @__PURE__ */ new Map();
577
+ FLAT_SEPARATOR = "\n";
537
578
  }
538
579
  });
539
580
 
@@ -855,10 +896,10 @@ var init_mdast_ydoc = __esm({
855
896
  });
856
897
 
857
898
  // src/server/file-io/markdown.ts
858
- import { unified } from "unified";
859
- import remarkParse from "remark-parse";
860
899
  import remarkGfm from "remark-gfm";
900
+ import remarkParse from "remark-parse";
861
901
  import remarkStringify from "remark-stringify";
902
+ import { unified } from "unified";
862
903
  function loadMarkdown(doc, markdown) {
863
904
  const tree = parser.parse(markdown);
864
905
  mdastToYDoc(doc, tree);
@@ -883,22 +924,6 @@ var init_markdown = __esm({
883
924
  }
884
925
  });
885
926
 
886
- // src/shared/offsets.ts
887
- function headingPrefixLength(level) {
888
- if (!level) return 0;
889
- return level + 1;
890
- }
891
- function headingPrefix(level) {
892
- return "#".repeat(level) + " ";
893
- }
894
- var FLAT_SEPARATOR;
895
- var init_offsets = __esm({
896
- "src/shared/offsets.ts"() {
897
- "use strict";
898
- FLAT_SEPARATOR = "\n";
899
- }
900
- });
901
-
902
927
  // src/server/mcp/document-model.ts
903
928
  import path3 from "path";
904
929
  import * as Y4 from "yjs";
@@ -1428,6 +1453,42 @@ var init_types = __esm({
1428
1453
  }
1429
1454
  });
1430
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
+
1431
1492
  // src/shared/positions/index.ts
1432
1493
  var init_positions = __esm({
1433
1494
  "src/shared/positions/index.ts"() {
@@ -1634,49 +1695,12 @@ function refreshAllRanges(annotations, ydoc, map) {
1634
1695
  var init_positions2 = __esm({
1635
1696
  "src/server/positions.ts"() {
1636
1697
  "use strict";
1637
- init_queue();
1638
1698
  init_positions();
1699
+ init_queue();
1639
1700
  init_document_model();
1640
1701
  }
1641
1702
  });
1642
1703
 
1643
- // src/shared/types.ts
1644
- import { z } from "zod";
1645
- var AnnotationTypeSchema, AnnotationStatusSchema, AnnotationPrioritySchema, HighlightColorSchema, SeveritySchema, TandemModeSchema, AuthorSchema, AnnotationActionSchema, ExportFormatSchema, DocumentFormatSchema, ToolErrorCodeSchema;
1646
- var init_types2 = __esm({
1647
- "src/shared/types.ts"() {
1648
- "use strict";
1649
- init_types();
1650
- AnnotationTypeSchema = z.enum([
1651
- "highlight",
1652
- "comment",
1653
- "suggestion",
1654
- "overlay",
1655
- "question",
1656
- "flag"
1657
- ]);
1658
- AnnotationStatusSchema = z.enum(["pending", "accepted", "dismissed"]);
1659
- AnnotationPrioritySchema = z.enum(["normal", "urgent"]);
1660
- HighlightColorSchema = z.enum(["yellow", "red", "green", "blue", "purple"]);
1661
- SeveritySchema = z.enum(["info", "warning", "error", "success"]);
1662
- TandemModeSchema = z.enum(["solo", "tandem"]);
1663
- AuthorSchema = z.enum(["user", "claude", "import"]);
1664
- AnnotationActionSchema = z.enum(["accept", "dismiss"]);
1665
- ExportFormatSchema = z.enum(["markdown", "json"]);
1666
- DocumentFormatSchema = z.enum(["md", "txt", "html", "docx"]);
1667
- ToolErrorCodeSchema = z.enum([
1668
- "RANGE_GONE",
1669
- "RANGE_MOVED",
1670
- "FILE_LOCKED",
1671
- "FILE_NOT_FOUND",
1672
- "NO_DOCUMENT",
1673
- "INVALID_RANGE",
1674
- "FORMAT_ERROR",
1675
- "PERMISSION_DENIED"
1676
- ]);
1677
- }
1678
- });
1679
-
1680
1704
  // src/server/file-io/docx-walker.ts
1681
1705
  import { parseDocument as parseDocument2 } from "htmlparser2";
1682
1706
  function isElement2(node) {
@@ -1807,8 +1831,8 @@ var init_docx_walker = __esm({
1807
1831
  });
1808
1832
 
1809
1833
  // src/server/file-io/docx-comments.ts
1810
- import JSZip from "jszip";
1811
1834
  import { parseDocument as parseDocument3 } from "htmlparser2";
1835
+ import JSZip from "jszip";
1812
1836
  async function extractDocxComments(buffer3) {
1813
1837
  const zip = await JSZip.loadAsync(buffer3);
1814
1838
  const commentsXml = await zip.file("word/comments.xml")?.async("text");
@@ -1916,9 +1940,9 @@ var init_docx_comments = __esm({
1916
1940
  "src/server/file-io/docx-comments.ts"() {
1917
1941
  "use strict";
1918
1942
  init_constants();
1919
- init_positions2();
1920
1943
  init_types2();
1921
1944
  init_queue();
1945
+ init_positions2();
1922
1946
  init_docx_walker();
1923
1947
  }
1924
1948
  });
@@ -2241,9 +2265,9 @@ var init_dist2 = __esm({
2241
2265
  });
2242
2266
 
2243
2267
  // src/server/file-io/docx-apply.ts
2244
- import JSZip2 from "jszip";
2245
- import { parseDocument as parseDocument4 } from "htmlparser2";
2246
2268
  import render from "dom-serializer";
2269
+ import { parseDocument as parseDocument4 } from "htmlparser2";
2270
+ import JSZip2 from "jszip";
2247
2271
  function buildOffsetMap(xml, targetOffsets) {
2248
2272
  const entries = /* @__PURE__ */ new Map();
2249
2273
  const commentParagraphIds = /* @__PURE__ */ new Map();
@@ -2799,10 +2823,10 @@ var markdownAdapter, plaintextAdapter, docxAdapter, adapters;
2799
2823
  var init_file_io = __esm({
2800
2824
  "src/server/file-io/index.ts"() {
2801
2825
  "use strict";
2802
- init_markdown();
2826
+ init_document_model();
2803
2827
  init_docx();
2804
2828
  init_docx_comments();
2805
- init_document_model();
2829
+ init_markdown();
2806
2830
  init_docx_apply();
2807
2831
  markdownAdapter = {
2808
2832
  canSave: true,
@@ -2883,28 +2907,6 @@ var init_notifications = __esm({
2883
2907
  }
2884
2908
  });
2885
2909
 
2886
- // src/shared/utils.ts
2887
- function generateId(prefix) {
2888
- return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
2889
- }
2890
- function generateAnnotationId() {
2891
- return generateId("ann");
2892
- }
2893
- function generateMessageId() {
2894
- return generateId("msg");
2895
- }
2896
- function generateEventId() {
2897
- return generateId("evt");
2898
- }
2899
- function generateNotificationId() {
2900
- return generateId("ntf");
2901
- }
2902
- var init_utils = __esm({
2903
- "src/shared/utils.ts"() {
2904
- "use strict";
2905
- }
2906
- });
2907
-
2908
2910
  // src/server/mcp/file-opener.ts
2909
2911
  var file_opener_exports = {};
2910
2912
  __export(file_opener_exports, {
@@ -2912,10 +2914,10 @@ __export(file_opener_exports, {
2912
2914
  openFileByPath: () => openFileByPath,
2913
2915
  openFileFromContent: () => openFileFromContent
2914
2916
  });
2915
- import fs4 from "fs/promises";
2917
+ import { randomUUID } from "crypto";
2916
2918
  import fsSync from "fs";
2919
+ import fs4 from "fs/promises";
2917
2920
  import path5 from "path";
2918
- import { randomUUID } from "crypto";
2919
2921
  async function openFileByPath(filePath, options) {
2920
2922
  let resolved = path5.resolve(filePath);
2921
2923
  try {
@@ -3264,19 +3266,19 @@ var reloadInProgress;
3264
3266
  var init_file_opener = __esm({
3265
3267
  "src/server/mcp/file-opener.ts"() {
3266
3268
  "use strict";
3267
- init_provider();
3268
3269
  init_constants();
3270
+ init_utils();
3269
3271
  init_queue();
3272
+ init_docx();
3273
+ init_docx_comments();
3274
+ init_docx_html();
3270
3275
  init_file_io();
3276
+ init_markdown();
3271
3277
  init_file_watcher();
3272
- init_positions2();
3273
3278
  init_notifications();
3274
- init_utils();
3275
- init_markdown();
3276
- init_docx();
3277
- init_docx_html();
3278
- init_docx_comments();
3279
+ init_positions2();
3279
3280
  init_manager();
3281
+ init_provider();
3280
3282
  init_document_model();
3281
3283
  init_document_service();
3282
3284
  reloadInProgress = /* @__PURE__ */ new Set();
@@ -3284,8 +3286,8 @@ var init_file_opener = __esm({
3284
3286
  });
3285
3287
 
3286
3288
  // src/server/mcp/document-service.ts
3287
- import path6 from "path";
3288
3289
  import { randomUUID as randomUUID2 } from "crypto";
3290
+ import path6 from "path";
3289
3291
  function getOpenDocs() {
3290
3292
  return openDocs;
3291
3293
  }
@@ -3445,11 +3447,11 @@ var openDocs, activeDocId;
3445
3447
  var init_document_service = __esm({
3446
3448
  "src/server/mcp/document-service.ts"() {
3447
3449
  "use strict";
3448
- init_provider();
3449
- init_manager();
3450
3450
  init_constants();
3451
3451
  init_queue();
3452
3452
  init_file_watcher();
3453
+ init_manager();
3454
+ init_provider();
3453
3455
  openDocs = /* @__PURE__ */ new Map();
3454
3456
  setShouldKeepDocument((name) => openDocs.has(name) || name === CTRL_ROOM);
3455
3457
  activeDocId = null;
@@ -3465,6 +3467,19 @@ var init_types3 = __esm({
3465
3467
  });
3466
3468
 
3467
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
+ }
3468
3483
  function getTrackableId(event) {
3469
3484
  switch (event.type) {
3470
3485
  case "annotation:created":
@@ -3594,7 +3609,7 @@ function attachObservers(docName, doc) {
3594
3609
  selectedText: selection.selectedText ?? ""
3595
3610
  }
3596
3611
  });
3597
- }, SELECTION_DWELL_DEFAULT_MS);
3612
+ }, getDwellMs());
3598
3613
  }
3599
3614
  };
3600
3615
  userAwareness.observe(awarenessObs);
@@ -3791,125 +3806,72 @@ function killClaude() {
3791
3806
  console.error("[Launcher] Failed to kill Claude process:", err);
3792
3807
  }
3793
3808
  }
3794
- claudeProcess = null;
3795
- }
3796
- }
3797
- var claudeProcess, TANDEM_SYSTEM_PROMPT;
3798
- var init_launcher = __esm({
3799
- "src/server/mcp/launcher.ts"() {
3800
- "use strict";
3801
- init_constants();
3802
- claudeProcess = null;
3803
- TANDEM_SYSTEM_PROMPT = [
3804
- "You are Claude, connected to Tandem \u2014 a collaborative document editor.",
3805
- "You will receive real-time push notifications via the tandem-channel when users",
3806
- "create annotations, send chat messages, accept/dismiss your suggestions, or switch documents.",
3807
- "Use your tandem MCP tools (tandem_getTextContent, tandem_comment, tandem_highlight,",
3808
- "tandem_suggest, tandem_edit, etc.) to review and annotate documents.",
3809
- "Start by calling tandem_checkInbox to see what needs attention."
3810
- ].join(" ");
3811
- }
3812
- });
3813
-
3814
- // src/server/index.ts
3815
- import path10 from "path";
3816
- import { fileURLToPath as fileURLToPath2 } from "url";
3817
-
3818
- // src/server/mcp/server.ts
3819
- import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
3820
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3821
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3822
- import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
3823
- import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
3824
- import { randomUUID as randomUUID3 } from "crypto";
3825
- import { existsSync } from "fs";
3826
- import { dirname, join } from "path";
3827
- import { fileURLToPath } from "url";
3828
- import { createRequire } from "module";
3829
-
3830
- // src/server/open-browser.ts
3831
- import { execFile } from "child_process";
3832
- function openBrowser(url) {
3833
- let command;
3834
- let args;
3835
- if (process.platform === "win32") {
3836
- command = "cmd";
3837
- args = ["/c", "start", "", url];
3838
- } else if (process.platform === "darwin") {
3839
- command = "open";
3840
- args = [url];
3841
- } else {
3842
- command = "xdg-open";
3843
- args = [url];
3844
- }
3845
- execFile(command, args, (err) => {
3846
- if (err) {
3847
- console.error("[Tandem] Could not open browser automatically.");
3848
- console.error(`[Tandem] Open this URL manually: ${url}`);
3849
- }
3850
- });
3851
- }
3852
-
3853
- // src/server/mcp/annotations.ts
3854
- init_constants();
3855
- init_queue();
3856
- init_provider();
3857
- import { z as z3 } from "zod";
3858
-
3859
- // src/server/mcp/document.ts
3860
- init_provider();
3861
- import { z as z2 } from "zod";
3862
- import * as Y8 from "yjs";
3863
-
3864
- // src/server/mcp/response.ts
3865
- function mcpSuccess(data) {
3866
- return {
3867
- content: [{ type: "text", text: JSON.stringify({ error: false, data }) }]
3868
- };
3869
- }
3870
- function mcpError(code, message, details) {
3871
- return {
3872
- content: [
3873
- {
3874
- type: "text",
3875
- text: JSON.stringify({ error: true, code, message, ...details && { details } })
3876
- }
3877
- ]
3878
- };
3879
- }
3880
- function noDocumentError() {
3881
- return mcpError("NO_DOCUMENT", "No document is open. Call tandem_open first.");
3882
- }
3883
- function getErrorMessage(err) {
3884
- return err instanceof Error ? err.message : String(err);
3885
- }
3886
- function withErrorBoundary(toolName, handler) {
3887
- return async (args) => {
3888
- try {
3889
- return await handler(args);
3890
- } catch (err) {
3891
- console.error(`[Tandem] Tool ${toolName} threw:`, err);
3892
- return mcpError("INTERNAL_ERROR", `${toolName} failed: ${getErrorMessage(err)}`);
3893
- }
3894
- };
3809
+ claudeProcess = null;
3810
+ }
3895
3811
  }
3896
- function escapeRegex(str) {
3897
- return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3812
+ var claudeProcess, TANDEM_SYSTEM_PROMPT;
3813
+ var init_launcher = __esm({
3814
+ "src/server/mcp/launcher.ts"() {
3815
+ "use strict";
3816
+ init_constants();
3817
+ claudeProcess = null;
3818
+ TANDEM_SYSTEM_PROMPT = [
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;
3841
+ }
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;
3898
3848
  }
3899
3849
 
3850
+ // src/server/index.ts
3851
+ init_queue();
3852
+ init_file_watcher();
3853
+
3900
3854
  // src/server/mcp/document.ts
3901
- init_notifications();
3902
- init_utils();
3855
+ init_constants();
3903
3856
  init_offsets();
3857
+ init_types2();
3858
+ init_utils();
3859
+ init_queue();
3904
3860
  init_file_io();
3905
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";
3906
3868
 
3907
3869
  // src/server/mcp/convert.ts
3870
+ init_file_io();
3908
3871
  init_provider();
3909
3872
  init_document_model();
3910
- init_file_io();
3911
- init_file_opener();
3912
3873
  init_document_service();
3874
+ init_file_opener();
3913
3875
  import fs5 from "fs/promises";
3914
3876
  import path7 from "path";
3915
3877
  async function findAvailablePath(basePath) {
@@ -4003,16 +3965,49 @@ async function convertToMarkdown(documentId, outputPath) {
4003
3965
  }
4004
3966
 
4005
3967
  // src/server/mcp/document.ts
4006
- init_manager();
4007
- init_file_opener();
4008
- init_constants();
4009
- init_types2();
4010
- init_queue();
4011
3968
  init_document_model();
4012
- init_positions2();
4013
3969
  init_document_service();
4014
- init_document_model();
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
4015
4009
  init_positions2();
4010
+ init_document_model();
4016
4011
  init_document_service();
4017
4012
  init_file_opener();
4018
4013
  function getOutline(fragment) {
@@ -4408,12 +4403,56 @@ function registerDocumentTools(server) {
4408
4403
  );
4409
4404
  }
4410
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
+
4411
4446
  // src/server/mcp/annotations.ts
4412
- init_docx();
4447
+ init_constants();
4413
4448
  init_types2();
4414
- init_positions2();
4415
- init_notifications();
4416
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";
4417
4456
  init_positions2();
4418
4457
  function getDocAndAnnotations(documentId) {
4419
4458
  const doc = getCurrentDoc(documentId);
@@ -4510,16 +4549,13 @@ function registerAnnotationTools(server) {
4510
4549
  color: HighlightColorSchema.describe("Highlight color"),
4511
4550
  note: z3.string().optional().describe("Optional note for the highlight"),
4512
4551
  documentId: z3.string().optional().describe("Target document ID (defaults to active document)"),
4513
- priority: AnnotationPrioritySchema.optional().describe(
4514
- "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."
4515
- ),
4516
4552
  textSnapshot: z3.string().optional().describe(
4517
4553
  "Expected text at [from, to] \u2014 returns RANGE_MOVED with relocated range on mismatch, or RANGE_GONE if text was deleted"
4518
4554
  )
4519
4555
  },
4520
4556
  withErrorBoundary(
4521
4557
  "tandem_highlight",
4522
- async ({ from: rawFrom, to: rawTo, color, note, documentId, priority, textSnapshot }) => {
4558
+ async ({ from: rawFrom, to: rawTo, color, note, documentId, textSnapshot }) => {
4523
4559
  const da = getDocAndAnnotations(documentId);
4524
4560
  if (!da) return noDocumentError();
4525
4561
  const from = toFlatOffset(rawFrom);
@@ -4532,7 +4568,6 @@ function registerAnnotationTools(server) {
4532
4568
  const snap = captureSnapshot(da.ydoc, result.range.from, result.range.to);
4533
4569
  const id = createAnnotation(da.map, da.ydoc, "highlight", result, note || "", {
4534
4570
  color,
4535
- ...priority ? { priority } : {},
4536
4571
  textSnapshot: snap
4537
4572
  });
4538
4573
  return mcpSuccess({ annotationId: id });
@@ -4547,16 +4582,13 @@ function registerAnnotationTools(server) {
4547
4582
  to: z3.number().describe("End position"),
4548
4583
  text: z3.string().describe("Comment text"),
4549
4584
  documentId: z3.string().optional().describe("Target document ID (defaults to active document)"),
4550
- priority: AnnotationPrioritySchema.optional().describe(
4551
- "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."
4552
- ),
4553
4585
  textSnapshot: z3.string().optional().describe(
4554
4586
  "Expected text at [from, to] \u2014 returns RANGE_MOVED with relocated range on mismatch, or RANGE_GONE if text was deleted"
4555
4587
  )
4556
4588
  },
4557
4589
  withErrorBoundary(
4558
4590
  "tandem_comment",
4559
- async ({ from: rawFrom, to: rawTo, text, documentId, priority, textSnapshot }) => {
4591
+ async ({ from: rawFrom, to: rawTo, text, documentId, textSnapshot }) => {
4560
4592
  const da = getDocAndAnnotations(documentId);
4561
4593
  if (!da) return noDocumentError();
4562
4594
  const from = toFlatOffset(rawFrom);
@@ -4568,7 +4600,6 @@ function registerAnnotationTools(server) {
4568
4600
  }
4569
4601
  const snap = captureSnapshot(da.ydoc, result.range.from, result.range.to);
4570
4602
  const id = createAnnotation(da.map, da.ydoc, "comment", result, text, {
4571
- ...priority ? { priority } : {},
4572
4603
  textSnapshot: snap
4573
4604
  });
4574
4605
  return mcpSuccess({ annotationId: id });
@@ -4584,16 +4615,13 @@ function registerAnnotationTools(server) {
4584
4615
  newText: z3.string().describe("Suggested replacement text"),
4585
4616
  reason: z3.string().optional().describe("Reason for the suggestion"),
4586
4617
  documentId: z3.string().optional().describe("Target document ID (defaults to active document)"),
4587
- priority: AnnotationPrioritySchema.optional().describe(
4588
- "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."
4589
- ),
4590
4618
  textSnapshot: z3.string().optional().describe(
4591
4619
  "Expected text at [from, to] \u2014 returns RANGE_MOVED with relocated range on mismatch, or RANGE_GONE if text was deleted"
4592
4620
  )
4593
4621
  },
4594
4622
  withErrorBoundary(
4595
4623
  "tandem_suggest",
4596
- async ({ from: rawFrom, to: rawTo, newText, reason, documentId, priority, textSnapshot }) => {
4624
+ async ({ from: rawFrom, to: rawTo, newText, reason, documentId, textSnapshot }) => {
4597
4625
  const da = getDocAndAnnotations(documentId);
4598
4626
  if (!da) return noDocumentError();
4599
4627
  const from = toFlatOffset(rawFrom);
@@ -4610,7 +4638,7 @@ function registerAnnotationTools(server) {
4610
4638
  "suggestion",
4611
4639
  result,
4612
4640
  JSON.stringify({ newText, reason: reason || "" }),
4613
- { ...priority ? { priority } : {}, textSnapshot: snap }
4641
+ { textSnapshot: snap }
4614
4642
  );
4615
4643
  return mcpSuccess({ annotationId: id });
4616
4644
  }
@@ -4624,16 +4652,13 @@ function registerAnnotationTools(server) {
4624
4652
  to: z3.number().describe("End position"),
4625
4653
  note: z3.string().optional().describe("Reason for flagging"),
4626
4654
  documentId: z3.string().optional().describe("Target document ID (defaults to active document)"),
4627
- priority: AnnotationPrioritySchema.optional().describe(
4628
- "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."
4629
- ),
4630
4655
  textSnapshot: z3.string().optional().describe(
4631
4656
  "Expected text at [from, to] \u2014 returns RANGE_MOVED with relocated range on mismatch, or RANGE_GONE if text was deleted"
4632
4657
  )
4633
4658
  },
4634
4659
  withErrorBoundary(
4635
4660
  "tandem_flag",
4636
- async ({ from: rawFrom, to: rawTo, note, documentId, priority, textSnapshot }) => {
4661
+ async ({ from: rawFrom, to: rawTo, note, documentId, textSnapshot }) => {
4637
4662
  const da = getDocAndAnnotations(documentId);
4638
4663
  if (!da) return noDocumentError();
4639
4664
  const from = toFlatOffset(rawFrom);
@@ -4645,7 +4670,6 @@ function registerAnnotationTools(server) {
4645
4670
  }
4646
4671
  const snap = captureSnapshot(da.ydoc, result.range.from, result.range.to);
4647
4672
  const id = createAnnotation(da.map, da.ydoc, "flag", result, note || "", {
4648
- ...priority ? { priority } : {},
4649
4673
  textSnapshot: snap
4650
4674
  });
4651
4675
  return mcpSuccess({ annotationId: id });
@@ -4801,20 +4825,20 @@ function registerAnnotationTools(server) {
4801
4825
  // src/server/mcp/api-routes.ts
4802
4826
  init_constants();
4803
4827
  init_types2();
4828
+ init_notifications();
4829
+ init_provider();
4804
4830
  init_document_model();
4805
- init_file_opener();
4806
4831
  init_document_service();
4807
- init_notifications();
4808
4832
 
4809
4833
  // src/server/mcp/docx-apply.ts
4810
- init_document_service();
4811
4834
  init_constants();
4835
+ init_file_io();
4812
4836
  init_positions2();
4813
4837
  init_document_model();
4814
- init_file_io();
4815
- import { z as z4 } from "zod";
4838
+ init_document_service();
4816
4839
  import fs6 from "fs/promises";
4817
4840
  import path8 from "path";
4841
+ import { z as z4 } from "zod";
4818
4842
  async function applyChangesCore(documentId, author, backupPath) {
4819
4843
  const r = requireDocument(documentId);
4820
4844
  if (!r) throw Object.assign(new Error("No document is open."), { code: "NO_DOCUMENT" });
@@ -4988,7 +5012,7 @@ function registerApplyTools(server) {
4988
5012
  }
4989
5013
 
4990
5014
  // src/server/mcp/api-routes.ts
4991
- init_provider();
5015
+ init_file_opener();
4992
5016
  function isHostAllowed(host) {
4993
5017
  const reqHost = (host ?? "").split(":")[0];
4994
5018
  return reqHost === "localhost" || reqHost === "127.0.0.1";
@@ -5225,12 +5249,12 @@ function registerApiRoutes(app, largeBody) {
5225
5249
  }
5226
5250
 
5227
5251
  // src/server/mcp/awareness.ts
5228
- init_provider();
5229
- import { z as z5 } from "zod";
5252
+ init_constants();
5230
5253
  init_types2();
5231
5254
  init_utils();
5232
- init_constants();
5233
5255
  init_queue();
5256
+ init_provider();
5257
+ import { z as z5 } from "zod";
5234
5258
  var surfacedIds = /* @__PURE__ */ new Set();
5235
5259
  function registerAwarenessTools(server) {
5236
5260
  server.tool(
@@ -5904,60 +5928,11 @@ async function startMcpServerHttp(port, host = "127.0.0.1") {
5904
5928
  });
5905
5929
  }
5906
5930
 
5907
- // src/server/index.ts
5908
- init_provider();
5909
- init_constants();
5910
- init_manager();
5911
- init_platform();
5912
-
5913
- // src/server/version-check.ts
5914
- import fs7 from "fs/promises";
5915
- import path9 from "path";
5916
- async function checkVersionChange(currentVersion, versionFilePath) {
5917
- let storedVersion = null;
5918
- try {
5919
- storedVersion = (await fs7.readFile(versionFilePath, "utf-8")).trim();
5920
- } catch (err) {
5921
- if (err.code !== "ENOENT") {
5922
- console.error("[Tandem] Failed to read last-seen-version:", err);
5923
- }
5924
- }
5925
- const result = storedVersion === null ? "first-install" : storedVersion === currentVersion ? "current" : "upgraded";
5926
- if (result !== "current") {
5927
- await fs7.mkdir(path9.dirname(versionFilePath), { recursive: true });
5928
- await fs7.writeFile(versionFilePath, currentVersion, "utf-8");
5929
- }
5930
- return result;
5931
- }
5932
-
5933
- // src/server/error-filter.ts
5934
- function isKnownHocuspocusError(err) {
5935
- if (!(err instanceof Error)) return false;
5936
- if ("code" in err) {
5937
- const code = err.code;
5938
- if (typeof code === "string" && code.startsWith("WS_ERR_")) {
5939
- return true;
5940
- }
5941
- }
5942
- const msg = err.message;
5943
- if (msg.startsWith("WebSocket is not open")) return true;
5944
- if (msg.includes("Unexpected end of array") || msg.includes("Integer out of Range")) return true;
5945
- if (msg.startsWith("Received a message with an unknown type:")) return true;
5946
- return false;
5947
- }
5948
-
5949
- // src/server/index.ts
5950
- init_queue();
5951
- init_document_service();
5952
- init_file_watcher();
5953
- init_file_opener();
5954
- init_document_model();
5955
-
5956
5931
  // src/server/mcp/tutorial-annotations.ts
5957
5932
  init_constants();
5933
+ init_types2();
5958
5934
  init_queue();
5959
5935
  init_positions2();
5960
- init_types2();
5961
5936
  init_document_model();
5962
5937
  var TUTORIAL_ANNOTATIONS = [
5963
5938
  {
@@ -6033,6 +6008,31 @@ function injectTutorialAnnotations(doc) {
6033
6008
  }
6034
6009
 
6035
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();
6036
6036
  var isProduction = process.env.TANDEM_OPEN_BROWSER === "1";
6037
6037
  var SUPPRESSED_PATTERNS = [/^\[mammoth\]/, /Invalid access/i, /^\s*add yjs type/i];
6038
6038
  var originalStderrWrite = process.stderr.write.bind(process.stderr);