tandem-editor 0.11.2 → 0.13.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.
Files changed (40) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +3 -3
  3. package/CHANGELOG.md +201 -72
  4. package/README.md +141 -238
  5. package/dist/channel/index.js +211 -81
  6. package/dist/channel/index.js.map +1 -1
  7. package/dist/cli/index.js +749 -170
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/client/assets/CoworkSettings-BOYbyKul.js +3 -0
  10. package/dist/client/assets/event-CNdo2oXa.js +1 -0
  11. package/dist/client/assets/index-D8uS4cj7.css +1 -0
  12. package/dist/client/assets/index-Dm_QtxGQ.js +1 -0
  13. package/dist/client/assets/index-g-KwmRn9.js +271 -0
  14. package/dist/client/assets/webview-KiZyy_pC.js +1 -0
  15. package/dist/client/assets/window-DePn7tLG.js +1 -0
  16. package/dist/client/fonts/OFL-Hanuman.txt +93 -0
  17. package/dist/client/fonts/OFL-InterTight.txt +93 -0
  18. package/dist/client/fonts/OFL-JetBrainsMono.txt +93 -0
  19. package/dist/client/fonts/OFL-SNPro.txt +93 -0
  20. package/dist/client/fonts/OFL-Sono.txt +93 -0
  21. package/dist/client/fonts/OFL-SourceSerif4.txt +93 -0
  22. package/dist/client/fonts/hanuman-latin.woff2 +0 -0
  23. package/dist/client/fonts/jetbrains-mono-latin.woff2 +0 -0
  24. package/dist/client/fonts/sn-pro-latin.woff2 +0 -0
  25. package/dist/client/fonts/sono-latin.woff2 +0 -0
  26. package/dist/client/fonts/source-serif-4-latin.woff2 +0 -0
  27. package/dist/client/index.html +206 -17
  28. package/dist/client/logo.png +0 -0
  29. package/dist/monitor/index.js +241 -160
  30. package/dist/monitor/index.js.map +1 -1
  31. package/dist/server/index.js +22828 -19659
  32. package/dist/server/index.js.map +1 -1
  33. package/package.json +12 -4
  34. package/sample/welcome.md +6 -6
  35. package/skills/tandem/SKILL.md +15 -0
  36. package/dist/client/assets/CoworkSettings-DK3jjdwK.js +0 -3
  37. package/dist/client/assets/index-CfT503n4.js +0 -297
  38. package/dist/client/assets/index-DeJe09pn.css +0 -1
  39. package/dist/client/assets/webview-Ben21ZLJ.js +0 -1
  40. package/dist/client/assets/window-BxBvHL5k.js +0 -1
@@ -17993,14 +17993,21 @@ var StdioServerTransport = class {
17993
17993
  }
17994
17994
  };
17995
17995
 
17996
+ // src/shared/api-paths.ts
17997
+ var API_EVENTS = "/api/events";
17998
+ var API_CHANNEL_AWARENESS = "/api/channel-awareness";
17999
+ var API_CHANNEL_ERROR = "/api/channel-error";
18000
+ var API_CHANNEL_REPLY = "/api/channel-reply";
18001
+ var API_CHANNEL_PERMISSION = "/api/channel-permission";
18002
+ var API_MODE = "/api/mode";
18003
+
17996
18004
  // src/shared/constants.ts
17997
18005
  var DEFAULT_MCP_PORT = 3479;
17998
18006
  var TANDEM_REPO_URL = "https://github.com/bloknayrb/tandem";
17999
18007
  var TANDEM_ISSUES_NEW_URL = `${TANDEM_REPO_URL}/issues/new`;
18000
18008
  var MAX_FILE_SIZE = 50 * 1024 * 1024;
18001
- var MAX_WS_PAYLOAD = 10 * 1024 * 1024;
18002
- var IDLE_TIMEOUT = 30 * 60 * 1e3;
18003
18009
  var SESSION_MAX_AGE = 30 * 24 * 60 * 60 * 1e3;
18010
+ var TANDEM_MODE_DEFAULT = "tandem";
18004
18011
  var CHANNEL_MAX_RETRIES = 5;
18005
18012
  var CHANNEL_RETRY_DELAY_MS = 2e3;
18006
18013
  var CHANNEL_CONNECT_FETCH_TIMEOUT_MS = 1e4;
@@ -18030,7 +18037,7 @@ function resolveTandemUrlCandidate(override) {
18030
18037
  for (const url of candidates) {
18031
18038
  if (url !== void 0 && url.trim() !== "") return url.trim();
18032
18039
  }
18033
- return `http://localhost:${DEFAULT_MCP_PORT}`;
18040
+ return `http://127.0.0.1:${DEFAULT_MCP_PORT}`;
18034
18041
  }
18035
18042
  function resolveAuthTokenCandidate(override) {
18036
18043
  const candidates = [
@@ -18186,52 +18193,109 @@ function formatEventMeta(event) {
18186
18193
  return meta;
18187
18194
  }
18188
18195
 
18189
- // src/channel/event-bridge.ts
18196
+ // src/shared/types.ts
18197
+ var AnnotationTypeSchema = external_exports.enum(["highlight", "note", "comment"]);
18198
+ var AnnotationStatusSchema = external_exports.enum(["pending", "accepted", "dismissed"]);
18199
+ var HighlightColorSchema = external_exports.enum(["yellow", "green", "blue", "pink"]);
18200
+ var SeveritySchema = external_exports.enum(["info", "warning", "error", "success"]);
18201
+ var TandemModeSchema = external_exports.enum(["solo", "tandem"]);
18202
+ var AuthorSchema = external_exports.enum(["user", "claude", "import"]);
18203
+ var ReplyAuthorSchema = external_exports.enum(["user", "claude"]);
18204
+ var AnnotationActionSchema = external_exports.enum(["accept", "dismiss"]);
18205
+ var ExportFormatSchema = external_exports.enum(["markdown", "json"]);
18206
+ var DocumentFormatSchema = external_exports.enum(["md", "txt", "html", "docx"]);
18207
+ var ToolErrorCodeSchema = external_exports.enum([
18208
+ "RANGE_GONE",
18209
+ "RANGE_MOVED",
18210
+ "FILE_LOCKED",
18211
+ "FILE_NOT_FOUND",
18212
+ "NO_DOCUMENT",
18213
+ "INVALID_RANGE",
18214
+ "INVALID_ARGUMENT",
18215
+ "NOT_FOUND",
18216
+ "ANNOTATION_RESOLVED",
18217
+ "FORMAT_ERROR",
18218
+ "PERMISSION_DENIED"
18219
+ ]);
18220
+ var ChannelErrorCodeSchema = external_exports.enum(["CHANNEL_CONNECT_FAILED", "MONITOR_CONNECT_FAILED"]);
18221
+ var CHANNEL_CONNECT_FAILED = "CHANNEL_CONNECT_FAILED";
18222
+
18223
+ // src/shared/sse-consumer.ts
18190
18224
  var AWARENESS_DEBOUNCE_MS = 500;
18225
+ var AWARENESS_CLEAR_MS = 3e3;
18191
18226
  var MODE_CACHE_TTL_MS = 2e3;
18192
- async function startEventBridge(mcp, tandemUrl) {
18227
+ var STABLE_CONNECTION_MS = 6e4;
18228
+ var RETRY_MAX_DELAY_MS = 3e4;
18229
+ var shutdownTimers = { awarenessTimer: null, clearAwarenessTimer: null, lastDocumentId: null };
18230
+ var outstandingAwareness = /* @__PURE__ */ new Set();
18231
+ function trackAwareness(p) {
18232
+ outstandingAwareness.add(p);
18233
+ p.finally(() => outstandingAwareness.delete(p));
18234
+ }
18235
+ var cachedMode = TANDEM_MODE_DEFAULT;
18236
+ var cachedModeAt = 0;
18237
+ var cachedModeFailedAt = 0;
18238
+ var _modeRefreshInFlight = null;
18239
+ async function runEventConsumer(opts) {
18240
+ await getCachedMode(opts.tandemUrl, opts.logPrefix).catch(() => {
18241
+ });
18193
18242
  let retries = 0;
18194
18243
  let lastEventId;
18195
18244
  while (retries < CHANNEL_MAX_RETRIES) {
18196
18245
  try {
18197
- await connectAndStream(mcp, tandemUrl, lastEventId, (id) => {
18198
- lastEventId = id;
18199
- retries = 0;
18246
+ await connectAndStreamOnce(opts, lastEventId, {
18247
+ onEventId: (id) => {
18248
+ lastEventId = id;
18249
+ },
18250
+ onStable: () => {
18251
+ retries = 0;
18252
+ }
18200
18253
  });
18201
18254
  } catch (err) {
18202
18255
  retries++;
18203
18256
  console.error(
18204
- `[Channel] SSE connection failed (${retries}/${CHANNEL_MAX_RETRIES}):`,
18257
+ `${opts.logPrefix} SSE connection failed (${retries}/${CHANNEL_MAX_RETRIES}):`,
18205
18258
  err instanceof Error ? err.message : err
18206
18259
  );
18207
18260
  if (retries >= CHANNEL_MAX_RETRIES) {
18208
- console.error("[Channel] SSE connection exhausted, reporting error and exiting");
18261
+ console.error(`${opts.logPrefix} SSE connection exhausted, reporting error and exiting`);
18209
18262
  try {
18210
18263
  await fetchWithTimeout(
18211
- `${tandemUrl}/api/channel-error`,
18264
+ `${opts.tandemUrl}${API_CHANNEL_ERROR}`,
18212
18265
  {
18213
18266
  method: "POST",
18214
18267
  headers: { "Content-Type": "application/json" },
18215
18268
  body: JSON.stringify({
18216
- error: "CHANNEL_CONNECT_FAILED",
18217
- message: `Channel shim lost connection after ${CHANNEL_MAX_RETRIES} retries.`
18269
+ error: opts.errorCode,
18270
+ message: `${opts.logPrefix} lost connection after ${CHANNEL_MAX_RETRIES} retries.`
18218
18271
  })
18219
18272
  },
18220
18273
  CHANNEL_ERROR_REPORT_TIMEOUT_MS
18221
18274
  );
18222
18275
  } catch (reportErr) {
18223
18276
  console.error(
18224
- "[Channel] Could not report failure to server:",
18225
- describeFetchError(reportErr, "/api/channel-error", CHANNEL_ERROR_REPORT_TIMEOUT_MS)
18277
+ `${opts.logPrefix} Could not report failure to server:`,
18278
+ describeFetchError(reportErr, API_CHANNEL_ERROR, CHANNEL_ERROR_REPORT_TIMEOUT_MS)
18226
18279
  );
18227
18280
  }
18281
+ opts.onExhaustion?.();
18228
18282
  process.exit(1);
18229
18283
  }
18230
- await new Promise((r) => setTimeout(r, CHANNEL_RETRY_DELAY_MS));
18284
+ const delay = Math.min(CHANNEL_RETRY_DELAY_MS * 2 ** (retries - 1), RETRY_MAX_DELAY_MS);
18285
+ console.error(
18286
+ `${opts.logPrefix} Retrying in ${delay}ms (attempt ${retries}/${CHANNEL_MAX_RETRIES})...`
18287
+ );
18288
+ await new Promise((r) => setTimeout(r, delay));
18231
18289
  }
18232
18290
  }
18291
+ console.error(
18292
+ `${opts.logPrefix} Retry loop exited unexpectedly (retries=${retries}/${CHANNEL_MAX_RETRIES})`
18293
+ );
18294
+ process.exit(1);
18233
18295
  }
18234
- async function connectAndStream(mcp, tandemUrl, lastEventId, onEventId) {
18296
+ async function connectAndStreamOnce(opts, lastEventId, cb) {
18297
+ const onStable = cb.onStable ?? (() => {
18298
+ });
18235
18299
  const headers = { Accept: "text/event-stream" };
18236
18300
  if (lastEventId) headers["Last-Event-ID"] = lastEventId;
18237
18301
  const connectCtrl = new AbortController();
@@ -18241,12 +18305,16 @@ async function connectAndStream(mcp, tandemUrl, lastEventId, onEventId) {
18241
18305
  );
18242
18306
  let res;
18243
18307
  try {
18244
- res = await authFetch(`${tandemUrl}/api/events`, { headers, signal: connectCtrl.signal });
18308
+ res = await authFetch(`${opts.tandemUrl}${API_EVENTS}`, {
18309
+ headers,
18310
+ signal: connectCtrl.signal
18311
+ });
18245
18312
  } finally {
18246
18313
  clearTimeout(connectTimer);
18247
18314
  }
18248
18315
  if (!res.ok) throw new Error(`SSE endpoint returned ${res.status}`);
18249
18316
  if (!res.body) throw new Error("SSE endpoint returned no body");
18317
+ const stableTimer = setTimeout(onStable, STABLE_CONNECTION_MS);
18250
18318
  const reader = res.body.getReader();
18251
18319
  const decoder = new TextDecoder();
18252
18320
  let buffer = "";
@@ -18259,13 +18327,10 @@ async function connectAndStream(mcp, tandemUrl, lastEventId, onEventId) {
18259
18327
  });
18260
18328
  }
18261
18329
  }, CHANNEL_SSE_INACTIVITY_TIMEOUT_MS / 4);
18262
- let awarenessTimer = null;
18263
- let clearAwarenessTimer = null;
18264
18330
  let pendingAwareness = null;
18265
- const AWARENESS_CLEAR_MS = 3e3;
18266
- function clearAwareness(documentId) {
18267
- fetchWithTimeout(
18268
- `${tandemUrl}/api/channel-awareness`,
18331
+ function clearAwarenessNow(documentId) {
18332
+ const p = fetchWithTimeout(
18333
+ `${opts.tandemUrl}${API_CHANNEL_AWARENESS}`,
18269
18334
  {
18270
18335
  method: "POST",
18271
18336
  headers: { "Content-Type": "application/json" },
@@ -18278,17 +18343,23 @@ async function connectAndStream(mcp, tandemUrl, lastEventId, onEventId) {
18278
18343
  CHANNEL_AWARENESS_FETCH_TIMEOUT_MS
18279
18344
  ).catch((err) => {
18280
18345
  console.error(
18281
- "[Channel] clearAwareness failed (non-fatal):",
18282
- describeFetchError(err, "/api/channel-awareness clear", CHANNEL_AWARENESS_FETCH_TIMEOUT_MS)
18346
+ `${opts.logPrefix} Awareness clear failed:`,
18347
+ describeFetchError(
18348
+ err,
18349
+ `${API_CHANNEL_AWARENESS} clear`,
18350
+ CHANNEL_AWARENESS_FETCH_TIMEOUT_MS
18351
+ )
18283
18352
  );
18284
18353
  });
18354
+ trackAwareness(p);
18285
18355
  }
18286
18356
  function flushAwareness() {
18287
18357
  if (!pendingAwareness) return;
18288
18358
  const event = pendingAwareness;
18289
18359
  pendingAwareness = null;
18290
- fetchWithTimeout(
18291
- `${tandemUrl}/api/channel-awareness`,
18360
+ if (event.documentId) shutdownTimers.lastDocumentId = event.documentId;
18361
+ const p = fetchWithTimeout(
18362
+ `${opts.tandemUrl}${API_CHANNEL_AWARENESS}`,
18292
18363
  {
18293
18364
  method: "POST",
18294
18365
  headers: { "Content-Type": "application/json" },
@@ -18301,21 +18372,25 @@ async function connectAndStream(mcp, tandemUrl, lastEventId, onEventId) {
18301
18372
  CHANNEL_AWARENESS_FETCH_TIMEOUT_MS
18302
18373
  ).catch((err) => {
18303
18374
  console.error(
18304
- "[Channel] Awareness update failed:",
18375
+ `${opts.logPrefix} Awareness update failed:`,
18305
18376
  describeFetchError(
18306
18377
  err,
18307
- "/api/channel-awareness update",
18378
+ `${API_CHANNEL_AWARENESS} update`,
18308
18379
  CHANNEL_AWARENESS_FETCH_TIMEOUT_MS
18309
18380
  )
18310
18381
  );
18311
18382
  });
18312
- if (clearAwarenessTimer) clearTimeout(clearAwarenessTimer);
18313
- clearAwarenessTimer = setTimeout(() => clearAwareness(event.documentId), AWARENESS_CLEAR_MS);
18383
+ trackAwareness(p);
18384
+ if (shutdownTimers.clearAwarenessTimer) clearTimeout(shutdownTimers.clearAwarenessTimer);
18385
+ shutdownTimers.clearAwarenessTimer = setTimeout(
18386
+ () => clearAwarenessNow(event.documentId),
18387
+ AWARENESS_CLEAR_MS
18388
+ );
18314
18389
  }
18315
18390
  function scheduleAwareness(event) {
18316
18391
  pendingAwareness = event;
18317
- if (awarenessTimer) clearTimeout(awarenessTimer);
18318
- awarenessTimer = setTimeout(flushAwareness, AWARENESS_DEBOUNCE_MS);
18392
+ if (shutdownTimers.awarenessTimer) clearTimeout(shutdownTimers.awarenessTimer);
18393
+ shutdownTimers.awarenessTimer = setTimeout(flushAwareness, AWARENESS_DEBOUNCE_MS);
18319
18394
  }
18320
18395
  try {
18321
18396
  while (true) {
@@ -18343,80 +18418,135 @@ async function connectAndStream(mcp, tandemUrl, lastEventId, onEventId) {
18343
18418
  else if (line.startsWith("data: ")) data = line.slice(6);
18344
18419
  }
18345
18420
  if (!data) continue;
18346
- let event;
18421
+ let raw;
18347
18422
  try {
18348
- event = parseTandemEvent(JSON.parse(data));
18349
- } catch {
18423
+ raw = JSON.parse(data);
18424
+ } catch (err) {
18350
18425
  console.error(
18351
- "[Channel] Malformed SSE event data (skipping), eventId=%s:",
18352
- eventId,
18353
- data.slice(0, 200)
18426
+ `${opts.logPrefix} SSE JSON parse failed (eventId=${eventId ?? "none"}, len=${data.length}): ${err instanceof Error ? err.message : err}. Tail:`,
18427
+ data.slice(Math.max(0, data.length - 200))
18354
18428
  );
18355
- if (eventId) onEventId(eventId);
18429
+ if (eventId) cb.onEventId(eventId);
18356
18430
  continue;
18357
18431
  }
18432
+ const event = parseTandemEvent(raw);
18358
18433
  if (!event) {
18359
18434
  console.error(
18360
- "[Channel] Invalid SSE event structure (skipping), eventId=%s:",
18361
- eventId,
18362
- data.slice(0, 200)
18435
+ `${opts.logPrefix} SSE event failed validation (eventId=${eventId ?? "none"}): shape mismatch`
18363
18436
  );
18364
- if (eventId) onEventId(eventId);
18437
+ if (eventId) cb.onEventId(eventId);
18365
18438
  continue;
18366
18439
  }
18367
18440
  if (event.type !== "chat:message") {
18368
- const mode = await getCachedMode(tandemUrl);
18369
- if (mode === "solo") {
18370
- console.error(`[Channel] Solo mode: suppressed ${event.type} event`);
18371
- if (eventId) onEventId(eventId);
18441
+ refreshMode(opts.tandemUrl, opts.logPrefix);
18442
+ if (getModeSync() === "solo") {
18443
+ console.error(`${opts.logPrefix} Solo mode: suppressed ${event.type} event`);
18444
+ if (eventId) cb.onEventId(eventId);
18372
18445
  continue;
18373
18446
  }
18374
18447
  }
18375
18448
  try {
18376
- await mcp.notification({
18377
- method: "notifications/claude/channel",
18378
- params: {
18379
- content: formatEventContent(event),
18380
- meta: formatEventMeta(event)
18381
- }
18382
- });
18449
+ await opts.onEvent(event, eventId);
18383
18450
  } catch (err) {
18384
- console.error("[Channel] MCP notification failed (transport broken?):", err);
18451
+ console.error(`${opts.logPrefix} onEvent failed (transport broken?):`, err);
18385
18452
  throw err;
18386
18453
  }
18387
- if (eventId) onEventId(eventId);
18454
+ if (eventId) cb.onEventId(eventId);
18388
18455
  scheduleAwareness(event);
18389
18456
  }
18390
18457
  }
18391
18458
  } finally {
18459
+ clearTimeout(stableTimer);
18392
18460
  clearInterval(watchdog);
18393
- if (awarenessTimer) clearTimeout(awarenessTimer);
18394
- if (clearAwarenessTimer) clearTimeout(clearAwarenessTimer);
18461
+ if (shutdownTimers.awarenessTimer) clearTimeout(shutdownTimers.awarenessTimer);
18462
+ if (shutdownTimers.clearAwarenessTimer) clearTimeout(shutdownTimers.clearAwarenessTimer);
18463
+ shutdownTimers.awarenessTimer = null;
18464
+ shutdownTimers.clearAwarenessTimer = null;
18465
+ pendingAwareness = null;
18395
18466
  }
18396
18467
  }
18397
- var cachedMode = "tandem";
18398
- var cachedModeAt = 0;
18399
- async function getCachedMode(tandemUrl) {
18400
- const now = Date.now();
18401
- if (now - cachedModeAt < MODE_CACHE_TTL_MS) return cachedMode;
18468
+ async function fetchMode(tandemUrl) {
18402
18469
  try {
18403
- const res = await fetchWithTimeout(`${tandemUrl}/api/mode`, {}, CHANNEL_MODE_FETCH_TIMEOUT_MS);
18404
- if (res.ok) {
18405
- const { mode } = await res.json();
18406
- cachedMode = mode;
18407
- } else {
18408
- console.error(`[Channel] Mode check returned ${res.status}, using cached: "${cachedMode}"`);
18409
- }
18410
- cachedModeAt = now;
18470
+ const res = await fetchWithTimeout(
18471
+ `${tandemUrl}${API_MODE}`,
18472
+ {},
18473
+ CHANNEL_MODE_FETCH_TIMEOUT_MS
18474
+ );
18475
+ if (!res.ok) return { ok: false, reason: `status ${res.status}` };
18476
+ const body = await res.json();
18477
+ const parsed = TandemModeSchema.safeParse(body.mode);
18478
+ if (!parsed.success) return { ok: false, reason: `invalid mode ${JSON.stringify(body.mode)}` };
18479
+ return { ok: true, mode: parsed.data };
18411
18480
  } catch (err) {
18481
+ return { ok: false, reason: describeFetchError(err, API_MODE, CHANNEL_MODE_FETCH_TIMEOUT_MS) };
18482
+ }
18483
+ }
18484
+ async function getCachedMode(tandemUrl, logPrefix = "[Tandem]") {
18485
+ const now = Date.now();
18486
+ if (now - cachedModeAt < MODE_CACHE_TTL_MS && cachedModeAt !== 0) return cachedMode;
18487
+ const result = await fetchMode(tandemUrl);
18488
+ if (!result.ok) {
18489
+ if (cachedModeAt !== 0) {
18490
+ console.error(
18491
+ `${logPrefix} Mode check failed (${result.reason}), preserving last known mode '${cachedMode}'`
18492
+ );
18493
+ return cachedMode;
18494
+ }
18412
18495
  console.error(
18413
- "[Channel] Mode check failed, delivering event (fail-open):",
18414
- describeFetchError(err, "/api/mode", CHANNEL_MODE_FETCH_TIMEOUT_MS)
18496
+ `${logPrefix} Mode check failed (${result.reason}), no prior mode \u2014 using cold-start default '${TANDEM_MODE_DEFAULT}'`
18415
18497
  );
18416
- cachedModeAt = now;
18498
+ cachedMode = TANDEM_MODE_DEFAULT;
18499
+ return TANDEM_MODE_DEFAULT;
18417
18500
  }
18501
+ cachedMode = result.mode;
18502
+ cachedModeAt = now;
18503
+ return cachedMode;
18504
+ }
18505
+ function getModeSync() {
18418
18506
  return cachedMode;
18419
18507
  }
18508
+ function refreshMode(tandemUrl, logPrefix) {
18509
+ if (_modeRefreshInFlight) return;
18510
+ const now = Date.now();
18511
+ if (now - cachedModeAt < MODE_CACHE_TTL_MS) return;
18512
+ if (now - cachedModeFailedAt < MODE_CACHE_TTL_MS) return;
18513
+ _modeRefreshInFlight = (async () => {
18514
+ try {
18515
+ const result = await fetchMode(tandemUrl);
18516
+ if (result.ok) {
18517
+ cachedMode = result.mode;
18518
+ cachedModeAt = Date.now();
18519
+ cachedModeFailedAt = 0;
18520
+ } else {
18521
+ cachedModeFailedAt = Date.now();
18522
+ console.error(
18523
+ `${logPrefix} Background mode refresh failed (${result.reason}), keeping cached`
18524
+ );
18525
+ }
18526
+ } finally {
18527
+ _modeRefreshInFlight = null;
18528
+ }
18529
+ })().catch((err) => {
18530
+ console.error(`${logPrefix} refreshMode unexpected error:`, err);
18531
+ cachedModeFailedAt = Date.now();
18532
+ });
18533
+ }
18534
+
18535
+ // src/channel/event-bridge.ts
18536
+ async function startEventBridge(mcp, tandemUrl) {
18537
+ return runEventConsumer({
18538
+ tandemUrl,
18539
+ logPrefix: "[Channel]",
18540
+ errorCode: CHANNEL_CONNECT_FAILED,
18541
+ onEvent: (event) => mcp.notification({
18542
+ method: "notifications/claude/channel",
18543
+ params: {
18544
+ content: formatEventContent(event),
18545
+ meta: formatEventMeta(event)
18546
+ }
18547
+ })
18548
+ });
18549
+ }
18420
18550
 
18421
18551
  // src/channel/run.ts
18422
18552
  async function runChannel(opts = {}) {
@@ -18473,7 +18603,7 @@ async function runChannel(opts = {}) {
18473
18603
  const args = req.params.arguments;
18474
18604
  try {
18475
18605
  const res = await fetchWithTimeout(
18476
- `${tandemUrl}/api/channel-reply`,
18606
+ `${tandemUrl}${API_CHANNEL_REPLY}`,
18477
18607
  {
18478
18608
  method: "POST",
18479
18609
  headers: { "Content-Type": "application/json" },
@@ -18507,7 +18637,7 @@ async function runChannel(opts = {}) {
18507
18637
  type: "text",
18508
18638
  text: `Failed to send reply: ${describeFetchError(
18509
18639
  err,
18510
- "/api/channel-reply",
18640
+ API_CHANNEL_REPLY,
18511
18641
  CHANNEL_REPLY_FETCH_TIMEOUT_MS
18512
18642
  )}`
18513
18643
  }
@@ -18530,7 +18660,7 @@ async function runChannel(opts = {}) {
18530
18660
  mcp.setNotificationHandler(PermissionRequestSchema, async ({ params }) => {
18531
18661
  try {
18532
18662
  const res = await fetchWithTimeout(
18533
- `${tandemUrl}/api/channel-permission`,
18663
+ `${tandemUrl}${API_CHANNEL_PERMISSION}`,
18534
18664
  {
18535
18665
  method: "POST",
18536
18666
  headers: { "Content-Type": "application/json" },
@@ -18551,7 +18681,7 @@ async function runChannel(opts = {}) {
18551
18681
  } catch (err) {
18552
18682
  console.error(
18553
18683
  "[Channel] Failed to forward permission request:",
18554
- describeFetchError(err, "/api/channel-permission", CHANNEL_PERMISSION_FETCH_TIMEOUT_MS)
18684
+ describeFetchError(err, API_CHANNEL_PERMISSION, CHANNEL_PERMISSION_FETCH_TIMEOUT_MS)
18555
18685
  );
18556
18686
  }
18557
18687
  });
@@ -18577,7 +18707,7 @@ async function checkServerReachable(url, timeoutMs = 2e3) {
18577
18707
  parsed = new URL(url);
18578
18708
  } catch {
18579
18709
  console.error(
18580
- `[Channel] Invalid TANDEM_URL: "${url}" \u2014 expected format: http://localhost:3479`
18710
+ `[Channel] Invalid TANDEM_URL: "${url}" \u2014 expected format: http://127.0.0.1:3479`
18581
18711
  );
18582
18712
  return false;
18583
18713
  }