tandem-editor 0.14.3 → 0.16.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.
@@ -61735,6 +61735,16 @@ var init_positions2 = __esm({
61735
61735
  }
61736
61736
  });
61737
61737
 
61738
+ // src/server/file-io/docx-comment-id.ts
61739
+ function isCanonicalWordId(raw) {
61740
+ return !!raw && /^\d{1,9}$/.test(raw) && String(Number(raw)) === raw;
61741
+ }
61742
+ var init_docx_comment_id = __esm({
61743
+ "src/server/file-io/docx-comment-id.ts"() {
61744
+ "use strict";
61745
+ }
61746
+ });
61747
+
61738
61748
  // src/server/file-io/docx-walker.ts
61739
61749
  function isElement3(node3) {
61740
61750
  return node3.type === "tag";
@@ -61871,7 +61881,7 @@ function importAnnotationId(commentId, from3, to, bodyText) {
61871
61881
  return `import-${hash2}`;
61872
61882
  }
61873
61883
  function importReplyId(rootCommentId, replyCommentId, bodyText) {
61874
- const hash2 = crypto3.createHash("sha256").update(`${rootCommentId}\0${replyCommentId}\0${bodyText}`).digest("hex").slice(0, 12);
61884
+ const hash2 = crypto3.createHash("sha256").update(`${rootCommentId} ${replyCommentId} ${bodyText}`).digest("hex").slice(0, 12);
61875
61885
  return `import-reply-${hash2}`;
61876
61886
  }
61877
61887
  async function extractDocxComments(buffer4) {
@@ -62018,7 +62028,28 @@ function injectCommentsAsAnnotations(doc, comments, fileName) {
62018
62028
  const sourceFile = fileName ?? "unknown";
62019
62029
  let injected = 0;
62020
62030
  let migrated = 0;
62031
+ let reanchored = 0;
62021
62032
  let injectedReplies = 0;
62033
+ const byCommentId = /* @__PURE__ */ new Map();
62034
+ for (const [key, val] of map5) {
62035
+ const cid = val?.importSource?.commentId;
62036
+ if (!isCanonicalWordId(cid)) continue;
62037
+ const isPromoted = val.promotedFrom === "note";
62038
+ if (val.author !== "import" && !isPromoted) continue;
62039
+ const existing = byCommentId.get(cid);
62040
+ if (!existing) {
62041
+ byCommentId.set(cid, { key, ann: val });
62042
+ } else if (isPromoted && existing.ann.promotedFrom !== "note") {
62043
+ byCommentId.set(cid, { key, ann: val });
62044
+ console.error(
62045
+ `[docx-comments] Duplicate imported commentId ${cid}: preferred promoted record ${key} over import note ${existing.key}.`
62046
+ );
62047
+ } else {
62048
+ console.error(
62049
+ `[docx-comments] Duplicate imported commentId ${cid}: kept ${existing.key}, ignored ${key}.`
62050
+ );
62051
+ }
62052
+ }
62022
62053
  withInternal(doc, () => {
62023
62054
  for (const comment of comments) {
62024
62055
  const result2 = anchoredRange(doc, toFlatOffset(comment.from), toFlatOffset(comment.to));
@@ -62028,60 +62059,79 @@ function injectCommentsAsAnnotations(doc, comments, fileName) {
62028
62059
  );
62029
62060
  continue;
62030
62061
  }
62031
- const id2 = importAnnotationId(comment.commentId, comment.from, comment.to, comment.bodyText);
62032
- if (map5.has(id2)) {
62033
- const existing = map5.get(id2);
62062
+ const offsetId = importAnnotationId(
62063
+ comment.commentId,
62064
+ comment.from,
62065
+ comment.to,
62066
+ comment.bodyText
62067
+ );
62068
+ let effectiveKey = offsetId;
62069
+ const importSource = {
62070
+ author: comment.authorName,
62071
+ file: sourceFile,
62072
+ commentId: comment.commentId.slice(0, IMPORT_COMMENT_ID_MAX)
62073
+ };
62074
+ if (map5.has(offsetId)) {
62075
+ const existing = map5.get(offsetId);
62034
62076
  if (existing && existing.author === "import" && (existing.type === "comment" || existing.audience !== "private")) {
62035
- map5.set(id2, {
62077
+ map5.set(offsetId, {
62036
62078
  ...existing,
62037
62079
  type: "note",
62038
62080
  audience: "private",
62039
62081
  content: comment.bodyText,
62040
- importSource: {
62041
- author: comment.authorName,
62042
- file: sourceFile,
62043
- commentId: comment.commentId.slice(0, IMPORT_COMMENT_ID_MAX)
62044
- },
62082
+ importSource,
62045
62083
  rev: nextRev(existing)
62046
62084
  });
62047
62085
  migrated++;
62048
62086
  } else if (existing && existing.author === "import" && existing.importSource && existing.importSource.commentId === void 0) {
62049
- map5.set(id2, {
62087
+ map5.set(offsetId, {
62050
62088
  ...existing,
62051
- importSource: {
62052
- ...existing.importSource,
62053
- commentId: comment.commentId.slice(0, IMPORT_COMMENT_ID_MAX)
62054
- },
62089
+ importSource: { ...existing.importSource, commentId: importSource.commentId },
62055
62090
  rev: nextRev(existing)
62056
62091
  });
62057
62092
  }
62058
62093
  } else {
62059
- const annotation = {
62060
- id: id2,
62061
- author: "import",
62062
- type: "note",
62063
- audience: "private",
62064
- range: { from: result2.range.from, to: result2.range.to },
62065
- content: comment.bodyText,
62066
- status: "pending",
62067
- timestamp: comment.date ? new Date(comment.date).getTime() : Date.now(),
62068
- rev: nextRev(),
62069
- importSource: {
62070
- author: comment.authorName,
62071
- file: sourceFile,
62072
- commentId: comment.commentId.slice(0, IMPORT_COMMENT_ID_MAX)
62073
- },
62074
- ...result2.fullyAnchored ? { relRange: result2.relRange } : {}
62075
- };
62076
- map5.set(id2, annotation);
62077
- injected++;
62094
+ const drift = isCanonicalWordId(comment.commentId) ? byCommentId.get(comment.commentId) : void 0;
62095
+ if (drift && drift.ann.author === "import") {
62096
+ effectiveKey = drift.key;
62097
+ const { relRange: _staleRel, textSnapshot: _staleSnap, ...existingRest } = drift.ann;
62098
+ map5.set(drift.key, {
62099
+ ...existingRest,
62100
+ type: "note",
62101
+ audience: "private",
62102
+ content: comment.bodyText,
62103
+ range: { from: result2.range.from, to: result2.range.to },
62104
+ importSource,
62105
+ rev: nextRev(drift.ann),
62106
+ ...result2.fullyAnchored ? { relRange: result2.relRange } : {}
62107
+ });
62108
+ reanchored++;
62109
+ } else if (drift && drift.ann.promotedFrom === "note") {
62110
+ effectiveKey = drift.key;
62111
+ } else {
62112
+ const annotation = {
62113
+ id: offsetId,
62114
+ author: "import",
62115
+ type: "note",
62116
+ audience: "private",
62117
+ range: { from: result2.range.from, to: result2.range.to },
62118
+ content: comment.bodyText,
62119
+ status: "pending",
62120
+ timestamp: comment.date ? new Date(comment.date).getTime() : Date.now(),
62121
+ rev: nextRev(),
62122
+ importSource,
62123
+ ...result2.fullyAnchored ? { relRange: result2.relRange } : {}
62124
+ };
62125
+ map5.set(offsetId, annotation);
62126
+ injected++;
62127
+ }
62078
62128
  }
62079
62129
  for (const reply of comment.replies ?? []) {
62080
62130
  const replyId = importReplyId(comment.commentId, reply.commentId, reply.bodyText);
62081
62131
  if (repliesMap.has(replyId)) continue;
62082
62132
  const replyRecord = {
62083
62133
  id: replyId,
62084
- annotationId: id2,
62134
+ annotationId: effectiveKey,
62085
62135
  author: "import",
62086
62136
  text: reply.bodyText.slice(0, IMPORT_REPLY_BODY_CAP),
62087
62137
  timestamp: reply.date ? new Date(reply.date).getTime() : Date.now(),
@@ -62094,9 +62144,9 @@ function injectCommentsAsAnnotations(doc, comments, fileName) {
62094
62144
  }
62095
62145
  }
62096
62146
  });
62097
- if (injected > 0 || migrated > 0 || injectedReplies > 0 || comments.length > 0) {
62147
+ if (injected > 0 || migrated > 0 || reanchored > 0 || injectedReplies > 0) {
62098
62148
  console.error(
62099
- `[docx-comments] Imported ${injected}/${comments.length} Word comments as private notes` + (injectedReplies > 0 ? ` + ${injectedReplies} threaded replies` : "") + (migrated > 0 ? ` (migrated ${migrated} legacy records to note shape)` : "")
62149
+ `[docx-comments] Imported ${injected}/${comments.length} Word comments as private notes` + (injectedReplies > 0 ? ` + ${injectedReplies} threaded replies` : "") + (migrated > 0 ? ` (migrated ${migrated} legacy records to note shape)` : "") + (reanchored > 0 ? ` (re-anchored ${reanchored} drifted notes)` : "")
62100
62150
  );
62101
62151
  }
62102
62152
  return injected;
@@ -62112,6 +62162,7 @@ var init_docx_comments = __esm({
62112
62162
  init_types3();
62113
62163
  init_schema();
62114
62164
  init_positions2();
62165
+ init_docx_comment_id();
62115
62166
  init_docx_walker();
62116
62167
  MAX_THREAD_DEPTH = 64;
62117
62168
  IMPORT_COMMENT_ID_MAX = 32;
@@ -82618,10 +82669,7 @@ function isImportReply(reply) {
82618
82669
  return reply.author === "import" && typeof reply.importAuthor === "string" && reply.importAuthor.length > 0;
82619
82670
  }
82620
82671
  function reusableCommentId(raw) {
82621
- if (!raw || !/^\d{1,9}$/.test(raw)) return null;
82622
- const n = Number(raw);
82623
- if (String(n) !== raw) return null;
82624
- return n;
82672
+ return isCanonicalWordId(raw) ? Number(raw) : null;
82625
82673
  }
82626
82674
  function authorLabel(ann) {
82627
82675
  const imported = ann.importSource?.author?.trim();
@@ -82753,6 +82801,7 @@ var init_docx_comment_export = __esm({
82753
82801
  init_sanitize();
82754
82802
  init_document_model();
82755
82803
  init_positions2();
82804
+ init_docx_comment_id();
82756
82805
  }
82757
82806
  });
82758
82807
 
@@ -93454,6 +93503,7 @@ __export(apply_exports, {
93454
93503
  refreshSkillIfStale: () => refreshSkillIfStale,
93455
93504
  removeConfigEntries: () => removeConfigEntries,
93456
93505
  resolveChannelDist: () => resolveChannelDist,
93506
+ resolveCliVersion: () => resolveCliVersion,
93457
93507
  shouldRegisterChannelShim: () => shouldRegisterChannelShim,
93458
93508
  validateChannelShimPrereq: () => validateChannelShimPrereq
93459
93509
  });
@@ -93492,6 +93542,19 @@ function resolveChannelDist(env3 = process.env, exists = existsSync) {
93492
93542
  }
93493
93543
  return resolve3(PACKAGE_ROOT, "dist/channel/index.js");
93494
93544
  }
93545
+ function resolveCliVersion() {
93546
+ if (true) return "0.16.0";
93547
+ if (true) return "0.16.0";
93548
+ for (const rel of ["../../package.json", "../../../package.json"]) {
93549
+ try {
93550
+ const pkg = JSON.parse(readFileSync2(resolve3(__dirname2, rel), "utf8"));
93551
+ if (pkg.version) return pkg.version;
93552
+ } catch {
93553
+ }
93554
+ }
93555
+ console.error("[Tandem] Could not resolve CLI version for npx pin; using unpinned fallback");
93556
+ return "0.0.0-unknown";
93557
+ }
93495
93558
  function applyOpsForCli(create9, opts) {
93496
93559
  return {
93497
93560
  create: create9,
@@ -93508,7 +93571,7 @@ function buildMcpEntries(channelPath, opts = {}) {
93508
93571
  }
93509
93572
  tandemEntry = {
93510
93573
  command: "npx",
93511
- args: ["-y", "tandem-editor", "mcp-stdio"],
93574
+ args: ["-y", `tandem-editor@${CLI_VERSION}`, "mcp-stdio"],
93512
93575
  env: env3
93513
93576
  };
93514
93577
  } else {
@@ -93928,7 +93991,7 @@ async function applyConfigWithToken(token, opts = {}) {
93928
93991
  }
93929
93992
  return { updated, errors };
93930
93993
  }
93931
- var __dirname2, PACKAGE_ROOT, CHANNEL_DIST, MCP_URL, MAX_CONFIG_BYTES, PathRejectedError, DEFAULT_ROOTS_CACHE, MSIX_PACKAGE_PATTERN, BUNDLED_SKILL_VERSION, lastSkillRefreshError;
93994
+ var __dirname2, PACKAGE_ROOT, CHANNEL_DIST, MCP_URL, CLI_VERSION, MAX_CONFIG_BYTES, PathRejectedError, DEFAULT_ROOTS_CACHE, MSIX_PACKAGE_PATTERN, BUNDLED_SKILL_VERSION, lastSkillRefreshError;
93932
93995
  var init_apply = __esm({
93933
93996
  "src/server/integrations/apply.ts"() {
93934
93997
  "use strict";
@@ -93945,6 +94008,7 @@ var init_apply = __esm({
93945
94008
  })();
93946
94009
  CHANNEL_DIST = resolveChannelDist();
93947
94010
  MCP_URL = `http://127.0.0.1:${DEFAULT_MCP_PORT}`;
94011
+ CLI_VERSION = resolveCliVersion();
93948
94012
  MAX_CONFIG_BYTES = 5 * 1024 * 1024;
93949
94013
  PathRejectedError = class extends Error {
93950
94014
  constructor(path33, reason, message) {
@@ -94694,7 +94758,7 @@ var init_license_gate = __esm({
94694
94758
  });
94695
94759
 
94696
94760
  // src/server/mcp/output-schemas.ts
94697
- var FlatRangeSchema, RelativeRangeSchema2, VisibleAnnotationTypeSchema, annotationBaseShape, VisibleReplySchema, openDocumentEntry, statusOutputShape, getTextContentOutputShape, annotationWithRepliesSchema, getAnnotationsOutputShape, userActionSchema, userResponseSchema, inboxChatMessageSchema, checkInboxOutputShape, listDocumentsOutputShape, searchOutputShape;
94761
+ var FlatRangeSchema, RelativeRangeSchema2, VisibleAnnotationTypeSchema, annotationBaseShape, VisibleReplySchema, openDocumentEntry, statusOutputShape, getTextContentOutputShape, annotationWithRepliesSchema, getAnnotationsOutputShape, userActionSchema, userResponseSchema, inboxChatMessageSchema, checkInboxOutputShape, listDocumentsOutputShape, searchOutputShape, doctorResultSchema, diagnosticsOutputShape;
94698
94762
  var init_output_schemas = __esm({
94699
94763
  "src/server/mcp/output-schemas.ts"() {
94700
94764
  "use strict";
@@ -94828,6 +94892,28 @@ var init_output_schemas = __esm({
94828
94892
  matches: external_exports.array(external_exports.object({ from: external_exports.number(), to: external_exports.number(), text: external_exports.string() })),
94829
94893
  count: external_exports.number()
94830
94894
  };
94895
+ doctorResultSchema = external_exports.object({
94896
+ check: external_exports.string(),
94897
+ status: external_exports.enum(["pass", "warn", "fail"]),
94898
+ message: external_exports.string(),
94899
+ fix: external_exports.string().optional().describe("Suggested remediation when the check did not pass"),
94900
+ data: external_exports.record(external_exports.string(), external_exports.unknown()).optional().describe("Per-check detail bag")
94901
+ });
94902
+ diagnosticsOutputShape = {
94903
+ ok: external_exports.boolean().describe("True when no checks failed"),
94904
+ crashed: external_exports.boolean().describe("True if the diagnostic collector itself threw"),
94905
+ failures: external_exports.number(),
94906
+ warnings: external_exports.number(),
94907
+ summary: external_exports.string().describe("One-line human-readable summary"),
94908
+ error: external_exports.string().nullable().describe("Collector-level error message, or null"),
94909
+ results: external_exports.array(doctorResultSchema),
94910
+ version: external_exports.string().describe("Running Tandem version"),
94911
+ transport: external_exports.enum(["http", "stdio"]).describe("Transport this MCP server is serving"),
94912
+ platform: external_exports.string().describe("process.platform"),
94913
+ arch: external_exports.string().describe("process.arch"),
94914
+ nodeVersion: external_exports.string().describe("process.version"),
94915
+ tauriSidecar: external_exports.boolean().describe("True when running as the Tauri desktop sidecar")
94916
+ };
94831
94917
  }
94832
94918
  });
94833
94919
 
@@ -129341,6 +129427,7 @@ var init_convert2 = __esm({
129341
129427
  });
129342
129428
 
129343
129429
  // src/cli/doctor.ts
129430
+ import { execFile as execFile3 } from "child_process";
129344
129431
  import { existsSync as existsSync2, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
129345
129432
  import { request } from "http";
129346
129433
  import { createConnection } from "net";
@@ -129716,6 +129803,58 @@ function summarizeDoctorResults(failures, warnings) {
129716
129803
  return `${warnings} warning(s) \u2014 Tandem should work, but check the items above.`;
129717
129804
  return "All checks passed. Tandem is ready.";
129718
129805
  }
129806
+ function globalTandemEditorVersion() {
129807
+ return new Promise((resolve5) => {
129808
+ execFile3(
129809
+ "npm",
129810
+ ["ls", "-g", "--depth=0", "--json", "tandem-editor"],
129811
+ { shell: true, windowsHide: true, timeout: 4e3, maxBuffer: 8 * 1024 * 1024 },
129812
+ (_err, stdout) => {
129813
+ if (!stdout || stdout.trim().length === 0) {
129814
+ resolve5(null);
129815
+ return;
129816
+ }
129817
+ try {
129818
+ const parsed = JSON.parse(stdout);
129819
+ resolve5(parsed.dependencies?.["tandem-editor"]?.version ?? null);
129820
+ } catch {
129821
+ resolve5(null);
129822
+ }
129823
+ }
129824
+ );
129825
+ });
129826
+ }
129827
+ function evaluateStaleGlobal(bundled, globalVersion) {
129828
+ if (globalVersion === null) {
129829
+ return null;
129830
+ }
129831
+ if (globalVersion === bundled) {
129832
+ return { status: "pass", message: `Global tandem-editor@${globalVersion} matches this build` };
129833
+ }
129834
+ return {
129835
+ status: "warn",
129836
+ message: `Global tandem-editor@${globalVersion} differs from this build (${bundled}) \u2014 a stale global can break \`npx tandem-editor\` (e.g. Claude Desktop's MCP bridge).`,
129837
+ fix: "npm uninstall -g tandem-editor (or: npm install -g tandem-editor@latest)",
129838
+ data: { globalVersion, bundledVersion: bundled }
129839
+ };
129840
+ }
129841
+ async function checkStaleGlobal(r) {
129842
+ const bundled = true ? "0.16.0" : null;
129843
+ if (!bundled) return;
129844
+ let globalVersion;
129845
+ try {
129846
+ globalVersion = await globalTandemEditorVersion();
129847
+ } catch {
129848
+ return;
129849
+ }
129850
+ const result2 = evaluateStaleGlobal(bundled, globalVersion);
129851
+ if (!result2) return;
129852
+ if (result2.status === "pass") {
129853
+ r.pass(result2.message);
129854
+ } else {
129855
+ r.warn(result2.message, result2.fix, result2.data);
129856
+ }
129857
+ }
129719
129858
  async function runDoctor(opts = {}) {
129720
129859
  const wsPort2 = opts.wsPort ?? DEFAULT_WS_PORT;
129721
129860
  const mcpPort2 = opts.mcpPort ?? DEFAULT_MCP_PORT;
@@ -129725,6 +129864,7 @@ async function runDoctor(opts = {}) {
129725
129864
  await r.check("mcp-json", () => checkMcpJson(r));
129726
129865
  await r.check("user-mcp-config", () => checkUserMcpConfig(r));
129727
129866
  await r.check("annotation-store", () => checkAnnotationStore(r));
129867
+ await r.check("stale-global", () => checkStaleGlobal(r));
129728
129868
  const { mcp } = await r.check("ports", () => checkPorts(r, wsPort2, mcpPort2));
129729
129869
  if (mcp) {
129730
129870
  const healthy = await r.check("health", () => checkHealth(r, mcpPort2));
@@ -129914,12 +130054,20 @@ var init_document_reload = __esm({
129914
130054
  async function handleResolveDocxConflict(req, res) {
129915
130055
  if (assertOriginAllowlisted(req, res, API_DOCX_CONFLICT_RESOLVE)) return;
129916
130056
  if (assertLoopbackForMutation(req, res)) return;
129917
- const { choice } = req.body ?? {};
130057
+ const { documentId, choice } = req.body ?? {};
129918
130058
  if (choice !== "keep" && choice !== "reload") {
129919
130059
  res.status(400).json({ error: "BAD_REQUEST", message: 'choice must be "keep" or "reload".' });
129920
130060
  return;
129921
130061
  }
129922
- const docId = getActiveDocId();
130062
+ if (documentId !== void 0 && typeof documentId !== "string") {
130063
+ res.status(400).json({ error: "BAD_REQUEST", message: "documentId must be a string" });
130064
+ return;
130065
+ }
130066
+ if (typeof documentId === "string" && documentId.length > 0 && !hasDoc(documentId)) {
130067
+ res.status(400).json({ error: "NO_DOCUMENT", message: `Document ${documentId} is not open.` });
130068
+ return;
130069
+ }
130070
+ const docId = documentId?.length ? documentId : getActiveDocId();
129923
130071
  if (!docId) {
129924
130072
  res.status(400).json({ error: "BAD_REQUEST", message: "No active document." });
129925
130073
  return;
@@ -130838,11 +130986,11 @@ function validateTandemEntry(entry) {
130838
130986
  }
130839
130987
  const args2 = Array.isArray(entry.args) ? entry.args : [];
130840
130988
  if (entry.command === "npx") {
130841
- const argsOk = args2.length === TANDEM_STDIO_NPX_ARGS.length && TANDEM_STDIO_NPX_ARGS.every((expected, i) => args2[i] === expected);
130989
+ const argsOk = args2.length === 3 && args2[0] === TANDEM_STDIO_NPX_HEAD[0] && typeof args2[1] === "string" && TANDEM_EDITOR_SPEC_RE.test(args2[1]) && args2[2] === TANDEM_STDIO_NPX_TAIL[0];
130842
130990
  if (!argsOk) {
130843
130991
  return {
130844
130992
  status: "invalid-args",
130845
- reason: `npx args must be ${JSON.stringify(TANDEM_STDIO_NPX_ARGS)}; got ${JSON.stringify(args2)}`
130993
+ reason: `npx args must be ${JSON.stringify(TANDEM_STDIO_NPX_ARGS)} (the package may be version-pinned as 'tandem-editor@<version>'); got ${JSON.stringify(args2)}`
130846
130994
  };
130847
130995
  }
130848
130996
  return { status: "valid" };
@@ -130929,19 +131077,22 @@ function extractEntry(mcp, name2) {
130929
131077
  function hasExistingTandemEntry(installs) {
130930
131078
  return installs.some((i) => i.tandemEntry !== void 0);
130931
131079
  }
130932
- var TANDEM_STDIO_NPX_ARGS;
131080
+ var TANDEM_STDIO_NPX_HEAD, TANDEM_STDIO_NPX_TAIL, TANDEM_EDITOR_SPEC_RE, TANDEM_STDIO_NPX_ARGS;
130933
131081
  var init_existing_config = __esm({
130934
131082
  "src/server/integrations/existing-config.ts"() {
130935
131083
  "use strict";
130936
131084
  init_shared();
130937
131085
  init_apply();
130938
131086
  init_schema2();
130939
- TANDEM_STDIO_NPX_ARGS = ["-y", "tandem-editor", "mcp-stdio"];
131087
+ TANDEM_STDIO_NPX_HEAD = ["-y"];
131088
+ TANDEM_STDIO_NPX_TAIL = ["mcp-stdio"];
131089
+ TANDEM_EDITOR_SPEC_RE = /^tandem-editor(@\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)?$/;
131090
+ TANDEM_STDIO_NPX_ARGS = [...TANDEM_STDIO_NPX_HEAD, "tandem-editor", ...TANDEM_STDIO_NPX_TAIL];
130940
131091
  }
130941
131092
  });
130942
131093
 
130943
131094
  // src/server/integrations/install-claude-cli.ts
130944
- import { execFile as execFile3 } from "child_process";
131095
+ import { execFile as execFile4 } from "child_process";
130945
131096
  import { mkdtemp, rm as rm2, writeFile as writeFile2 } from "fs/promises";
130946
131097
  import { get as httpsGetDefault } from "https";
130947
131098
  import { tmpdir as tmpdir3 } from "os";
@@ -131139,7 +131290,7 @@ var init_install_claude_cli = __esm({
131139
131290
  "use strict";
131140
131291
  init_acl_win();
131141
131292
  init_apply();
131142
- execFileAsyncDefault = promisify3(execFile3);
131293
+ execFileAsyncDefault = promisify3(execFile4);
131143
131294
  INSTALLER_HOST = "claude.ai";
131144
131295
  INSTALLER_URL_POSIX = "https://claude.ai/install.sh";
131145
131296
  INSTALLER_URL_WIN = "https://claude.ai/install.ps1";
@@ -132124,15 +132275,20 @@ var init_contract2 = __esm({
132124
132275
  // src/server/launcher/supervisor.ts
132125
132276
  var supervisor_exports = {};
132126
132277
  __export(supervisor_exports, {
132278
+ RESUME_CONFIRM_MS: () => RESUME_CONFIRM_MS,
132127
132279
  createSupervisor: () => createSupervisor,
132128
132280
  resolveRouteCwd: () => resolveRouteCwd,
132129
- resolveSafeCwd: () => resolveSafeCwd
132281
+ resolveSafeCwd: () => resolveSafeCwd,
132282
+ shouldClearSession: () => shouldClearSession
132130
132283
  });
132131
132284
  import { spawn as spawn2 } from "child_process";
132132
132285
  import { randomUUID as randomUUID9 } from "crypto";
132133
132286
  import fs19 from "fs";
132134
132287
  import os2 from "os";
132135
132288
  import path28 from "path";
132289
+ function shouldClearSession(opts) {
132290
+ return opts.resuming && opts.code !== null && opts.code !== 0 && !opts.resumeConfirmed;
132291
+ }
132136
132292
  function createSupervisor(opts) {
132137
132293
  let child = null;
132138
132294
  let currentCwd;
@@ -132141,6 +132297,7 @@ function createSupervisor(opts) {
132141
132297
  let stopRequested = false;
132142
132298
  let restartIndex = 0;
132143
132299
  let restartTimer = null;
132300
+ let confirmTimer = null;
132144
132301
  let recentAttempts = [];
132145
132302
  let breakerTripped = false;
132146
132303
  let opLock = Promise.resolve();
@@ -132266,11 +132423,22 @@ function createSupervisor(opts) {
132266
132423
  writeSavedSession(plan.sessionId);
132267
132424
  }
132268
132425
  const spawnedAt = Date.now();
132426
+ let resumeConfirmed = !plan.resuming;
132427
+ if (plan.resuming) {
132428
+ confirmTimer = setTimeout(() => {
132429
+ resumeConfirmed = true;
132430
+ confirmTimer = null;
132431
+ }, RESUME_CONFIRM_MS);
132432
+ }
132269
132433
  child.stderr?.on("data", (chunk2) => {
132270
132434
  const line = chunk2.toString().trimEnd();
132271
132435
  if (line) console.error(`[Claude] ${line}`);
132272
132436
  });
132273
132437
  child.on("error", (err) => {
132438
+ if (confirmTimer) {
132439
+ clearTimeout(confirmTimer);
132440
+ confirmTimer = null;
132441
+ }
132274
132442
  child = null;
132275
132443
  currentCwd = void 0;
132276
132444
  currentSessionId = void 0;
@@ -132291,8 +132459,12 @@ function createSupervisor(opts) {
132291
132459
  const ranFor = Date.now() - spawnedAt;
132292
132460
  console.error(`[Launcher] Reaper exited (code=${code3} signal=${signal} after ${ranFor}ms)`);
132293
132461
  child = null;
132294
- if (plan.resuming && code3 !== 0 && ranFor < RESUME_GRACE_MS) {
132295
- console.error("[Launcher] Resume failed within grace window \u2014 clearing saved session");
132462
+ if (confirmTimer) {
132463
+ clearTimeout(confirmTimer);
132464
+ confirmTimer = null;
132465
+ }
132466
+ if (shouldClearSession({ resuming: plan.resuming, code: code3, resumeConfirmed })) {
132467
+ console.error("[Launcher] Resume failed before confirmation \u2014 clearing saved session");
132296
132468
  clearSavedSession();
132297
132469
  }
132298
132470
  if (stopRequested) return;
@@ -132360,7 +132532,7 @@ function createSupervisor(opts) {
132360
132532
  await spawnOnce(plan);
132361
132533
  setTimeout(() => {
132362
132534
  if (child) restartIndex = 0;
132363
- }, RESUME_GRACE_MS);
132535
+ }, RESUME_CONFIRM_MS);
132364
132536
  } catch (err) {
132365
132537
  console.error("[Launcher] Spawn failed:", err);
132366
132538
  }
@@ -132384,6 +132556,10 @@ function createSupervisor(opts) {
132384
132556
  clearTimeout(restartTimer);
132385
132557
  restartTimer = null;
132386
132558
  }
132559
+ if (confirmTimer) {
132560
+ clearTimeout(confirmTimer);
132561
+ confirmTimer = null;
132562
+ }
132387
132563
  const c = child;
132388
132564
  if (!c || c.killed) return;
132389
132565
  try {
@@ -132476,14 +132652,14 @@ function resolveRouteCwd(candidate, opts = {}) {
132476
132652
  if (rel.startsWith("..") || path28.isAbsolute(rel)) return null;
132477
132653
  return safe2;
132478
132654
  }
132479
- var SESSION_FILE_NAME, RESUME_GRACE_MS, RESTART_BACKOFFS_MS, CIRCUIT_BREAKER_MAX_ATTEMPTS, CIRCUIT_BREAKER_WINDOW_MS, UUID_V4_PATTERN;
132655
+ var SESSION_FILE_NAME, RESUME_CONFIRM_MS, RESTART_BACKOFFS_MS, CIRCUIT_BREAKER_MAX_ATTEMPTS, CIRCUIT_BREAKER_WINDOW_MS, UUID_V4_PATTERN;
132480
132656
  var init_supervisor = __esm({
132481
132657
  "src/server/launcher/supervisor.ts"() {
132482
132658
  "use strict";
132483
132659
  init_contract2();
132484
132660
  init_storage2();
132485
132661
  SESSION_FILE_NAME = "launcher-session.json";
132486
- RESUME_GRACE_MS = 5e3;
132662
+ RESUME_CONFIRM_MS = 3e4;
132487
132663
  RESTART_BACKOFFS_MS = [1e3, 5e3, 3e4];
132488
132664
  CIRCUIT_BREAKER_MAX_ATTEMPTS = 10;
132489
132665
  CIRCUIT_BREAKER_WINDOW_MS = 5 * 6e4;
@@ -157904,7 +158080,7 @@ var init_childProcess = __esm({
157904
158080
  });
157905
158081
 
157906
158082
  // node_modules/@sentry/node-core/build/esm/integrations/context.js
157907
- import { execFile as execFile4 } from "child_process";
158083
+ import { execFile as execFile5 } from "child_process";
157908
158084
  import { readFile as readFile3, readdir as readdir2 } from "fs";
157909
158085
  import * as os3 from "os";
157910
158086
  import { join as join10 } from "path";
@@ -158081,7 +158257,7 @@ async function getDarwinInfo() {
158081
158257
  };
158082
158258
  try {
158083
158259
  const output = await new Promise((resolve5, reject2) => {
158084
- execFile4("/usr/bin/sw_vers", (error5, stdout) => {
158260
+ execFile5("/usr/bin/sw_vers", (error5, stdout) => {
158085
158261
  if (error5) {
158086
158262
  reject2(error5);
158087
158263
  return;
@@ -187897,12 +188073,48 @@ function registerChannelRoutes(app, apiMiddleware2) {
187897
188073
  });
187898
188074
  }
187899
188075
 
188076
+ // src/server/mcp/diagnostics.ts
188077
+ init_doctor();
188078
+ init_constants();
188079
+ init_output_schemas();
188080
+ init_response();
188081
+ init_diagnostics();
188082
+ function registerDiagnosticsTools(server, deps = {}) {
188083
+ const version3 = deps.version ?? "unknown";
188084
+ const transport = deps.transport ?? "http";
188085
+ const wsPort2 = deps.wsPort ?? DEFAULT_WS_PORT;
188086
+ const mcpPort2 = deps.mcpPort ?? DEFAULT_MCP_PORT;
188087
+ const collect = deps.collect ?? runDoctor;
188088
+ server.registerTool(
188089
+ "tandem_diagnostics",
188090
+ {
188091
+ description: "Read connection and boot health (Node version, config registration, port probes, /health, SSE) as a structured report. No params. Use this to self-diagnose a broken Tandem connection over MCP instead of asking the user to run `tandem doctor`.",
188092
+ inputSchema: {},
188093
+ outputSchema: diagnosticsOutputShape
188094
+ },
188095
+ withStructuredErrors(
188096
+ withErrorBoundary("tandem_diagnostics", async () => {
188097
+ const report = filterDevRepoChecks(await collect({ wsPort: wsPort2, mcpPort: mcpPort2 }));
188098
+ return mcpStructured({
188099
+ ...report,
188100
+ version: version3,
188101
+ transport,
188102
+ platform: process.platform,
188103
+ arch: process.arch,
188104
+ nodeVersion: process.version,
188105
+ tauriSidecar: process.env.TANDEM_TAURI_SIDECAR === "1"
188106
+ });
188107
+ })
188108
+ )
188109
+ );
188110
+ }
188111
+
187900
188112
  // src/server/mcp/server.ts
187901
188113
  init_document2();
187902
188114
  init_document_service();
187903
188115
  init_docx_apply2();
187904
188116
  var esmRequire2 = createRequire2(import.meta.url);
187905
- var APP_VERSION = true ? "0.14.3" : _readVersionFromDisk();
188117
+ var APP_VERSION = true ? "0.16.0" : _readVersionFromDisk();
187906
188118
  var MCP_SDK_VERSION = true ? "1.27.1" : "0.0.0-unknown";
187907
188119
  var __dirname3 = dirname3(fileURLToPath4(import.meta.url));
187908
188120
  var CLIENT_DIST = join7(__dirname3, "../client");
@@ -187928,7 +188140,7 @@ var WORKFLOWS_PATH = findRepoFile(__dirname3, "docs/workflows.md");
187928
188140
  var mcpServer = null;
187929
188141
  var currentTransport = null;
187930
188142
  var connectingPromise = null;
187931
- function createMcpServer() {
188143
+ function createMcpServer(diagnostics = {}) {
187932
188144
  const server = new McpServer({
187933
188145
  name: "tandem",
187934
188146
  version: APP_VERSION
@@ -187938,6 +188150,7 @@ function createMcpServer() {
187938
188150
  registerNavigationTools(server);
187939
188151
  registerAwarenessTools(server);
187940
188152
  registerApplyTools(server);
188153
+ registerDiagnosticsTools(server, diagnostics);
187941
188154
  return server;
187942
188155
  }
187943
188156
  function jsonrpcId(body) {
@@ -187973,7 +188186,7 @@ async function closeMcpSession() {
187973
188186
  }
187974
188187
  }
187975
188188
  async function startMcpServerStdio() {
187976
- const server = createMcpServer();
188189
+ const server = createMcpServer({ version: APP_VERSION, transport: "stdio" });
187977
188190
  const transport = new StdioServerTransport();
187978
188191
  await server.connect(transport);
187979
188192
  }
@@ -187986,7 +188199,7 @@ function snapshotToolCount(server) {
187986
188199
  return Object.keys(tools).length;
187987
188200
  }
187988
188201
  async function startMcpServerHttp(port, host = DEFAULT_BIND_HOST, token, resolvedLanIP, launcher, wsPort2 = DEFAULT_WS_PORT, shutdownWiring) {
187989
- mcpServer = createMcpServer();
188202
+ mcpServer = createMcpServer({ version: APP_VERSION, transport: "http", wsPort: wsPort2, mcpPort: port });
187990
188203
  const toolCount = snapshotToolCount(mcpServer);
187991
188204
  const { default: express2 } = await Promise.resolve().then(() => __toESM(require_express2(), 1));
187992
188205
  const app = express2();