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.
- package/.claude-plugin/marketplace.json +19 -0
- package/.claude-plugin/plugin.json +27 -0
- package/CHANGELOG.md +80 -0
- package/README.md +68 -23
- package/dist/channel/index.js +171 -190
- package/dist/channel/index.js.map +1 -1
- package/dist/cli/index.js +718 -133
- package/dist/cli/index.js.map +1 -1
- package/dist/client/assets/index-mo5ZOPfU.js +349 -0
- package/dist/client/assets/webview-0tvvWtyc.js +1 -0
- package/dist/client/index.html +63 -2
- package/dist/monitor/index.js +4570 -0
- package/dist/monitor/index.js.map +1 -0
- package/dist/server/index.js +1042 -801
- package/dist/server/index.js.map +1 -1
- package/package.json +13 -8
- package/sample/welcome.md +3 -3
- package/skills/tandem/SKILL.md +93 -0
- package/dist/client/assets/index-D6wQrQ7U.js +0 -308
package/dist/server/index.js
CHANGED
|
@@ -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,
|
|
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
|
|
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,
|
|
8972
|
+
Promise2.prototype._execute = function(executor, resolve3, reject2) {
|
|
8965
8973
|
try {
|
|
8966
|
-
executor(
|
|
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,
|
|
8997
|
+
function cancellationExecute(executor, resolve3, reject2) {
|
|
8990
8998
|
var promise = this;
|
|
8991
8999
|
try {
|
|
8992
|
-
executor(
|
|
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
|
|
12647
|
+
var resolve3;
|
|
12640
12648
|
var reject2;
|
|
12641
12649
|
var promise = new bluebird.Promise(function(resolveArg, rejectArg) {
|
|
12642
|
-
|
|
12650
|
+
resolve3 = resolveArg;
|
|
12643
12651
|
reject2 = rejectArg;
|
|
12644
12652
|
});
|
|
12645
12653
|
return {
|
|
12646
|
-
resolve:
|
|
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(
|
|
15321
|
-
function
|
|
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 =
|
|
15475
|
-
function
|
|
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(
|
|
16015
|
+
return new external.Promise(function(resolve3, reject2) {
|
|
16008
16016
|
var reader = new FileReader();
|
|
16009
16017
|
reader.onload = function(e) {
|
|
16010
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
32473
|
+
var fs9 = __require("fs");
|
|
32466
32474
|
var url = __require("url");
|
|
32467
32475
|
var os2 = __require("os");
|
|
32468
|
-
var
|
|
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 ?
|
|
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(
|
|
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
|
|
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(
|
|
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 = (
|
|
36371
|
+
callAll = (fs9, args2, i = 0) => {
|
|
36364
36372
|
try {
|
|
36365
|
-
for (; i <
|
|
36366
|
-
|
|
36373
|
+
for (; i < fs9.length; i++) {
|
|
36374
|
+
fs9[i](...args2);
|
|
36367
36375
|
}
|
|
36368
36376
|
} finally {
|
|
36369
|
-
if (i <
|
|
36370
|
-
callAll(
|
|
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((
|
|
36920
|
+
this.whenLoaded = create5((resolve3) => {
|
|
36913
36921
|
this.on("load", () => {
|
|
36914
36922
|
this.isLoaded = true;
|
|
36915
|
-
|
|
36923
|
+
resolve3(this);
|
|
36916
36924
|
});
|
|
36917
36925
|
});
|
|
36918
|
-
const provideSyncedPromise = () => create5((
|
|
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
|
-
|
|
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
|
|
38403
|
+
const fs9 = [];
|
|
38396
38404
|
transaction.changed.forEach(
|
|
38397
|
-
(subs, itemtype) =>
|
|
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
|
-
|
|
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
|
-
|
|
38422
|
+
fs9.push(() => {
|
|
38415
38423
|
callEventHandlerListeners(type2._dEH, events, transaction);
|
|
38416
38424
|
});
|
|
38417
38425
|
}
|
|
38418
38426
|
});
|
|
38419
|
-
|
|
38420
|
-
|
|
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(
|
|
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
|
|
49046
|
-
if (
|
|
49047
|
-
events =
|
|
49048
|
-
called.push(
|
|
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(
|
|
55680
|
-
assertNonEmpty(
|
|
55681
|
-
assertPart(
|
|
55682
|
-
this.path = default2.join(this.dirname || "",
|
|
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(
|
|
55711
|
+
set dirname(dirname4) {
|
|
55704
55712
|
assertPath(this.basename, "dirname");
|
|
55705
|
-
this.path = default2.join(
|
|
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(
|
|
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 (
|
|
56384
|
-
|
|
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(
|
|
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 (
|
|
56501
|
-
|
|
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(
|
|
60813
|
-
deferredResolve =
|
|
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(
|
|
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
|
-
|
|
60975
|
+
resolve3(ret2);
|
|
60968
60976
|
}
|
|
60969
60977
|
});
|
|
60970
60978
|
} else {
|
|
60971
|
-
|
|
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((
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
67964
|
-
import
|
|
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
|
|
67969
|
-
const tempPath =
|
|
67970
|
-
await
|
|
67971
|
-
await
|
|
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 =
|
|
67975
|
-
await
|
|
68416
|
+
const tempPath = path6.join(path6.dirname(filePath), `.tandem-tmp-${Date.now()}`);
|
|
68417
|
+
await fs2.writeFile(tempPath, content3);
|
|
67976
68418
|
try {
|
|
67977
|
-
await
|
|
68419
|
+
await fs2.rename(tempPath, filePath);
|
|
67978
68420
|
} catch (err) {
|
|
67979
|
-
await
|
|
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
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
69007
|
-
await saveSession(docState.filePath, docState.format, doc);
|
|
69105
|
+
await deleteSession(docState.filePath);
|
|
69008
69106
|
} catch (err) {
|
|
69009
|
-
console.error(`[Tandem] Failed to
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
69480
|
-
|
|
69481
|
-
|
|
69482
|
-
|
|
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
|
-
|
|
69507
|
-
|
|
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
|
-
}
|
|
69510
|
-
|
|
69511
|
-
|
|
69512
|
-
|
|
69513
|
-
|
|
69514
|
-
|
|
69515
|
-
|
|
69516
|
-
message: `
|
|
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
|
-
|
|
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(() =>
|
|
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)
|
|
70416
|
+
if (!selection || selection.from === selection.to) {
|
|
70417
|
+
selectionBuffer.delete(docName);
|
|
70418
|
+
return;
|
|
70419
|
+
}
|
|
70196
70420
|
selectionDwellTimer = setTimeout(() => {
|
|
70197
70421
|
selectionDwellTimer = null;
|
|
70198
|
-
|
|
70199
|
-
|
|
70200
|
-
|
|
70201
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
89064
|
-
var
|
|
89065
|
-
var
|
|
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
|
|
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 =
|
|
89103
|
-
var dir =
|
|
89104
|
-
var file =
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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",
|
|
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
|
|
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 =
|
|
92743
|
+
var name2 = basename2(filename);
|
|
92496
92744
|
var isQuotedString = TEXT_REGEXP.test(name2);
|
|
92497
|
-
var fallbackName = typeof fallback !== "string" ? fallback && getlatin1(name2) :
|
|
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
|
|
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
|
|
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 ?
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 ?
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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:
|
|
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,
|
|
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,
|
|
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
|
|
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
|
|
100870
|
-
import { fileURLToPath as
|
|
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((
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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(
|
|
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((
|
|
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((
|
|
111326
|
+
return new Promise((resolve3) => {
|
|
111079
111327
|
const json = serializeMessage(message);
|
|
111080
111328
|
if (this._stdout.write(json)) {
|
|
111081
|
-
|
|
111329
|
+
resolve3();
|
|
111082
111330
|
} else {
|
|
111083
|
-
this._stdout.once("drain",
|
|
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((
|
|
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((
|
|
112308
|
+
return new Promise((resolve3) => {
|
|
112061
112309
|
this._streamMapping.set(streamId, {
|
|
112062
|
-
resolveJson:
|
|
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
|
|
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
|
-
|
|
112433
|
-
|
|
112434
|
-
|
|
112435
|
-
|
|
112436
|
-
|
|
112437
|
-
|
|
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
|
|
112529
|
-
var
|
|
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,
|
|
112532
|
-
|
|
112533
|
-
tandem: {
|
|
112534
|
-
|
|
112535
|
-
|
|
112536
|
-
|
|
112537
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
112600
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
112731
|
-
const [origStat, backupStat] = await Promise.all([
|
|
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
|
|
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
|
|
112966
|
+
await fs7.copyFile(backupPath, filePath);
|
|
112794
112967
|
const [backupStat, restoredStat] = await Promise.all([
|
|
112795
|
-
|
|
112796
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
113608
|
-
|
|
113609
|
-
|
|
113610
|
-
|
|
113611
|
-
|
|
113612
|
-
|
|
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
|
-
|
|
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
|
|
113663
|
-
var CLIENT_DIST = join3(
|
|
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((
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
113916
|
-
await
|
|
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(
|
|
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") {
|