tandem-editor 0.16.0 → 0.18.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.
@@ -93543,8 +93543,8 @@ function resolveChannelDist(env3 = process.env, exists = existsSync) {
93543
93543
  return resolve3(PACKAGE_ROOT, "dist/channel/index.js");
93544
93544
  }
93545
93545
  function resolveCliVersion() {
93546
- if (true) return "0.16.0";
93547
- if (true) return "0.16.0";
93546
+ if (true) return "0.18.0";
93547
+ if (true) return "0.18.0";
93548
93548
  for (const rel of ["../../package.json", "../../../package.json"]) {
93549
93549
  try {
93550
93550
  const pkg = JSON.parse(readFileSync2(resolve3(__dirname2, rel), "utf8"));
@@ -129433,6 +129433,10 @@ import { request } from "http";
129433
129433
  import { createConnection } from "net";
129434
129434
  import { homedir as homedir3, platform } from "os";
129435
129435
  import { join as join5 } from "path";
129436
+ function errorClass(err) {
129437
+ if (err instanceof Error) return err.name;
129438
+ return typeof err;
129439
+ }
129436
129440
  function checkNodeVersion(r) {
129437
129441
  const version3 = process.version;
129438
129442
  const major2 = Number.parseInt(version3.slice(1), 10);
@@ -129452,6 +129456,201 @@ function checkNodeModules(r) {
129452
129456
  r.fail("node_modules/ not found", "npm install");
129453
129457
  }
129454
129458
  }
129459
+ function recordEvaluation(r, result2) {
129460
+ if (!result2) return;
129461
+ if (result2.status === "warn") {
129462
+ r.warn(result2.message, result2.fix, result2.data);
129463
+ return;
129464
+ }
129465
+ if (result2.status === "skip") {
129466
+ r.pass(`skipped \u2014 ${result2.message}`, result2.fix, { ...result2.data, skipped: true });
129467
+ return;
129468
+ }
129469
+ r.pass(result2.message, result2.fix, result2.data);
129470
+ }
129471
+ function probeTandemEditorRepo(dir) {
129472
+ const read = readJson2(join5(dir, "package.json"));
129473
+ if (read.kind === "absent") return "no";
129474
+ if (read.kind === "unreadable") return "unreadable";
129475
+ const parsed = read.value;
129476
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
129477
+ return "unreadable";
129478
+ }
129479
+ return parsed.name === "tandem-editor" ? "yes" : "no";
129480
+ }
129481
+ function readJson2(path33) {
129482
+ let raw;
129483
+ try {
129484
+ raw = readFileSync3(path33, "utf-8");
129485
+ } catch (err) {
129486
+ const code3 = err.code;
129487
+ if (code3 === "ENOENT") return { kind: "absent" };
129488
+ return { kind: "unreadable", reason: code3 ?? errorClass(err) };
129489
+ }
129490
+ try {
129491
+ return { kind: "ok", value: JSON.parse(raw) };
129492
+ } catch {
129493
+ return { kind: "unreadable", reason: "not valid JSON" };
129494
+ }
129495
+ }
129496
+ function parseLockfileEntry(value) {
129497
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
129498
+ const entry = value;
129499
+ if (entry.version !== void 0 && typeof entry.version !== "string") return null;
129500
+ if (entry.optional !== void 0 && typeof entry.optional !== "boolean") return null;
129501
+ return {
129502
+ version: entry.version,
129503
+ optional: entry.optional
129504
+ };
129505
+ }
129506
+ function parseLockfileJson(value) {
129507
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
129508
+ const obj2 = value;
129509
+ if (obj2.version !== void 0 && typeof obj2.version !== "string") return null;
129510
+ const version3 = obj2.version;
129511
+ if (obj2.packages === void 0) return { version: version3 };
129512
+ if (typeof obj2.packages !== "object" || obj2.packages === null || Array.isArray(obj2.packages)) {
129513
+ return null;
129514
+ }
129515
+ const packages = {};
129516
+ for (const [path33, raw] of Object.entries(obj2.packages)) {
129517
+ const entry = parseLockfileEntry(raw);
129518
+ if (!entry) return null;
129519
+ packages[path33] = entry;
129520
+ }
129521
+ return { version: version3, packages };
129522
+ }
129523
+ function evaluateNpmStaleness(pkgInput, lockInput, hiddenLockInput) {
129524
+ if (!pkgInput || !lockInput || !hiddenLockInput) return null;
129525
+ const lock = parseLockfileJson(lockInput);
129526
+ if (!lock) {
129527
+ return {
129528
+ status: "skip",
129529
+ message: "cannot compare (package-lock.json has an unexpected structure)",
129530
+ fix: "Restore it from git: git checkout package-lock.json",
129531
+ data: { reason: "malformed-lock" }
129532
+ };
129533
+ }
129534
+ const hiddenLock = parseLockfileJson(hiddenLockInput);
129535
+ if (!hiddenLock) {
129536
+ return {
129537
+ status: "skip",
129538
+ message: "cannot compare (node_modules/.package-lock.json has an unexpected structure)",
129539
+ fix: "npm install",
129540
+ data: { reason: "malformed-hidden-lock" }
129541
+ };
129542
+ }
129543
+ const pkg = typeof pkgInput === "object" && !Array.isArray(pkgInput) ? pkgInput : {};
129544
+ const pkgVersion = typeof pkg.version === "string" ? pkg.version : void 0;
129545
+ if (pkgVersion && lock.version && pkgVersion !== lock.version) {
129546
+ return {
129547
+ status: "warn",
129548
+ message: `package-lock.json (v${lock.version}) is out of date with package.json (v${pkgVersion})`,
129549
+ fix: "npm install",
129550
+ data: { packageVersion: pkgVersion, lockVersion: lock.version }
129551
+ };
129552
+ }
129553
+ if (lock.version && hiddenLock.version && lock.version !== hiddenLock.version) {
129554
+ return {
129555
+ status: "warn",
129556
+ message: `node_modules was installed from a different lockfile (v${hiddenLock.version} installed, v${lock.version} expected)`,
129557
+ fix: "npm install",
129558
+ data: { lockVersion: lock.version, installedVersion: hiddenLock.version }
129559
+ };
129560
+ }
129561
+ const wanted = lock.packages ?? {};
129562
+ const installed = hiddenLock.packages ?? {};
129563
+ const wantedCount = Object.keys(wanted).filter((path33) => path33 !== "").length;
129564
+ if (wantedCount === 0) {
129565
+ return {
129566
+ status: "skip",
129567
+ message: "cannot compare (package-lock.json lists no packages \u2014 a lockfileVersion 1 file, or one written by something other than npm)",
129568
+ fix: "npm install",
129569
+ // No inferred `lockfileVersion` here: we did not read that field, and
129570
+ // guessing it from the presence of `packages` would put a fabricated
129571
+ // value under a real npm field name.
129572
+ data: { reason: "no-comparable-packages" }
129573
+ };
129574
+ }
129575
+ const drifted = [];
129576
+ for (const [path33, entry] of Object.entries(wanted)) {
129577
+ if (path33 === "") continue;
129578
+ const got = installed[path33];
129579
+ if (!got) {
129580
+ if (!entry.optional) drifted.push(path33);
129581
+ } else if (entry.version !== got.version) {
129582
+ drifted.push(path33);
129583
+ }
129584
+ }
129585
+ for (const path33 of Object.keys(installed)) {
129586
+ if (path33 !== "" && !(path33 in wanted)) drifted.push(path33);
129587
+ }
129588
+ if (drifted.length > 0) {
129589
+ return {
129590
+ status: "warn",
129591
+ message: `node_modules is stale \u2014 ${drifted.length} package(s) differ from package-lock.json (e.g. after a pull or branch switch)`,
129592
+ fix: "npm install",
129593
+ data: { driftCount: drifted.length, sample: drifted.slice(0, 5) }
129594
+ };
129595
+ }
129596
+ const missingVersions = [];
129597
+ if (!pkgVersion) missingVersions.push("package.json");
129598
+ if (!lock.version) missingVersions.push("package-lock.json");
129599
+ if (!hiddenLock.version) missingVersions.push("node_modules/.package-lock.json");
129600
+ if (missingVersions.length > 0) {
129601
+ return {
129602
+ status: "skip",
129603
+ message: `node_modules matches package-lock.json, but the version check could not run \u2014 no "version" field in ${missingVersions.join(", ")}`,
129604
+ data: { reason: "no-comparable-version", missingVersions }
129605
+ };
129606
+ }
129607
+ return {
129608
+ status: "pass",
129609
+ message: "node_modules matches package-lock.json",
129610
+ data: { packageCount: Object.keys(installed).length }
129611
+ };
129612
+ }
129613
+ function readLockfileOrReport(r, path33, label) {
129614
+ const read = readJson2(path33);
129615
+ if (read.kind === "absent") {
129616
+ r.pass(`skipped \u2014 cannot compare (${label} not found)`, void 0, {
129617
+ skipped: true,
129618
+ reason: "absent",
129619
+ path: label
129620
+ });
129621
+ return { ok: false };
129622
+ }
129623
+ if (read.kind === "ok") {
129624
+ if (typeof read.value === "object" && read.value !== null && !Array.isArray(read.value)) {
129625
+ return { ok: true, value: read.value };
129626
+ }
129627
+ r.warn(
129628
+ `${label} is not a JSON object \u2014 npm install staleness cannot be checked`,
129629
+ "Check for a truncated or hand-edited file, then: npm install",
129630
+ { reason: "not-an-object", path: label }
129631
+ );
129632
+ return { ok: false };
129633
+ }
129634
+ r.warn(
129635
+ `${label} could not be read (${read.reason}) \u2014 npm install staleness cannot be checked`,
129636
+ "Check for merge-conflict markers or a truncated file, then: npm install",
129637
+ { reason: read.reason, path: label }
129638
+ );
129639
+ return { ok: false };
129640
+ }
129641
+ function checkNpmStaleness(r, repoDir) {
129642
+ const pkgRead = readJson2(join5(repoDir, "package.json"));
129643
+ const pkg = pkgRead.kind === "ok" ? pkgRead.value : null;
129644
+ const lock = readLockfileOrReport(r, join5(repoDir, "package-lock.json"), "package-lock.json");
129645
+ if (!lock.ok) return;
129646
+ const hiddenLock = readLockfileOrReport(
129647
+ r,
129648
+ join5(repoDir, "node_modules", ".package-lock.json"),
129649
+ "node_modules/.package-lock.json"
129650
+ );
129651
+ if (!hiddenLock.ok) return;
129652
+ recordEvaluation(r, evaluateNpmStaleness(pkg, lock.value, hiddenLock.value));
129653
+ }
129455
129654
  function checkMcpJson(r) {
129456
129655
  const mcpPath = join5(process.cwd(), ".mcp.json");
129457
129656
  if (!existsSync2(mcpPath)) {
@@ -129577,14 +129776,64 @@ async function checkPorts(r, wsPort2, mcpPort2) {
129577
129776
  }
129578
129777
  return { ws, mcp };
129579
129778
  }
129779
+ function evaluateOrphanedVite(input) {
129780
+ const { viteUp, viteConfirmed, wsUp, mcpUp, wsPort: wsPort2, mcpPort: mcpPort2, vitePort } = input;
129781
+ if (!viteUp) return null;
129782
+ if (!viteConfirmed) {
129783
+ return {
129784
+ status: "skip",
129785
+ message: `cannot identify the process on :${vitePort} \u2014 it is listening but did not answer /@vite/client, so it is probably not a Vite dev server`,
129786
+ data: { vite: false, ws: wsUp, mcp: mcpUp, reason: "unconfirmed-vite" }
129787
+ };
129788
+ }
129789
+ if (!wsUp && !mcpUp) {
129790
+ return {
129791
+ status: "warn",
129792
+ message: `Vite dev server on :${vitePort} is running but the backend (:${wsPort2} + :${mcpPort2}) is down \u2014 likely orphaned by a crashed dev session`,
129793
+ fix: `If you meant to run the client alone (npm run dev:client), this is expected. Otherwise kill the process on :${vitePort} and restart: npm run dev:standalone`,
129794
+ data: { vite: true, ws: wsUp, mcp: mcpUp }
129795
+ };
129796
+ }
129797
+ if (wsUp !== mcpUp) {
129798
+ return {
129799
+ status: "skip",
129800
+ message: `cannot tell whether the Vite dev server on :${vitePort} is orphaned \u2014 the backend is only partially up (:${wsPort2} ${wsUp ? "up" : "down"}, :${mcpPort2} ${mcpUp ? "up" : "down"}); see the ports check`,
129801
+ data: { vite: true, ws: wsUp, mcp: mcpUp, reason: "partial-backend" }
129802
+ };
129803
+ }
129804
+ return {
129805
+ status: "pass",
129806
+ message: `Vite dev server (:${vitePort}) running alongside the backend`,
129807
+ data: { vite: true, ws: wsUp, mcp: mcpUp }
129808
+ };
129809
+ }
129810
+ async function checkOrphanedVite(r, wsUp, mcpUp, wsPort2, mcpPort2, vitePort) {
129811
+ const viteUp = await probePort(vitePort);
129812
+ const viteConfirmed = viteUp ? await isViteDevServer(vitePort) : false;
129813
+ recordEvaluation(
129814
+ r,
129815
+ evaluateOrphanedVite({ viteUp, viteConfirmed, wsUp, mcpUp, wsPort: wsPort2, mcpPort: mcpPort2, vitePort })
129816
+ );
129817
+ }
129580
129818
  function httpGet(url, timeoutMs = 3e3) {
129581
129819
  return new Promise((resolve5) => {
129582
129820
  const req = request(url, { timeout: timeoutMs }, (res) => {
129583
129821
  let body = "";
129822
+ let bytes = 0;
129823
+ let truncated = false;
129584
129824
  res.on("data", (chunk2) => {
129825
+ if (truncated) return;
129826
+ bytes += Buffer.byteLength(chunk2);
129827
+ if (bytes > HTTP_MAX_BYTES) {
129828
+ truncated = true;
129829
+ res.destroy();
129830
+ resolve5({ status: res.statusCode, data: null });
129831
+ return;
129832
+ }
129585
129833
  body += chunk2;
129586
129834
  });
129587
129835
  res.on("end", () => {
129836
+ if (truncated) return;
129588
129837
  try {
129589
129838
  resolve5({ status: res.statusCode, data: JSON.parse(body) });
129590
129839
  } catch {
@@ -129600,6 +129849,10 @@ function httpGet(url, timeoutMs = 3e3) {
129600
129849
  req.end();
129601
129850
  });
129602
129851
  }
129852
+ async function isViteDevServer(port) {
129853
+ const result2 = await httpGet(`http://127.0.0.1:${port}/@vite/client`, 2e3);
129854
+ return result2?.status === 200;
129855
+ }
129603
129856
  async function checkHealth(r, mcpPort2) {
129604
129857
  const result2 = await httpGet(`http://127.0.0.1:${mcpPort2}/health`);
129605
129858
  if (!result2) {
@@ -129839,7 +130092,7 @@ function evaluateStaleGlobal(bundled, globalVersion) {
129839
130092
  };
129840
130093
  }
129841
130094
  async function checkStaleGlobal(r) {
129842
- const bundled = true ? "0.16.0" : null;
130095
+ const bundled = true ? "0.18.0" : null;
129843
130096
  if (!bundled) return;
129844
130097
  let globalVersion;
129845
130098
  try {
@@ -129858,16 +130111,37 @@ async function checkStaleGlobal(r) {
129858
130111
  async function runDoctor(opts = {}) {
129859
130112
  const wsPort2 = opts.wsPort ?? DEFAULT_WS_PORT;
129860
130113
  const mcpPort2 = opts.mcpPort ?? DEFAULT_MCP_PORT;
130114
+ const vitePort = opts.vitePort ?? VITE_DEV_PORT;
129861
130115
  const r = new Recorder();
130116
+ const cwd = process.cwd();
130117
+ const repo = probeTandemEditorRepo(cwd);
130118
+ const devRepo = repo === "yes";
129862
130119
  await r.check("node-version", () => checkNodeVersion(r));
129863
130120
  await r.check("node-modules", () => checkNodeModules(r));
130121
+ if (repo === "unreadable") {
130122
+ await r.check(
130123
+ "dev-repo",
130124
+ () => r.warn(
130125
+ "package.json in the current directory could not be read \u2014 if this is the tandem-editor checkout, the npm-staleness and orphaned-Vite checks are being skipped",
130126
+ "Check for merge-conflict markers or a truncated file: git checkout package.json"
130127
+ )
130128
+ );
130129
+ }
130130
+ if (devRepo) {
130131
+ await r.check("npm-staleness", () => checkNpmStaleness(r, cwd));
130132
+ }
129864
130133
  await r.check("mcp-json", () => checkMcpJson(r));
129865
130134
  await r.check("user-mcp-config", () => checkUserMcpConfig(r));
129866
130135
  await r.check("annotation-store", () => checkAnnotationStore(r));
129867
130136
  await r.check("stale-global", () => checkStaleGlobal(r));
129868
- const { mcp } = await r.check("ports", () => checkPorts(r, wsPort2, mcpPort2));
130137
+ const ports = await r.check("ports", () => checkPorts(r, wsPort2, mcpPort2));
130138
+ const ws = ports?.ws ?? false;
130139
+ const mcp = ports?.mcp ?? false;
130140
+ if (devRepo) {
130141
+ await r.check("orphaned-vite", () => checkOrphanedVite(r, ws, mcp, wsPort2, mcpPort2, vitePort));
130142
+ }
129869
130143
  if (mcp) {
129870
- const healthy = await r.check("health", () => checkHealth(r, mcpPort2));
130144
+ const healthy = await r.check("health", () => checkHealth(r, mcpPort2)) ?? false;
129871
130145
  if (healthy) {
129872
130146
  await r.check("sse", () => checkSseEndpoint(r, mcpPort2));
129873
130147
  }
@@ -129882,7 +130156,7 @@ async function runDoctor(opts = {}) {
129882
130156
  results: r.results
129883
130157
  };
129884
130158
  }
129885
- var Recorder;
130159
+ var Recorder, VITE_DEV_PORT, HTTP_MAX_BYTES;
129886
130160
  var init_doctor = __esm({
129887
130161
  "src/cli/doctor.ts"() {
129888
130162
  "use strict";
@@ -129893,11 +130167,29 @@ var init_doctor = __esm({
129893
130167
  warnings = 0;
129894
130168
  results = [];
129895
130169
  currentCheck = "";
130170
+ /**
130171
+ * Run one check under `name`. A throwing check is contained here and
130172
+ * recorded as a `fail` rather than taking the whole report down.
130173
+ *
130174
+ * Returns `T | undefined` — NOT `T`. The `undefined` is load-bearing and
130175
+ * must not be cast away: a crashed check produced no value, and the two
130176
+ * callers that consume one (`ports` destructures `{ws, mcp}`; `health`
130177
+ * gates the `sse` check) have to say what they want instead. Returning
130178
+ * `undefined as T` would satisfy the compiler and then throw a TypeError
130179
+ * on the destructure — converting a clear crash into a confusing one on
130180
+ * the exact path this containment exists to protect.
130181
+ */
129896
130182
  async check(name2, fn) {
129897
130183
  const prev = this.currentCheck;
129898
130184
  this.currentCheck = name2;
129899
130185
  try {
129900
130186
  return await fn();
130187
+ } catch (err) {
130188
+ this.fail(
130189
+ `${name2} check crashed (${errorClass(err)}) \u2014 the rest of the report is still valid`,
130190
+ "Please report this at https://github.com/bloknayrb/tandem/issues"
130191
+ );
130192
+ return void 0;
129901
130193
  } finally {
129902
130194
  this.currentCheck = prev;
129903
130195
  }
@@ -129920,6 +130212,8 @@ var init_doctor = __esm({
129920
130212
  this.record("fail", msg, fix, fields);
129921
130213
  }
129922
130214
  };
130215
+ VITE_DEV_PORT = 5173;
130216
+ HTTP_MAX_BYTES = 256 * 1024;
129923
130217
  }
129924
130218
  });
129925
130219
 
@@ -129974,7 +130268,13 @@ var init_diagnostics = __esm({
129974
130268
  "use strict";
129975
130269
  init_doctor();
129976
130270
  init_middleware();
129977
- DEV_REPO_CHECKS = /* @__PURE__ */ new Set(["node-modules", "mcp-json"]);
130271
+ DEV_REPO_CHECKS = /* @__PURE__ */ new Set([
130272
+ "node-modules",
130273
+ "mcp-json",
130274
+ "npm-staleness",
130275
+ "orphaned-vite",
130276
+ "dev-repo"
130277
+ ]);
129978
130278
  }
129979
130279
  });
129980
130280
 
@@ -174831,7 +175131,7 @@ function registerAwarenessTools(server) {
174831
175131
  server.registerTool(
174832
175132
  "tandem_checkInbox",
174833
175133
  {
174834
- description: "Check for user actions you haven't seen yet \u2014 new comments, chat messages, and responses to your annotations. Call after completing any task, between steps, and whenever you pause. Low token cost.",
175134
+ description: "Check for user actions you haven't seen yet \u2014 new comments, chat messages, and responses to your annotations. You cannot tell whether real-time push is reaching you, so poll at a steady cadence: every 2-3 tool calls, after completing any task, between steps, and whenever you pause. Already-seen items are de-duplicated, so frequent calls are cheap and never double-report. Low token cost \u2014 when in doubt, call it.",
174835
175135
  inputSchema: {
174836
175136
  documentId: external_exports.string().optional().describe("Target document ID (defaults to active document)")
174837
175137
  },
@@ -187922,6 +188222,10 @@ function sseHandler(req, res) {
187922
188222
  }
187923
188223
  res.write(": connected\n\n");
187924
188224
  let keepalive;
188225
+ function cleanup() {
188226
+ clearInterval(keepalive);
188227
+ unsubscribe(onEvent);
188228
+ }
187925
188229
  const onEvent = (event) => {
187926
188230
  try {
187927
188231
  writeEvent(res, event);
@@ -187930,17 +188234,23 @@ function sseHandler(req, res) {
187930
188234
  "[SSE] Write failed, cleaning up subscriber:",
187931
188235
  err instanceof Error ? err.message : err
187932
188236
  );
187933
- clearInterval(keepalive);
187934
- unsubscribe(onEvent);
188237
+ cleanup();
187935
188238
  }
187936
188239
  };
187937
188240
  subscribe2(onEvent);
187938
188241
  keepalive = setInterval(() => {
187939
- res.write(": keepalive\n\n");
188242
+ try {
188243
+ if (!res.writableEnded) res.write(": keepalive\n\n");
188244
+ } catch (err) {
188245
+ console.error(
188246
+ "[SSE] Keepalive write failed, cleaning up subscriber:",
188247
+ err instanceof Error ? err.message : err
188248
+ );
188249
+ cleanup();
188250
+ }
187940
188251
  }, CHANNEL_SSE_KEEPALIVE_MS);
187941
188252
  req.on("close", () => {
187942
- clearInterval(keepalive);
187943
- unsubscribe(onEvent);
188253
+ cleanup();
187944
188254
  console.error("[SSE] Client disconnected from /api/events");
187945
188255
  });
187946
188256
  console.error("[SSE] Client connected to /api/events");
@@ -188114,7 +188424,7 @@ init_document2();
188114
188424
  init_document_service();
188115
188425
  init_docx_apply2();
188116
188426
  var esmRequire2 = createRequire2(import.meta.url);
188117
- var APP_VERSION = true ? "0.16.0" : _readVersionFromDisk();
188427
+ var APP_VERSION = true ? "0.18.0" : _readVersionFromDisk();
188118
188428
  var MCP_SDK_VERSION = true ? "1.27.1" : "0.0.0-unknown";
188119
188429
  var __dirname3 = dirname3(fileURLToPath4(import.meta.url));
188120
188430
  var CLIENT_DIST = join7(__dirname3, "../client");