tandem-editor 0.6.2 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -257,7 +257,7 @@ var init_cli_runtime = __esm({
257
257
  });
258
258
 
259
259
  // src/cli/preflight.ts
260
- async function ensureTandemServer(opts = {}) {
260
+ async function probeTandemServer(opts = {}) {
261
261
  const url = resolveTandemUrl(opts.url);
262
262
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
263
263
  const controller = new AbortController();
@@ -265,22 +265,36 @@ async function ensureTandemServer(opts = {}) {
265
265
  try {
266
266
  const res = await fetch(`${url}/health`, { signal: controller.signal });
267
267
  if (!res.ok) {
268
- fail(url, `health endpoint returned HTTP ${res.status}`);
268
+ return {
269
+ ok: false,
270
+ url,
271
+ reason: `health endpoint returned HTTP ${res.status}`,
272
+ kind: "unhealthy"
273
+ };
269
274
  }
275
+ return { ok: true };
270
276
  } catch (err) {
271
- const msg = err instanceof Error ? err.message : String(err);
272
- fail(url, msg);
277
+ return {
278
+ ok: false,
279
+ url,
280
+ reason: err instanceof Error ? err.message : String(err),
281
+ kind: "unreachable"
282
+ };
273
283
  } finally {
274
284
  clearTimeout(timer);
275
285
  }
276
286
  }
277
- function fail(url, detail) {
278
- process.stderr.write(
279
- `[tandem] Tandem server not reachable at ${url} (${detail}).
280
- [tandem] Start the Tauri app or run \`tandem start\` on the host, then retry.
287
+ async function ensureTandemServer(opts = {}) {
288
+ const probe = await probeTandemServer(opts);
289
+ if (!probe.ok) {
290
+ const guidance = probe.kind === "unreachable" ? "Start the Tauri app or run `tandem start` on the host, then retry." : "The Tandem server is running but unhealthy \u2014 check the host logs.";
291
+ process.stderr.write(
292
+ `[tandem] Tandem server preflight failed at ${probe.url} (${probe.reason}).
293
+ [tandem] ${guidance}
281
294
  `
282
- );
283
- process.exit(1);
295
+ );
296
+ process.exit(1);
297
+ }
284
298
  }
285
299
  var DEFAULT_TIMEOUT_MS;
286
300
  var init_preflight = __esm({
@@ -295,80 +309,165 @@ var init_preflight = __esm({
295
309
  var mcp_stdio_exports = {};
296
310
  __export(mcp_stdio_exports, {
297
311
  getRequestId: () => getRequestId,
312
+ getResponseId: () => getResponseId,
298
313
  runMcpStdio: () => runMcpStdio
299
314
  });
300
315
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
301
316
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
302
317
  async function runMcpStdio() {
303
318
  const baseUrl = resolveTandemUrl();
304
- await ensureTandemServer({ url: baseUrl });
305
319
  const http = new StreamableHTTPClientTransport(new URL(`${baseUrl}/mcp`));
306
320
  const stdio = new StdioServerTransport();
321
+ const pendingIds = /* @__PURE__ */ new Set();
322
+ const preReadyBuffer = [];
307
323
  let shuttingDown = false;
308
- const shutdown = async (code = 0) => {
324
+ let httpReady = false;
325
+ async function sendErrorResponse(id, message, detail) {
326
+ const errorResponse = {
327
+ jsonrpc: "2.0",
328
+ id,
329
+ error: {
330
+ // -32000 is the implementation-defined server error range per
331
+ // JSON-RPC 2.0 §5.1 — upstream unavailability is an application-
332
+ // level condition, not a generic Internal Error.
333
+ code: -32e3,
334
+ message,
335
+ ...detail !== void 0 ? { data: { detail } } : {}
336
+ }
337
+ };
338
+ try {
339
+ await stdio.send(errorResponse);
340
+ } catch (err) {
341
+ const detail2 = err instanceof Error ? err.message : String(err);
342
+ process.stderr.write(
343
+ `[tandem mcp-stdio] failed to send synthesized error for id ${id}: ${detail2}
344
+ `
345
+ );
346
+ }
347
+ }
348
+ function forwardToUpstream(msg) {
349
+ const requestId = getRequestId(msg);
350
+ if (requestId !== void 0) pendingIds.add(requestId);
351
+ http.send(msg).catch((err) => {
352
+ const detail = err instanceof Error ? err.message : String(err);
353
+ process.stderr.write(`[tandem mcp-stdio] upstream send failed: ${detail}
354
+ `);
355
+ if (requestId !== void 0 && pendingIds.delete(requestId)) {
356
+ void sendErrorResponse(requestId, "Tandem HTTP upstream unreachable", detail);
357
+ }
358
+ });
359
+ }
360
+ async function synthesizeBuffered(message, detail) {
361
+ const buffered2 = preReadyBuffer.splice(0);
362
+ const ids = buffered2.map((msg) => getRequestId(msg)).filter((id) => id !== void 0);
363
+ for (const id of ids) {
364
+ await sendErrorResponse(id, message, detail);
365
+ }
366
+ }
367
+ async function synthesizePending(message, detail) {
368
+ if (pendingIds.size === 0) return;
369
+ const ids = [...pendingIds];
370
+ pendingIds.clear();
371
+ await Promise.all(ids.map((id) => sendErrorResponse(id, message, detail)));
372
+ }
373
+ const shutdown = async (code = 0, synth) => {
309
374
  if (!shuttingDown) {
310
375
  shuttingDown = true;
311
- await http.close().catch(() => {
376
+ if (synth) {
377
+ await synthesizeBuffered(synth.message, synth.detail);
378
+ await synthesizePending(synth.message, synth.detail);
379
+ }
380
+ await http.close().catch((err) => {
381
+ const detail = err instanceof Error ? err.message : String(err);
382
+ process.stderr.write(`[tandem mcp-stdio] http.close failed: ${detail}
383
+ `);
312
384
  });
313
- await stdio.close().catch(() => {
385
+ await stdio.close().catch((err) => {
386
+ const detail = err instanceof Error ? err.message : String(err);
387
+ process.stderr.write(`[tandem mcp-stdio] stdio.close failed: ${detail}
388
+ `);
314
389
  });
315
390
  }
316
391
  process.exit(code);
317
392
  };
393
+ function deferredShutdown(synth) {
394
+ setTimeout(() => void shutdown(1, synth), PREFLIGHT_GRACE_MS);
395
+ }
318
396
  stdio.onmessage = (msg) => {
319
- http.send(msg).catch((err) => {
320
- const detail = err instanceof Error ? err.message : String(err);
321
- process.stderr.write(`[tandem mcp-stdio] upstream send failed: ${detail}
322
- `);
323
- const requestId = getRequestId(msg);
324
- if (requestId !== void 0) {
325
- const errorResponse = {
326
- jsonrpc: "2.0",
327
- id: requestId,
328
- error: {
329
- // -32000 is the implementation-defined server error range per
330
- // JSON-RPC 2.0 §5.1 — the upstream being unreachable is an
331
- // application-level condition, not a generic Internal Error.
332
- code: -32e3,
333
- message: "Tandem HTTP upstream unreachable",
334
- data: { detail }
335
- }
336
- };
337
- stdio.send(errorResponse).catch(() => {
338
- });
339
- }
340
- });
397
+ if (!httpReady) {
398
+ preReadyBuffer.push(msg);
399
+ return;
400
+ }
401
+ forwardToUpstream(msg);
341
402
  };
342
403
  http.onmessage = (msg) => {
343
- stdio.send(msg).catch((err) => {
344
- const detail = err instanceof Error ? err.message : String(err);
345
- process.stderr.write(`[tandem mcp-stdio] stdio write failed: ${detail}
346
- `);
347
- });
404
+ const responseId = getResponseId(msg);
405
+ stdio.send(msg).then(
406
+ () => {
407
+ if (responseId !== void 0) pendingIds.delete(responseId);
408
+ },
409
+ (err) => {
410
+ const detail = err instanceof Error ? err.message : String(err);
411
+ process.stderr.write(
412
+ `[tandem mcp-stdio] stdio write failed for id ${responseId ?? "<notification>"}: ${detail}
413
+ `
414
+ );
415
+ void shutdown(1, {
416
+ message: "Tandem stdio write failed",
417
+ detail
418
+ });
419
+ }
420
+ );
348
421
  };
349
422
  stdio.onerror = (err) => {
350
423
  process.stderr.write(`[tandem mcp-stdio] stdio error: ${err.message}
424
+ ${err.stack ?? ""}
351
425
  `);
352
426
  };
353
427
  http.onerror = (err) => {
354
- process.stderr.write(`[tandem mcp-stdio] http error: ${err.message}
355
- `);
428
+ const cause = err.cause;
429
+ process.stderr.write(
430
+ `[tandem mcp-stdio] http error: ${err.message}
431
+ ${err.stack ?? ""}${cause !== void 0 ? `
432
+ cause: ${cause}` : ""}
433
+ `
434
+ );
356
435
  };
357
436
  stdio.onclose = () => {
358
437
  void shutdown(0);
359
438
  };
360
439
  http.onclose = () => {
361
- void shutdown(0);
440
+ if (shuttingDown) return;
441
+ void shutdown(1, {
442
+ message: "Tandem HTTP upstream closed unexpectedly",
443
+ detail: "upstream connection dropped mid-session"
444
+ });
362
445
  };
363
446
  await stdio.start();
447
+ const probe = await probeTandemServer({ url: baseUrl });
448
+ if (!probe.ok) {
449
+ const guidance = probe.kind === "unreachable" ? "Start the Tauri app or run `tandem start` on the host, then retry." : "The Tandem server is running but unhealthy \u2014 check the host logs.";
450
+ process.stderr.write(
451
+ `[tandem mcp-stdio] Tandem server preflight failed at ${probe.url} (${probe.reason}).
452
+ [tandem mcp-stdio] ${guidance}
453
+ `
454
+ );
455
+ const synthMessage = probe.kind === "unreachable" ? "Tandem server not running. Start the Tauri app or run `tandem start`." : "Tandem server unhealthy (check host logs).";
456
+ deferredShutdown({ message: synthMessage, detail: probe.reason });
457
+ return;
458
+ }
364
459
  try {
365
460
  await http.start();
366
461
  } catch (err) {
367
462
  const detail = err instanceof Error ? err.message : String(err);
368
463
  process.stderr.write(`[tandem mcp-stdio] upstream http start failed: ${detail}
369
464
  `);
370
- await shutdown(1);
465
+ deferredShutdown({ message: "Tandem HTTP upstream failed to start", detail });
466
+ return;
371
467
  }
468
+ httpReady = true;
469
+ const buffered = preReadyBuffer.splice(0);
470
+ for (const msg of buffered) forwardToUpstream(msg);
372
471
  }
373
472
  function getRequestId(msg) {
374
473
  const m = msg;
@@ -376,12 +475,34 @@ function getRequestId(msg) {
376
475
  if (typeof m.id === "string" || typeof m.id === "number") return m.id;
377
476
  return void 0;
378
477
  }
478
+ function getResponseId(msg) {
479
+ const m = msg;
480
+ if (typeof m.method === "string") return void 0;
481
+ if (typeof m.id === "string" || typeof m.id === "number") return m.id;
482
+ return void 0;
483
+ }
484
+ var PREFLIGHT_GRACE_MS;
379
485
  var init_mcp_stdio = __esm({
380
486
  "src/cli/mcp-stdio.ts"() {
381
487
  "use strict";
382
488
  init_cli_runtime();
383
489
  init_preflight();
384
490
  redirectConsoleToStderr();
491
+ PREFLIGHT_GRACE_MS = 1500;
492
+ process.once("uncaughtException", (err) => {
493
+ process.stderr.write(
494
+ `[tandem mcp-stdio] uncaughtException: ${err.message}
495
+ ${err.stack ?? ""}
496
+ `
497
+ );
498
+ process.exit(1);
499
+ });
500
+ process.once("unhandledRejection", (reason) => {
501
+ const detail = reason instanceof Error ? reason.message : String(reason);
502
+ process.stderr.write(`[tandem mcp-stdio] unhandledRejection: ${detail}
503
+ `);
504
+ process.exit(1);
505
+ });
385
506
  }
386
507
  });
387
508
 
@@ -905,7 +1026,7 @@ var init_start = __esm({
905
1026
 
906
1027
  // src/cli/index.ts
907
1028
  import updateNotifier from "update-notifier";
908
- var version = true ? "0.6.2" : "0.0.0-dev";
1029
+ var version = true ? "0.6.3" : "0.0.0-dev";
909
1030
  var args = process.argv.slice(2);
910
1031
  var isStdioMode = args[0] === "mcp-stdio" || args[0] === "channel";
911
1032
  if (!isStdioMode) {