tandem-editor 0.11.2 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -40,7 +40,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
40
40
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
41
41
 
42
42
  // src/shared/constants.ts
43
- var DEFAULT_WS_PORT, DEFAULT_MCP_PORT, TANDEM_REPO_URL, TANDEM_ISSUES_NEW_URL, 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, Y_MAP_SELECTION, Y_MAP_ACTIVITY, Y_MAP_OPEN_DOCUMENTS, Y_MAP_ACTIVE_DOCUMENT_ID, Y_MAP_GENERATION_ID, Y_MAP_STORE_READ_ONLY, NOTIFICATION_BUFFER_SIZE, TUTORIAL_ANNOTATION_PREFIX, CHANNEL_EVENT_BUFFER_SIZE, CHANNEL_EVENT_BUFFER_AGE_MS, CHANNEL_SSE_KEEPALIVE_MS, TOKEN_FILE_NAME, DEFAULT_BIND_HOST, TANDEM_ALLOW_UNAUTHENTICATED_LAN_ENV, TAURI_HOSTNAME;
43
+ var DEFAULT_WS_PORT, DEFAULT_MCP_PORT, TANDEM_REPO_URL, TANDEM_ISSUES_NEW_URL, SUPPORTED_EXTENSIONS, MAX_FILE_SIZE, 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, Y_MAP_SELECTION, Y_MAP_ACTIVITY, Y_MAP_CLAUDE, Y_MAP_OPEN_DOCUMENTS, Y_MAP_ACTIVE_DOCUMENT_ID, Y_MAP_GENERATION_ID, Y_MAP_READ_ONLY, Y_MAP_STORE_READ_ONLY, NOTIFICATION_BUFFER_SIZE, TUTORIAL_ANNOTATION_PREFIX, CHANNEL_EVENT_BUFFER_SIZE, CHANNEL_EVENT_BUFFER_AGE_MS, CHANNEL_SSE_KEEPALIVE_MS, TOKEN_FILE_NAME, DEFAULT_BIND_HOST, TANDEM_ALLOW_UNAUTHENTICATED_LAN_ENV, TAURI_HOSTNAME;
44
44
  var init_constants = __esm({
45
45
  "src/shared/constants.ts"() {
46
46
  "use strict";
@@ -50,8 +50,6 @@ var init_constants = __esm({
50
50
  TANDEM_ISSUES_NEW_URL = `${TANDEM_REPO_URL}/issues/new`;
51
51
  SUPPORTED_EXTENSIONS = /* @__PURE__ */ new Set([".md", ".txt", ".html", ".htm", ".docx"]);
52
52
  MAX_FILE_SIZE = 50 * 1024 * 1024;
53
- MAX_WS_PAYLOAD = 10 * 1024 * 1024;
54
- IDLE_TIMEOUT = 30 * 60 * 1e3;
55
53
  SESSION_MAX_AGE = 30 * 24 * 60 * 60 * 1e3;
56
54
  TANDEM_MODE_DEFAULT = "tandem";
57
55
  SELECTION_DWELL_DEFAULT_MS = 1e3;
@@ -73,9 +71,11 @@ var init_constants = __esm({
73
71
  Y_MAP_AUTHORSHIP = "authorship";
74
72
  Y_MAP_SELECTION = "selection";
75
73
  Y_MAP_ACTIVITY = "activity";
74
+ Y_MAP_CLAUDE = "claude";
76
75
  Y_MAP_OPEN_DOCUMENTS = "openDocuments";
77
76
  Y_MAP_ACTIVE_DOCUMENT_ID = "activeDocumentId";
78
77
  Y_MAP_GENERATION_ID = "generationId";
78
+ Y_MAP_READ_ONLY = "readOnly";
79
79
  Y_MAP_STORE_READ_ONLY = "storeReadOnly";
80
80
  NOTIFICATION_BUFFER_SIZE = 50;
81
81
  TUTORIAL_ANNOTATION_PREFIX = "tutorial-";
@@ -18580,7 +18580,7 @@ var init_constructs = __esm({
18580
18580
  });
18581
18581
 
18582
18582
  // node_modules/micromark/lib/create-tokenizer.js
18583
- function createTokenizer(parser2, initialize, from3) {
18583
+ function createTokenizer(parser, initialize, from3) {
18584
18584
  let point3 = {
18585
18585
  _bufferIndex: -1,
18586
18586
  _index: 0,
@@ -18609,7 +18609,7 @@ function createTokenizer(parser2, initialize, from3) {
18609
18609
  defineSkip,
18610
18610
  events: [],
18611
18611
  now,
18612
- parser: parser2,
18612
+ parser,
18613
18613
  previous: null,
18614
18614
  sliceSerialize,
18615
18615
  sliceStream,
@@ -18917,7 +18917,7 @@ function parse(options) {
18917
18917
  /** @type {FullNormalizedExtension} */
18918
18918
  combineExtensions([constructs_exports, ...settings.extensions || []])
18919
18919
  );
18920
- const parser2 = {
18920
+ const parser = {
18921
18921
  constructs: constructs2,
18922
18922
  content: create9(content2),
18923
18923
  defined: [],
@@ -18927,11 +18927,11 @@ function parse(options) {
18927
18927
  string: create9(string),
18928
18928
  text: create9(text3)
18929
18929
  };
18930
- return parser2;
18930
+ return parser;
18931
18931
  function create9(initial2) {
18932
18932
  return creator;
18933
18933
  function creator(from3) {
18934
- return createTokenizer(parser2, initial2, from3);
18934
+ return createTokenizer(parser, initial2, from3);
18935
18935
  }
18936
18936
  }
18937
18937
  }
@@ -19828,8 +19828,8 @@ var init_mdast_util_from_markdown = __esm({
19828
19828
  // node_modules/remark-parse/lib/index.js
19829
19829
  function remarkParse(options) {
19830
19830
  const self2 = this;
19831
- self2.parser = parser2;
19832
- function parser2(doc) {
19831
+ self2.parser = parser;
19832
+ function parser(doc) {
19833
19833
  return fromMarkdown(doc, {
19834
19834
  ...self2.data("settings"),
19835
19835
  ...options,
@@ -21027,9 +21027,9 @@ var init_lib22 = __esm({
21027
21027
  parse(file) {
21028
21028
  this.freeze();
21029
21029
  const realFile = vfile(file);
21030
- const parser2 = this.parser || this.Parser;
21031
- assertParser("parse", parser2);
21032
- return parser2(String(realFile), realFile);
21030
+ const parser = this.parser || this.Parser;
21031
+ assertParser("parse", parser);
21032
+ return parser(String(realFile), realFile);
21033
21033
  }
21034
21034
  /**
21035
21035
  * Process the given file as configured on the processor.
@@ -21914,15 +21914,50 @@ var init_mdast_ydoc = __esm({
21914
21914
  });
21915
21915
 
21916
21916
  // src/server/file-io/markdown.ts
21917
+ function normalizeLabel(s) {
21918
+ return s.replace(/[\t\n\r ]+/g, " ").trim().toLowerCase();
21919
+ }
21917
21920
  function loadMarkdown(doc, markdown) {
21918
- const tree = parser.parse(markdown);
21921
+ const tree = mdParser.parse(markdown);
21919
21922
  mdastToYDoc(doc, tree);
21920
21923
  }
21921
21924
  function saveMarkdown(doc) {
21922
- const tree = yDocToMdast(doc);
21923
- return serializer.stringify(tree);
21925
+ return serializeMdast(yDocToMdast(doc));
21926
+ }
21927
+ function serializeMdast(tree) {
21928
+ const refDefs = /* @__PURE__ */ new Set();
21929
+ visit(tree, "definition", (node2) => {
21930
+ refDefs.add(node2.identifier);
21931
+ });
21932
+ return unified().use(remarkGfm).use(remarkStringify, {
21933
+ ...stringifyOptions,
21934
+ handlers: {
21935
+ // Call state.safe() first (mirroring the default text handler) so
21936
+ // block-context escapes (line-leading `# `, `- `, `> `, fence runs,
21937
+ // table pipes, setext underlines) remain intact, then selectively
21938
+ // un-escape intra-text noise that the default `unsafe` table over-flags.
21939
+ //
21940
+ // GFM extensions (autolink-literal `@`/`.`/`:`, strikethrough `~~`,
21941
+ // table `|`) register no `text` handler and contribute `unsafe` entries
21942
+ // that flow through safe(). Of those, only `~` is un-escaped below
21943
+ // (single `~` is harmless because GFM strikethrough requires `~~`).
21944
+ text(node2, _parent, state, info) {
21945
+ let s = state.safe(node2.value, info);
21946
+ const nextStartsBracket = typeof info.after === "string" && info.after.startsWith("[");
21947
+ s = s.replace(/\\\[([^\\\]\n`]+)\](?!\s*[:([])/g, (match, label, offset) => {
21948
+ const atEnd = offset + match.length === s.length;
21949
+ if (atEnd && nextStartsBracket) return match;
21950
+ return refDefs.has(normalizeLabel(label)) ? match : `[${label}]`;
21951
+ });
21952
+ s = s.replace(/(?<=\w)\\_(?=\w)/g, "_");
21953
+ s = s.replace(/(?<![`\\])\\`(?!`)/g, "`");
21954
+ s = s.replace(/\\~(?!~)/g, "~");
21955
+ return s;
21956
+ }
21957
+ }
21958
+ }).stringify(tree);
21924
21959
  }
21925
- var parser, serializer;
21960
+ var mdParser, stringifyOptions;
21926
21961
  var init_markdown = __esm({
21927
21962
  "src/server/file-io/markdown.ts"() {
21928
21963
  "use strict";
@@ -21930,15 +21965,16 @@ var init_markdown = __esm({
21930
21965
  init_remark_parse();
21931
21966
  init_remark_stringify();
21932
21967
  init_unified();
21968
+ init_unist_util_visit();
21933
21969
  init_mdast_ydoc();
21934
- parser = unified().use(remarkParse).use(remarkGfm).freeze();
21935
- serializer = unified().use(remarkGfm).use(remarkStringify, {
21970
+ mdParser = unified().use(remarkParse).use(remarkGfm).freeze();
21971
+ stringifyOptions = {
21936
21972
  bullet: "-",
21937
21973
  emphasis: "*",
21938
21974
  strong: "*",
21939
21975
  listItemIndent: "one",
21940
21976
  rule: "-"
21941
- }).freeze();
21977
+ };
21942
21978
  }
21943
21979
  });
21944
21980
 
@@ -51802,8 +51838,8 @@ var require_parser = __commonJS({
51802
51838
  "use strict";
51803
51839
  var TokenIterator = require_TokenIterator();
51804
51840
  exports3.Parser = function(options) {
51805
- var parseTokens = function(parser2, tokens) {
51806
- return parser2(new TokenIterator(tokens));
51841
+ var parseTokens = function(parser, tokens) {
51842
+ return parser(new TokenIterator(tokens));
51807
51843
  };
51808
51844
  return {
51809
51845
  parseTokens
@@ -52127,16 +52163,16 @@ var require_rules = __commonJS({
52127
52163
  parsers = Array.prototype.slice.call(arguments, 1);
52128
52164
  }
52129
52165
  return function(input) {
52130
- return lazyIterators.fromArray(parsers).map(function(parser2) {
52131
- return parser2(input);
52166
+ return lazyIterators.fromArray(parsers).map(function(parser) {
52167
+ return parser(input);
52132
52168
  }).filter(function(result2) {
52133
52169
  return result2.isSuccess() || result2.isError();
52134
52170
  }).first() || describeTokenMismatch(input, name2);
52135
52171
  };
52136
52172
  };
52137
- exports3.then = function(parser2, func) {
52173
+ exports3.then = function(parser, func) {
52138
52174
  return function(input) {
52139
- var result2 = parser2(input);
52175
+ var result2 = parser(input);
52140
52176
  if (!result2.map) {
52141
52177
  console.log(result2);
52142
52178
  }
@@ -52146,19 +52182,19 @@ var require_rules = __commonJS({
52146
52182
  exports3.sequence = function() {
52147
52183
  var parsers = Array.prototype.slice.call(arguments, 0);
52148
52184
  var rule = function(input) {
52149
- var result2 = _3.foldl(parsers, function(memo, parser2) {
52185
+ var result2 = _3.foldl(parsers, function(memo, parser) {
52150
52186
  var result3 = memo.result;
52151
52187
  var hasCut = memo.hasCut;
52152
52188
  if (!result3.isSuccess()) {
52153
52189
  return { result: result3, hasCut };
52154
52190
  }
52155
- var subResult = parser2(result3.remaining());
52191
+ var subResult = parser(result3.remaining());
52156
52192
  if (subResult.isCut()) {
52157
52193
  return { result: result3, hasCut: true };
52158
52194
  } else if (subResult.isSuccess()) {
52159
52195
  var values2;
52160
- if (parser2.isCaptured) {
52161
- values2 = result3.value().withValue(parser2, subResult.value());
52196
+ if (parser.isCaptured) {
52197
+ values2 = result3.value().withValue(parser, subResult.value());
52162
52198
  } else {
52163
52199
  values2 = result3.value();
52164
52200
  }
@@ -52536,8 +52572,8 @@ var require_bottom_up = __commonJS({
52536
52572
  }
52537
52573
  exports3.infix = function(name2, ruleBuilder) {
52538
52574
  function map5(func) {
52539
- return exports3.infix(name2, function(parser2) {
52540
- var rule = ruleBuilder(parser2);
52575
+ return exports3.infix(name2, function(parser) {
52576
+ var rule = ruleBuilder(parser);
52541
52577
  return function(tokens) {
52542
52578
  var result2 = rule(tokens);
52543
52579
  return result2.map(function(right) {
@@ -53089,8 +53125,8 @@ var require_style_reader = __commonJS({
53089
53125
  );
53090
53126
  function parseString(rule, string5) {
53091
53127
  var tokens = tokenise(string5);
53092
- var parser2 = lop.Parser();
53093
- var parseResult = parser2.parseTokens(rule, tokens);
53128
+ var parser = lop.Parser();
53129
+ var parseResult = parser.parseTokens(rule, tokens);
53094
53130
  if (parseResult.isSuccess()) {
53095
53131
  return results.success(parseResult.value());
53096
53132
  } else {
@@ -55712,8 +55748,8 @@ var init_dist2 = __esm({
55712
55748
  this.options = options ?? defaultOptions;
55713
55749
  this.elementCB = elementCB ?? null;
55714
55750
  }
55715
- onparserinit(parser2) {
55716
- this.parser = parser2;
55751
+ onparserinit(parser) {
55752
+ this.parser = parser;
55717
55753
  }
55718
55754
  // Resets the handler back to starting state
55719
55755
  onreset() {
@@ -60792,7 +60828,7 @@ var init_types2 = __esm({
60792
60828
  });
60793
60829
 
60794
60830
  // src/shared/types.ts
60795
- var AnnotationTypeSchema, AnnotationStatusSchema, HighlightColorSchema, SeveritySchema, TandemModeSchema, AuthorSchema, ReplyAuthorSchema, AnnotationActionSchema, ExportFormatSchema, DocumentFormatSchema, ToolErrorCodeSchema;
60831
+ var AnnotationTypeSchema, AnnotationStatusSchema, HighlightColorSchema, SeveritySchema, TandemModeSchema, AuthorSchema, ReplyAuthorSchema, AnnotationActionSchema, ExportFormatSchema, DocumentFormatSchema, ToolErrorCodeSchema, ChannelErrorCodeSchema;
60796
60832
  var init_types3 = __esm({
60797
60833
  "src/shared/types.ts"() {
60798
60834
  "use strict";
@@ -60815,9 +60851,13 @@ var init_types3 = __esm({
60815
60851
  "FILE_NOT_FOUND",
60816
60852
  "NO_DOCUMENT",
60817
60853
  "INVALID_RANGE",
60854
+ "INVALID_ARGUMENT",
60855
+ "NOT_FOUND",
60856
+ "ANNOTATION_RESOLVED",
60818
60857
  "FORMAT_ERROR",
60819
60858
  "PERMISSION_DENIED"
60820
60859
  ]);
60860
+ ChannelErrorCodeSchema = external_exports.enum(["CHANNEL_CONNECT_FAILED", "MONITOR_CONNECT_FAILED"]);
60821
60861
  }
60822
60862
  });
60823
60863
 
@@ -67354,7 +67394,7 @@ async function startHocuspocus(port) {
67354
67394
  throw new Error("Connection rejected: missing origin header");
67355
67395
  }
67356
67396
  const url = new URL(origin);
67357
- if (url.hostname !== "localhost" && url.hostname !== "127.0.0.1" && url.hostname !== TAURI_HOSTNAME) {
67397
+ if (url.hostname !== "127.0.0.1" && url.hostname !== TAURI_HOSTNAME) {
67358
67398
  console.error(`[Hocuspocus] Rejected connection from origin: ${origin}`);
67359
67399
  throw new Error("Connection rejected: invalid origin");
67360
67400
  }
@@ -67924,7 +67964,7 @@ function makeAwarenessObserver(deps) {
67924
67964
  const userAwareness = doc.getMap(Y_MAP_USER_AWARENESS);
67925
67965
  let selectionDwellTimer = null;
67926
67966
  const awarenessObs = (event, txn) => {
67927
- if (txn.origin === MCP_ORIGIN) return;
67967
+ if (txn.origin === MCP_ORIGIN || txn.origin === FILE_SYNC_ORIGIN) return;
67928
67968
  if (event.keysChanged.has(Y_MAP_SELECTION)) {
67929
67969
  const selection = userAwareness.get(Y_MAP_SELECTION);
67930
67970
  if (selectionDwellTimer) {
@@ -68169,13 +68209,18 @@ function refreshRange(ann, ydoc, map5) {
68169
68209
  if (map5) map5.set(ann.id, updated);
68170
68210
  return updated;
68171
68211
  }
68172
- function refreshAllRanges(annotations, ydoc, map5) {
68212
+ function refreshAllRanges(annotations, ydoc, map5, opts) {
68173
68213
  const results = [];
68174
- ydoc.transact(() => {
68214
+ const run2 = () => {
68175
68215
  for (const ann of annotations) {
68176
68216
  results.push(refreshRange(ann, ydoc, map5));
68177
68217
  }
68178
- }, MCP_ORIGIN);
68218
+ };
68219
+ if (opts?.skipTransact) {
68220
+ run2();
68221
+ } else {
68222
+ ydoc.transact(run2, MCP_ORIGIN);
68223
+ }
68179
68224
  return results;
68180
68225
  }
68181
68226
  var init_positions2 = __esm({
@@ -69350,7 +69395,7 @@ function registerDocumentTools(server) {
69350
69395
  const doc = getOrCreateDocument(current.docName);
69351
69396
  const awarenessMap = doc.getMap(Y_MAP_AWARENESS);
69352
69397
  doc.transact(
69353
- () => awarenessMap.set("claude", {
69398
+ () => awarenessMap.set(Y_MAP_CLAUDE, {
69354
69399
  status: text5,
69355
69400
  timestamp: Date.now(),
69356
69401
  active: true,
@@ -69540,6 +69585,13 @@ function addReplyToAnnotation(ydoc, annotationsMap, annotationId, text5, author,
69540
69585
  const raw = annotationsMap.get(annotationId);
69541
69586
  if (!raw) return { ok: false, error: `Annotation ${annotationId} not found`, code: "NOT_FOUND" };
69542
69587
  const ann = sanitizeAnnotation(raw, makeOnLossy(void 0));
69588
+ if (ann.type !== "comment") {
69589
+ return {
69590
+ ok: false,
69591
+ error: `Cannot reply to a ${ann.type} annotation; only comments support replies`,
69592
+ code: "INVALID_ARGUMENT"
69593
+ };
69594
+ }
69543
69595
  if (ann.status !== "pending") {
69544
69596
  return {
69545
69597
  ok: false,
@@ -69787,7 +69839,7 @@ function registerAnnotationTools(server) {
69787
69839
  const repliesMap = getRepliesMap(da.ydoc);
69788
69840
  const annotationsWithReplies = results.map((ann) => ({
69789
69841
  ...ann,
69790
- replies: collectRepliesForAnnotation(repliesMap, ann.id)
69842
+ replies: ann.type === "comment" ? collectRepliesForAnnotation(repliesMap, ann.id) : []
69791
69843
  }));
69792
69844
  return mcpSuccess({
69793
69845
  annotations: annotationsWithReplies,
@@ -69808,8 +69860,11 @@ function registerAnnotationTools(server) {
69808
69860
  const da = getDocAndAnnotations(documentId);
69809
69861
  if (!da) return noDocumentError();
69810
69862
  const raw = da.map.get(id2);
69811
- if (!raw) return mcpError("INVALID_RANGE", `Annotation ${id2} not found`);
69863
+ if (!raw) return mcpError("NOT_FOUND", `Annotation ${id2} not found`);
69812
69864
  const ann = sanitizeAnnotation(raw, makeOnLossy(da.docHash));
69865
+ if (ann.status !== "pending") {
69866
+ return mcpError("ANNOTATION_NOT_PENDING", `Annotation ${id2} is already ${ann.status}`);
69867
+ }
69813
69868
  const updated = {
69814
69869
  ...ann,
69815
69870
  status: action === "accept" ? "accepted" : "dismissed",
@@ -69830,7 +69885,7 @@ function registerAnnotationTools(server) {
69830
69885
  const da = getDocAndAnnotations(documentId);
69831
69886
  if (!da) return noDocumentError();
69832
69887
  const result2 = removeAnnotationById(da.ydoc, da.map, da.filePath, id2);
69833
- if (!result2.ok) return mcpError("INVALID_RANGE", result2.error);
69888
+ if (!result2.ok) return mcpError("NOT_FOUND", result2.error);
69834
69889
  return mcpSuccess({ removed: true, id: id2 });
69835
69890
  })
69836
69891
  );
@@ -69850,20 +69905,20 @@ function registerAnnotationTools(server) {
69850
69905
  const da = getDocAndAnnotations(documentId);
69851
69906
  if (!da) return noDocumentError();
69852
69907
  const raw = da.map.get(id2);
69853
- if (!raw) return mcpError("INVALID_RANGE", `Annotation ${id2} not found`);
69908
+ if (!raw) return mcpError("NOT_FOUND", `Annotation ${id2} not found`);
69854
69909
  const ann = sanitizeAnnotation(raw, makeOnLossy(da.docHash));
69855
69910
  if (ann.status !== "pending") {
69856
- return mcpError("INVALID_RANGE", `Cannot edit a ${ann.status} annotation`);
69911
+ return mcpError("ANNOTATION_RESOLVED", `Cannot edit a ${ann.status} annotation`);
69857
69912
  }
69858
69913
  if (content3 === void 0 && newText === void 0 && reason === void 0) {
69859
69914
  return mcpError(
69860
- "INVALID_RANGE",
69915
+ "INVALID_ARGUMENT",
69861
69916
  "No editable fields provided. Use content, newText, or reason."
69862
69917
  );
69863
69918
  }
69864
69919
  if (newText !== void 0 && ann.type !== "comment") {
69865
69920
  return mcpError(
69866
- "INVALID_RANGE",
69921
+ "INVALID_ARGUMENT",
69867
69922
  `Cannot set replacement text on a ${ann.type} annotation. Only comments support suggestedText.`
69868
69923
  );
69869
69924
  }
@@ -69903,7 +69958,8 @@ function registerAnnotationTools(server) {
69903
69958
  const fullText = extractText(ydoc);
69904
69959
  const enriched = exportable.map((ann) => ({
69905
69960
  ...ann,
69906
- replies: collectRepliesForAnnotation(repliesMap, ann.id),
69961
+ // ADR-027: only comments surface replies (see tandem_getAnnotations).
69962
+ replies: ann.type === "comment" ? collectRepliesForAnnotation(repliesMap, ann.id) : [],
69907
69963
  textSnippet: fullText.slice(
69908
69964
  Math.max(0, ann.range.from),
69909
69965
  Math.min(fullText.length, ann.range.to)
@@ -69935,7 +69991,7 @@ function registerAnnotationTools(server) {
69935
69991
  MCP_ORIGIN
69936
69992
  );
69937
69993
  if (!result2.ok) {
69938
- const code3 = result2.code === "NOT_FOUND" ? "NOT_FOUND" : result2.code === "ANNOTATION_RESOLVED" ? "ANNOTATION_RESOLVED" : "INVALID_RANGE";
69994
+ const code3 = result2.code === "NOT_FOUND" ? "NOT_FOUND" : result2.code === "ANNOTATION_RESOLVED" ? "ANNOTATION_RESOLVED" : result2.code === "INVALID_ARGUMENT" ? "INVALID_ARGUMENT" : "INVALID_RANGE";
69939
69995
  return mcpError(code3, result2.error);
69940
69996
  }
69941
69997
  return mcpSuccess({ replyId: result2.replyId, annotationId });
@@ -69972,6 +70028,7 @@ var init_annotations2 = __esm({
69972
70028
  var file_opener_exports = {};
69973
70029
  __export(file_opener_exports, {
69974
70030
  SUPPORTED_EXTENSIONS: () => SUPPORTED_EXTENSIONS,
70031
+ __testEvictPartialDocState: () => evictPartialDocState,
69975
70032
  openFileByPath: () => openFileByPath,
69976
70033
  openFileFromContent: () => openFileFromContent,
69977
70034
  openScratchpad: () => openScratchpad
@@ -69995,7 +70052,8 @@ async function openFileByPath(filePath, options) {
69995
70052
  const forceReload = options?.force === true;
69996
70053
  if (forceReload) {
69997
70054
  const doc2 = getDocument(id2) ?? getOrCreateDocument(id2);
69998
- await clearAndReload(id2, doc2, resolved, format, existing);
70055
+ const reloadBuffer = format === "docx" ? await fs4.readFile(resolved) : await fs4.readFile(resolved, "utf-8");
70056
+ await clearAndReload(id2, doc2, resolved, format, existing, reloadBuffer);
69999
70057
  addDoc(id2, { id: id2, filePath: resolved, format, readOnly: readOnly2, source: "file" });
70000
70058
  setActiveDocId(id2);
70001
70059
  await wireAnnotationStore(id2, doc2, resolved);
@@ -70027,7 +70085,7 @@ async function openFileByPath(filePath, options) {
70027
70085
  const doc = getOrCreateDocument(id2);
70028
70086
  const restoredFromSession = await maybeRestoreSession(resolved, doc, fileName);
70029
70087
  if (!restoredFromSession) {
70030
- await loadContentIntoDoc(doc, format, resolved);
70088
+ await loadContentIntoDoc(doc, format, resolved, id2);
70031
70089
  }
70032
70090
  await finalizeDocOpen(id2, doc, resolved, fileName, format, readOnly2);
70033
70091
  return {
@@ -70062,8 +70120,10 @@ async function openFileFromContent(fileName, content3) {
70062
70120
  const syntheticPath = `${UPLOAD_PREFIX}${randomUUID2()}/${fileName}`;
70063
70121
  const id2 = docIdFromPath(syntheticPath);
70064
70122
  const doc = getOrCreateDocument(id2);
70065
- const adapter = getAdapter(format);
70066
- await adapter.load(doc, content3);
70123
+ await populateDocFromContent(doc, format, content3, id2, {
70124
+ displayName: fileName,
70125
+ dedupSource: syntheticPath
70126
+ });
70067
70127
  addDoc(id2, { id: id2, filePath: syntheticPath, format, readOnly: readOnly2, source: "upload" });
70068
70128
  setActiveDocId(id2);
70069
70129
  writeDocMeta(doc, id2, fileName, format, readOnly2);
@@ -70151,7 +70211,7 @@ function handleAlreadyOpen(id2, doc, format, resolved, readOnly2, existing, expl
70151
70211
  if (explicitReadOnly && !existing.readOnly) {
70152
70212
  addDoc(id2, { ...existing, readOnly: true });
70153
70213
  const meta2 = doc.getMap(Y_MAP_DOCUMENT_META);
70154
- doc.transact(() => meta2.set("readOnly", true), MCP_ORIGIN);
70214
+ doc.transact(() => meta2.set(Y_MAP_READ_ONLY, true), MCP_ORIGIN);
70155
70215
  }
70156
70216
  setActiveDocId(id2);
70157
70217
  broadcastOpenDocs();
@@ -70185,11 +70245,131 @@ async function maybeRestoreSession(resolved, doc, fileName) {
70185
70245
  }
70186
70246
  return false;
70187
70247
  }
70188
- async function loadContentIntoDoc(doc, format, resolved) {
70189
- const adapter = getAdapter(format);
70190
- const isDocx = format === "docx";
70191
- const fileContent = isDocx ? await fs4.readFile(resolved) : await fs4.readFile(resolved, "utf-8");
70192
- await adapter.load(doc, fileContent);
70248
+ async function prepareContent(format, source, ctx) {
70249
+ if (format === "docx") {
70250
+ if (!Buffer.isBuffer(source)) {
70251
+ throw Object.assign(new Error("prepareContent: docx requires Buffer source"), {
70252
+ code: "INVALID_SOURCE"
70253
+ });
70254
+ }
70255
+ const buffer3 = source;
70256
+ const [html2, comments] = await Promise.all([
70257
+ loadDocx(buffer3),
70258
+ extractDocxComments(buffer3).catch((err) => {
70259
+ console.error("[docx-comments] Comment extraction failed:", err);
70260
+ pushNotification({
70261
+ id: generateNotificationId(),
70262
+ type: "annotation-error",
70263
+ severity: "warning",
70264
+ message: `Failed to import Word comments from ${ctx.displayName}. Document opened without comments.`,
70265
+ dedupKey: `docx-comments:${ctx.dedupSource}`,
70266
+ timestamp: Date.now()
70267
+ });
70268
+ return [];
70269
+ })
70270
+ ]);
70271
+ return { format: "docx", html: html2, comments };
70272
+ }
70273
+ const content3 = typeof source === "string" ? source : source.toString("utf-8");
70274
+ if (format === "md") return { format: "md", content: content3 };
70275
+ return { format: "other", content: content3 };
70276
+ }
70277
+ function applyPreparedContent(doc, prepared, ctx) {
70278
+ switch (prepared.format) {
70279
+ case "docx": {
70280
+ htmlToYDoc(doc, prepared.html);
70281
+ if (prepared.comments.length > 0) {
70282
+ const annotMap = doc.getMap(Y_MAP_ANNOTATIONS);
70283
+ const before2 = new Set(annotMap.keys());
70284
+ try {
70285
+ injectCommentsAsAnnotations(doc, prepared.comments);
70286
+ } catch (err) {
70287
+ for (const k of annotMap.keys()) {
70288
+ if (!before2.has(k)) annotMap.delete(k);
70289
+ }
70290
+ console.error(
70291
+ "[docx-comments] inject failed mid-transact; document loads without imported comments:",
70292
+ err
70293
+ );
70294
+ pushNotification({
70295
+ id: generateNotificationId(),
70296
+ type: "annotation-error",
70297
+ severity: "warning",
70298
+ message: `Failed to import some Word comments from ${ctx.displayName}. Document opened, but comments may be missing.`,
70299
+ dedupKey: `docx-comments-inject:${ctx.dedupSource}`,
70300
+ timestamp: Date.now()
70301
+ });
70302
+ }
70303
+ }
70304
+ return;
70305
+ }
70306
+ case "md":
70307
+ loadMarkdown(doc, prepared.content);
70308
+ return;
70309
+ case "other":
70310
+ populateYDoc(doc, prepared.content);
70311
+ return;
70312
+ }
70313
+ }
70314
+ async function loadContentIntoDoc(doc, format, resolved, docId) {
70315
+ const buffer3 = await fs4.readFile(resolved);
70316
+ await populateDocFromContent(doc, format, buffer3, docId, {
70317
+ displayName: path8.basename(resolved),
70318
+ dedupSource: resolved
70319
+ });
70320
+ }
70321
+ async function populateDocFromContent(doc, format, source, docId, ctx) {
70322
+ const prepared = await prepareContent(format, source, ctx);
70323
+ try {
70324
+ doc.transact(() => applyPreparedContent(doc, prepared, ctx), MCP_ORIGIN);
70325
+ } catch (err) {
70326
+ let cleanupOk = true;
70327
+ try {
70328
+ doc.transact(() => {
70329
+ const fragment = doc.getXmlFragment("default");
70330
+ fragment.delete(0, fragment.length);
70331
+ const annotations = doc.getMap(Y_MAP_ANNOTATIONS);
70332
+ for (const k of [...annotations.keys()]) annotations.delete(k);
70333
+ }, MCP_ORIGIN);
70334
+ } catch (cleanupErr) {
70335
+ cleanupOk = false;
70336
+ console.error(
70337
+ "[Tandem] populateDocFromContent: cleanup after populate failure also failed:",
70338
+ cleanupErr
70339
+ );
70340
+ try {
70341
+ evictPartialDocState(doc, docId);
70342
+ } catch (evictErr) {
70343
+ console.error(
70344
+ "[Tandem] populateDocFromContent: eviction after cleanup failure also failed:",
70345
+ evictErr
70346
+ );
70347
+ }
70348
+ }
70349
+ console.error(
70350
+ "[Tandem] populateDocFromContent: populate failed; partial state cleared before rethrow.",
70351
+ { format, displayName: ctx.displayName, cleanupOk },
70352
+ err
70353
+ );
70354
+ throw err;
70355
+ }
70356
+ }
70357
+ function evictPartialDocState(doc, docId) {
70358
+ if (docId) {
70359
+ clearFileSyncContext(docId);
70360
+ }
70361
+ doc.transact(() => {
70362
+ const annotations = doc.getMap(Y_MAP_ANNOTATIONS);
70363
+ annotations.forEach((_3, k) => annotations.delete(k));
70364
+ const annotationReplies = doc.getMap(Y_MAP_ANNOTATION_REPLIES);
70365
+ annotationReplies.forEach((_3, k) => annotationReplies.delete(k));
70366
+ const awareness = doc.getMap(Y_MAP_AWARENESS);
70367
+ awareness.forEach((_3, k) => awareness.delete(k));
70368
+ const userAwareness = doc.getMap(Y_MAP_USER_AWARENESS);
70369
+ userAwareness.forEach((_3, k) => userAwareness.delete(k));
70370
+ const fragment = doc.getXmlFragment("default");
70371
+ fragment.delete(0, fragment.length);
70372
+ }, FILE_SYNC_ORIGIN);
70193
70373
  }
70194
70374
  async function finalizeDocOpen(id2, doc, resolved, fileName, format, readOnly2) {
70195
70375
  addDoc(id2, { id: id2, filePath: resolved, format, readOnly: readOnly2, source: "file" });
@@ -70226,7 +70406,7 @@ async function wireAnnotationStore(id2, doc, filePath) {
70226
70406
  });
70227
70407
  }
70228
70408
  }
70229
- async function clearAndReload(id2, doc, filePath, format, existing) {
70409
+ async function clearAndReload(id2, doc, resolved, format, existing, source) {
70230
70410
  console.error(`[Tandem] clearAndReload: reloading ${id2} from disk`);
70231
70411
  const dropped = clearFileSyncContext(id2);
70232
70412
  if (dropped) {
@@ -70236,25 +70416,12 @@ async function clearAndReload(id2, doc, filePath, format, existing) {
70236
70416
  console.error("[Tandem] clearAndReload: store.clear failed for %s:", id2, err);
70237
70417
  }
70238
70418
  }
70419
+ const ctx = {
70420
+ displayName: path8.basename(resolved),
70421
+ dedupSource: resolved
70422
+ };
70423
+ const prepared = await prepareContent(format, source, ctx);
70239
70424
  const isDocx = format === "docx";
70240
- let preparedHtml;
70241
- let preparedComments;
70242
- let preparedContent;
70243
- if (isDocx) {
70244
- const buffer3 = await fs4.readFile(filePath);
70245
- [preparedHtml, preparedComments] = await Promise.all([
70246
- loadDocx(buffer3),
70247
- extractDocxComments(buffer3).catch((err) => {
70248
- console.error(
70249
- "[docx-comments] Comment extraction failed; document will reload without imported comments:",
70250
- err
70251
- );
70252
- return [];
70253
- })
70254
- ]);
70255
- } else {
70256
- preparedContent = await fs4.readFile(filePath, "utf-8");
70257
- }
70258
70425
  try {
70259
70426
  doc.transact(() => {
70260
70427
  const annotations = doc.getMap(Y_MAP_ANNOTATIONS);
@@ -70265,21 +70432,12 @@ async function clearAndReload(id2, doc, filePath, format, existing) {
70265
70432
  awareness.forEach((_3, k) => awareness.delete(k));
70266
70433
  const userAwareness = doc.getMap(Y_MAP_USER_AWARENESS);
70267
70434
  userAwareness.forEach((_3, k) => userAwareness.delete(k));
70268
- if (isDocx && preparedHtml !== void 0) {
70269
- htmlToYDoc(doc, preparedHtml);
70270
- if (preparedComments && preparedComments.length > 0) {
70271
- injectCommentsAsAnnotations(doc, preparedComments);
70272
- }
70273
- } else if (format === "md" && preparedContent !== void 0) {
70274
- loadMarkdown(doc, preparedContent);
70275
- } else if (preparedContent !== void 0) {
70276
- populateYDoc(doc, preparedContent);
70277
- }
70435
+ applyPreparedContent(doc, prepared, ctx);
70278
70436
  const meta2 = doc.getMap(Y_MAP_DOCUMENT_META);
70279
- meta2.set("readOnly", isDocx);
70437
+ meta2.set(Y_MAP_READ_ONLY, isDocx);
70280
70438
  meta2.set("format", format);
70281
70439
  meta2.set("documentId", id2);
70282
- meta2.set("fileName", path8.basename(filePath));
70440
+ meta2.set("fileName", path8.basename(resolved));
70283
70441
  meta2.set(Y_MAP_SAVED_AT_VERSION, Date.now());
70284
70442
  }, MCP_ORIGIN);
70285
70443
  attachObservers(id2, doc);
@@ -70307,7 +70465,7 @@ async function initSavedBaseline(doc, filePath) {
70307
70465
  function writeDocMeta(doc, id2, fileName, format, readOnly2) {
70308
70466
  const meta2 = doc.getMap(Y_MAP_DOCUMENT_META);
70309
70467
  doc.transact(() => {
70310
- meta2.set("readOnly", readOnly2);
70468
+ meta2.set(Y_MAP_READ_ONLY, readOnly2);
70311
70469
  meta2.set("format", format);
70312
70470
  meta2.set("documentId", id2);
70313
70471
  meta2.set("fileName", fileName);
@@ -70354,7 +70512,7 @@ async function reloadFromDisk(id2, filePath, format) {
70354
70512
  } else {
70355
70513
  populateYDoc(doc, fileContent);
70356
70514
  }
70357
- }, MCP_ORIGIN);
70515
+ }, FILE_SYNC_ORIGIN);
70358
70516
  const annotationMap = doc.getMap(Y_MAP_ANNOTATIONS);
70359
70517
  const annotations = [];
70360
70518
  const reloadDocHash = docHash(filePath);
@@ -70367,8 +70525,8 @@ async function reloadFromDisk(id2, filePath, format) {
70367
70525
  )
70368
70526
  );
70369
70527
  if (annotations.length > 0) {
70370
- const refreshed = refreshAllRanges(annotations, doc, annotationMap);
70371
70528
  doc.transact(() => {
70529
+ const refreshed = refreshAllRanges(annotations, doc, annotationMap, { skipTransact: true });
70372
70530
  for (const ann of refreshed) {
70373
70531
  if (!ann.textSnapshot) continue;
70374
70532
  const vr = validateRange(doc, ann.range.from, ann.range.to, {
@@ -70840,7 +70998,8 @@ function makeRepliesObserver(deps) {
70840
70998
  const reply = repliesMap.get(key);
70841
70999
  if (!reply || reply.author !== "user") continue;
70842
71000
  const parentAnn = annotationsMap.get(reply.annotationId);
70843
- const textSnippet = parentAnn?.textSnapshot ?? "";
71001
+ if (!parentAnn || parentAnn.type !== "comment") continue;
71002
+ const textSnippet = parentAnn.textSnapshot ?? "";
70844
71003
  pushEvent2({
70845
71004
  id: generateEventId(),
70846
71005
  type: "annotation:reply",
@@ -72687,133 +72846,6 @@ var require_browser = __commonJS({
72687
72846
  }
72688
72847
  });
72689
72848
 
72690
- // node_modules/has-flag/index.js
72691
- var require_has_flag = __commonJS({
72692
- "node_modules/has-flag/index.js"(exports3, module3) {
72693
- "use strict";
72694
- module3.exports = (flag, argv = process.argv) => {
72695
- const prefix = flag.startsWith("-") ? "" : flag.length === 1 ? "-" : "--";
72696
- const position2 = argv.indexOf(prefix + flag);
72697
- const terminatorPosition = argv.indexOf("--");
72698
- return position2 !== -1 && (terminatorPosition === -1 || position2 < terminatorPosition);
72699
- };
72700
- }
72701
- });
72702
-
72703
- // node_modules/supports-color/index.js
72704
- var require_supports_color = __commonJS({
72705
- "node_modules/supports-color/index.js"(exports3, module3) {
72706
- "use strict";
72707
- var os2 = __require("os");
72708
- var tty = __require("tty");
72709
- var hasFlag = require_has_flag();
72710
- var { env: env3 } = process;
72711
- var flagForceColor;
72712
- if (hasFlag("no-color") || hasFlag("no-colors") || hasFlag("color=false") || hasFlag("color=never")) {
72713
- flagForceColor = 0;
72714
- } else if (hasFlag("color") || hasFlag("colors") || hasFlag("color=true") || hasFlag("color=always")) {
72715
- flagForceColor = 1;
72716
- }
72717
- function envForceColor() {
72718
- if ("FORCE_COLOR" in env3) {
72719
- if (env3.FORCE_COLOR === "true") {
72720
- return 1;
72721
- }
72722
- if (env3.FORCE_COLOR === "false") {
72723
- return 0;
72724
- }
72725
- return env3.FORCE_COLOR.length === 0 ? 1 : Math.min(Number.parseInt(env3.FORCE_COLOR, 10), 3);
72726
- }
72727
- }
72728
- function translateLevel(level) {
72729
- if (level === 0) {
72730
- return false;
72731
- }
72732
- return {
72733
- level,
72734
- hasBasic: true,
72735
- has256: level >= 2,
72736
- has16m: level >= 3
72737
- };
72738
- }
72739
- function supportsColor2(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
72740
- const noFlagForceColor = envForceColor();
72741
- if (noFlagForceColor !== void 0) {
72742
- flagForceColor = noFlagForceColor;
72743
- }
72744
- const forceColor2 = sniffFlags ? flagForceColor : noFlagForceColor;
72745
- if (forceColor2 === 0) {
72746
- return 0;
72747
- }
72748
- if (sniffFlags) {
72749
- if (hasFlag("color=16m") || hasFlag("color=full") || hasFlag("color=truecolor")) {
72750
- return 3;
72751
- }
72752
- if (hasFlag("color=256")) {
72753
- return 2;
72754
- }
72755
- }
72756
- if (haveStream && !streamIsTTY && forceColor2 === void 0) {
72757
- return 0;
72758
- }
72759
- const min5 = forceColor2 || 0;
72760
- if (env3.TERM === "dumb") {
72761
- return min5;
72762
- }
72763
- if (process.platform === "win32") {
72764
- const osRelease = os2.release().split(".");
72765
- if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) {
72766
- return Number(osRelease[2]) >= 14931 ? 3 : 2;
72767
- }
72768
- return 1;
72769
- }
72770
- if ("CI" in env3) {
72771
- if (["TRAVIS", "CIRCLECI", "APPVEYOR", "GITLAB_CI", "GITHUB_ACTIONS", "BUILDKITE", "DRONE"].some((sign) => sign in env3) || env3.CI_NAME === "codeship") {
72772
- return 1;
72773
- }
72774
- return min5;
72775
- }
72776
- if ("TEAMCITY_VERSION" in env3) {
72777
- return /^(9\.(0*[1-9]\d*)\.|\d{2,}\.)/.test(env3.TEAMCITY_VERSION) ? 1 : 0;
72778
- }
72779
- if (env3.COLORTERM === "truecolor") {
72780
- return 3;
72781
- }
72782
- if ("TERM_PROGRAM" in env3) {
72783
- const version3 = Number.parseInt((env3.TERM_PROGRAM_VERSION || "").split(".")[0], 10);
72784
- switch (env3.TERM_PROGRAM) {
72785
- case "iTerm.app":
72786
- return version3 >= 3 ? 3 : 2;
72787
- case "Apple_Terminal":
72788
- return 2;
72789
- }
72790
- }
72791
- if (/-256(color)?$/i.test(env3.TERM)) {
72792
- return 2;
72793
- }
72794
- if (/^screen|^xterm|^vt100|^vt220|^rxvt|color|ansi|cygwin|linux/i.test(env3.TERM)) {
72795
- return 1;
72796
- }
72797
- if ("COLORTERM" in env3) {
72798
- return 1;
72799
- }
72800
- return min5;
72801
- }
72802
- function getSupportLevel(stream, options = {}) {
72803
- const level = supportsColor2(stream, {
72804
- streamIsTTY: stream && stream.isTTY,
72805
- ...options
72806
- });
72807
- return translateLevel(level);
72808
- }
72809
- module3.exports = {
72810
- supportsColor: getSupportLevel,
72811
- stdout: getSupportLevel({ isTTY: tty.isatty(1) }),
72812
- stderr: getSupportLevel({ isTTY: tty.isatty(2) })
72813
- };
72814
- }
72815
- });
72816
-
72817
72849
  // node_modules/debug/src/node.js
72818
72850
  var require_node2 = __commonJS({
72819
72851
  "node_modules/debug/src/node.js"(exports3, module3) {
@@ -72833,7 +72865,7 @@ var require_node2 = __commonJS({
72833
72865
  );
72834
72866
  exports3.colors = [6, 2, 3, 4, 5, 1];
72835
72867
  try {
72836
- const supportsColor2 = require_supports_color();
72868
+ const supportsColor2 = __require("supports-color");
72837
72869
  if (supportsColor2 && (supportsColor2.stderr || supportsColor2).level >= 2) {
72838
72870
  exports3.colors = [
72839
72871
  20,
@@ -102722,7 +102754,6 @@ var require_dist3 = __commonJS({
102722
102754
  // src/server/mcp/launcher.ts
102723
102755
  var launcher_exports = {};
102724
102756
  __export(launcher_exports, {
102725
- killClaude: () => killClaude,
102726
102757
  launchClaude: () => launchClaude
102727
102758
  });
102728
102759
  import { spawn } from "child_process";
@@ -102731,7 +102762,7 @@ function launchClaude() {
102731
102762
  return { status: "already_running", pid: claudeProcess.pid };
102732
102763
  }
102733
102764
  const claudeCmd = process.env.TANDEM_CLAUDE_CMD || "claude";
102734
- const tandemUrl = `http://localhost:${process.env.TANDEM_MCP_PORT || DEFAULT_MCP_PORT}`;
102765
+ const tandemUrl = `http://127.0.0.1:${process.env.TANDEM_MCP_PORT || DEFAULT_MCP_PORT}`;
102735
102766
  const args2 = [
102736
102767
  "--dangerously-load-development-channels",
102737
102768
  "server:tandem-channel",
@@ -102777,20 +102808,6 @@ function launchClaude() {
102777
102808
  console.error(`[Launcher] Claude Code launched (pid: ${pid})`);
102778
102809
  return { status: "launched", pid };
102779
102810
  }
102780
- function killClaude() {
102781
- if (claudeProcess && !claudeProcess.killed) {
102782
- console.error(`[Launcher] Killing Claude Code (pid: ${claudeProcess.pid})`);
102783
- try {
102784
- claudeProcess.kill("SIGTERM");
102785
- } catch (err) {
102786
- const code3 = err.code;
102787
- if (code3 !== "ESRCH") {
102788
- console.error("[Launcher] Failed to kill Claude process:", err);
102789
- }
102790
- }
102791
- claudeProcess = null;
102792
- }
102793
- }
102794
102811
  var claudeProcess, TANDEM_SYSTEM_PROMPT;
102795
102812
  var init_launcher = __esm({
102796
102813
  "src/server/mcp/launcher.ts"() {
@@ -114664,33 +114681,34 @@ function createAuthMiddleware(getToken) {
114664
114681
  };
114665
114682
  }
114666
114683
 
114667
- // src/server/open-browser.ts
114668
- import { execFile } from "child_process";
114669
- function openBrowser(url) {
114670
- let command;
114671
- let args2;
114672
- if (process.platform === "win32") {
114673
- command = "cmd";
114674
- args2 = ["/c", "start", "", url];
114675
- } else if (process.platform === "darwin") {
114676
- command = "open";
114677
- args2 = [url];
114678
- } else {
114679
- command = "xdg-open";
114680
- args2 = [url];
114681
- }
114682
- execFile(command, args2, (err) => {
114683
- if (err) {
114684
- console.error("[Tandem] Could not open browser automatically.");
114685
- console.error(`[Tandem] Open this URL manually: ${url}`);
114686
- }
114687
- });
114688
- }
114689
-
114690
114684
  // src/server/mcp/server.ts
114691
114685
  init_platform();
114692
114686
  init_annotations2();
114693
114687
 
114688
+ // src/shared/api-paths.ts
114689
+ var API_EVENTS = "/api/events";
114690
+ var API_NOTIFY_STREAM = "/api/notify-stream";
114691
+ var API_CHANNEL_AWARENESS = "/api/channel-awareness";
114692
+ var API_CHANNEL_ERROR = "/api/channel-error";
114693
+ var API_CHANNEL_REPLY = "/api/channel-reply";
114694
+ var API_CHANNEL_PERMISSION = "/api/channel-permission";
114695
+ var API_CHANNEL_PERMISSION_VERDICT = "/api/channel-permission-verdict";
114696
+ var API_LAUNCH_CLAUDE = "/api/launch-claude";
114697
+ var API_MODE = "/api/mode";
114698
+ var API_INFO = "/api/info";
114699
+ var API_OPEN = "/api/open";
114700
+ var API_CLOSE = "/api/close";
114701
+ var API_SAVE = "/api/save";
114702
+ var API_UPLOAD = "/api/upload";
114703
+ var API_SCRATCHPAD = "/api/scratchpad";
114704
+ var API_CONVERT = "/api/convert";
114705
+ var API_APPLY_CHANGES = "/api/apply-changes";
114706
+ var API_ANNOTATION_REPLY = "/api/annotation-reply";
114707
+ var API_REMOVE_ANNOTATION = "/api/remove-annotation";
114708
+ var API_CHAT = "/api/chat";
114709
+ var API_SETUP = "/api/setup";
114710
+ var API_ROTATE_TOKEN = "/api/rotate-token";
114711
+
114694
114712
  // src/server/mcp/api-routes.ts
114695
114713
  init_constants();
114696
114714
 
@@ -114720,7 +114738,7 @@ function handleAnnotationReply(req, res) {
114720
114738
  const annotationsMap = ydoc.getMap(Y_MAP_ANNOTATIONS);
114721
114739
  const result2 = addReplyToAnnotation(ydoc, annotationsMap, annotationId, text5, "user");
114722
114740
  if (!result2.ok) {
114723
- const status = result2.code === "ANNOTATION_RESOLVED" ? 409 : 404;
114741
+ const status = result2.code === "ANNOTATION_RESOLVED" ? 409 : result2.code === "INVALID_ARGUMENT" ? 400 : 404;
114724
114742
  console.warn(`[Tandem] API error (${status}): annotation reply failed: ${result2.error}`);
114725
114743
  pushNotification({
114726
114744
  id: generateNotificationId(),
@@ -114945,11 +114963,15 @@ function errorCodeToHttpStatus(code3) {
114945
114963
  case "ENOENT":
114946
114964
  case "FILE_NOT_FOUND":
114947
114965
  case "NO_DOCUMENT":
114966
+ case "NOT_FOUND":
114948
114967
  return 404;
114949
114968
  case "INVALID_PATH":
114950
114969
  case "UNSUPPORTED_FORMAT":
114951
114970
  case "NO_SUGGESTIONS":
114971
+ case "INVALID_ARGUMENT":
114952
114972
  return 400;
114973
+ case "ANNOTATION_RESOLVED":
114974
+ return 409;
114953
114975
  case "FILE_TOO_LARGE":
114954
114976
  return 413;
114955
114977
  case "EBUSY":
@@ -114968,12 +114990,16 @@ function errorCodeToLabel(code3) {
114968
114990
  case "ENOENT":
114969
114991
  case "FILE_NOT_FOUND":
114970
114992
  case "NO_DOCUMENT":
114993
+ case "NOT_FOUND":
114971
114994
  return "NOT_FOUND";
114972
114995
  case "INVALID_PATH":
114973
114996
  return "INVALID_PATH";
114974
114997
  case "UNSUPPORTED_FORMAT":
114975
114998
  case "NO_SUGGESTIONS":
114999
+ case "INVALID_ARGUMENT":
114976
115000
  return "BAD_REQUEST";
115001
+ case "ANNOTATION_RESOLVED":
115002
+ return "ANNOTATION_RESOLVED";
114977
115003
  case "FILE_TOO_LARGE":
114978
115004
  return "FILE_TOO_LARGE";
114979
115005
  case "EBUSY":
@@ -115320,7 +115346,7 @@ var SKILL_CONTENT = readFileSync(SKILL_PATH, "utf-8");
115320
115346
  var __dirname2 = dirname2(fileURLToPath3(import.meta.url));
115321
115347
  var PACKAGE_ROOT = resolve3(__dirname2, "../..");
115322
115348
  var CHANNEL_DIST = resolve3(PACKAGE_ROOT, "dist/channel/index.js");
115323
- var MCP_URL = `http://localhost:${DEFAULT_MCP_PORT}`;
115349
+ var MCP_URL = `http://127.0.0.1:${DEFAULT_MCP_PORT}`;
115324
115350
  function buildMcpEntries(channelPath, opts = {}) {
115325
115351
  const isDesktop = opts.targetKind === "claude-desktop";
115326
115352
  let tandemEntry;
@@ -115584,7 +115610,7 @@ async function handleUpload(req, res) {
115584
115610
  // src/server/mcp/api-routes.ts
115585
115611
  function isHostAllowed(host, extraHosts = []) {
115586
115612
  const reqHost = (host ?? "").split(":")[0];
115587
- if (reqHost === "localhost" || reqHost === "127.0.0.1" || reqHost === TAURI_HOSTNAME) {
115613
+ if (reqHost === "127.0.0.1" || reqHost === TAURI_HOSTNAME) {
115588
115614
  return true;
115589
115615
  }
115590
115616
  return extraHosts.includes(reqHost);
@@ -115593,7 +115619,7 @@ function escapeRegExp2(s) {
115593
115619
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
115594
115620
  }
115595
115621
  var LOCALHOST_ORIGIN_RE = new RegExp(
115596
- `^https?://(localhost|127\\.0\\.0\\.1|${escapeRegExp2(TAURI_HOSTNAME)})(:\\d+)?$`
115622
+ `^https?://(127\\.0\\.0\\.1|${escapeRegExp2(TAURI_HOSTNAME)})(:\\d+)?$`
115597
115623
  );
115598
115624
  function isLocalhostOrigin(origin) {
115599
115625
  return LOCALHOST_ORIGIN_RE.test(origin ?? "");
@@ -115617,32 +115643,32 @@ function createApiMiddleware(extraHosts = []) {
115617
115643
  }
115618
115644
  var apiMiddleware = createApiMiddleware();
115619
115645
  function registerApiRoutes(app, largeBody, token, mw, setCurrentToken, getCurrentToken, infoHandlerDeps) {
115620
- app.get("/api/info", mw, makeInfoHandler(infoHandlerDeps));
115621
- app.get("/api/notify-stream", mw, handleNotifyStream);
115622
- app.get("/api/mode", mw, handleMode);
115623
- app.options("/api/open", mw);
115624
- app.post("/api/open", mw, largeBody, handleOpen);
115625
- app.options("/api/close", mw);
115626
- app.post("/api/close", mw, largeBody, handleClose);
115627
- app.options("/api/save", mw);
115628
- app.post("/api/save", mw, largeBody, handleSave);
115629
- app.options("/api/upload", mw);
115630
- app.post("/api/upload", mw, largeBody, handleUpload);
115631
- app.options("/api/scratchpad", mw);
115632
- app.post("/api/scratchpad", mw, handleScratchpad);
115633
- app.options("/api/convert", mw);
115634
- app.post("/api/convert", mw, largeBody, handleConvert);
115635
- app.options("/api/apply-changes", mw);
115636
- app.post("/api/apply-changes", mw, largeBody, handleApplyChanges);
115637
- app.options("/api/setup", mw);
115638
- app.post("/api/setup", mw, largeBody, makeSetupHandler({ token }));
115639
- app.options("/api/annotation-reply", mw);
115640
- app.post("/api/annotation-reply", mw, largeBody, handleAnnotationReply);
115641
- app.options("/api/remove-annotation", mw);
115642
- app.post("/api/remove-annotation", mw, largeBody, handleRemoveAnnotation);
115643
- app.options("/api/rotate-token", mw);
115646
+ app.get(API_INFO, mw, makeInfoHandler(infoHandlerDeps));
115647
+ app.get(API_NOTIFY_STREAM, mw, handleNotifyStream);
115648
+ app.get(API_MODE, mw, handleMode);
115649
+ app.options(API_OPEN, mw);
115650
+ app.post(API_OPEN, mw, largeBody, handleOpen);
115651
+ app.options(API_CLOSE, mw);
115652
+ app.post(API_CLOSE, mw, largeBody, handleClose);
115653
+ app.options(API_SAVE, mw);
115654
+ app.post(API_SAVE, mw, largeBody, handleSave);
115655
+ app.options(API_UPLOAD, mw);
115656
+ app.post(API_UPLOAD, mw, largeBody, handleUpload);
115657
+ app.options(API_SCRATCHPAD, mw);
115658
+ app.post(API_SCRATCHPAD, mw, handleScratchpad);
115659
+ app.options(API_CONVERT, mw);
115660
+ app.post(API_CONVERT, mw, largeBody, handleConvert);
115661
+ app.options(API_APPLY_CHANGES, mw);
115662
+ app.post(API_APPLY_CHANGES, mw, largeBody, handleApplyChanges);
115663
+ app.options(API_SETUP, mw);
115664
+ app.post(API_SETUP, mw, largeBody, makeSetupHandler({ token }));
115665
+ app.options(API_ANNOTATION_REPLY, mw);
115666
+ app.post(API_ANNOTATION_REPLY, mw, largeBody, handleAnnotationReply);
115667
+ app.options(API_REMOVE_ANNOTATION, mw);
115668
+ app.post(API_REMOVE_ANNOTATION, mw, largeBody, handleRemoveAnnotation);
115669
+ app.options(API_ROTATE_TOKEN, mw);
115644
115670
  app.post(
115645
- "/api/rotate-token",
115671
+ API_ROTATE_TOKEN,
115646
115672
  mw,
115647
115673
  largeBody,
115648
115674
  makeRotateTokenHandler({ setCurrentToken, getCurrentToken })
@@ -115847,6 +115873,7 @@ function processUnsurfacedInboxAnnotations(unsurfaced, fullText, surfaced, wasCh
115847
115873
 
115848
115874
  // src/server/mcp/channel-routes.ts
115849
115875
  init_constants();
115876
+ init_types3();
115850
115877
  init_utils();
115851
115878
  init_queue();
115852
115879
 
@@ -115904,9 +115931,9 @@ init_provider();
115904
115931
  var pendingPermissions = /* @__PURE__ */ new Map();
115905
115932
  var PERMISSION_TTL_MS = 3e4;
115906
115933
  function registerChannelRoutes(app, apiMiddleware2) {
115907
- app.get("/api/events", apiMiddleware2, sseHandler);
115908
- app.options("/api/channel-awareness", apiMiddleware2);
115909
- app.post("/api/channel-awareness", apiMiddleware2, (req, res) => {
115934
+ app.get(API_EVENTS, apiMiddleware2, sseHandler);
115935
+ app.options(API_CHANNEL_AWARENESS, apiMiddleware2);
115936
+ app.post(API_CHANNEL_AWARENESS, apiMiddleware2, (req, res) => {
115910
115937
  const { documentId, status, active, focusParagraph, focusOffset } = req.body ?? {};
115911
115938
  if (typeof status !== "string") {
115912
115939
  res.status(400).json({ error: "BAD_REQUEST", message: "status is required" });
@@ -115923,18 +115950,24 @@ function registerChannelRoutes(app, apiMiddleware2) {
115923
115950
  focusParagraph: typeof focusParagraph === "number" ? focusParagraph : null,
115924
115951
  focusOffset: typeof focusOffset === "number" ? focusOffset : null
115925
115952
  };
115926
- doc.transact(() => awarenessMap.set("claude", state), MCP_ORIGIN);
115953
+ doc.transact(() => awarenessMap.set(Y_MAP_CLAUDE, state), MCP_ORIGIN);
115927
115954
  }
115928
115955
  res.json({ ok: true, written: !!docId });
115929
115956
  });
115930
- app.options("/api/channel-error", apiMiddleware2);
115931
- app.post("/api/channel-error", apiMiddleware2, (req, res) => {
115957
+ app.options(API_CHANNEL_ERROR, apiMiddleware2);
115958
+ app.post(API_CHANNEL_ERROR, apiMiddleware2, (req, res) => {
115932
115959
  const { error: error2, message } = req.body ?? {};
115933
- console.error(`[Channel] Error: ${error2} \u2014 ${message}`);
115960
+ const parsed = ChannelErrorCodeSchema.safeParse(error2);
115961
+ if (!parsed.success) {
115962
+ console.error(`[Channel] Error: UNKNOWN_CODE (${String(error2)}) \u2014 ${message}`);
115963
+ res.status(400).json({ error: "BAD_REQUEST", message: "error must be a known ChannelErrorCode" });
115964
+ return;
115965
+ }
115966
+ console.error(`[Channel] Error: ${parsed.data} \u2014 ${message}`);
115934
115967
  res.json({ ok: true });
115935
115968
  });
115936
- app.options("/api/channel-reply", apiMiddleware2);
115937
- app.post("/api/channel-reply", apiMiddleware2, (req, res) => {
115969
+ app.options(API_CHANNEL_REPLY, apiMiddleware2);
115970
+ app.post(API_CHANNEL_REPLY, apiMiddleware2, (req, res) => {
115938
115971
  const { text: text5, documentId, replyTo } = req.body ?? {};
115939
115972
  if (typeof text5 !== "string") {
115940
115973
  res.status(400).json({ error: "BAD_REQUEST", message: "text is required" });
@@ -115955,8 +115988,8 @@ function registerChannelRoutes(app, apiMiddleware2) {
115955
115988
  ctrlDoc.transact(() => chatMap.set(id2, msg), MCP_ORIGIN);
115956
115989
  res.json({ sent: true, messageId: id2 });
115957
115990
  });
115958
- app.options("/api/channel-permission", apiMiddleware2);
115959
- app.post("/api/channel-permission", apiMiddleware2, (req, res) => {
115991
+ app.options(API_CHANNEL_PERMISSION, apiMiddleware2);
115992
+ app.post(API_CHANNEL_PERMISSION, apiMiddleware2, (req, res) => {
115960
115993
  const { requestId, toolName, description: description2, inputPreview } = req.body ?? {};
115961
115994
  if (typeof requestId !== "string" || typeof toolName !== "string") {
115962
115995
  res.status(400).json({ error: "BAD_REQUEST", message: "requestId and toolName required" });
@@ -115972,15 +116005,15 @@ function registerChannelRoutes(app, apiMiddleware2) {
115972
116005
  console.error(`[Channel] Permission request: ${toolName} \u2014 ${description2} (id: ${requestId})`);
115973
116006
  res.json({ ok: true });
115974
116007
  });
115975
- app.get("/api/channel-permission", apiMiddleware2, (_req, res) => {
116008
+ app.get(API_CHANNEL_PERMISSION, apiMiddleware2, (_req, res) => {
115976
116009
  const now = Date.now();
115977
116010
  for (const [id2, perm] of pendingPermissions) {
115978
116011
  if (now - perm.createdAt > PERMISSION_TTL_MS) pendingPermissions.delete(id2);
115979
116012
  }
115980
116013
  res.json({ pending: Array.from(pendingPermissions.values()) });
115981
116014
  });
115982
- app.options("/api/channel-permission-verdict", apiMiddleware2);
115983
- app.post("/api/channel-permission-verdict", apiMiddleware2, (req, res) => {
116015
+ app.options(API_CHANNEL_PERMISSION_VERDICT, apiMiddleware2);
116016
+ app.post(API_CHANNEL_PERMISSION_VERDICT, apiMiddleware2, (req, res) => {
115984
116017
  const { requestId, approved } = req.body ?? {};
115985
116018
  if (typeof requestId !== "string") {
115986
116019
  res.status(400).json({ error: "BAD_REQUEST", message: "requestId is required" });
@@ -115990,8 +116023,8 @@ function registerChannelRoutes(app, apiMiddleware2) {
115990
116023
  console.error(`[Channel] Permission verdict: ${requestId} \u2192 ${approved ? "allow" : "deny"}`);
115991
116024
  res.json({ ok: true, requestId, behavior: approved ? "allow" : "deny" });
115992
116025
  });
115993
- app.options("/api/chat", apiMiddleware2);
115994
- app.delete("/api/chat", apiMiddleware2, (_req, res) => {
116026
+ app.options(API_CHAT, apiMiddleware2);
116027
+ app.delete(API_CHAT, apiMiddleware2, (_req, res) => {
115995
116028
  const ctrlDoc = getOrCreateDocument(CTRL_ROOM);
115996
116029
  const chatMap = ctrlDoc.getMap(Y_MAP_CHAT);
115997
116030
  const count = chatMap.size;
@@ -116002,8 +116035,8 @@ function registerChannelRoutes(app, apiMiddleware2) {
116002
116035
  }, MCP_ORIGIN);
116003
116036
  res.json({ ok: true, cleared: count });
116004
116037
  });
116005
- app.options("/api/launch-claude", apiMiddleware2);
116006
- app.post("/api/launch-claude", apiMiddleware2, async (_req, res) => {
116038
+ app.options(API_LAUNCH_CLAUDE, apiMiddleware2);
116039
+ app.post(API_LAUNCH_CLAUDE, apiMiddleware2, async (_req, res) => {
116007
116040
  try {
116008
116041
  const { launchClaude: launchClaude2 } = await Promise.resolve().then(() => (init_launcher(), launcher_exports));
116009
116042
  const result2 = launchClaude2();
@@ -116146,7 +116179,7 @@ function registerNavigationTools(server) {
116146
116179
 
116147
116180
  // src/server/mcp/server.ts
116148
116181
  var esmRequire = createRequire(import.meta.url);
116149
- var APP_VERSION = true ? "0.11.2" : _readVersionFromDisk();
116182
+ var APP_VERSION = true ? "0.12.0" : _readVersionFromDisk();
116150
116183
  var MCP_SDK_VERSION = true ? "1.27.1" : "0.0.0-unknown";
116151
116184
  var __dirname3 = dirname3(fileURLToPath4(import.meta.url));
116152
116185
  var CLIENT_DIST = join3(__dirname3, "../client");
@@ -116296,9 +116329,9 @@ async function startMcpServerHttp(port, host = DEFAULT_BIND_HOST, token, resolve
116296
116329
  (_req, res) => {
116297
116330
  res.header("Access-Control-Allow-Origin", "*");
116298
116331
  res.json({
116299
- resource: `http://localhost:${port}/mcp`,
116332
+ resource: `http://127.0.0.1:${port}/mcp`,
116300
116333
  bearer_methods_supported: ["header"],
116301
- authorization_servers: [`http://localhost:${port}`]
116334
+ authorization_servers: [`http://127.0.0.1:${port}`]
116302
116335
  });
116303
116336
  }
116304
116337
  );
@@ -116307,9 +116340,9 @@ async function startMcpServerHttp(port, host = DEFAULT_BIND_HOST, token, resolve
116307
116340
  (_req, res) => {
116308
116341
  res.header("Access-Control-Allow-Origin", "*");
116309
116342
  res.json({
116310
- resource: `http://localhost:${port}/mcp`,
116343
+ resource: `http://127.0.0.1:${port}/mcp`,
116311
116344
  bearer_methods_supported: ["header"],
116312
- authorization_servers: [`http://localhost:${port}`]
116345
+ authorization_servers: [`http://127.0.0.1:${port}`]
116313
116346
  });
116314
116347
  }
116315
116348
  );
@@ -116352,13 +116385,6 @@ async function startMcpServerHttp(port, host = DEFAULT_BIND_HOST, token, resolve
116352
116385
  httpServer2.removeListener("error", reject2);
116353
116386
  httpServer2.on("error", (err) => console.error("[Tandem] HTTP server error:", err));
116354
116387
  console.error(`[Tandem] MCP HTTP server on http://${host}:${port}/mcp`);
116355
- if (process.env.TANDEM_OPEN_BROWSER === "1") {
116356
- if (existsSync2(CLIENT_DIST)) {
116357
- openBrowser(`http://localhost:${port}`);
116358
- } else {
116359
- console.error("[Tandem] Skipping browser open \u2014 no client assets found");
116360
- }
116361
- }
116362
116388
  resolve4(httpServer2);
116363
116389
  });
116364
116390
  httpServer2.on("error", reject2);
@@ -116430,10 +116456,17 @@ function injectTutorialAnnotations(doc) {
116430
116456
  }
116431
116457
  const annotation = {
116432
116458
  id: def.id,
116433
- author: "claude",
116459
+ // Notes are user-private (ADR-027); Claude can't author user-private content.
116460
+ // Comments and highlights are seeded as if Claude wrote them so the user
116461
+ // sees the cross-author authorship indicator.
116462
+ author: def.type === "note" ? "user" : "claude",
116434
116463
  type: def.type,
116435
116464
  range: result2.range,
116436
- relRange: result2.relRange,
116465
+ // Only attach a CRDT-anchored relRange when fully resolved. Matches
116466
+ // the reloadFromDisk pattern (file-opener.ts) — a partial anchor
116467
+ // leaks a half-resolved RelativePosition that downstream code would
116468
+ // re-anchor anyway via the lazy-attach path in refreshRange.
116469
+ ...result2.fullyAnchored ? { relRange: result2.relRange } : {},
116437
116470
  content: def.content,
116438
116471
  status: "pending",
116439
116472
  timestamp: Date.now(),
@@ -116456,6 +116489,25 @@ init_notifications();
116456
116489
  init_platform();
116457
116490
  init_manager();
116458
116491
 
116492
+ // src/server/startup-file.ts
116493
+ init_document_service();
116494
+ init_file_opener();
116495
+ async function maybeOpenStartupFile(envPath) {
116496
+ if (!envPath || envPath.trim() === "") return false;
116497
+ let result2;
116498
+ try {
116499
+ result2 = await openFileByPath(envPath);
116500
+ } catch (err) {
116501
+ console.error(
116502
+ `[Tandem] TANDEM_OPEN_FILE failed (${envPath}): ${err instanceof Error ? err.message : String(err)}`
116503
+ );
116504
+ return false;
116505
+ }
116506
+ setActiveDocId(result2.documentId);
116507
+ console.error(`[Tandem] Opened TANDEM_OPEN_FILE on startup: ${envPath}`);
116508
+ return true;
116509
+ }
116510
+
116459
116511
  // src/server/version-check.ts
116460
116512
  import fs11 from "fs/promises";
116461
116513
  import path15 from "path";
@@ -116478,7 +116530,7 @@ async function checkVersionChange(currentVersion, versionFilePath) {
116478
116530
 
116479
116531
  // src/server/index.ts
116480
116532
  init_provider();
116481
- var isProduction = process.env.TANDEM_OPEN_BROWSER === "1";
116533
+ var isProduction = process.env.TANDEM_TAURI_SIDECAR === "1";
116482
116534
  var SUPPRESSED_PATTERNS = [/^\[mammoth\]/, /Invalid access/i, /^\s*add yjs type/i];
116483
116535
  var originalStderrWrite = process.stderr.write.bind(process.stderr);
116484
116536
  if (isProduction) {
@@ -116709,7 +116761,12 @@ async function main2() {
116709
116761
  } catch (err) {
116710
116762
  console.error("[Tandem] Version check / changelog open failed (non-fatal):", err);
116711
116763
  }
116764
+ const startupFileRequested = !!process.env.TANDEM_OPEN_FILE?.trim();
116765
+ const startupFileOpened = await maybeOpenStartupFile(process.env.TANDEM_OPEN_FILE);
116712
116766
  if (docCount() === 0 && !process.env.TANDEM_NO_SAMPLE) {
116767
+ if (startupFileRequested && !startupFileOpened) {
116768
+ console.error("[Tandem] Falling back to welcome.md after TANDEM_OPEN_FILE failure");
116769
+ }
116713
116770
  const sampleBase = process.env.TANDEM_DATA_DIR || projectRoot;
116714
116771
  const samplePath = path16.join(sampleBase, "sample/welcome.md");
116715
116772
  try {
@@ -116731,16 +116788,16 @@ async function main2() {
116731
116788
  const [srv] = await Promise.all([
116732
116789
  startMcpServerHttp(mcpPort, bindHost, authToken, resolvedLanIP),
116733
116790
  startHocuspocus(wsPort).then(() => {
116734
- console.error(`[Tandem] Hocuspocus WebSocket server running on ws://localhost:${wsPort}`);
116791
+ console.error(`[Tandem] Hocuspocus WebSocket server running on ws://127.0.0.1:${wsPort}`);
116735
116792
  })
116736
116793
  ]);
116737
116794
  httpServer = srv;
116738
116795
  console.error("");
116739
116796
  console.error(` Tandem v${APP_VERSION}`);
116740
116797
  console.error("");
116741
- console.error(` MCP HTTP: http://localhost:${mcpPort}/mcp`);
116742
- console.error(` WebSocket: ws://localhost:${wsPort}`);
116743
- console.error(` Health: http://localhost:${mcpPort}/health`);
116798
+ console.error(` MCP HTTP: http://127.0.0.1:${mcpPort}/mcp`);
116799
+ console.error(` WebSocket: ws://127.0.0.1:${wsPort}`);
116800
+ console.error(` Health: http://127.0.0.1:${mcpPort}/health`);
116744
116801
  console.error("");
116745
116802
  console.error(" Open Claude Code and ask Claude to review a document.");
116746
116803
  console.error("");
@@ -116753,7 +116810,7 @@ async function main2() {
116753
116810
  console.error(`[Tandem] ${err instanceof Error ? err.message : err} \u2014 proceeding anyway`);
116754
116811
  }
116755
116812
  await startHocuspocus(wsPort);
116756
- console.error(`[Tandem] Hocuspocus WebSocket server running on ws://localhost:${wsPort}`);
116813
+ console.error(`[Tandem] Hocuspocus WebSocket server running on ws://127.0.0.1:${wsPort}`);
116757
116814
  })().catch((err) => {
116758
116815
  console.error("[Tandem] Hocuspocus startup error:", err);
116759
116816
  });