tandem-editor 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -40,7 +40,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
40
40
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
41
41
 
42
42
  // src/shared/constants.ts
43
- 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, TAURI_HOSTNAME;
43
+ 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_ANNOTATION_REPLIES, Y_MAP_SAVED_AT_VERSION, Y_MAP_AUTHORSHIP, NOTIFICATION_BUFFER_SIZE, TUTORIAL_ANNOTATION_PREFIX, CHANNEL_EVENT_BUFFER_SIZE, CHANNEL_EVENT_BUFFER_AGE_MS, CHANNEL_SSE_KEEPALIVE_MS, TAURI_HOSTNAME;
44
44
  var init_constants = __esm({
45
45
  "src/shared/constants.ts"() {
46
46
  "use strict";
@@ -66,7 +66,9 @@ var init_constants = __esm({
66
66
  Y_MAP_DWELL_MS = "selectionDwellMs";
67
67
  Y_MAP_CHAT = "chat";
68
68
  Y_MAP_DOCUMENT_META = "documentMeta";
69
+ Y_MAP_ANNOTATION_REPLIES = "annotationReplies";
69
70
  Y_MAP_SAVED_AT_VERSION = "savedAtVersion";
71
+ Y_MAP_AUTHORSHIP = "authorship";
70
72
  NOTIFICATION_BUFFER_SIZE = 50;
71
73
  TUTORIAL_ANNOTATION_PREFIX = "tutorial-";
72
74
  CHANNEL_EVENT_BUFFER_SIZE = 200;
@@ -4305,9 +4307,15 @@ function generateMessageId() {
4305
4307
  function generateEventId() {
4306
4308
  return generateId("evt");
4307
4309
  }
4310
+ function generateReplyId() {
4311
+ return generateId("rpl");
4312
+ }
4308
4313
  function generateNotificationId() {
4309
4314
  return generateId("ntf");
4310
4315
  }
4316
+ function generateAuthorshipId(author) {
4317
+ return generateId(author);
4318
+ }
4311
4319
  var init_utils = __esm({
4312
4320
  "src/shared/utils.ts"() {
4313
4321
  "use strict";
@@ -8420,13 +8428,13 @@ var require_thenables = __commonJS({
8420
8428
  promise._captureStackTrace();
8421
8429
  if (context) context._popContext();
8422
8430
  var synchronous = true;
8423
- var result2 = util2.tryCatch(then).call(x, resolve2, reject2);
8431
+ var result2 = util2.tryCatch(then).call(x, resolve3, reject2);
8424
8432
  synchronous = false;
8425
8433
  if (promise && result2 === errorObj2) {
8426
8434
  promise._rejectCallback(result2.e, true, true);
8427
8435
  promise = null;
8428
8436
  }
8429
- function resolve2(value) {
8437
+ function resolve3(value) {
8430
8438
  if (!promise) return;
8431
8439
  promise._resolveCallback(value);
8432
8440
  promise = null;
@@ -8961,9 +8969,9 @@ var require_debuggability = __commonJS({
8961
8969
  return false;
8962
8970
  }
8963
8971
  Promise2.prototype._fireEvent = defaultFireEvent;
8964
- Promise2.prototype._execute = function(executor, resolve2, reject2) {
8972
+ Promise2.prototype._execute = function(executor, resolve3, reject2) {
8965
8973
  try {
8966
- executor(resolve2, reject2);
8974
+ executor(resolve3, reject2);
8967
8975
  } catch (e) {
8968
8976
  return e;
8969
8977
  }
@@ -8986,10 +8994,10 @@ var require_debuggability = __commonJS({
8986
8994
  ;
8987
8995
  ;
8988
8996
  };
8989
- function cancellationExecute(executor, resolve2, reject2) {
8997
+ function cancellationExecute(executor, resolve3, reject2) {
8990
8998
  var promise = this;
8991
8999
  try {
8992
- executor(resolve2, reject2, function(onCancel) {
9000
+ executor(resolve3, reject2, function(onCancel) {
8993
9001
  if (typeof onCancel !== "function") {
8994
9002
  throw new TypeError("onCancel must be a function, got: " + util2.toString(onCancel));
8995
9003
  }
@@ -12636,14 +12644,14 @@ var require_promises = __commonJS({
12636
12644
  });
12637
12645
  };
12638
12646
  function defer() {
12639
- var resolve2;
12647
+ var resolve3;
12640
12648
  var reject2;
12641
12649
  var promise = new bluebird.Promise(function(resolveArg, rejectArg) {
12642
- resolve2 = resolveArg;
12650
+ resolve3 = resolveArg;
12643
12651
  reject2 = rejectArg;
12644
12652
  });
12645
12653
  return {
12646
- resolve: resolve2,
12654
+ resolve: resolve3,
12647
12655
  reject: reject2,
12648
12656
  promise
12649
12657
  };
@@ -15317,8 +15325,8 @@ var require_lib2 = __commonJS({
15317
15325
  return this;
15318
15326
  }
15319
15327
  var p = this.constructor;
15320
- return this.then(resolve3, reject3);
15321
- function resolve3(value) {
15328
+ return this.then(resolve4, reject3);
15329
+ function resolve4(value) {
15322
15330
  function yes() {
15323
15331
  return value;
15324
15332
  }
@@ -15471,8 +15479,8 @@ var require_lib2 = __commonJS({
15471
15479
  }
15472
15480
  return out;
15473
15481
  }
15474
- Promise2.resolve = resolve2;
15475
- function resolve2(value) {
15482
+ Promise2.resolve = resolve3;
15483
+ function resolve3(value) {
15476
15484
  if (value instanceof this) {
15477
15485
  return value;
15478
15486
  }
@@ -16004,10 +16012,10 @@ var require_utils = __commonJS({
16004
16012
  var promise = external.Promise.resolve(inputData).then(function(data) {
16005
16013
  var isBlob = support.blob && (data instanceof Blob || ["[object File]", "[object Blob]"].indexOf(Object.prototype.toString.call(data)) !== -1);
16006
16014
  if (isBlob && typeof FileReader !== "undefined") {
16007
- return new external.Promise(function(resolve2, reject2) {
16015
+ return new external.Promise(function(resolve3, reject2) {
16008
16016
  var reader = new FileReader();
16009
16017
  reader.onload = function(e) {
16010
- resolve2(e.target.result);
16018
+ resolve3(e.target.result);
16011
16019
  };
16012
16020
  reader.onerror = function(e) {
16013
16021
  reject2(e.target.error);
@@ -16562,7 +16570,7 @@ var require_StreamHelper = __commonJS({
16562
16570
  }
16563
16571
  }
16564
16572
  function accumulate(helper, updateCallback) {
16565
- return new external.Promise(function(resolve2, reject2) {
16573
+ return new external.Promise(function(resolve3, reject2) {
16566
16574
  var dataArray = [];
16567
16575
  var chunkType = helper._internalType, resultType = helper._outputType, mimeType = helper._mimeType;
16568
16576
  helper.on("data", function(data, meta2) {
@@ -16576,7 +16584,7 @@ var require_StreamHelper = __commonJS({
16576
16584
  }).on("end", function() {
16577
16585
  try {
16578
16586
  var result2 = transformZipOutput(resultType, concat(chunkType, dataArray), mimeType);
16579
- resolve2(result2);
16587
+ resolve3(result2);
16580
16588
  } catch (e) {
16581
16589
  reject2(e);
16582
16590
  }
@@ -22689,7 +22697,7 @@ var require_load = __commonJS({
22689
22697
  var Crc32Probe = require_Crc32Probe();
22690
22698
  var nodejsUtils = require_nodejsUtils();
22691
22699
  function checkEntryCRC32(zipEntry) {
22692
- return new external.Promise(function(resolve2, reject2) {
22700
+ return new external.Promise(function(resolve3, reject2) {
22693
22701
  var worker = zipEntry.decompressed.getContentWorker().pipe(new Crc32Probe());
22694
22702
  worker.on("error", function(e) {
22695
22703
  reject2(e);
@@ -22697,7 +22705,7 @@ var require_load = __commonJS({
22697
22705
  if (worker.streamInfo.crc32 !== zipEntry.decompressed.crc32) {
22698
22706
  reject2(new Error("Corrupted zip : CRC32 mismatch"));
22699
22707
  } else {
22700
- resolve2();
22708
+ resolve3();
22701
22709
  }
22702
22710
  }).resume();
22703
22711
  });
@@ -32462,10 +32470,10 @@ var require_path_is_absolute = __commonJS({
32462
32470
  var require_files = __commonJS({
32463
32471
  "node_modules/mammoth/lib/docx/files.js"(exports3) {
32464
32472
  "use strict";
32465
- var fs8 = __require("fs");
32473
+ var fs9 = __require("fs");
32466
32474
  var url = __require("url");
32467
32475
  var os2 = __require("os");
32468
- var dirname3 = __require("path").dirname;
32476
+ var dirname4 = __require("path").dirname;
32469
32477
  var resolvePath = __require("path").resolve;
32470
32478
  var isAbsolutePath = require_path_is_absolute();
32471
32479
  var promises = require_promises();
@@ -32480,7 +32488,7 @@ var require_files = __commonJS({
32480
32488
  }
32481
32489
  };
32482
32490
  }
32483
- var base = options.relativeToFile ? dirname3(options.relativeToFile) : null;
32491
+ var base = options.relativeToFile ? dirname4(options.relativeToFile) : null;
32484
32492
  function read(uri, encoding) {
32485
32493
  return resolveUri(uri).then(function(path13) {
32486
32494
  return readFile(path13, encoding).caught(function(error2) {
@@ -32503,7 +32511,7 @@ var require_files = __commonJS({
32503
32511
  read
32504
32512
  };
32505
32513
  }
32506
- var readFile = promises.promisify(fs8.readFile.bind(fs8));
32514
+ var readFile = promises.promisify(fs9.readFile.bind(fs9));
32507
32515
  function uriToPath(uriString, platform) {
32508
32516
  if (!platform) {
32509
32517
  platform = os2.platform();
@@ -35245,11 +35253,11 @@ var require_options_reader = __commonJS({
35245
35253
  var require_unzip = __commonJS({
35246
35254
  "node_modules/mammoth/lib/unzip.js"(exports3) {
35247
35255
  "use strict";
35248
- var fs8 = __require("fs");
35256
+ var fs9 = __require("fs");
35249
35257
  var promises = require_promises();
35250
35258
  var zipfile = require_zipfile();
35251
35259
  exports3.openZip = openZip;
35252
- var readFile = promises.promisify(fs8.readFile);
35260
+ var readFile = promises.promisify(fs9.readFile);
35253
35261
  function openZip(options) {
35254
35262
  if (options.path) {
35255
35263
  return readFile(options.path).then(zipfile.openArrayBuffer);
@@ -36360,14 +36368,14 @@ var callAll, id, isOneOf;
36360
36368
  var init_function = __esm({
36361
36369
  "node_modules/lib0/function.js"() {
36362
36370
  "use strict";
36363
- callAll = (fs8, args2, i = 0) => {
36371
+ callAll = (fs9, args2, i = 0) => {
36364
36372
  try {
36365
- for (; i < fs8.length; i++) {
36366
- fs8[i](...args2);
36373
+ for (; i < fs9.length; i++) {
36374
+ fs9[i](...args2);
36367
36375
  }
36368
36376
  } finally {
36369
- if (i < fs8.length) {
36370
- callAll(fs8, args2, i + 1);
36377
+ if (i < fs9.length) {
36378
+ callAll(fs9, args2, i + 1);
36371
36379
  }
36372
36380
  }
36373
36381
  };
@@ -36909,17 +36917,17 @@ var init_yjs = __esm({
36909
36917
  this.isLoaded = false;
36910
36918
  this.isSynced = false;
36911
36919
  this.isDestroyed = false;
36912
- this.whenLoaded = create5((resolve2) => {
36920
+ this.whenLoaded = create5((resolve3) => {
36913
36921
  this.on("load", () => {
36914
36922
  this.isLoaded = true;
36915
- resolve2(this);
36923
+ resolve3(this);
36916
36924
  });
36917
36925
  });
36918
- const provideSyncedPromise = () => create5((resolve2) => {
36926
+ const provideSyncedPromise = () => create5((resolve3) => {
36919
36927
  const eventHandler = (isSynced) => {
36920
36928
  if (isSynced === void 0 || isSynced === true) {
36921
36929
  this.off("sync", eventHandler);
36922
- resolve2();
36930
+ resolve3();
36923
36931
  }
36924
36932
  };
36925
36933
  this.on("sync", eventHandler);
@@ -38392,15 +38400,15 @@ var init_yjs = __esm({
38392
38400
  sortAndMergeDeleteSet(ds);
38393
38401
  transaction.afterState = getStateVector(transaction.doc.store);
38394
38402
  doc.emit("beforeObserverCalls", [transaction, doc]);
38395
- const fs8 = [];
38403
+ const fs9 = [];
38396
38404
  transaction.changed.forEach(
38397
- (subs, itemtype) => fs8.push(() => {
38405
+ (subs, itemtype) => fs9.push(() => {
38398
38406
  if (itemtype._item === null || !itemtype._item.deleted) {
38399
38407
  itemtype._callObserver(transaction, subs);
38400
38408
  }
38401
38409
  })
38402
38410
  );
38403
- fs8.push(() => {
38411
+ fs9.push(() => {
38404
38412
  transaction.changedParentTypes.forEach((events, type2) => {
38405
38413
  if (type2._dEH.l.length > 0 && (type2._item === null || !type2._item.deleted)) {
38406
38414
  events = events.filter(
@@ -38411,19 +38419,19 @@ var init_yjs = __esm({
38411
38419
  event._path = null;
38412
38420
  });
38413
38421
  events.sort((event1, event2) => event1.path.length - event2.path.length);
38414
- fs8.push(() => {
38422
+ fs9.push(() => {
38415
38423
  callEventHandlerListeners(type2._dEH, events, transaction);
38416
38424
  });
38417
38425
  }
38418
38426
  });
38419
- fs8.push(() => doc.emit("afterTransaction", [transaction, doc]));
38420
- fs8.push(() => {
38427
+ fs9.push(() => doc.emit("afterTransaction", [transaction, doc]));
38428
+ fs9.push(() => {
38421
38429
  if (transaction._needFormattingCleanup) {
38422
38430
  cleanupYTextAfterTransaction(transaction);
38423
38431
  }
38424
38432
  });
38425
38433
  });
38426
- callAll(fs8, []);
38434
+ callAll(fs9, []);
38427
38435
  } finally {
38428
38436
  if (doc.gc) {
38429
38437
  tryGcDeleteSet(ds, store, doc.gcFilter);
@@ -49042,10 +49050,10 @@ function resolveAll(constructs2, events, context) {
49042
49050
  const called = [];
49043
49051
  let index2 = -1;
49044
49052
  while (++index2 < constructs2.length) {
49045
- const resolve2 = constructs2[index2].resolveAll;
49046
- if (resolve2 && !called.includes(resolve2)) {
49047
- events = resolve2(events, context);
49048
- called.push(resolve2);
49053
+ const resolve3 = constructs2[index2].resolveAll;
49054
+ if (resolve3 && !called.includes(resolve3)) {
49055
+ events = resolve3(events, context);
49056
+ called.push(resolve3);
49049
49057
  }
49050
49058
  }
49051
49059
  return events;
@@ -55676,10 +55684,10 @@ var init_lib21 = __esm({
55676
55684
  * @returns {undefined}
55677
55685
  * Nothing.
55678
55686
  */
55679
- set basename(basename) {
55680
- assertNonEmpty(basename, "basename");
55681
- assertPart(basename, "basename");
55682
- this.path = default2.join(this.dirname || "", basename);
55687
+ set basename(basename2) {
55688
+ assertNonEmpty(basename2, "basename");
55689
+ assertPart(basename2, "basename");
55690
+ this.path = default2.join(this.dirname || "", basename2);
55683
55691
  }
55684
55692
  /**
55685
55693
  * Get the parent path (example: `'~'`).
@@ -55700,9 +55708,9 @@ var init_lib21 = __esm({
55700
55708
  * @returns {undefined}
55701
55709
  * Nothing.
55702
55710
  */
55703
- set dirname(dirname3) {
55711
+ set dirname(dirname4) {
55704
55712
  assertPath(this.basename, "dirname");
55705
- this.path = default2.join(dirname3 || "", this.basename);
55713
+ this.path = default2.join(dirname4 || "", this.basename);
55706
55714
  }
55707
55715
  /**
55708
55716
  * Get the extname (including dot) (example: `'.js'`).
@@ -56349,7 +56357,7 @@ var init_lib22 = __esm({
56349
56357
  assertParser("process", this.parser || this.Parser);
56350
56358
  assertCompiler("process", this.compiler || this.Compiler);
56351
56359
  return done ? executor(void 0, done) : new Promise(executor);
56352
- function executor(resolve2, reject2) {
56360
+ function executor(resolve3, reject2) {
56353
56361
  const realFile = vfile(file);
56354
56362
  const parseTree = (
56355
56363
  /** @type {HeadTree extends undefined ? Node : HeadTree} */
@@ -56380,8 +56388,8 @@ var init_lib22 = __esm({
56380
56388
  function realDone(error2, file2) {
56381
56389
  if (error2 || !file2) {
56382
56390
  reject2(error2);
56383
- } else if (resolve2) {
56384
- resolve2(file2);
56391
+ } else if (resolve3) {
56392
+ resolve3(file2);
56385
56393
  } else {
56386
56394
  ok(done, "`done` is defined if `resolve` is not");
56387
56395
  done(void 0, file2);
@@ -56483,7 +56491,7 @@ var init_lib22 = __esm({
56483
56491
  file = void 0;
56484
56492
  }
56485
56493
  return done ? executor(void 0, done) : new Promise(executor);
56486
- function executor(resolve2, reject2) {
56494
+ function executor(resolve3, reject2) {
56487
56495
  ok(
56488
56496
  typeof file !== "function",
56489
56497
  "`file` can\u2019t be a `done` anymore, we checked"
@@ -56497,8 +56505,8 @@ var init_lib22 = __esm({
56497
56505
  );
56498
56506
  if (error2) {
56499
56507
  reject2(error2);
56500
- } else if (resolve2) {
56501
- resolve2(resultingTree);
56508
+ } else if (resolve3) {
56509
+ resolve3(resultingTree);
56502
56510
  } else {
56503
56511
  ok(done, "`done` is defined if `resolve` is not");
56504
56512
  done(void 0, resultingTree, file2);
@@ -60809,8 +60817,8 @@ var require_lib7 = __commonJS({
60809
60817
  if (typeof cb2 !== "function") {
60810
60818
  opts = cb2;
60811
60819
  cb2 = null;
60812
- deferred = new this.Promise(function(resolve2, reject2) {
60813
- deferredResolve = resolve2;
60820
+ deferred = new this.Promise(function(resolve3, reject2) {
60821
+ deferredResolve = resolve3;
60814
60822
  deferredReject = reject2;
60815
60823
  });
60816
60824
  }
@@ -60958,17 +60966,17 @@ var require_lib7 = __commonJS({
60958
60966
  if (typeof cb2 === "function") {
60959
60967
  fnx(cb2);
60960
60968
  } else {
60961
- return new this.Promise(function(resolve2, reject2) {
60969
+ return new this.Promise(function(resolve3, reject2) {
60962
60970
  if (fnx.length === 1) {
60963
60971
  fnx(function(err, ret2) {
60964
60972
  if (err) {
60965
60973
  reject2(err);
60966
60974
  } else {
60967
- resolve2(ret2);
60975
+ resolve3(ret2);
60968
60976
  }
60969
60977
  });
60970
60978
  } else {
60971
- resolve2(fnx());
60979
+ resolve3(fnx());
60972
60980
  }
60973
60981
  });
60974
60982
  }
@@ -66729,7 +66737,7 @@ var init_hocuspocus_server_esm = __esm({
66729
66737
  process.on("SIGQUIT", signalHandler);
66730
66738
  process.on("SIGTERM", signalHandler);
66731
66739
  }
66732
- return new Promise((resolve2, reject2) => {
66740
+ return new Promise((resolve3, reject2) => {
66733
66741
  var _a;
66734
66742
  (_a = this.server) === null || _a === void 0 ? void 0 : _a.httpServer.listen({
66735
66743
  port: this.configuration.port,
@@ -66745,7 +66753,7 @@ var init_hocuspocus_server_esm = __esm({
66745
66753
  };
66746
66754
  try {
66747
66755
  await this.hooks("onListen", onListenPayload);
66748
- resolve2(this);
66756
+ resolve3(this);
66749
66757
  } catch (e) {
66750
66758
  reject2(e);
66751
66759
  }
@@ -66829,19 +66837,19 @@ var init_hocuspocus_server_esm = __esm({
66829
66837
  * Destroy the server
66830
66838
  */
66831
66839
  async destroy() {
66832
- await new Promise(async (resolve2) => {
66840
+ await new Promise(async (resolve3) => {
66833
66841
  var _a, _b, _c, _d;
66834
66842
  (_b = (_a = this.server) === null || _a === void 0 ? void 0 : _a.httpServer) === null || _b === void 0 ? void 0 : _b.close();
66835
66843
  try {
66836
66844
  this.configuration.extensions.push({
66837
66845
  async afterUnloadDocument({ instance }) {
66838
66846
  if (instance.getDocumentsCount() === 0)
66839
- resolve2("");
66847
+ resolve3("");
66840
66848
  }
66841
66849
  });
66842
66850
  (_d = (_c = this.server) === null || _c === void 0 ? void 0 : _c.webSocketServer) === null || _d === void 0 ? void 0 : _d.close();
66843
66851
  if (this.getDocumentsCount() === 0)
66844
- resolve2("");
66852
+ resolve3("");
66845
66853
  this.closeConnections();
66846
66854
  } catch (error2) {
66847
66855
  console.error(error2);
@@ -67181,6 +67189,440 @@ var init_provider = __esm({
67181
67189
  }
67182
67190
  });
67183
67191
 
67192
+ // node_modules/is-safe-filename/index.js
67193
+ function isSafeFilename(filename) {
67194
+ if (typeof filename !== "string") {
67195
+ return false;
67196
+ }
67197
+ const trimmed = filename.trim();
67198
+ return trimmed !== "" && trimmed !== "." && trimmed !== ".." && !filename.includes("/") && !filename.includes("\\") && !filename.includes("\0");
67199
+ }
67200
+ function assertSafeFilename(filename) {
67201
+ if (typeof filename !== "string") {
67202
+ throw new TypeError("Expected a string");
67203
+ }
67204
+ if (!isSafeFilename(filename)) {
67205
+ throw new Error(`Unsafe filename: ${JSON.stringify(filename)}`);
67206
+ }
67207
+ }
67208
+ var unsafeFilenameFixtures;
67209
+ var init_is_safe_filename = __esm({
67210
+ "node_modules/is-safe-filename/index.js"() {
67211
+ "use strict";
67212
+ unsafeFilenameFixtures = Object.freeze([
67213
+ "",
67214
+ " ",
67215
+ ".",
67216
+ "..",
67217
+ " .",
67218
+ ". ",
67219
+ " ..",
67220
+ ".. ",
67221
+ "../",
67222
+ "../foo",
67223
+ "foo/../bar",
67224
+ "foo/bar",
67225
+ "foo\\bar",
67226
+ "foo\0bar"
67227
+ ]);
67228
+ }
67229
+ });
67230
+
67231
+ // node_modules/env-paths/index.js
67232
+ import path3 from "path";
67233
+ import os from "os";
67234
+ import process2 from "process";
67235
+ function envPaths(name2, { suffix = "nodejs" } = {}) {
67236
+ assertSafeFilename(name2);
67237
+ if (suffix) {
67238
+ name2 += `-${suffix}`;
67239
+ }
67240
+ assertSafeFilename(name2);
67241
+ if (process2.platform === "darwin") {
67242
+ return macos(name2);
67243
+ }
67244
+ if (process2.platform === "win32") {
67245
+ return windows(name2);
67246
+ }
67247
+ return linux(name2);
67248
+ }
67249
+ var homedir, tmpdir, env2, macos, windows, linux;
67250
+ var init_env_paths = __esm({
67251
+ "node_modules/env-paths/index.js"() {
67252
+ "use strict";
67253
+ init_is_safe_filename();
67254
+ homedir = os.homedir();
67255
+ tmpdir = os.tmpdir();
67256
+ ({ env: env2 } = process2);
67257
+ macos = (name2) => {
67258
+ const library = path3.join(homedir, "Library");
67259
+ return {
67260
+ data: path3.join(library, "Application Support", name2),
67261
+ config: path3.join(library, "Preferences", name2),
67262
+ cache: path3.join(library, "Caches", name2),
67263
+ log: path3.join(library, "Logs", name2),
67264
+ temp: path3.join(tmpdir, name2)
67265
+ };
67266
+ };
67267
+ windows = (name2) => {
67268
+ const appData = env2.APPDATA || path3.join(homedir, "AppData", "Roaming");
67269
+ const localAppData = env2.LOCALAPPDATA || path3.join(homedir, "AppData", "Local");
67270
+ return {
67271
+ // Data/config/cache/log are invented by me as Windows isn't opinionated about this
67272
+ data: path3.join(localAppData, name2, "Data"),
67273
+ config: path3.join(appData, name2, "Config"),
67274
+ cache: path3.join(localAppData, name2, "Cache"),
67275
+ log: path3.join(localAppData, name2, "Log"),
67276
+ temp: path3.join(tmpdir, name2)
67277
+ };
67278
+ };
67279
+ linux = (name2) => {
67280
+ const username = path3.basename(homedir);
67281
+ return {
67282
+ data: path3.join(env2.XDG_DATA_HOME || path3.join(homedir, ".local", "share"), name2),
67283
+ config: path3.join(env2.XDG_CONFIG_HOME || path3.join(homedir, ".config"), name2),
67284
+ cache: path3.join(env2.XDG_CACHE_HOME || path3.join(homedir, ".cache"), name2),
67285
+ // https://wiki.debian.org/XDGBaseDirectorySpecification#state
67286
+ log: path3.join(env2.XDG_STATE_HOME || path3.join(homedir, ".local", "state"), name2),
67287
+ temp: path3.join(tmpdir, username, name2)
67288
+ };
67289
+ };
67290
+ }
67291
+ });
67292
+
67293
+ // src/server/platform.ts
67294
+ import { execSync } from "child_process";
67295
+ import net from "net";
67296
+ import path4 from "path";
67297
+ function freePort(port) {
67298
+ try {
67299
+ if (process.platform === "win32") {
67300
+ freePortWindows(port);
67301
+ } else {
67302
+ freePortUnix(port);
67303
+ }
67304
+ } catch (err) {
67305
+ console.error(`[Tandem] freePort(${port}): ${err instanceof Error ? err.message : err}`);
67306
+ }
67307
+ }
67308
+ async function waitForPort(port, timeoutMs = 5e3) {
67309
+ const start = Date.now();
67310
+ while (Date.now() - start < timeoutMs) {
67311
+ if (await tryBind(port)) return;
67312
+ await new Promise((r) => setTimeout(r, 200));
67313
+ }
67314
+ throw new Error(`Port ${port} still not available after ${timeoutMs}ms`);
67315
+ }
67316
+ function tryBind(port) {
67317
+ return new Promise((resolve3, reject2) => {
67318
+ const srv = net.createServer();
67319
+ srv.once("error", (err) => {
67320
+ srv.close(() => {
67321
+ if (err.code === "EADDRINUSE") {
67322
+ resolve3(false);
67323
+ } else {
67324
+ reject2(err);
67325
+ }
67326
+ });
67327
+ });
67328
+ srv.listen(port, "127.0.0.1", () => {
67329
+ srv.close(() => resolve3(true));
67330
+ });
67331
+ });
67332
+ }
67333
+ function parseLsofPids(output) {
67334
+ return output.trim().split("\n").map((line) => parseInt(line.trim(), 10)).filter((pid) => Number.isFinite(pid) && pid > 0);
67335
+ }
67336
+ function parseSsPid(output) {
67337
+ const match = output.match(/pid=(\d+)/);
67338
+ return match ? parseInt(match[1], 10) : null;
67339
+ }
67340
+ function freePortWindows(port) {
67341
+ const out = execSync(`netstat -ano | findstr ":${port}.*LISTENING"`, {
67342
+ encoding: "utf-8",
67343
+ stdio: ["pipe", "pipe", "ignore"]
67344
+ });
67345
+ const pid = out.trim().split(/\s+/).at(-1);
67346
+ if (pid && /^\d+$/.test(pid)) {
67347
+ execSync(`taskkill /PID ${pid} /F`, { stdio: "ignore" });
67348
+ console.error(`[Tandem] Killed stale PID ${pid} holding port ${port}`);
67349
+ }
67350
+ }
67351
+ function freePortUnix(port) {
67352
+ let pids = [];
67353
+ try {
67354
+ const out = execSync(`lsof -ti TCP:${port} -sTCP:LISTEN`, {
67355
+ encoding: "utf-8",
67356
+ stdio: ["pipe", "pipe", "ignore"]
67357
+ });
67358
+ pids = parseLsofPids(out);
67359
+ } catch {
67360
+ try {
67361
+ const out = execSync(`ss -tlnp sport = :${port}`, {
67362
+ encoding: "utf-8",
67363
+ stdio: ["pipe", "pipe", "ignore"]
67364
+ });
67365
+ const pid = parseSsPid(out);
67366
+ if (pid) pids = [pid];
67367
+ } catch {
67368
+ }
67369
+ }
67370
+ for (const pid of pids) {
67371
+ try {
67372
+ process.kill(pid, "SIGKILL");
67373
+ console.error(`[Tandem] Killed stale PID ${pid} holding port ${port}`);
67374
+ } catch {
67375
+ }
67376
+ }
67377
+ }
67378
+ var paths, SESSION_DIR, LAST_SEEN_VERSION_FILE;
67379
+ var init_platform = __esm({
67380
+ "src/server/platform.ts"() {
67381
+ "use strict";
67382
+ init_env_paths();
67383
+ paths = envPaths("tandem", { suffix: "" });
67384
+ SESSION_DIR = path4.join(paths.data, "sessions");
67385
+ LAST_SEEN_VERSION_FILE = path4.join(paths.data, "last-seen-version");
67386
+ }
67387
+ });
67388
+
67389
+ // src/server/session/manager.ts
67390
+ import fs from "fs/promises";
67391
+ import path5 from "path";
67392
+ async function atomicWrite(sessionPath, content3) {
67393
+ const tmpPath = `${sessionPath}.tmp`;
67394
+ await fs.writeFile(tmpPath, content3, "utf-8");
67395
+ for (let attempt = 0; attempt < RENAME_MAX_RETRIES; attempt++) {
67396
+ try {
67397
+ await fs.rename(tmpPath, sessionPath);
67398
+ return;
67399
+ } catch (err) {
67400
+ const code3 = err.code;
67401
+ if ((code3 === "EPERM" || code3 === "EACCES") && attempt < RENAME_MAX_RETRIES - 1) {
67402
+ await new Promise((r) => setTimeout(r, RENAME_RETRY_BASE_MS * 2 ** attempt));
67403
+ continue;
67404
+ }
67405
+ await fs.unlink(tmpPath).catch(() => {
67406
+ });
67407
+ throw err;
67408
+ }
67409
+ }
67410
+ }
67411
+ function sessionKey(filePath) {
67412
+ return encodeURIComponent(filePath.replace(/\\/g, "/"));
67413
+ }
67414
+ async function saveSession(filePath, format, doc) {
67415
+ const key = sessionKey(filePath);
67416
+ let sourceFileMtime = 0;
67417
+ if (!filePath.startsWith("upload://")) {
67418
+ try {
67419
+ const stat = await fs.stat(filePath);
67420
+ sourceFileMtime = stat.mtimeMs;
67421
+ } catch {
67422
+ }
67423
+ }
67424
+ const state = encodeStateAsUpdate(doc);
67425
+ const ydocState = Buffer.from(state).toString("base64");
67426
+ const data = {
67427
+ filePath,
67428
+ format,
67429
+ ydocState,
67430
+ sourceFileMtime,
67431
+ lastAccessed: Date.now()
67432
+ };
67433
+ if (!sessionDirReady) {
67434
+ await fs.mkdir(SESSION_DIR, { recursive: true });
67435
+ sessionDirReady = true;
67436
+ }
67437
+ const sessionPath = path5.join(SESSION_DIR, `${key}.json`);
67438
+ await atomicWrite(sessionPath, JSON.stringify(data));
67439
+ }
67440
+ async function loadSession(filePath) {
67441
+ const key = sessionKey(filePath);
67442
+ const sessionPath = path5.join(SESSION_DIR, `${key}.json`);
67443
+ try {
67444
+ const content3 = await fs.readFile(sessionPath, "utf-8");
67445
+ return JSON.parse(content3);
67446
+ } catch (err) {
67447
+ const code3 = err.code;
67448
+ if (code3 === "ENOENT") return null;
67449
+ if (err instanceof SyntaxError) {
67450
+ console.error(`[Tandem] Corrupted session file ${sessionPath}, removing:`, err.message);
67451
+ await fs.unlink(sessionPath).catch((unlinkErr) => {
67452
+ console.error(`[Tandem] Failed to remove corrupted session ${sessionPath}:`, unlinkErr);
67453
+ });
67454
+ return null;
67455
+ }
67456
+ console.error(`[Tandem] Failed to read session ${sessionPath}:`, err);
67457
+ return null;
67458
+ }
67459
+ }
67460
+ function restoreYDoc(doc, session) {
67461
+ const state = Buffer.from(session.ydocState, "base64");
67462
+ applyUpdate(doc, new Uint8Array(state));
67463
+ }
67464
+ async function sourceFileChanged(session) {
67465
+ if (session.filePath.startsWith("upload://")) return false;
67466
+ try {
67467
+ const stat = await fs.stat(session.filePath);
67468
+ return stat.mtimeMs !== session.sourceFileMtime;
67469
+ } catch {
67470
+ return true;
67471
+ }
67472
+ }
67473
+ async function deleteSession(filePath) {
67474
+ const key = sessionKey(filePath);
67475
+ const sessionPath = path5.join(SESSION_DIR, `${key}.json`);
67476
+ try {
67477
+ await fs.unlink(sessionPath);
67478
+ } catch (err) {
67479
+ const code3 = err.code;
67480
+ if (code3 !== "ENOENT") {
67481
+ console.error(`[Tandem] deleteSession: failed to delete ${sessionPath}:`, err);
67482
+ }
67483
+ }
67484
+ }
67485
+ async function saveCtrlSession(doc) {
67486
+ if (!sessionDirReady) {
67487
+ await fs.mkdir(SESSION_DIR, { recursive: true });
67488
+ sessionDirReady = true;
67489
+ }
67490
+ const chatMap = doc.getMap(Y_MAP_CHAT);
67491
+ const entries = [];
67492
+ chatMap.forEach((value, key) => {
67493
+ const msg = value;
67494
+ entries.push({ id: key, timestamp: msg.timestamp });
67495
+ });
67496
+ if (entries.length > 200) {
67497
+ entries.sort((a, b) => a.timestamp - b.timestamp);
67498
+ const toDelete = entries.slice(0, entries.length - 200);
67499
+ doc.transact(() => {
67500
+ for (const entry of toDelete) {
67501
+ chatMap.delete(entry.id);
67502
+ }
67503
+ }, MCP_ORIGIN);
67504
+ }
67505
+ const state = encodeStateAsUpdate(doc);
67506
+ const ydocState = Buffer.from(state).toString("base64");
67507
+ const data = { ydocState, lastAccessed: Date.now() };
67508
+ const sessionPath = path5.join(SESSION_DIR, `${CTRL_SESSION_KEY}.json`);
67509
+ await atomicWrite(sessionPath, JSON.stringify(data));
67510
+ }
67511
+ async function loadCtrlSession() {
67512
+ const sessionPath = path5.join(SESSION_DIR, `${CTRL_SESSION_KEY}.json`);
67513
+ try {
67514
+ const content3 = await fs.readFile(sessionPath, "utf-8");
67515
+ const data = JSON.parse(content3);
67516
+ return data.ydocState ?? null;
67517
+ } catch (err) {
67518
+ const code3 = err.code;
67519
+ if (code3 === "ENOENT") return null;
67520
+ if (err instanceof SyntaxError) {
67521
+ console.error(`[Tandem] Corrupted ctrl session ${sessionPath}, removing:`, err.message);
67522
+ await fs.unlink(sessionPath).catch((unlinkErr) => {
67523
+ console.error(
67524
+ `[Tandem] Failed to remove corrupted ctrl session ${sessionPath}:`,
67525
+ unlinkErr
67526
+ );
67527
+ });
67528
+ return null;
67529
+ }
67530
+ console.error(`[Tandem] Failed to read ctrl session:`, err);
67531
+ return null;
67532
+ }
67533
+ }
67534
+ function restoreCtrlDoc(doc, base64State) {
67535
+ const state = Buffer.from(base64State, "base64");
67536
+ applyUpdate(doc, new Uint8Array(state));
67537
+ }
67538
+ async function listSessionFilePaths() {
67539
+ try {
67540
+ await fs.mkdir(SESSION_DIR, { recursive: true });
67541
+ const files2 = await fs.readdir(SESSION_DIR);
67542
+ const results = [];
67543
+ for (const file of files2) {
67544
+ if (!file.endsWith(".json")) continue;
67545
+ if (file === `${encodeURIComponent(CTRL_ROOM)}.json`) continue;
67546
+ try {
67547
+ const raw = await fs.readFile(path5.join(SESSION_DIR, file), "utf-8");
67548
+ const data = JSON.parse(raw);
67549
+ if (!data.filePath || data.filePath.startsWith("upload://")) continue;
67550
+ results.push({ filePath: data.filePath, lastAccessed: data.lastAccessed ?? 0 });
67551
+ } catch (err) {
67552
+ console.error(`[Tandem] Skipping unreadable session file ${file}:`, err);
67553
+ }
67554
+ }
67555
+ results.sort((a, b) => b.lastAccessed - a.lastAccessed);
67556
+ return results;
67557
+ } catch (err) {
67558
+ console.error("[Tandem] Failed to read session directory:", err);
67559
+ return [];
67560
+ }
67561
+ }
67562
+ async function cleanupSessions() {
67563
+ let cleaned = 0;
67564
+ let files2;
67565
+ try {
67566
+ files2 = await fs.readdir(SESSION_DIR);
67567
+ } catch (err) {
67568
+ if (err.code === "ENOENT") return 0;
67569
+ console.error("[Tandem] Failed to read session directory:", err);
67570
+ return 0;
67571
+ }
67572
+ const now = Date.now();
67573
+ for (const file of files2) {
67574
+ try {
67575
+ const filePath = path5.join(SESSION_DIR, file);
67576
+ const stat = await fs.stat(filePath);
67577
+ if (now - stat.mtimeMs > SESSION_MAX_AGE) {
67578
+ await fs.unlink(filePath);
67579
+ cleaned++;
67580
+ }
67581
+ } catch (err) {
67582
+ console.error(`[Tandem] cleanupSessions: failed to process ${file}:`, err);
67583
+ }
67584
+ }
67585
+ return cleaned;
67586
+ }
67587
+ function isAutoSaveRunning() {
67588
+ return autoSaveTimer !== null;
67589
+ }
67590
+ function startAutoSave(callback) {
67591
+ stopAutoSave();
67592
+ autoSaveCallback = callback;
67593
+ autoSaveTimer = setInterval(async () => {
67594
+ try {
67595
+ await autoSaveCallback?.();
67596
+ } catch (err) {
67597
+ console.error("[Tandem] Auto-save failed:", err);
67598
+ }
67599
+ }, AUTO_SAVE_INTERVAL);
67600
+ }
67601
+ function stopAutoSave() {
67602
+ if (autoSaveTimer) {
67603
+ clearInterval(autoSaveTimer);
67604
+ autoSaveTimer = null;
67605
+ }
67606
+ autoSaveCallback = null;
67607
+ }
67608
+ var AUTO_SAVE_INTERVAL, RENAME_MAX_RETRIES, RENAME_RETRY_BASE_MS, sessionDirReady, CTRL_SESSION_KEY, autoSaveTimer, autoSaveCallback;
67609
+ var init_manager = __esm({
67610
+ "src/server/session/manager.ts"() {
67611
+ "use strict";
67612
+ init_yjs();
67613
+ init_constants();
67614
+ init_queue();
67615
+ init_platform();
67616
+ AUTO_SAVE_INTERVAL = 60 * 1e3;
67617
+ RENAME_MAX_RETRIES = 3;
67618
+ RENAME_RETRY_BASE_MS = 50;
67619
+ sessionDirReady = false;
67620
+ CTRL_SESSION_KEY = CTRL_ROOM;
67621
+ autoSaveTimer = null;
67622
+ autoSaveCallback = null;
67623
+ }
67624
+ });
67625
+
67184
67626
  // src/server/file-io/docx-walker.ts
67185
67627
  function isElement3(node2) {
67186
67628
  return node2.type === "tag";
@@ -67960,23 +68402,23 @@ var init_docx_apply = __esm({
67960
68402
  });
67961
68403
 
67962
68404
  // src/server/file-io/index.ts
67963
- import fs from "fs/promises";
67964
- import path3 from "path";
68405
+ import fs2 from "fs/promises";
68406
+ import path6 from "path";
67965
68407
  function getAdapter(format) {
67966
68408
  return adapters[format] ?? plaintextAdapter;
67967
68409
  }
67968
- async function atomicWrite(filePath, content3) {
67969
- const tempPath = path3.join(path3.dirname(filePath), `.tandem-tmp-${Date.now()}`);
67970
- await fs.writeFile(tempPath, content3, "utf-8");
67971
- await fs.rename(tempPath, filePath);
68410
+ async function atomicWrite2(filePath, content3) {
68411
+ const tempPath = path6.join(path6.dirname(filePath), `.tandem-tmp-${Date.now()}`);
68412
+ await fs2.writeFile(tempPath, content3, "utf-8");
68413
+ await fs2.rename(tempPath, filePath);
67972
68414
  }
67973
68415
  async function atomicWriteBuffer(filePath, content3) {
67974
- const tempPath = path3.join(path3.dirname(filePath), `.tandem-tmp-${Date.now()}`);
67975
- await fs.writeFile(tempPath, content3);
68416
+ const tempPath = path6.join(path6.dirname(filePath), `.tandem-tmp-${Date.now()}`);
68417
+ await fs2.writeFile(tempPath, content3);
67976
68418
  try {
67977
- await fs.rename(tempPath, filePath);
68419
+ await fs2.rename(tempPath, filePath);
67978
68420
  } catch (err) {
67979
- await fs.unlink(tempPath).catch(() => {
68421
+ await fs2.unlink(tempPath).catch(() => {
67980
68422
  });
67981
68423
  throw err;
67982
68424
  }
@@ -68040,12 +68482,12 @@ var init_file_io = __esm({
68040
68482
  });
68041
68483
 
68042
68484
  // src/server/file-watcher.ts
68043
- import fs2 from "fs";
68485
+ import fs3 from "fs";
68044
68486
  function watchFile(filePath, onChanged) {
68045
68487
  if (watched.has(filePath)) return;
68046
68488
  let watcher;
68047
68489
  try {
68048
- watcher = fs2.watch(filePath, (eventType) => {
68490
+ watcher = fs3.watch(filePath, (eventType) => {
68049
68491
  if (eventType !== "change") return;
68050
68492
  const entry = watched.get(filePath);
68051
68493
  if (!entry) return;
@@ -68107,440 +68549,6 @@ var init_file_watcher = __esm({
68107
68549
  }
68108
68550
  });
68109
68551
 
68110
- // node_modules/is-safe-filename/index.js
68111
- function isSafeFilename(filename) {
68112
- if (typeof filename !== "string") {
68113
- return false;
68114
- }
68115
- const trimmed = filename.trim();
68116
- return trimmed !== "" && trimmed !== "." && trimmed !== ".." && !filename.includes("/") && !filename.includes("\\") && !filename.includes("\0");
68117
- }
68118
- function assertSafeFilename(filename) {
68119
- if (typeof filename !== "string") {
68120
- throw new TypeError("Expected a string");
68121
- }
68122
- if (!isSafeFilename(filename)) {
68123
- throw new Error(`Unsafe filename: ${JSON.stringify(filename)}`);
68124
- }
68125
- }
68126
- var unsafeFilenameFixtures;
68127
- var init_is_safe_filename = __esm({
68128
- "node_modules/is-safe-filename/index.js"() {
68129
- "use strict";
68130
- unsafeFilenameFixtures = Object.freeze([
68131
- "",
68132
- " ",
68133
- ".",
68134
- "..",
68135
- " .",
68136
- ". ",
68137
- " ..",
68138
- ".. ",
68139
- "../",
68140
- "../foo",
68141
- "foo/../bar",
68142
- "foo/bar",
68143
- "foo\\bar",
68144
- "foo\0bar"
68145
- ]);
68146
- }
68147
- });
68148
-
68149
- // node_modules/env-paths/index.js
68150
- import path4 from "path";
68151
- import os from "os";
68152
- import process2 from "process";
68153
- function envPaths(name2, { suffix = "nodejs" } = {}) {
68154
- assertSafeFilename(name2);
68155
- if (suffix) {
68156
- name2 += `-${suffix}`;
68157
- }
68158
- assertSafeFilename(name2);
68159
- if (process2.platform === "darwin") {
68160
- return macos(name2);
68161
- }
68162
- if (process2.platform === "win32") {
68163
- return windows(name2);
68164
- }
68165
- return linux(name2);
68166
- }
68167
- var homedir, tmpdir, env2, macos, windows, linux;
68168
- var init_env_paths = __esm({
68169
- "node_modules/env-paths/index.js"() {
68170
- "use strict";
68171
- init_is_safe_filename();
68172
- homedir = os.homedir();
68173
- tmpdir = os.tmpdir();
68174
- ({ env: env2 } = process2);
68175
- macos = (name2) => {
68176
- const library = path4.join(homedir, "Library");
68177
- return {
68178
- data: path4.join(library, "Application Support", name2),
68179
- config: path4.join(library, "Preferences", name2),
68180
- cache: path4.join(library, "Caches", name2),
68181
- log: path4.join(library, "Logs", name2),
68182
- temp: path4.join(tmpdir, name2)
68183
- };
68184
- };
68185
- windows = (name2) => {
68186
- const appData = env2.APPDATA || path4.join(homedir, "AppData", "Roaming");
68187
- const localAppData = env2.LOCALAPPDATA || path4.join(homedir, "AppData", "Local");
68188
- return {
68189
- // Data/config/cache/log are invented by me as Windows isn't opinionated about this
68190
- data: path4.join(localAppData, name2, "Data"),
68191
- config: path4.join(appData, name2, "Config"),
68192
- cache: path4.join(localAppData, name2, "Cache"),
68193
- log: path4.join(localAppData, name2, "Log"),
68194
- temp: path4.join(tmpdir, name2)
68195
- };
68196
- };
68197
- linux = (name2) => {
68198
- const username = path4.basename(homedir);
68199
- return {
68200
- data: path4.join(env2.XDG_DATA_HOME || path4.join(homedir, ".local", "share"), name2),
68201
- config: path4.join(env2.XDG_CONFIG_HOME || path4.join(homedir, ".config"), name2),
68202
- cache: path4.join(env2.XDG_CACHE_HOME || path4.join(homedir, ".cache"), name2),
68203
- // https://wiki.debian.org/XDGBaseDirectorySpecification#state
68204
- log: path4.join(env2.XDG_STATE_HOME || path4.join(homedir, ".local", "state"), name2),
68205
- temp: path4.join(tmpdir, username, name2)
68206
- };
68207
- };
68208
- }
68209
- });
68210
-
68211
- // src/server/platform.ts
68212
- import { execSync } from "child_process";
68213
- import net from "net";
68214
- import path5 from "path";
68215
- function freePort(port) {
68216
- try {
68217
- if (process.platform === "win32") {
68218
- freePortWindows(port);
68219
- } else {
68220
- freePortUnix(port);
68221
- }
68222
- } catch (err) {
68223
- console.error(`[Tandem] freePort(${port}): ${err instanceof Error ? err.message : err}`);
68224
- }
68225
- }
68226
- async function waitForPort(port, timeoutMs = 5e3) {
68227
- const start = Date.now();
68228
- while (Date.now() - start < timeoutMs) {
68229
- if (await tryBind(port)) return;
68230
- await new Promise((r) => setTimeout(r, 200));
68231
- }
68232
- throw new Error(`Port ${port} still not available after ${timeoutMs}ms`);
68233
- }
68234
- function tryBind(port) {
68235
- return new Promise((resolve2, reject2) => {
68236
- const srv = net.createServer();
68237
- srv.once("error", (err) => {
68238
- srv.close(() => {
68239
- if (err.code === "EADDRINUSE") {
68240
- resolve2(false);
68241
- } else {
68242
- reject2(err);
68243
- }
68244
- });
68245
- });
68246
- srv.listen(port, "127.0.0.1", () => {
68247
- srv.close(() => resolve2(true));
68248
- });
68249
- });
68250
- }
68251
- function parseLsofPids(output) {
68252
- return output.trim().split("\n").map((line) => parseInt(line.trim(), 10)).filter((pid) => Number.isFinite(pid) && pid > 0);
68253
- }
68254
- function parseSsPid(output) {
68255
- const match = output.match(/pid=(\d+)/);
68256
- return match ? parseInt(match[1], 10) : null;
68257
- }
68258
- function freePortWindows(port) {
68259
- const out = execSync(`netstat -ano | findstr ":${port}.*LISTENING"`, {
68260
- encoding: "utf-8",
68261
- stdio: ["pipe", "pipe", "ignore"]
68262
- });
68263
- const pid = out.trim().split(/\s+/).at(-1);
68264
- if (pid && /^\d+$/.test(pid)) {
68265
- execSync(`taskkill /PID ${pid} /F`, { stdio: "ignore" });
68266
- console.error(`[Tandem] Killed stale PID ${pid} holding port ${port}`);
68267
- }
68268
- }
68269
- function freePortUnix(port) {
68270
- let pids = [];
68271
- try {
68272
- const out = execSync(`lsof -ti TCP:${port} -sTCP:LISTEN`, {
68273
- encoding: "utf-8",
68274
- stdio: ["pipe", "pipe", "ignore"]
68275
- });
68276
- pids = parseLsofPids(out);
68277
- } catch {
68278
- try {
68279
- const out = execSync(`ss -tlnp sport = :${port}`, {
68280
- encoding: "utf-8",
68281
- stdio: ["pipe", "pipe", "ignore"]
68282
- });
68283
- const pid = parseSsPid(out);
68284
- if (pid) pids = [pid];
68285
- } catch {
68286
- }
68287
- }
68288
- for (const pid of pids) {
68289
- try {
68290
- process.kill(pid, "SIGKILL");
68291
- console.error(`[Tandem] Killed stale PID ${pid} holding port ${port}`);
68292
- } catch {
68293
- }
68294
- }
68295
- }
68296
- var paths, SESSION_DIR, LAST_SEEN_VERSION_FILE;
68297
- var init_platform = __esm({
68298
- "src/server/platform.ts"() {
68299
- "use strict";
68300
- init_env_paths();
68301
- paths = envPaths("tandem", { suffix: "" });
68302
- SESSION_DIR = path5.join(paths.data, "sessions");
68303
- LAST_SEEN_VERSION_FILE = path5.join(paths.data, "last-seen-version");
68304
- }
68305
- });
68306
-
68307
- // src/server/session/manager.ts
68308
- import fs3 from "fs/promises";
68309
- import path6 from "path";
68310
- async function atomicWrite2(sessionPath, content3) {
68311
- const tmpPath = `${sessionPath}.tmp`;
68312
- await fs3.writeFile(tmpPath, content3, "utf-8");
68313
- for (let attempt = 0; attempt < RENAME_MAX_RETRIES; attempt++) {
68314
- try {
68315
- await fs3.rename(tmpPath, sessionPath);
68316
- return;
68317
- } catch (err) {
68318
- const code3 = err.code;
68319
- if ((code3 === "EPERM" || code3 === "EACCES") && attempt < RENAME_MAX_RETRIES - 1) {
68320
- await new Promise((r) => setTimeout(r, RENAME_RETRY_BASE_MS * 2 ** attempt));
68321
- continue;
68322
- }
68323
- await fs3.unlink(tmpPath).catch(() => {
68324
- });
68325
- throw err;
68326
- }
68327
- }
68328
- }
68329
- function sessionKey(filePath) {
68330
- return encodeURIComponent(filePath.replace(/\\/g, "/"));
68331
- }
68332
- async function saveSession(filePath, format, doc) {
68333
- const key = sessionKey(filePath);
68334
- let sourceFileMtime = 0;
68335
- if (!filePath.startsWith("upload://")) {
68336
- try {
68337
- const stat = await fs3.stat(filePath);
68338
- sourceFileMtime = stat.mtimeMs;
68339
- } catch {
68340
- }
68341
- }
68342
- const state = encodeStateAsUpdate(doc);
68343
- const ydocState = Buffer.from(state).toString("base64");
68344
- const data = {
68345
- filePath,
68346
- format,
68347
- ydocState,
68348
- sourceFileMtime,
68349
- lastAccessed: Date.now()
68350
- };
68351
- if (!sessionDirReady) {
68352
- await fs3.mkdir(SESSION_DIR, { recursive: true });
68353
- sessionDirReady = true;
68354
- }
68355
- const sessionPath = path6.join(SESSION_DIR, `${key}.json`);
68356
- await atomicWrite2(sessionPath, JSON.stringify(data));
68357
- }
68358
- async function loadSession(filePath) {
68359
- const key = sessionKey(filePath);
68360
- const sessionPath = path6.join(SESSION_DIR, `${key}.json`);
68361
- try {
68362
- const content3 = await fs3.readFile(sessionPath, "utf-8");
68363
- return JSON.parse(content3);
68364
- } catch (err) {
68365
- const code3 = err.code;
68366
- if (code3 === "ENOENT") return null;
68367
- if (err instanceof SyntaxError) {
68368
- console.error(`[Tandem] Corrupted session file ${sessionPath}, removing:`, err.message);
68369
- await fs3.unlink(sessionPath).catch((unlinkErr) => {
68370
- console.error(`[Tandem] Failed to remove corrupted session ${sessionPath}:`, unlinkErr);
68371
- });
68372
- return null;
68373
- }
68374
- console.error(`[Tandem] Failed to read session ${sessionPath}:`, err);
68375
- return null;
68376
- }
68377
- }
68378
- function restoreYDoc(doc, session) {
68379
- const state = Buffer.from(session.ydocState, "base64");
68380
- applyUpdate(doc, new Uint8Array(state));
68381
- }
68382
- async function sourceFileChanged(session) {
68383
- if (session.filePath.startsWith("upload://")) return false;
68384
- try {
68385
- const stat = await fs3.stat(session.filePath);
68386
- return stat.mtimeMs !== session.sourceFileMtime;
68387
- } catch {
68388
- return true;
68389
- }
68390
- }
68391
- async function deleteSession(filePath) {
68392
- const key = sessionKey(filePath);
68393
- const sessionPath = path6.join(SESSION_DIR, `${key}.json`);
68394
- try {
68395
- await fs3.unlink(sessionPath);
68396
- } catch (err) {
68397
- const code3 = err.code;
68398
- if (code3 !== "ENOENT") {
68399
- console.error(`[Tandem] deleteSession: failed to delete ${sessionPath}:`, err);
68400
- }
68401
- }
68402
- }
68403
- async function saveCtrlSession(doc) {
68404
- if (!sessionDirReady) {
68405
- await fs3.mkdir(SESSION_DIR, { recursive: true });
68406
- sessionDirReady = true;
68407
- }
68408
- const chatMap = doc.getMap(Y_MAP_CHAT);
68409
- const entries = [];
68410
- chatMap.forEach((value, key) => {
68411
- const msg = value;
68412
- entries.push({ id: key, timestamp: msg.timestamp });
68413
- });
68414
- if (entries.length > 200) {
68415
- entries.sort((a, b) => a.timestamp - b.timestamp);
68416
- const toDelete = entries.slice(0, entries.length - 200);
68417
- doc.transact(() => {
68418
- for (const entry of toDelete) {
68419
- chatMap.delete(entry.id);
68420
- }
68421
- }, MCP_ORIGIN);
68422
- }
68423
- const state = encodeStateAsUpdate(doc);
68424
- const ydocState = Buffer.from(state).toString("base64");
68425
- const data = { ydocState, lastAccessed: Date.now() };
68426
- const sessionPath = path6.join(SESSION_DIR, `${CTRL_SESSION_KEY}.json`);
68427
- await atomicWrite2(sessionPath, JSON.stringify(data));
68428
- }
68429
- async function loadCtrlSession() {
68430
- const sessionPath = path6.join(SESSION_DIR, `${CTRL_SESSION_KEY}.json`);
68431
- try {
68432
- const content3 = await fs3.readFile(sessionPath, "utf-8");
68433
- const data = JSON.parse(content3);
68434
- return data.ydocState ?? null;
68435
- } catch (err) {
68436
- const code3 = err.code;
68437
- if (code3 === "ENOENT") return null;
68438
- if (err instanceof SyntaxError) {
68439
- console.error(`[Tandem] Corrupted ctrl session ${sessionPath}, removing:`, err.message);
68440
- await fs3.unlink(sessionPath).catch((unlinkErr) => {
68441
- console.error(
68442
- `[Tandem] Failed to remove corrupted ctrl session ${sessionPath}:`,
68443
- unlinkErr
68444
- );
68445
- });
68446
- return null;
68447
- }
68448
- console.error(`[Tandem] Failed to read ctrl session:`, err);
68449
- return null;
68450
- }
68451
- }
68452
- function restoreCtrlDoc(doc, base64State) {
68453
- const state = Buffer.from(base64State, "base64");
68454
- applyUpdate(doc, new Uint8Array(state));
68455
- }
68456
- async function listSessionFilePaths() {
68457
- try {
68458
- await fs3.mkdir(SESSION_DIR, { recursive: true });
68459
- const files2 = await fs3.readdir(SESSION_DIR);
68460
- const results = [];
68461
- for (const file of files2) {
68462
- if (!file.endsWith(".json")) continue;
68463
- if (file === `${encodeURIComponent(CTRL_ROOM)}.json`) continue;
68464
- try {
68465
- const raw = await fs3.readFile(path6.join(SESSION_DIR, file), "utf-8");
68466
- const data = JSON.parse(raw);
68467
- if (!data.filePath || data.filePath.startsWith("upload://")) continue;
68468
- results.push({ filePath: data.filePath, lastAccessed: data.lastAccessed ?? 0 });
68469
- } catch (err) {
68470
- console.error(`[Tandem] Skipping unreadable session file ${file}:`, err);
68471
- }
68472
- }
68473
- results.sort((a, b) => b.lastAccessed - a.lastAccessed);
68474
- return results;
68475
- } catch (err) {
68476
- console.error("[Tandem] Failed to read session directory:", err);
68477
- return [];
68478
- }
68479
- }
68480
- async function cleanupSessions() {
68481
- let cleaned = 0;
68482
- let files2;
68483
- try {
68484
- files2 = await fs3.readdir(SESSION_DIR);
68485
- } catch (err) {
68486
- if (err.code === "ENOENT") return 0;
68487
- console.error("[Tandem] Failed to read session directory:", err);
68488
- return 0;
68489
- }
68490
- const now = Date.now();
68491
- for (const file of files2) {
68492
- try {
68493
- const filePath = path6.join(SESSION_DIR, file);
68494
- const stat = await fs3.stat(filePath);
68495
- if (now - stat.mtimeMs > SESSION_MAX_AGE) {
68496
- await fs3.unlink(filePath);
68497
- cleaned++;
68498
- }
68499
- } catch (err) {
68500
- console.error(`[Tandem] cleanupSessions: failed to process ${file}:`, err);
68501
- }
68502
- }
68503
- return cleaned;
68504
- }
68505
- function isAutoSaveRunning() {
68506
- return autoSaveTimer !== null;
68507
- }
68508
- function startAutoSave(callback) {
68509
- stopAutoSave();
68510
- autoSaveCallback = callback;
68511
- autoSaveTimer = setInterval(async () => {
68512
- try {
68513
- await autoSaveCallback?.();
68514
- } catch (err) {
68515
- console.error("[Tandem] Auto-save failed:", err);
68516
- }
68517
- }, AUTO_SAVE_INTERVAL);
68518
- }
68519
- function stopAutoSave() {
68520
- if (autoSaveTimer) {
68521
- clearInterval(autoSaveTimer);
68522
- autoSaveTimer = null;
68523
- }
68524
- autoSaveCallback = null;
68525
- }
68526
- var AUTO_SAVE_INTERVAL, RENAME_MAX_RETRIES, RENAME_RETRY_BASE_MS, sessionDirReady, CTRL_SESSION_KEY, autoSaveTimer, autoSaveCallback;
68527
- var init_manager = __esm({
68528
- "src/server/session/manager.ts"() {
68529
- "use strict";
68530
- init_yjs();
68531
- init_constants();
68532
- init_queue();
68533
- init_platform();
68534
- AUTO_SAVE_INTERVAL = 60 * 1e3;
68535
- RENAME_MAX_RETRIES = 3;
68536
- RENAME_RETRY_BASE_MS = 50;
68537
- sessionDirReady = false;
68538
- CTRL_SESSION_KEY = CTRL_ROOM;
68539
- autoSaveTimer = null;
68540
- autoSaveCallback = null;
68541
- }
68542
- });
68543
-
68544
68552
  // src/server/mcp/file-opener.ts
68545
68553
  var file_opener_exports = {};
68546
68554
  __export(file_opener_exports, {
@@ -68654,7 +68662,7 @@ async function openFileByPath(filePath, options) {
68654
68662
  addDoc(id2, { id: id2, filePath: resolved, format, readOnly, source: "file" });
68655
68663
  setActiveDocId(id2);
68656
68664
  writeDocMeta(doc, id2, fileName, format, readOnly);
68657
- initSavedBaseline(doc);
68665
+ await initSavedBaseline(doc, resolved);
68658
68666
  broadcastOpenDocs();
68659
68667
  ensureAutoSave();
68660
68668
  if (format !== "docx") {
@@ -68697,7 +68705,7 @@ async function openFileFromContent(fileName, content3) {
68697
68705
  addDoc(id2, { id: id2, filePath: syntheticPath, format, readOnly, source: "upload" });
68698
68706
  setActiveDocId(id2);
68699
68707
  writeDocMeta(doc, id2, fileName, format, readOnly);
68700
- initSavedBaseline(doc);
68708
+ await initSavedBaseline(doc);
68701
68709
  broadcastOpenDocs();
68702
68710
  ensureAutoSave();
68703
68711
  return buildResult(doc, {
@@ -68735,6 +68743,8 @@ async function clearAndReload(id2, doc, filePath, format, existing) {
68735
68743
  doc.transact(() => {
68736
68744
  const annotations = doc.getMap(Y_MAP_ANNOTATIONS);
68737
68745
  annotations.forEach((_3, k) => annotations.delete(k));
68746
+ const annotationReplies = doc.getMap(Y_MAP_ANNOTATION_REPLIES);
68747
+ annotationReplies.forEach((_3, k) => annotationReplies.delete(k));
68738
68748
  const awareness = doc.getMap(Y_MAP_AWARENESS);
68739
68749
  awareness.forEach((_3, k) => awareness.delete(k));
68740
68750
  const userAwareness = doc.getMap(Y_MAP_USER_AWARENESS);
@@ -68769,9 +68779,14 @@ async function clearAndReload(id2, doc, filePath, format, existing) {
68769
68779
  });
68770
68780
  console.error(`[Tandem] clearAndReload: complete for ${id2}`);
68771
68781
  }
68772
- function initSavedBaseline(doc) {
68782
+ async function initSavedBaseline(doc, filePath) {
68783
+ let baseline = Date.now();
68784
+ if (filePath) {
68785
+ const stat = await fs4.stat(filePath).catch(() => null);
68786
+ if (stat) baseline = stat.mtimeMs;
68787
+ }
68773
68788
  const meta2 = doc.getMap(Y_MAP_DOCUMENT_META);
68774
- doc.transact(() => meta2.set(Y_MAP_SAVED_AT_VERSION, Date.now()), MCP_ORIGIN);
68789
+ doc.transact(() => meta2.set(Y_MAP_SAVED_AT_VERSION, baseline), MCP_ORIGIN);
68775
68790
  }
68776
68791
  function writeDocMeta(doc, id2, fileName, format, readOnly) {
68777
68792
  const meta2 = doc.getMap(Y_MAP_DOCUMENT_META);
@@ -68894,6 +68909,7 @@ function ensureAutoSave() {
68894
68909
  const d = getOrCreateDocument(docId);
68895
68910
  await saveSession(state.filePath, state.format, d);
68896
68911
  }
68912
+ await autoSaveAllToDisk();
68897
68913
  });
68898
68914
  }
68899
68915
  var reloadInProgress;
@@ -68922,6 +68938,7 @@ var init_file_opener = __esm({
68922
68938
 
68923
68939
  // src/server/mcp/document-service.ts
68924
68940
  import { randomUUID as randomUUID3 } from "crypto";
68941
+ import fs5 from "fs/promises";
68925
68942
  import path8 from "path";
68926
68943
  function getOpenDocs() {
68927
68944
  return openDocs;
@@ -68960,6 +68977,86 @@ function requireDocument(documentId) {
68960
68977
  docId: current.id
68961
68978
  };
68962
68979
  }
68980
+ async function saveDocumentToDisk(docId, source = "auto-save") {
68981
+ const docState = openDocs.get(docId);
68982
+ if (!docState) return { status: "skipped", reason: "Document not open" };
68983
+ if (docState.source === "upload") {
68984
+ return { status: "skipped", reason: "Upload-only document" };
68985
+ }
68986
+ if (docState.readOnly) {
68987
+ return { status: "skipped", reason: "Read-only document" };
68988
+ }
68989
+ if (!AUTO_SAVE_FORMATS.has(docState.format)) {
68990
+ return { status: "skipped", reason: `Format '${docState.format}' not eligible for disk save` };
68991
+ }
68992
+ const adapter = getAdapter(docState.format);
68993
+ if (!adapter.canSave) {
68994
+ return { status: "skipped", reason: "Adapter cannot save" };
68995
+ }
68996
+ if (savingDocs.has(docId)) {
68997
+ return { status: "skipped", reason: "Save already in progress" };
68998
+ }
68999
+ savingDocs.add(docId);
69000
+ try {
69001
+ try {
69002
+ const stat = await fs5.stat(docState.filePath);
69003
+ const meta3 = getOrCreateDocument(docId).getMap(Y_MAP_DOCUMENT_META);
69004
+ const lastSavedAt = meta3.get(Y_MAP_SAVED_AT_VERSION);
69005
+ if (lastSavedAt && stat.mtimeMs > lastSavedAt + 1e3) {
69006
+ return { status: "skipped", reason: "File modified externally" };
69007
+ }
69008
+ } catch (err) {
69009
+ const code3 = err.code;
69010
+ if (code3 === "ENOENT") {
69011
+ return { status: "skipped", reason: "Source file no longer exists" };
69012
+ }
69013
+ console.error(`[AutoSave] Unexpected stat error for ${docState.filePath}:`, err);
69014
+ return { status: "skipped", reason: `Cannot verify file state: ${code3}` };
69015
+ }
69016
+ const doc = getOrCreateDocument(docId);
69017
+ const output = adapter.save(doc);
69018
+ if (output == null) {
69019
+ return { status: "skipped", reason: "Adapter returned null" };
69020
+ }
69021
+ suppressNextChange(docState.filePath);
69022
+ await atomicWrite2(docState.filePath, output);
69023
+ await saveSession(docState.filePath, docState.format, doc);
69024
+ const meta2 = doc.getMap(Y_MAP_DOCUMENT_META);
69025
+ doc.transact(() => meta2.set(Y_MAP_SAVED_AT_VERSION, Date.now()), MCP_ORIGIN);
69026
+ return { status: "saved" };
69027
+ } catch (err) {
69028
+ const msg = err instanceof Error ? err.message : String(err);
69029
+ const errCode = err.code ?? "UNKNOWN";
69030
+ pushNotification({
69031
+ id: generateNotificationId(),
69032
+ type: "save-error",
69033
+ severity: "error",
69034
+ message: `Save failed for ${path8.basename(docState.filePath)}: ${msg}`,
69035
+ toolName: source,
69036
+ errorCode: errCode,
69037
+ documentId: docId,
69038
+ dedupKey: `${source}:${docId}`,
69039
+ timestamp: Date.now()
69040
+ });
69041
+ return { status: "error", reason: msg, errorCode: err.code };
69042
+ } finally {
69043
+ savingDocs.delete(docId);
69044
+ }
69045
+ }
69046
+ async function autoSaveAllToDisk() {
69047
+ for (const [docId, state] of openDocs) {
69048
+ if (state.source === "upload" || state.readOnly) continue;
69049
+ if (!AUTO_SAVE_FORMATS.has(state.format)) continue;
69050
+ try {
69051
+ const result2 = await saveDocumentToDisk(docId);
69052
+ if (result2.status === "saved") {
69053
+ console.error(`[AutoSave] Saved ${path8.basename(state.filePath)} to disk`);
69054
+ }
69055
+ } catch (err) {
69056
+ console.error(`[AutoSave] Unexpected error saving ${state.filePath}:`, err);
69057
+ }
69058
+ }
69059
+ }
68963
69060
  function toDocListEntry(d) {
68964
69061
  return {
68965
69062
  id: d.id,
@@ -69002,13 +69099,13 @@ async function closeDocumentById(id2) {
69002
69099
  }
69003
69100
  const closedPath = docState.filePath;
69004
69101
  unwatchFile(docState.filePath);
69102
+ savingDocs.delete(id2);
69103
+ removeDoc(id2);
69005
69104
  try {
69006
- const doc = getOrCreateDocument(id2);
69007
- await saveSession(docState.filePath, docState.format, doc);
69105
+ await deleteSession(docState.filePath);
69008
69106
  } catch (err) {
69009
- console.error(`[Tandem] Failed to save session before closing ${id2}:`, err);
69107
+ console.error(`[Tandem] Failed to delete session for ${id2}:`, err);
69010
69108
  }
69011
- removeDoc(id2);
69012
69109
  if (getActiveDocId() === id2) {
69013
69110
  const remaining = Array.from(openDocs.keys());
69014
69111
  setActiveDocId(remaining.length > 0 ? remaining[0] : null);
@@ -69078,23 +69175,28 @@ async function restoreOpenDocuments(previousActiveDocId) {
69078
69175
  }
69079
69176
  return restoredCount;
69080
69177
  }
69081
- var openDocs, activeDocId;
69178
+ var openDocs, activeDocId, savingDocs, AUTO_SAVE_FORMATS;
69082
69179
  var init_document_service = __esm({
69083
69180
  "src/server/mcp/document-service.ts"() {
69084
69181
  "use strict";
69085
69182
  init_constants();
69183
+ init_utils();
69086
69184
  init_queue();
69185
+ init_file_io();
69087
69186
  init_file_watcher();
69187
+ init_notifications();
69088
69188
  init_manager();
69089
69189
  init_provider();
69090
69190
  openDocs = /* @__PURE__ */ new Map();
69091
69191
  setShouldKeepDocument((name2) => openDocs.has(name2) || name2 === CTRL_ROOM);
69092
69192
  activeDocId = null;
69193
+ savingDocs = /* @__PURE__ */ new Set();
69194
+ AUTO_SAVE_FORMATS = /* @__PURE__ */ new Set(["md", "txt"]);
69093
69195
  }
69094
69196
  });
69095
69197
 
69096
69198
  // src/server/mcp/convert.ts
69097
- import fs5 from "fs/promises";
69199
+ import fs6 from "fs/promises";
69098
69200
  import path9 from "path";
69099
69201
  async function findAvailablePath(basePath) {
69100
69202
  const dir = path9.dirname(basePath);
@@ -69105,7 +69207,7 @@ async function findAvailablePath(basePath) {
69105
69207
  let counter = 0;
69106
69208
  while (counter <= MAX_ATTEMPTS) {
69107
69209
  try {
69108
- await fs5.access(candidate);
69210
+ await fs6.access(candidate);
69109
69211
  counter++;
69110
69212
  candidate = path9.join(dir, `${name2}-${counter}${ext}`);
69111
69213
  } catch (err) {
@@ -69154,7 +69256,7 @@ async function convertToMarkdown(documentId, outputPath) {
69154
69256
  });
69155
69257
  }
69156
69258
  try {
69157
- const stat = await fs5.stat(resolvedOutput);
69259
+ const stat = await fs6.stat(resolvedOutput);
69158
69260
  if (stat.isDirectory()) {
69159
69261
  const baseName = path9.basename(docState.filePath, path9.extname(docState.filePath));
69160
69262
  resolvedOutput = path9.join(resolvedOutput, `${baseName}.md`);
@@ -69168,7 +69270,7 @@ async function convertToMarkdown(documentId, outputPath) {
69168
69270
  resolvedOutput = path9.join(sourceDir, `${baseName}.md`);
69169
69271
  }
69170
69272
  resolvedOutput = await findAvailablePath(resolvedOutput);
69171
- await atomicWrite(resolvedOutput, markdown);
69273
+ await atomicWrite2(resolvedOutput, markdown);
69172
69274
  try {
69173
69275
  const openResult = await openFileByPath(resolvedOutput);
69174
69276
  return {
@@ -69463,6 +69565,25 @@ function registerDocumentTools(server) {
69463
69565
  }
69464
69566
  }, MCP_ORIGIN);
69465
69567
  }
69568
+ if (newText.length > 0) {
69569
+ const newFrom = from3;
69570
+ const newTo = toFlatOffset(newFrom + newText.length);
69571
+ const anchored = anchoredRange(r.doc, newFrom, newTo);
69572
+ if (anchored.ok) {
69573
+ const authorshipMap = r.doc.getMap(Y_MAP_AUTHORSHIP);
69574
+ const rangeId = generateAuthorshipId("claude");
69575
+ const entry = {
69576
+ id: rangeId,
69577
+ author: "claude",
69578
+ range: anchored.range,
69579
+ relRange: anchored.fullyAnchored ? anchored.relRange : void 0,
69580
+ timestamp: Date.now()
69581
+ };
69582
+ r.doc.transact(() => {
69583
+ authorshipMap.set(rangeId, entry);
69584
+ }, MCP_ORIGIN);
69585
+ }
69586
+ }
69466
69587
  return mcpSuccess({ edited: true, from: from3, to, newTextLength: newText.length });
69467
69588
  }
69468
69589
  )
@@ -69476,55 +69597,44 @@ function registerDocumentTools(server) {
69476
69597
  withErrorBoundary("tandem_save", async ({ documentId }) => {
69477
69598
  const r = requireDocument(documentId);
69478
69599
  if (!r) return noDocumentError();
69479
- try {
69480
- const docState = getCurrentDoc(documentId);
69481
- const format = docState?.format ?? "txt";
69482
- const readOnly = docState?.readOnly ?? false;
69483
- if (docState?.source === "upload") {
69484
- await saveSession(r.filePath, format, r.doc);
69485
- return mcpSuccess({
69486
- saved: true,
69487
- sessionOnly: true,
69488
- filePath: r.filePath,
69489
- message: "Session saved (annotations preserved). This file was uploaded \u2014 no disk path to save to."
69490
- });
69491
- }
69492
- const adapter = getAdapter(format);
69493
- if (readOnly || !adapter.canSave) {
69494
- await saveSession(r.filePath, format, r.doc);
69495
- return mcpSuccess({
69496
- saved: true,
69497
- sessionOnly: true,
69498
- filePath: r.filePath,
69499
- message: "Session saved (annotations preserved). Source file unchanged \u2014 document is read-only."
69500
- });
69501
- }
69502
- const output = adapter.save(r.doc);
69503
- suppressNextChange(r.filePath);
69504
- await atomicWrite(r.filePath, output);
69600
+ const docState = getCurrentDoc(documentId);
69601
+ const format = docState?.format ?? "txt";
69602
+ const readOnly = docState?.readOnly ?? false;
69603
+ if (docState?.source === "upload") {
69505
69604
  await saveSession(r.filePath, format, r.doc);
69506
- const meta2 = r.doc.getMap(Y_MAP_DOCUMENT_META);
69507
- r.doc.transact(() => meta2.set(Y_MAP_SAVED_AT_VERSION, Date.now()), MCP_ORIGIN);
69605
+ return mcpSuccess({
69606
+ saved: true,
69607
+ sessionOnly: true,
69608
+ filePath: r.filePath,
69609
+ message: "Session saved (annotations preserved). This file was uploaded \u2014 no disk path to save to."
69610
+ });
69611
+ }
69612
+ if (readOnly) {
69613
+ await saveSession(r.filePath, format, r.doc);
69614
+ return mcpSuccess({
69615
+ saved: true,
69616
+ sessionOnly: true,
69617
+ filePath: r.filePath,
69618
+ message: "Session saved (annotations preserved). Source file unchanged \u2014 document is read-only."
69619
+ });
69620
+ }
69621
+ const result2 = await saveDocumentToDisk(r.docId, "mcp");
69622
+ if (result2.status === "saved") {
69508
69623
  return mcpSuccess({ saved: true, filePath: r.filePath });
69509
- } catch (err) {
69510
- const errCode = err.code;
69511
- const msg = getErrorMessage(err);
69512
- pushNotification({
69513
- id: generateNotificationId(),
69514
- type: "save-error",
69515
- severity: "error",
69516
- message: `Save failed: ${msg}`,
69517
- toolName: "tandem_save",
69518
- errorCode: errCode ?? "UNKNOWN",
69519
- documentId: r.docId,
69520
- dedupKey: `save:${r.docId}`,
69521
- timestamp: Date.now()
69624
+ }
69625
+ if (result2.status === "skipped") {
69626
+ await saveSession(r.filePath, format, r.doc);
69627
+ return mcpSuccess({
69628
+ saved: true,
69629
+ sessionOnly: true,
69630
+ filePath: r.filePath,
69631
+ message: `Session saved. Disk save skipped: ${result2.reason}`
69522
69632
  });
69523
- if (errCode === "EACCES" || errCode === "EPERM") {
69524
- return mcpError("FILE_LOCKED", msg);
69525
- }
69526
- return mcpError("FORMAT_ERROR", `Save failed: ${msg}`);
69527
69633
  }
69634
+ if (result2.errorCode === "EACCES" || result2.errorCode === "EPERM") {
69635
+ return mcpError("FILE_LOCKED", result2.reason ?? "Save failed");
69636
+ }
69637
+ return mcpError("FORMAT_ERROR", result2.reason ?? "Save failed");
69528
69638
  })
69529
69639
  );
69530
69640
  server.tool(
@@ -69640,9 +69750,6 @@ var init_document2 = __esm({
69640
69750
  init_types3();
69641
69751
  init_utils();
69642
69752
  init_queue();
69643
- init_file_io();
69644
- init_file_watcher();
69645
- init_notifications();
69646
69753
  init_positions2();
69647
69754
  init_manager();
69648
69755
  init_provider();
@@ -69665,6 +69772,47 @@ function getDocAndAnnotations(documentId) {
69665
69772
  const ydoc = getOrCreateDocument(doc.docName);
69666
69773
  return { ydoc, map: ydoc.getMap(Y_MAP_ANNOTATIONS) };
69667
69774
  }
69775
+ function getRepliesMap(ydoc) {
69776
+ return ydoc.getMap(Y_MAP_ANNOTATION_REPLIES);
69777
+ }
69778
+ function collectRepliesForAnnotation(repliesMap, annotationId) {
69779
+ const replies = [];
69780
+ repliesMap.forEach((value) => {
69781
+ const reply = value;
69782
+ if (reply && typeof reply === "object" && reply.annotationId === annotationId) {
69783
+ replies.push(reply);
69784
+ }
69785
+ });
69786
+ replies.sort((a, b) => a.timestamp - b.timestamp);
69787
+ return replies;
69788
+ }
69789
+ function addReplyToAnnotation(ydoc, annotationsMap, annotationId, text5, author, origin) {
69790
+ const raw = annotationsMap.get(annotationId);
69791
+ if (!raw) return { ok: false, error: `Annotation ${annotationId} not found`, code: "NOT_FOUND" };
69792
+ const ann = sanitizeAnnotation(raw);
69793
+ if (ann.status !== "pending") {
69794
+ return {
69795
+ ok: false,
69796
+ error: `Cannot reply to a ${ann.status} annotation`,
69797
+ code: "ANNOTATION_RESOLVED"
69798
+ };
69799
+ }
69800
+ const replyId = generateReplyId();
69801
+ const reply = {
69802
+ id: replyId,
69803
+ annotationId,
69804
+ author,
69805
+ text: text5,
69806
+ timestamp: Date.now()
69807
+ };
69808
+ const repliesMap = getRepliesMap(ydoc);
69809
+ if (origin) {
69810
+ ydoc.transact(() => repliesMap.set(replyId, reply), origin);
69811
+ } else {
69812
+ ydoc.transact(() => repliesMap.set(replyId, reply));
69813
+ }
69814
+ return { ok: true, replyId };
69815
+ }
69668
69816
  function rangeFailureMessage(result2) {
69669
69817
  if (result2.code === "RANGE_GONE") return "Target text no longer exists in the document.";
69670
69818
  if (result2.code === "RANGE_MOVED") return "Target text has moved.";
@@ -69906,7 +70054,15 @@ function registerAnnotationTools(server) {
69906
70054
  if (author) results = results.filter((a) => a.author === author);
69907
70055
  if (type2) results = results.filter((a) => a.type === type2);
69908
70056
  if (status) results = results.filter((a) => a.status === status);
69909
- return mcpSuccess({ annotations: results, count: results.length });
70057
+ const repliesMap = getRepliesMap(da.ydoc);
70058
+ const annotationsWithReplies = results.map((ann) => ({
70059
+ ...ann,
70060
+ replies: collectRepliesForAnnotation(repliesMap, ann.id)
70061
+ }));
70062
+ return mcpSuccess({
70063
+ annotations: annotationsWithReplies,
70064
+ count: annotationsWithReplies.length
70065
+ });
69910
70066
  })
69911
70067
  );
69912
70068
  server.tool(
@@ -69942,7 +70098,16 @@ function registerAnnotationTools(server) {
69942
70098
  const da = getDocAndAnnotations(documentId);
69943
70099
  if (!da) return noDocumentError();
69944
70100
  if (!da.map.has(id2)) return mcpError("INVALID_RANGE", `Annotation ${id2} not found`);
69945
- da.ydoc.transact(() => da.map.delete(id2), MCP_ORIGIN);
70101
+ da.ydoc.transact(() => {
70102
+ da.map.delete(id2);
70103
+ const repliesMap = getRepliesMap(da.ydoc);
70104
+ const toDelete = [];
70105
+ repliesMap.forEach((value, key) => {
70106
+ const reply = value;
70107
+ if (reply && reply.annotationId === id2) toDelete.push(key);
70108
+ });
70109
+ for (const key of toDelete) repliesMap.delete(key);
70110
+ }, MCP_ORIGIN);
69946
70111
  return mcpSuccess({ removed: true, id: id2 });
69947
70112
  })
69948
70113
  );
@@ -70008,10 +70173,12 @@ function registerAnnotationTools(server) {
70008
70173
  if (!da) return noDocumentError();
70009
70174
  const annotations = refreshAllRanges(collectAnnotations(da.map), da.ydoc, da.map);
70010
70175
  const { ydoc } = da;
70176
+ const repliesMap = getRepliesMap(ydoc);
70011
70177
  if (format === "json") {
70012
70178
  const fullText = extractText(ydoc);
70013
70179
  const enriched = annotations.map((ann) => ({
70014
70180
  ...ann,
70181
+ replies: collectRepliesForAnnotation(repliesMap, ann.id),
70015
70182
  textSnippet: fullText.slice(
70016
70183
  Math.max(0, ann.range.from),
70017
70184
  Math.min(fullText.length, ann.range.to)
@@ -70023,6 +70190,32 @@ function registerAnnotationTools(server) {
70023
70190
  return mcpSuccess({ markdown, count: annotations.length });
70024
70191
  })
70025
70192
  );
70193
+ server.tool(
70194
+ "tandem_annotationReply",
70195
+ "Reply to an annotation thread. Only works on pending annotations.",
70196
+ {
70197
+ annotationId: external_exports.string().describe("The annotation ID to reply to"),
70198
+ text: external_exports.string().describe("Reply text"),
70199
+ documentId: external_exports.string().optional().describe("Target document ID (defaults to active document)")
70200
+ },
70201
+ withErrorBoundary("tandem_annotationReply", async ({ annotationId, text: text5, documentId }) => {
70202
+ const da = getDocAndAnnotations(documentId);
70203
+ if (!da) return noDocumentError();
70204
+ const result2 = addReplyToAnnotation(
70205
+ da.ydoc,
70206
+ da.map,
70207
+ annotationId,
70208
+ text5,
70209
+ "claude",
70210
+ MCP_ORIGIN
70211
+ );
70212
+ if (!result2.ok) {
70213
+ const code3 = result2.code === "NOT_FOUND" ? "NOT_FOUND" : result2.code === "ANNOTATION_RESOLVED" ? "ANNOTATION_RESOLVED" : "INVALID_RANGE";
70214
+ return mcpError(code3, result2.error);
70215
+ }
70216
+ return mcpSuccess({ replyId: result2.replyId, annotationId });
70217
+ })
70218
+ );
70026
70219
  }
70027
70220
  var SNAPSHOT_CAP;
70028
70221
  var init_annotations = __esm({
@@ -70074,6 +70267,8 @@ function getTrackableId(event) {
70074
70267
  case "annotation:accepted":
70075
70268
  case "annotation:dismissed":
70076
70269
  return event.payload.annotationId;
70270
+ case "annotation:reply":
70271
+ return event.payload.replyId;
70077
70272
  case "chat:message":
70078
70273
  return event.payload.messageId;
70079
70274
  default:
@@ -70182,6 +70377,32 @@ function attachObservers(docName, doc) {
70182
70377
  };
70183
70378
  annotationsMap.observe(annotationsObs);
70184
70379
  cleanups.push(() => annotationsMap.unobserve(annotationsObs));
70380
+ const repliesMap = doc.getMap(Y_MAP_ANNOTATION_REPLIES);
70381
+ const repliesObs = (event, txn) => {
70382
+ if (txn.origin === MCP_ORIGIN) return;
70383
+ for (const [key, change] of event.changes.keys) {
70384
+ if (change.action !== "add") continue;
70385
+ const reply = repliesMap.get(key);
70386
+ if (!reply || reply.author !== "user") continue;
70387
+ const parentAnn = annotationsMap.get(reply.annotationId);
70388
+ const textSnippet = parentAnn?.textSnapshot ?? "";
70389
+ pushEvent({
70390
+ id: generateEventId(),
70391
+ type: "annotation:reply",
70392
+ timestamp: Date.now(),
70393
+ documentId: docName,
70394
+ payload: {
70395
+ annotationId: reply.annotationId,
70396
+ replyId: reply.id,
70397
+ replyText: reply.text,
70398
+ replyAuthor: reply.author,
70399
+ textSnippet
70400
+ }
70401
+ });
70402
+ }
70403
+ };
70404
+ repliesMap.observe(repliesObs);
70405
+ cleanups.push(() => repliesMap.unobserve(repliesObs));
70185
70406
  const userAwareness = doc.getMap(Y_MAP_USER_AWARENESS);
70186
70407
  let selectionDwellTimer = null;
70187
70408
  const awarenessObs = (event, txn) => {
@@ -70192,19 +70413,16 @@ function attachObservers(docName, doc) {
70192
70413
  clearTimeout(selectionDwellTimer);
70193
70414
  selectionDwellTimer = null;
70194
70415
  }
70195
- if (!selection || selection.from === selection.to) return;
70416
+ if (!selection || selection.from === selection.to) {
70417
+ selectionBuffer.delete(docName);
70418
+ return;
70419
+ }
70196
70420
  selectionDwellTimer = setTimeout(() => {
70197
70421
  selectionDwellTimer = null;
70198
- pushEvent({
70199
- id: generateEventId(),
70200
- type: "selection:changed",
70201
- timestamp: Date.now(),
70202
- documentId: docName,
70203
- payload: {
70204
- from: selection.from,
70205
- to: selection.to,
70206
- selectedText: selection.selectedText ?? ""
70207
- }
70422
+ selectionBuffer.set(docName, {
70423
+ from: selection.from,
70424
+ to: selection.to,
70425
+ selectedText: selection.selectedText ?? ""
70208
70426
  });
70209
70427
  }, getDwellMs());
70210
70428
  }
@@ -70213,6 +70431,7 @@ function attachObservers(docName, doc) {
70213
70431
  cleanups.push(() => {
70214
70432
  userAwareness.unobserve(awarenessObs);
70215
70433
  if (selectionDwellTimer) clearTimeout(selectionDwellTimer);
70434
+ selectionBuffer.delete(docName);
70216
70435
  });
70217
70436
  docObservers.set(docName, cleanups);
70218
70437
  console.error(`[EventQueue] Attached observers for document: ${docName}`);
@@ -70239,6 +70458,32 @@ function attachCtrlObservers() {
70239
70458
  if (change.action !== "add") continue;
70240
70459
  const msg = chatMap.get(key);
70241
70460
  if (!msg || msg.author !== "user") continue;
70461
+ let selection;
70462
+ if (msg.documentId) {
70463
+ const buffered = selectionBuffer.get(msg.documentId);
70464
+ if (buffered) {
70465
+ selectionBuffer.delete(msg.documentId);
70466
+ try {
70467
+ const doc = getOrCreateDocument(msg.documentId);
70468
+ const validation = validateRange(
70469
+ doc,
70470
+ buffered.from,
70471
+ buffered.to
70472
+ );
70473
+ if (validation.ok) {
70474
+ selection = buffered;
70475
+ } else {
70476
+ selection = { selectedText: buffered.selectedText };
70477
+ }
70478
+ } catch (err) {
70479
+ console.warn(
70480
+ `[EventQueue] Failed to validate buffered selection for doc=${msg.documentId}:`,
70481
+ err
70482
+ );
70483
+ selection = { selectedText: buffered.selectedText };
70484
+ }
70485
+ }
70486
+ }
70242
70487
  pushEvent({
70243
70488
  id: generateEventId(),
70244
70489
  type: "chat:message",
@@ -70248,7 +70493,8 @@ function attachCtrlObservers() {
70248
70493
  messageId: msg.id,
70249
70494
  text: msg.text,
70250
70495
  replyTo: msg.replyTo ?? null,
70251
- anchor: msg.anchor ?? null
70496
+ anchor: msg.anchor ?? null,
70497
+ ...selection ? { selection } : {}
70252
70498
  }
70253
70499
  });
70254
70500
  }
@@ -70317,17 +70563,19 @@ function attachCtrlObservers() {
70317
70563
  function reattachCtrlObservers() {
70318
70564
  attachCtrlObservers();
70319
70565
  }
70320
- var MCP_ORIGIN, docObservers, emittedPayloadIds, buffer2, subscribers2, ctrlCleanups;
70566
+ var MCP_ORIGIN, docObservers, selectionBuffer, emittedPayloadIds, buffer2, subscribers2, ctrlCleanups;
70321
70567
  var init_queue = __esm({
70322
70568
  "src/server/events/queue.ts"() {
70323
70569
  "use strict";
70324
70570
  init_constants();
70325
70571
  init_annotations();
70326
70572
  init_document_service();
70573
+ init_positions2();
70327
70574
  init_provider();
70328
70575
  init_types4();
70329
70576
  MCP_ORIGIN = "mcp";
70330
70577
  docObservers = /* @__PURE__ */ new Map();
70578
+ selectionBuffer = /* @__PURE__ */ new Map();
70331
70579
  emittedPayloadIds = /* @__PURE__ */ new Map();
70332
70580
  buffer2 = [];
70333
70581
  subscribers2 = /* @__PURE__ */ new Set();
@@ -75722,10 +75970,10 @@ var require_raw_body = __commonJS({
75722
75970
  if (done) {
75723
75971
  return readStream(stream, encoding, length3, limit, wrap3(done));
75724
75972
  }
75725
- return new Promise(function executor(resolve2, reject2) {
75973
+ return new Promise(function executor(resolve3, reject2) {
75726
75974
  readStream(stream, encoding, length3, limit, function onRead(err, buf) {
75727
75975
  if (err) return reject2(err);
75728
- resolve2(buf);
75976
+ resolve3(buf);
75729
75977
  });
75730
75978
  });
75731
75979
  }
@@ -89060,12 +89308,12 @@ var require_view = __commonJS({
89060
89308
  "use strict";
89061
89309
  var debug = require_src()("express:view");
89062
89310
  var path13 = __require("path");
89063
- var fs8 = __require("fs");
89064
- var dirname3 = path13.dirname;
89065
- var basename = path13.basename;
89311
+ var fs9 = __require("fs");
89312
+ var dirname4 = path13.dirname;
89313
+ var basename2 = path13.basename;
89066
89314
  var extname = path13.extname;
89067
89315
  var join4 = path13.join;
89068
- var resolve2 = path13.resolve;
89316
+ var resolve3 = path13.resolve;
89069
89317
  module3.exports = View;
89070
89318
  function View(name2, options) {
89071
89319
  var opts = options || {};
@@ -89099,9 +89347,9 @@ var require_view = __commonJS({
89099
89347
  debug('lookup "%s"', name2);
89100
89348
  for (var i = 0; i < roots.length && !path14; i++) {
89101
89349
  var root3 = roots[i];
89102
- var loc = resolve2(root3, name2);
89103
- var dir = dirname3(loc);
89104
- var file = basename(loc);
89350
+ var loc = resolve3(root3, name2);
89351
+ var dir = dirname4(loc);
89352
+ var file = basename2(loc);
89105
89353
  path14 = this.resolve(dir, file);
89106
89354
  }
89107
89355
  return path14;
@@ -89124,14 +89372,14 @@ var require_view = __commonJS({
89124
89372
  });
89125
89373
  sync = false;
89126
89374
  };
89127
- View.prototype.resolve = function resolve3(dir, file) {
89375
+ View.prototype.resolve = function resolve4(dir, file) {
89128
89376
  var ext = this.ext;
89129
89377
  var path14 = join4(dir, file);
89130
89378
  var stat = tryStat(path14);
89131
89379
  if (stat && stat.isFile()) {
89132
89380
  return path14;
89133
89381
  }
89134
- path14 = join4(dir, basename(file, ext), "index" + ext);
89382
+ path14 = join4(dir, basename2(file, ext), "index" + ext);
89135
89383
  stat = tryStat(path14);
89136
89384
  if (stat && stat.isFile()) {
89137
89385
  return path14;
@@ -89140,7 +89388,7 @@ var require_view = __commonJS({
89140
89388
  function tryStat(path14) {
89141
89389
  debug('stat "%s"', path14);
89142
89390
  try {
89143
- return fs8.statSync(path14);
89391
+ return fs9.statSync(path14);
89144
89392
  } catch (e) {
89145
89393
  return void 0;
89146
89394
  }
@@ -91307,7 +91555,7 @@ var require_application = __commonJS({
91307
91555
  var compileETag = require_utils4().compileETag;
91308
91556
  var compileQueryParser = require_utils4().compileQueryParser;
91309
91557
  var compileTrust = require_utils4().compileTrust;
91310
- var resolve2 = __require("path").resolve;
91558
+ var resolve3 = __require("path").resolve;
91311
91559
  var once = require_once();
91312
91560
  var Router = require_router();
91313
91561
  var slice2 = Array.prototype.slice;
@@ -91361,7 +91609,7 @@ var require_application = __commonJS({
91361
91609
  this.mountpath = "/";
91362
91610
  this.locals.settings = this.settings;
91363
91611
  this.set("view", View);
91364
- this.set("views", resolve2("views"));
91612
+ this.set("views", resolve3("views"));
91365
91613
  this.set("jsonp callback name", "callback");
91366
91614
  if (env3 === "production") {
91367
91615
  this.enable("view cache");
@@ -92457,7 +92705,7 @@ var require_content_disposition = __commonJS({
92457
92705
  "use strict";
92458
92706
  module3.exports = contentDisposition;
92459
92707
  module3.exports.parse = parse4;
92460
- var basename = __require("path").basename;
92708
+ var basename2 = __require("path").basename;
92461
92709
  var ENCODE_URL_ATTR_CHAR_REGEXP = /[\x00-\x20"'()*,/:;<=>?@[\\\]{}\x7f]/g;
92462
92710
  var HEX_ESCAPE_REGEXP = /%[0-9A-Fa-f]{2}/;
92463
92711
  var HEX_ESCAPE_REPLACE_REGEXP = /%([0-9A-Fa-f]{2})/g;
@@ -92492,9 +92740,9 @@ var require_content_disposition = __commonJS({
92492
92740
  if (typeof fallback === "string" && NON_LATIN1_REGEXP.test(fallback)) {
92493
92741
  throw new TypeError("fallback must be ISO-8859-1 string");
92494
92742
  }
92495
- var name2 = basename(filename);
92743
+ var name2 = basename2(filename);
92496
92744
  var isQuotedString = TEXT_REGEXP.test(name2);
92497
- var fallbackName = typeof fallback !== "string" ? fallback && getlatin1(name2) : basename(fallback);
92745
+ var fallbackName = typeof fallback !== "string" ? fallback && getlatin1(name2) : basename2(fallback);
92498
92746
  var hasFallback = typeof fallbackName === "string" && fallbackName !== name2;
92499
92747
  if (hasFallback || !isQuotedString || HEX_ESCAPE_REGEXP.test(name2)) {
92500
92748
  params2["filename*"] = name2;
@@ -92809,7 +93057,7 @@ var require_send = __commonJS({
92809
93057
  var escapeHtml = require_escape_html();
92810
93058
  var etag = require_etag();
92811
93059
  var fresh = require_fresh();
92812
- var fs8 = __require("fs");
93060
+ var fs9 = __require("fs");
92813
93061
  var mime = require_mime_types();
92814
93062
  var ms = require_ms();
92815
93063
  var onFinished = require_on_finished();
@@ -92821,7 +93069,7 @@ var require_send = __commonJS({
92821
93069
  var extname = path13.extname;
92822
93070
  var join4 = path13.join;
92823
93071
  var normalize = path13.normalize;
92824
- var resolve2 = path13.resolve;
93072
+ var resolve3 = path13.resolve;
92825
93073
  var sep = path13.sep;
92826
93074
  var BYTES_RANGE_REGEXP = /^ *bytes=/;
92827
93075
  var MAX_MAXAGE = 60 * 60 * 24 * 365 * 1e3;
@@ -92850,7 +93098,7 @@ var require_send = __commonJS({
92850
93098
  this._maxage = opts.maxAge || opts.maxage;
92851
93099
  this._maxage = typeof this._maxage === "string" ? ms(this._maxage) : Number(this._maxage);
92852
93100
  this._maxage = !isNaN(this._maxage) ? Math.min(Math.max(0, this._maxage), MAX_MAXAGE) : 0;
92853
- this._root = opts.root ? resolve2(opts.root) : null;
93101
+ this._root = opts.root ? resolve3(opts.root) : null;
92854
93102
  }
92855
93103
  util2.inherits(SendStream, Stream);
92856
93104
  SendStream.prototype.error = function error2(status, err) {
@@ -92999,7 +93247,7 @@ var require_send = __commonJS({
92999
93247
  return res;
93000
93248
  }
93001
93249
  parts = normalize(path14).split(sep);
93002
- path14 = resolve2(path14);
93250
+ path14 = resolve3(path14);
93003
93251
  }
93004
93252
  if (containsDotFile(parts)) {
93005
93253
  debug('%s dotfile "%s"', this._dotfiles, path14);
@@ -93091,7 +93339,7 @@ var require_send = __commonJS({
93091
93339
  var i = 0;
93092
93340
  var self2 = this;
93093
93341
  debug('stat "%s"', path14);
93094
- fs8.stat(path14, function onstat(err, stat) {
93342
+ fs9.stat(path14, function onstat(err, stat) {
93095
93343
  var pathEndsWithSep = path14[path14.length - 1] === sep;
93096
93344
  if (err && err.code === "ENOENT" && !extname(path14) && !pathEndsWithSep) {
93097
93345
  return next(err);
@@ -93108,7 +93356,7 @@ var require_send = __commonJS({
93108
93356
  }
93109
93357
  var p = path14 + "." + self2._extensions[i++];
93110
93358
  debug('stat "%s"', p);
93111
- fs8.stat(p, function(err2, stat) {
93359
+ fs9.stat(p, function(err2, stat) {
93112
93360
  if (err2) return next(err2);
93113
93361
  if (stat.isDirectory()) return next();
93114
93362
  self2.emit("file", p, stat);
@@ -93126,7 +93374,7 @@ var require_send = __commonJS({
93126
93374
  }
93127
93375
  var p = join4(path14, self2._index[i]);
93128
93376
  debug('stat "%s"', p);
93129
- fs8.stat(p, function(err2, stat) {
93377
+ fs9.stat(p, function(err2, stat) {
93130
93378
  if (err2) return next(err2);
93131
93379
  if (stat.isDirectory()) return next();
93132
93380
  self2.emit("file", p, stat);
@@ -93138,7 +93386,7 @@ var require_send = __commonJS({
93138
93386
  SendStream.prototype.stream = function stream(path14, options) {
93139
93387
  var self2 = this;
93140
93388
  var res = this.res;
93141
- var stream2 = fs8.createReadStream(path14, options);
93389
+ var stream2 = fs9.createReadStream(path14, options);
93142
93390
  this.emit("stream", stream2);
93143
93391
  stream2.pipe(res);
93144
93392
  function cleanup() {
@@ -93377,7 +93625,7 @@ var require_response = __commonJS({
93377
93625
  var cookie = require_cookie();
93378
93626
  var send = require_send();
93379
93627
  var extname = path13.extname;
93380
- var resolve2 = path13.resolve;
93628
+ var resolve3 = path13.resolve;
93381
93629
  var vary = require_vary();
93382
93630
  var { Buffer: Buffer2 } = __require("buffer");
93383
93631
  var res = Object.create(http.ServerResponse.prototype);
@@ -93583,7 +93831,7 @@ var require_response = __commonJS({
93583
93831
  }
93584
93832
  opts = Object.create(opts);
93585
93833
  opts.headers = headers;
93586
- var fullPath = !opts.root ? resolve2(path14) : path14;
93834
+ var fullPath = !opts.root ? resolve3(path14) : path14;
93587
93835
  return this.sendFile(fullPath, opts, done);
93588
93836
  };
93589
93837
  res.contentType = res.type = function contentType(type2) {
@@ -93832,7 +94080,7 @@ var require_serve_static = __commonJS({
93832
94080
  var encodeUrl = require_encodeurl();
93833
94081
  var escapeHtml = require_escape_html();
93834
94082
  var parseUrl = require_parseurl();
93835
- var resolve2 = __require("path").resolve;
94083
+ var resolve3 = __require("path").resolve;
93836
94084
  var send = require_send();
93837
94085
  var url = __require("url");
93838
94086
  module3.exports = serveStatic;
@@ -93851,7 +94099,7 @@ var require_serve_static = __commonJS({
93851
94099
  throw new TypeError("option setHeaders must be function");
93852
94100
  }
93853
94101
  opts.maxage = opts.maxage || opts.maxAge || 0;
93854
- opts.root = resolve2(root3);
94102
+ opts.root = resolve3(root3);
93855
94103
  var onDirectory = redirect ? createRedirectDirectoryListener() : createNotFoundDirectoryListener();
93856
94104
  return function serveStatic2(req, res, next) {
93857
94105
  if (req.method !== "GET" && req.method !== "HEAD") {
@@ -96928,7 +97176,7 @@ var require_compile = __commonJS({
96928
97176
  const schOrFunc = root3.refs[ref];
96929
97177
  if (schOrFunc)
96930
97178
  return schOrFunc;
96931
- let _sch = resolve2.call(this, root3, ref);
97179
+ let _sch = resolve3.call(this, root3, ref);
96932
97180
  if (_sch === void 0) {
96933
97181
  const schema = (_a = root3.localRefs) === null || _a === void 0 ? void 0 : _a[ref];
96934
97182
  const { schemaId } = this.opts;
@@ -96955,7 +97203,7 @@ var require_compile = __commonJS({
96955
97203
  function sameSchemaEnv(s1, s2) {
96956
97204
  return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
96957
97205
  }
96958
- function resolve2(root3, ref) {
97206
+ function resolve3(root3, ref) {
96959
97207
  let sch;
96960
97208
  while (typeof (sch = this.refs[ref]) == "string")
96961
97209
  ref = sch;
@@ -97530,7 +97778,7 @@ var require_fast_uri = __commonJS({
97530
97778
  }
97531
97779
  return uri;
97532
97780
  }
97533
- function resolve2(baseURI, relativeURI, options) {
97781
+ function resolve3(baseURI, relativeURI, options) {
97534
97782
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
97535
97783
  const resolved = resolveComponent(parse4(baseURI, schemelessOptions), parse4(relativeURI, schemelessOptions), schemelessOptions, true);
97536
97784
  schemelessOptions.skipEscape = true;
@@ -97757,7 +98005,7 @@ var require_fast_uri = __commonJS({
97757
98005
  var fastUri = {
97758
98006
  SCHEMES,
97759
98007
  normalize,
97760
- resolve: resolve2,
98008
+ resolve: resolve3,
97761
98009
  resolveComponent,
97762
98010
  equal,
97763
98011
  serialize: serialize2,
@@ -100733,12 +100981,12 @@ var require_dist3 = __commonJS({
100733
100981
  throw new Error(`Unknown format "${name2}"`);
100734
100982
  return f;
100735
100983
  };
100736
- function addFormats(ajv, list4, fs8, exportName) {
100984
+ function addFormats(ajv, list4, fs9, exportName) {
100737
100985
  var _a;
100738
100986
  var _b;
100739
100987
  (_a = (_b = ajv.opts.code).formats) !== null && _a !== void 0 ? _a : _b.formats = (0, codegen_1._)`require("ajv-formats/dist/formats").${exportName}`;
100740
100988
  for (const f of list4)
100741
- ajv.addFormat(f, fs8[f]);
100989
+ ajv.addFormat(f, fs9[f]);
100742
100990
  }
100743
100991
  module3.exports = exports3 = formatsPlugin;
100744
100992
  Object.defineProperty(exports3, "__esModule", { value: true });
@@ -100838,7 +101086,7 @@ var init_launcher = __esm({
100838
101086
  // src/server/index.ts
100839
101087
  init_constants();
100840
101088
  import path12 from "path";
100841
- import { fileURLToPath as fileURLToPath4 } from "url";
101089
+ import { fileURLToPath as fileURLToPath5 } from "url";
100842
101090
 
100843
101091
  // src/server/error-filter.ts
100844
101092
  function isKnownHocuspocusError(err) {
@@ -100866,8 +101114,8 @@ init_file_opener();
100866
101114
 
100867
101115
  // src/server/mcp/server.ts
100868
101116
  import { existsSync as existsSync2 } from "fs";
100869
- import { dirname as dirname2, join as join3 } from "path";
100870
- import { fileURLToPath as fileURLToPath3 } from "url";
101117
+ import { dirname as dirname3, join as join3 } from "path";
101118
+ import { fileURLToPath as fileURLToPath4 } from "url";
100871
101119
 
100872
101120
  // node_modules/@modelcontextprotocol/sdk/dist/esm/server/express.js
100873
101121
  var import_express = __toESM(require_express2(), 1);
@@ -108970,7 +109218,7 @@ var Protocol = class {
108970
109218
  return;
108971
109219
  }
108972
109220
  const pollInterval = task2.pollInterval ?? this._options?.defaultTaskPollInterval ?? 1e3;
108973
- await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
109221
+ await new Promise((resolve3) => setTimeout(resolve3, pollInterval));
108974
109222
  options?.signal?.throwIfAborted();
108975
109223
  }
108976
109224
  } catch (error2) {
@@ -108987,7 +109235,7 @@ var Protocol = class {
108987
109235
  */
108988
109236
  request(request, resultSchema, options) {
108989
109237
  const { relatedRequestId, resumptionToken, onresumptiontoken, task, relatedTask } = options ?? {};
108990
- return new Promise((resolve2, reject2) => {
109238
+ return new Promise((resolve3, reject2) => {
108991
109239
  const earlyReject = (error2) => {
108992
109240
  reject2(error2);
108993
109241
  };
@@ -109065,7 +109313,7 @@ var Protocol = class {
109065
109313
  if (!parseResult.success) {
109066
109314
  reject2(parseResult.error);
109067
109315
  } else {
109068
- resolve2(parseResult.data);
109316
+ resolve3(parseResult.data);
109069
109317
  }
109070
109318
  } catch (error2) {
109071
109319
  reject2(error2);
@@ -109326,12 +109574,12 @@ var Protocol = class {
109326
109574
  }
109327
109575
  } catch {
109328
109576
  }
109329
- return new Promise((resolve2, reject2) => {
109577
+ return new Promise((resolve3, reject2) => {
109330
109578
  if (signal.aborted) {
109331
109579
  reject2(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
109332
109580
  return;
109333
109581
  }
109334
- const timeoutId = setTimeout(resolve2, interval);
109582
+ const timeoutId = setTimeout(resolve3, interval);
109335
109583
  signal.addEventListener("abort", () => {
109336
109584
  clearTimeout(timeoutId);
109337
109585
  reject2(new McpError(ErrorCode.InvalidRequest, "Request cancelled"));
@@ -110432,7 +110680,7 @@ var McpServer = class {
110432
110680
  let task = createTaskResult.task;
110433
110681
  const pollInterval = task.pollInterval ?? 5e3;
110434
110682
  while (task.status !== "completed" && task.status !== "failed" && task.status !== "cancelled") {
110435
- await new Promise((resolve2) => setTimeout(resolve2, pollInterval));
110683
+ await new Promise((resolve3) => setTimeout(resolve3, pollInterval));
110436
110684
  const updatedTask = await extra.taskStore.getTask(taskId);
110437
110685
  if (!updatedTask) {
110438
110686
  throw new McpError(ErrorCode.InternalError, `Task ${taskId} not found during polling`);
@@ -111075,12 +111323,12 @@ var StdioServerTransport = class {
111075
111323
  this.onclose?.();
111076
111324
  }
111077
111325
  send(message) {
111078
- return new Promise((resolve2) => {
111326
+ return new Promise((resolve3) => {
111079
111327
  const json = serializeMessage(message);
111080
111328
  if (this._stdout.write(json)) {
111081
- resolve2();
111329
+ resolve3();
111082
111330
  } else {
111083
- this._stdout.once("drain", resolve2);
111331
+ this._stdout.once("drain", resolve3);
111084
111332
  }
111085
111333
  });
111086
111334
  }
@@ -111557,7 +111805,7 @@ var responseViaResponseObject = async (res, outgoing, options = {}) => {
111557
111805
  });
111558
111806
  if (!chunk2) {
111559
111807
  if (i === 1) {
111560
- await new Promise((resolve2) => setTimeout(resolve2));
111808
+ await new Promise((resolve3) => setTimeout(resolve3));
111561
111809
  maxReadCount = 3;
111562
111810
  continue;
111563
111811
  }
@@ -112057,9 +112305,9 @@ data:
112057
112305
  const initRequest = messages.find((m) => isInitializeRequest(m));
112058
112306
  const clientProtocolVersion = initRequest ? initRequest.params.protocolVersion : req.headers.get("mcp-protocol-version") ?? DEFAULT_NEGOTIATED_PROTOCOL_VERSION;
112059
112307
  if (this._enableJsonResponse) {
112060
- return new Promise((resolve2) => {
112308
+ return new Promise((resolve3) => {
112061
112309
  this._streamMapping.set(streamId, {
112062
- resolveJson: resolve2,
112310
+ resolveJson: resolve3,
112063
112311
  cleanup: () => {
112064
112312
  this._streamMapping.delete(streamId);
112065
112313
  }
@@ -112422,124 +112670,37 @@ init_annotations();
112422
112670
  // src/cli/setup.ts
112423
112671
  init_constants();
112424
112672
  import { randomUUID as randomUUID4 } from "crypto";
112425
- import { existsSync, readFileSync } from "fs";
112673
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
112426
112674
  import { copyFile, mkdir, rename, unlink, writeFile } from "fs/promises";
112427
112675
  import { homedir as homedir2 } from "os";
112428
- import { dirname, join as join2, resolve } from "path";
112429
- import { fileURLToPath as fileURLToPath2 } from "url";
112676
+ import { basename, dirname as dirname2, join as join2, resolve as resolve2 } from "path";
112677
+ import { fileURLToPath as fileURLToPath3 } from "url";
112430
112678
 
112431
112679
  // src/cli/skill-content.ts
112432
- var SKILL_CONTENT = `---
112433
- name: tandem
112434
- description: >
112435
- Use when tandem_* MCP tools are available, the user asks about Tandem
112436
- document editing, or collaborative document review. Provides workflow
112437
- guidance, annotation strategy, and tool usage patterns for the Tandem
112438
- collaborative editor.
112439
- ---
112440
-
112441
- # Tandem \u2014 Collaborative Document Editor
112442
-
112443
- Tandem lets you annotate and edit documents alongside the user in real time. The user sees your changes in a browser editor; you interact via the tandem_* MCP tool suite.
112444
-
112445
- ## Hard Rules
112446
-
112447
- These prevent the most common failures. Follow them always.
112448
-
112449
- 1. **Resolve before mutating.** Call \`tandem_resolveRange\` (or \`tandem_search\`) to get offsets before calling \`tandem_edit\`, \`tandem_highlight\`, \`tandem_comment\`, \`tandem_suggest\`, or \`tandem_flag\`. Never compute offsets by counting characters in previously-read text \u2014 they go stale when the user edits.
112450
- 2. **Pass \`textSnapshot\`.** Include the matched text as \`textSnapshot\` on mutations and annotations. If the text moved, the server returns \`RANGE_MOVED\` with relocated coordinates instead of corrupting the document.
112451
- 3. **Use \`tandem_getTextContent\`, not \`tandem_getContent\`.** \`getContent\` returns ProseMirror JSON and burns tokens. Use \`getTextContent({ section: "Section Name" })\` for targeted reads. The \`section\` parameter is case-insensitive.
112452
- 4. **\`tandem_edit\` cannot create paragraphs.** Newlines become literal characters. For multi-paragraph changes, use multiple \`tandem_edit\` calls or \`tandem_suggest\`.
112453
- 5. **\`.docx\` files are read-only.** Use annotations instead of \`tandem_edit\`. Offer \`tandem_convertToMarkdown\` if the user wants an editable copy.
112454
-
112455
- ## Workflow
112456
-
112457
- Standard review sequence:
112458
-
112459
- 1. \`tandem_status\` \u2014 check for already-open documents (sessions restore automatically)
112460
- 2. \`tandem_getOutline\` \u2014 understand document structure
112461
- 3. \`tandem_setStatus("Reviewing [section]...", { focusParagraph: N })\` \u2014 show progress (use \`index\` from outline)
112462
- 4. \`tandem_getTextContent({ section: "..." })\` \u2014 read one section at a time
112463
- 5. Annotate findings (see annotation guide below)
112464
- 6. \`tandem_checkInbox\` \u2014 check for user messages and actions
112465
- 7. Repeat steps 3-6 for each section
112466
- 8. \`tandem_save\` \u2014 persist edits to disk when done
112467
-
112468
- ## Annotation Guide
112469
-
112470
- Choose the right type for each finding:
112471
-
112472
- - **\`tandem_highlight\`** \u2014 Visual marker with a short note. Colors: green (verified/good), red (problem), yellow (needs attention). Use when the finding is self-evident from the color and a brief note.
112473
- - **\`tandem_comment\`** \u2014 Observation requiring explanation. Use when you need more than one sentence to convey reasoning.
112474
- - **\`tandem_suggest\`** \u2014 Specific text replacement. **Prefer over comment when you can provide replacement text** \u2014 the user gets one-click accept/reject. Cannot create new paragraphs.
112475
- - **\`tandem_flag\`** \u2014 Factual errors, compliance risks, missing required content. Signals a blocking issue the user must address before the document ships.
112476
-
112477
- **User-created types:** \`question\` annotation is created by users, not Claude. When you see a \`question\` in \`tandem_checkInbox\` or \`tandem_getAnnotations\`, respond with a \`tandem_comment\` on the same range or \`tandem_reply\` for conversational answers.
112478
-
112479
- ## Collaboration Mode
112480
-
112481
- Check \`mode\` from \`tandem_status\` or \`tandem_checkInbox\` and adapt:
112482
-
112483
- - **Tandem** (\`"tandem"\`, default) \u2014 Full collaboration. Annotate freely and react to selections and document changes.
112484
- - **Solo** (\`"solo"\`) \u2014 The user wants to write undisturbed. Only respond when the user sends a chat message. Do not proactively annotate or react to document activity.
112485
-
112486
- ## Reacting to Document Events
112487
-
112488
- Selection events can reach you two ways. Over the real-time channel they arrive as notifications with \`meta.respond_via = "tandem_reply"\`. When polling via \`tandem_checkInbox\`, the current selection shows up under \`activity.selectedText\` (no \`meta\` field \u2014 that only exists on channel pushes). Either way, when the user holds a selection, briefly acknowledge what they highlighted via \`tandem_reply\` \u2014 don't annotate unless asked. Use \`tandem_reply\` for any document-context reaction (chat messages, selections, question annotations); reserve terminal output for non-document work the user explicitly requests. In Solo mode, hold reactions until the user sends a chat message.
112489
-
112490
- ## Collaboration Etiquette
112491
-
112492
- - Check \`tandem_getActivity()\` before annotating near the user's cursor. If \`isTyping\` is true, wait for typing to stop before annotating that area.
112493
- - Use \`tandem_setStatus\` to show what you're working on \u2014 the user sees it in the browser status bar.
112494
- - **Call \`tandem_checkInbox\` every 2-3 tool calls**, not just at the end of a task. The real-time channel is often not connected; polling is the reliable path.
112495
- - Reply to chat messages with \`tandem_reply\`, not annotations.
112496
-
112497
- ## .docx Review Workflow
112498
-
112499
- 1. \`tandem_open\` \u2014 opens in read-only mode (\`readOnly: true\`)
112500
- 2. \`tandem_getAnnotations({ author: "import" })\` \u2014 check for imported Word comments; read and act on them
112501
- 3. Annotate with findings (highlight, comment, suggest, flag)
112502
- 4. \`tandem_exportAnnotations\` \u2014 generate a review summary the user can share
112503
- 5. If the user wants editable text, offer \`tandem_convertToMarkdown\`
112504
-
112505
- ## Error Recovery
112506
-
112507
- - **\`RANGE_MOVED\`** \u2014 Text shifted since you read it. The response includes \`resolvedFrom\`/\`resolvedTo\` \u2014 use those coordinates for your next call.
112508
- - **\`RANGE_GONE\`** \u2014 The text was deleted. Re-read the section with \`tandem_getTextContent\` and re-assess.
112509
- - **\`INVALID_RANGE\`** \u2014 You hit heading markup (e.g., \`## \`). Target text content only, not the heading prefix.
112510
- - **\`FORMAT_ERROR\`** \u2014 Attempted \`tandem_edit\` on a read-only \`.docx\`. Use annotations instead.
112511
-
112512
- ## Session Handoff
112513
-
112514
- When starting a new Claude session with Tandem already running:
112515
-
112516
- 1. \`tandem_status()\` \u2014 check \`openDocuments\` array for restored sessions
112517
- 2. \`tandem_listDocuments()\` \u2014 see all open docs with details
112518
- 3. \`tandem_getOutline()\` \u2014 orient on the active document
112519
- 4. \`tandem_getAnnotations()\` \u2014 see what was already reviewed
112520
- 5. Continue where the previous session left off
112521
-
112522
- ## Multi-Document
112523
-
112524
- When multiple documents are open, always pass \`documentId\` explicitly \u2014 omitting it targets the active document, which may have changed since your last call. Use \`tandem_listDocuments\` to see what's available. Cross-reference by reading both docs via \`tandem_getTextContent({ documentId: "..." })\` and annotating the relevant one.
112525
- `;
112680
+ import { readFileSync } from "fs";
112681
+ import { dirname, resolve } from "path";
112682
+ import { fileURLToPath as fileURLToPath2 } from "url";
112683
+ var __dirname = dirname(fileURLToPath2(import.meta.url));
112684
+ var SKILL_PATH = resolve(__dirname, "../../skills/tandem/SKILL.md");
112685
+ var SKILL_CONTENT = readFileSync(SKILL_PATH, "utf-8");
112526
112686
 
112527
112687
  // src/cli/setup.ts
112528
- var __dirname = dirname(fileURLToPath2(import.meta.url));
112529
- var CHANNEL_DIST = resolve(__dirname, "../channel/index.js");
112688
+ var __dirname2 = dirname2(fileURLToPath3(import.meta.url));
112689
+ var PACKAGE_ROOT = resolve2(__dirname2, "../..");
112690
+ var CHANNEL_DIST = resolve2(PACKAGE_ROOT, "dist/channel/index.js");
112530
112691
  var MCP_URL = `http://localhost:${DEFAULT_MCP_PORT}`;
112531
- function buildMcpEntries(channelPath, nodeBinary = "node") {
112532
- return {
112533
- tandem: {
112534
- type: "http",
112535
- url: `${MCP_URL}/mcp`
112536
- },
112537
- "tandem-channel": {
112538
- command: nodeBinary,
112692
+ function buildMcpEntries(channelPath, opts = {}) {
112693
+ const entries = {
112694
+ tandem: { type: "http", url: `${MCP_URL}/mcp` }
112695
+ };
112696
+ if (opts.withChannelShim) {
112697
+ entries["tandem-channel"] = {
112698
+ command: opts.nodeBinary ?? "node",
112539
112699
  args: [channelPath],
112540
112700
  env: { TANDEM_URL: MCP_URL }
112541
- }
112542
- };
112701
+ };
112702
+ }
112703
+ return entries;
112543
112704
  }
112544
112705
  function detectTargets(opts = {}) {
112545
112706
  const home = opts.homeOverride ?? homedir2();
@@ -112570,7 +112731,7 @@ function detectTargets(opts = {}) {
112570
112731
  return targets;
112571
112732
  }
112572
112733
  async function atomicWrite3(content3, dest) {
112573
- const tmp = join2(dirname(dest), `.tandem-setup-${randomUUID4()}.tmp`);
112734
+ const tmp = join2(dirname2(dest), `.tandem-setup-${randomUUID4()}.tmp`);
112574
112735
  await writeFile(tmp, content3, "utf-8");
112575
112736
  try {
112576
112737
  await rename(tmp, dest);
@@ -112591,14 +112752,23 @@ async function atomicWrite3(content3, dest) {
112591
112752
  async function applyConfig(configPath, entries) {
112592
112753
  let existing = {};
112593
112754
  try {
112594
- existing = JSON.parse(readFileSync(configPath, "utf-8"));
112755
+ existing = JSON.parse(readFileSync2(configPath, "utf-8"));
112595
112756
  } catch (err) {
112596
112757
  const code3 = err.code;
112597
112758
  if (code3 === "ENOENT") {
112598
112759
  } else if (err instanceof SyntaxError) {
112599
- console.error(
112600
- ` Warning: ${configPath} contains malformed JSON \u2014 replacing with fresh config`
112601
- );
112760
+ const backupPath = `${configPath}.broken-${Date.now()}`;
112761
+ try {
112762
+ await copyFile(configPath, backupPath);
112763
+ console.error(
112764
+ ` Warning: ${configPath} contains malformed JSON \u2014 backed up to ${basename(backupPath)}, replacing with fresh config`
112765
+ );
112766
+ } catch (copyErr) {
112767
+ console.error(
112768
+ ` Warning: ${configPath} contains malformed JSON and backup failed (${copyErr instanceof Error ? copyErr.message : copyErr}) \u2014 refusing to overwrite. Fix the JSON manually and rerun 'tandem setup'.`
112769
+ );
112770
+ throw copyErr;
112771
+ }
112602
112772
  } else {
112603
112773
  throw err;
112604
112774
  }
@@ -112610,22 +112780,25 @@ async function applyConfig(configPath, entries) {
112610
112780
  ...entries
112611
112781
  }
112612
112782
  };
112613
- await mkdir(dirname(configPath), { recursive: true });
112783
+ await mkdir(dirname2(configPath), { recursive: true });
112614
112784
  await atomicWrite3(JSON.stringify(updated, null, 2) + "\n", configPath);
112615
112785
  }
112616
112786
  async function installSkill(opts = {}) {
112617
112787
  const home = opts.homeOverride ?? homedir2();
112618
112788
  const skillPath = join2(home, ".claude", "skills", "tandem", "SKILL.md");
112619
- await mkdir(dirname(skillPath), { recursive: true });
112789
+ await mkdir(dirname2(skillPath), { recursive: true });
112620
112790
  await atomicWrite3(SKILL_CONTENT, skillPath);
112621
112791
  }
112622
112792
 
112623
112793
  // src/server/mcp/api-routes.ts
112624
112794
  init_constants();
112625
112795
  init_types3();
112796
+ init_utils();
112626
112797
  init_notifications();
112627
112798
  init_provider();
112799
+ init_annotations();
112628
112800
  init_convert();
112801
+ init_document2();
112629
112802
  init_document_model();
112630
112803
  init_document_service();
112631
112804
 
@@ -112638,7 +112811,7 @@ init_annotations();
112638
112811
  init_document_model();
112639
112812
  init_document_service();
112640
112813
  init_response();
112641
- import fs6 from "fs/promises";
112814
+ import fs7 from "fs/promises";
112642
112815
  import path10 from "path";
112643
112816
  async function applyChangesCore(documentId, author, backupPath) {
112644
112817
  const r = requireDocument(documentId);
@@ -112714,21 +112887,21 @@ async function applyChangesCore(documentId, author, backupPath) {
112714
112887
  throw Object.assign(new Error("No accepted suggestions to apply."), { code: "NO_SUGGESTIONS" });
112715
112888
  }
112716
112889
  const ydocFlatText = extractText(ydoc);
112717
- const buffer3 = await fs6.readFile(filePath);
112890
+ const buffer3 = await fs7.readFile(filePath);
112718
112891
  const result2 = await applyTrackedChanges(buffer3, suggestions, {
112719
112892
  author: author ?? "Tandem Review",
112720
112893
  ydocFlatText
112721
112894
  });
112722
112895
  let resolvedBackup = backupPath ?? filePath.replace(/\.docx$/i, ".backup.docx");
112723
112896
  try {
112724
- await fs6.access(resolvedBackup);
112897
+ await fs7.access(resolvedBackup);
112725
112898
  const ext = path10.extname(resolvedBackup);
112726
112899
  const base = resolvedBackup.slice(0, -ext.length);
112727
112900
  resolvedBackup = `${base}-${Date.now()}${ext}`;
112728
112901
  } catch {
112729
112902
  }
112730
- await fs6.copyFile(filePath, resolvedBackup);
112731
- const [origStat, backupStat] = await Promise.all([fs6.stat(filePath), fs6.stat(resolvedBackup)]);
112903
+ await fs7.copyFile(filePath, resolvedBackup);
112904
+ const [origStat, backupStat] = await Promise.all([fs7.stat(filePath), fs7.stat(resolvedBackup)]);
112732
112905
  if (origStat.size !== backupStat.size) {
112733
112906
  throw Object.assign(new Error("Backup verification failed: file sizes do not match."), {
112734
112907
  code: "BACKUP_FAILED"
@@ -112783,17 +112956,17 @@ function registerApplyTools(server) {
112783
112956
  const { filePath } = r;
112784
112957
  const backupPath = filePath.replace(/\.docx$/i, ".backup.docx");
112785
112958
  try {
112786
- await fs6.access(backupPath);
112959
+ await fs7.access(backupPath);
112787
112960
  } catch (err) {
112788
112961
  if (err.code === "ENOENT") {
112789
112962
  return mcpError("FILE_NOT_FOUND", `No backup file found at ${backupPath}`);
112790
112963
  }
112791
112964
  throw err;
112792
112965
  }
112793
- await fs6.copyFile(backupPath, filePath);
112966
+ await fs7.copyFile(backupPath, filePath);
112794
112967
  const [backupStat, restoredStat] = await Promise.all([
112795
- fs6.stat(backupPath),
112796
- fs6.stat(filePath)
112968
+ fs7.stat(backupPath),
112969
+ fs7.stat(filePath)
112797
112970
  ]);
112798
112971
  if (backupStat.size !== restoredStat.size) {
112799
112972
  throw new Error("Restore verification failed: file sizes do not match.");
@@ -112865,7 +113038,7 @@ async function runSetupHandler(input, homeOverride) {
112865
113038
  };
112866
113039
  }
112867
113040
  const targets = detectTargets({ homeOverride });
112868
- const entries = buildMcpEntries(channelPath, nodeBinary);
113041
+ const entries = buildMcpEntries(channelPath, { withChannelShim: true, nodeBinary });
112869
113042
  const configured = [];
112870
113043
  const errors = [];
112871
113044
  for (const target of targets) {
@@ -112873,7 +113046,9 @@ async function runSetupHandler(input, homeOverride) {
112873
113046
  await applyConfig(target.configPath, entries);
112874
113047
  configured.push(target.label);
112875
113048
  } catch (err) {
112876
- errors.push(`${target.label}: ${err instanceof Error ? err.message : String(err)}`);
113049
+ const msg = err instanceof Error ? err.message : String(err);
113050
+ errors.push(`${target.label}: ${msg}`);
113051
+ console.error(`[Setup] target=${target.label} failed: ${msg}`);
112877
113052
  }
112878
113053
  }
112879
113054
  let skillInstalled = false;
@@ -112881,10 +113056,17 @@ async function runSetupHandler(input, homeOverride) {
112881
113056
  await installSkill({ homeOverride });
112882
113057
  skillInstalled = true;
112883
113058
  } catch (err) {
112884
- errors.push(`Skill install: ${err instanceof Error ? err.message : String(err)}`);
112885
- }
113059
+ const msg = err instanceof Error ? err.message : String(err);
113060
+ errors.push(`Skill install: ${msg}`);
113061
+ console.error(`[Setup] skill install failed: ${msg}`);
113062
+ }
113063
+ const totalFailed = configured.length === 0 && !skillInstalled;
113064
+ const anyFailed = errors.length > 0;
113065
+ let status = 200;
113066
+ if (totalFailed) status = 500;
113067
+ else if (anyFailed) status = 207;
112886
113068
  return {
112887
- status: 200,
113069
+ status,
112888
113070
  body: { data: { targets, configured, errors, skillInstalled } }
112889
113071
  };
112890
113072
  }
@@ -113036,6 +113218,25 @@ function registerApiRoutes(app, largeBody) {
113036
113218
  sendApiError(res, err);
113037
113219
  }
113038
113220
  });
113221
+ app.options("/api/save", apiMiddleware);
113222
+ app.post("/api/save", apiMiddleware, largeBody, async (req, res) => {
113223
+ const { documentId } = req.body ?? {};
113224
+ if (documentId !== void 0 && typeof documentId !== "string") {
113225
+ res.status(400).json({ error: "BAD_REQUEST", message: "documentId must be a string" });
113226
+ return;
113227
+ }
113228
+ const targetId = documentId ?? getActiveDocId();
113229
+ if (!targetId) {
113230
+ res.status(404).json({ error: "NOT_FOUND", message: "No document to save." });
113231
+ return;
113232
+ }
113233
+ try {
113234
+ const result2 = await saveDocumentToDisk(targetId, "manual");
113235
+ res.json({ data: result2 });
113236
+ } catch (err) {
113237
+ sendApiError(res, err);
113238
+ }
113239
+ });
113039
113240
  app.options("/api/upload", apiMiddleware);
113040
113241
  app.post("/api/upload", apiMiddleware, largeBody, async (req, res) => {
113041
113242
  const { fileName, content: content3 } = req.body ?? {};
@@ -113126,6 +113327,40 @@ function registerApiRoutes(app, largeBody) {
113126
113327
  });
113127
113328
  }
113128
113329
  });
113330
+ app.options("/api/annotation-reply", apiMiddleware);
113331
+ app.post("/api/annotation-reply", apiMiddleware, largeBody, (req, res) => {
113332
+ const { annotationId, text: text5, documentId } = req.body ?? {};
113333
+ if (!annotationId || typeof annotationId !== "string") {
113334
+ res.status(400).json({ error: "BAD_REQUEST", message: "annotationId is required" });
113335
+ return;
113336
+ }
113337
+ if (!text5 || typeof text5 !== "string") {
113338
+ res.status(400).json({ error: "BAD_REQUEST", message: "text is required" });
113339
+ return;
113340
+ }
113341
+ const doc = getCurrentDoc(typeof documentId === "string" ? documentId : void 0);
113342
+ if (!doc) {
113343
+ res.status(404).json({ error: "NOT_FOUND", message: "No document open" });
113344
+ return;
113345
+ }
113346
+ const ydoc = getOrCreateDocument(doc.docName);
113347
+ const annotationsMap = ydoc.getMap(Y_MAP_ANNOTATIONS);
113348
+ const result2 = addReplyToAnnotation(ydoc, annotationsMap, annotationId, text5, "user");
113349
+ if (!result2.ok) {
113350
+ const status = result2.code === "ANNOTATION_RESOLVED" ? 409 : 404;
113351
+ pushNotification({
113352
+ id: generateNotificationId(),
113353
+ type: "annotation-error",
113354
+ severity: "error",
113355
+ message: `Reply failed: ${result2.error}`,
113356
+ dedupKey: `reply-error:${annotationId}`,
113357
+ timestamp: Date.now()
113358
+ });
113359
+ res.status(status).json({ error: result2.code, message: result2.error });
113360
+ return;
113361
+ }
113362
+ res.json({ data: { replyId: result2.replyId, annotationId } });
113363
+ });
113129
113364
  }
113130
113365
 
113131
113366
  // src/server/mcp/awareness.ts
@@ -113381,7 +113616,7 @@ function registerChannelRoutes(app, apiMiddleware2) {
113381
113616
  app.get("/api/events", apiMiddleware2, sseHandler);
113382
113617
  app.options("/api/channel-awareness", apiMiddleware2);
113383
113618
  app.post("/api/channel-awareness", apiMiddleware2, (req, res) => {
113384
- const { documentId, status, active, focusParagraph } = req.body ?? {};
113619
+ const { documentId, status, active, focusParagraph, focusOffset } = req.body ?? {};
113385
113620
  if (typeof status !== "string") {
113386
113621
  res.status(400).json({ error: "BAD_REQUEST", message: "status is required" });
113387
113622
  return;
@@ -113394,7 +113629,8 @@ function registerChannelRoutes(app, apiMiddleware2) {
113394
113629
  status,
113395
113630
  timestamp: Date.now(),
113396
113631
  active: active === true,
113397
- focusParagraph: typeof focusParagraph === "number" ? focusParagraph : null
113632
+ focusParagraph: typeof focusParagraph === "number" ? focusParagraph : null,
113633
+ focusOffset: typeof focusOffset === "number" ? focusOffset : null
113398
113634
  };
113399
113635
  doc.transact(() => awarenessMap.set("claude", state), MCP_ORIGIN);
113400
113636
  }
@@ -113602,29 +113838,34 @@ function registerNavigationTools(server) {
113602
113838
  {
113603
113839
  text: external_exports.string().describe("Status text"),
113604
113840
  focusParagraph: external_exports.number().optional().describe("Index of paragraph Claude is focusing on"),
113841
+ focusOffset: external_exports.number().optional().describe("Flat character offset for precise cursor positioning within the document"),
113605
113842
  documentId: external_exports.string().optional().describe("Target document ID (defaults to active document)")
113606
113843
  },
113607
- withErrorBoundary("tandem_setStatus", async ({ text: text5, focusParagraph, documentId }) => {
113608
- const current = getCurrentDoc(documentId);
113609
- if (!current) {
113610
- return mcpSuccess({
113611
- status: text5,
113612
- warning: "No document open \u2014 status not broadcast to editor."
113613
- });
113844
+ withErrorBoundary(
113845
+ "tandem_setStatus",
113846
+ async ({ text: text5, focusParagraph, focusOffset, documentId }) => {
113847
+ const current = getCurrentDoc(documentId);
113848
+ if (!current) {
113849
+ return mcpSuccess({
113850
+ status: text5,
113851
+ warning: "No document open \u2014 status not broadcast to editor."
113852
+ });
113853
+ }
113854
+ const doc = getOrCreateDocument(current.docName);
113855
+ const awarenessMap = doc.getMap(Y_MAP_AWARENESS);
113856
+ doc.transact(
113857
+ () => awarenessMap.set("claude", {
113858
+ status: text5,
113859
+ timestamp: Date.now(),
113860
+ active: true,
113861
+ focusParagraph: focusParagraph ?? null,
113862
+ focusOffset: focusOffset ?? null
113863
+ }),
113864
+ MCP_ORIGIN
113865
+ );
113866
+ return mcpSuccess({ status: text5 });
113614
113867
  }
113615
- const doc = getOrCreateDocument(current.docName);
113616
- const awarenessMap = doc.getMap(Y_MAP_AWARENESS);
113617
- doc.transact(
113618
- () => awarenessMap.set("claude", {
113619
- status: text5,
113620
- timestamp: Date.now(),
113621
- active: true,
113622
- focusParagraph: focusParagraph ?? null
113623
- }),
113624
- MCP_ORIGIN
113625
- );
113626
- return mcpSuccess({ status: text5 });
113627
- })
113868
+ )
113628
113869
  );
113629
113870
  server.tool(
113630
113871
  "tandem_getContext",
@@ -113659,8 +113900,8 @@ try {
113659
113900
  `[Tandem] Could not read version from package.json: ${err instanceof Error ? err.message : err}`
113660
113901
  );
113661
113902
  }
113662
- var __dirname2 = dirname2(fileURLToPath3(import.meta.url));
113663
- var CLIENT_DIST = join3(__dirname2, "../client");
113903
+ var __dirname3 = dirname3(fileURLToPath4(import.meta.url));
113904
+ var CLIENT_DIST = join3(__dirname3, "../client");
113664
113905
  var mcpServer = null;
113665
113906
  var currentTransport = null;
113666
113907
  var connectingPromise = null;
@@ -113798,7 +114039,7 @@ async function startMcpServerHttp(port, host = "127.0.0.1") {
113798
114039
  } else {
113799
114040
  console.error(`[Tandem] No client dist at ${CLIENT_DIST} \u2014 run 'npm run build' first`);
113800
114041
  }
113801
- return new Promise((resolve2, reject2) => {
114042
+ return new Promise((resolve3, reject2) => {
113802
114043
  const httpServer2 = app.listen(port, host, () => {
113803
114044
  httpServer2.removeListener("error", reject2);
113804
114045
  httpServer2.on("error", (err) => console.error("[Tandem] HTTP server error:", err));
@@ -113810,7 +114051,7 @@ async function startMcpServerHttp(port, host = "127.0.0.1") {
113810
114051
  console.error("[Tandem] Skipping browser open \u2014 no client assets found");
113811
114052
  }
113812
114053
  }
113813
- resolve2(httpServer2);
114054
+ resolve3(httpServer2);
113814
114055
  });
113815
114056
  httpServer2.on("error", reject2);
113816
114057
  });
@@ -113899,12 +114140,12 @@ init_platform();
113899
114140
  init_manager();
113900
114141
 
113901
114142
  // src/server/version-check.ts
113902
- import fs7 from "fs/promises";
114143
+ import fs8 from "fs/promises";
113903
114144
  import path11 from "path";
113904
114145
  async function checkVersionChange(currentVersion, versionFilePath) {
113905
114146
  let storedVersion = null;
113906
114147
  try {
113907
- storedVersion = (await fs7.readFile(versionFilePath, "utf-8")).trim();
114148
+ storedVersion = (await fs8.readFile(versionFilePath, "utf-8")).trim();
113908
114149
  } catch (err) {
113909
114150
  if (err.code !== "ENOENT") {
113910
114151
  console.error("[Tandem] Failed to read last-seen-version:", err);
@@ -113912,8 +114153,8 @@ async function checkVersionChange(currentVersion, versionFilePath) {
113912
114153
  }
113913
114154
  const result2 = storedVersion === null ? "first-install" : storedVersion === currentVersion ? "current" : "upgraded";
113914
114155
  if (result2 !== "current") {
113915
- await fs7.mkdir(path11.dirname(versionFilePath), { recursive: true });
113916
- await fs7.writeFile(versionFilePath, currentVersion, "utf-8");
114156
+ await fs8.mkdir(path11.dirname(versionFilePath), { recursive: true });
114157
+ await fs8.writeFile(versionFilePath, currentVersion, "utf-8");
113917
114158
  }
113918
114159
  return result2;
113919
114160
  }
@@ -114028,7 +114269,7 @@ async function main2() {
114028
114269
  } catch (err) {
114029
114270
  console.error(`[Tandem] ${err instanceof Error ? err.message : err} \u2014 proceeding anyway`);
114030
114271
  }
114031
- const projectRoot = path12.resolve(path12.dirname(fileURLToPath4(import.meta.url)), "../..");
114272
+ const projectRoot = path12.resolve(path12.dirname(fileURLToPath5(import.meta.url)), "../..");
114032
114273
  try {
114033
114274
  const versionStatus = await checkVersionChange(APP_VERSION, LAST_SEEN_VERSION_FILE);
114034
114275
  if (versionStatus === "upgraded") {