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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +3 -3
- package/CHANGELOG.md +201 -72
- package/README.md +141 -238
- package/dist/channel/index.js +211 -81
- package/dist/channel/index.js.map +1 -1
- package/dist/cli/index.js +749 -170
- package/dist/cli/index.js.map +1 -1
- package/dist/client/assets/CoworkSettings-BOYbyKul.js +3 -0
- package/dist/client/assets/event-CNdo2oXa.js +1 -0
- package/dist/client/assets/index-D8uS4cj7.css +1 -0
- package/dist/client/assets/index-Dm_QtxGQ.js +1 -0
- package/dist/client/assets/index-g-KwmRn9.js +271 -0
- package/dist/client/assets/webview-KiZyy_pC.js +1 -0
- package/dist/client/assets/window-DePn7tLG.js +1 -0
- package/dist/client/fonts/OFL-Hanuman.txt +93 -0
- package/dist/client/fonts/OFL-InterTight.txt +93 -0
- package/dist/client/fonts/OFL-JetBrainsMono.txt +93 -0
- package/dist/client/fonts/OFL-SNPro.txt +93 -0
- package/dist/client/fonts/OFL-Sono.txt +93 -0
- package/dist/client/fonts/OFL-SourceSerif4.txt +93 -0
- package/dist/client/fonts/hanuman-latin.woff2 +0 -0
- package/dist/client/fonts/jetbrains-mono-latin.woff2 +0 -0
- package/dist/client/fonts/sn-pro-latin.woff2 +0 -0
- package/dist/client/fonts/sono-latin.woff2 +0 -0
- package/dist/client/fonts/source-serif-4-latin.woff2 +0 -0
- package/dist/client/index.html +206 -17
- package/dist/client/logo.png +0 -0
- package/dist/monitor/index.js +241 -160
- package/dist/monitor/index.js.map +1 -1
- package/dist/server/index.js +22828 -19659
- package/dist/server/index.js.map +1 -1
- package/package.json +12 -4
- package/sample/welcome.md +6 -6
- package/skills/tandem/SKILL.md +15 -0
- package/dist/client/assets/CoworkSettings-DK3jjdwK.js +0 -3
- package/dist/client/assets/index-CfT503n4.js +0 -297
- package/dist/client/assets/index-DeJe09pn.css +0 -1
- package/dist/client/assets/webview-Ben21ZLJ.js +0 -1
- package/dist/client/assets/window-BxBvHL5k.js +0 -1
package/dist/channel/index.js
CHANGED
|
@@ -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://
|
|
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/
|
|
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
|
-
|
|
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
|
|
18198
|
-
|
|
18199
|
-
|
|
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
|
-
|
|
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(
|
|
18261
|
+
console.error(`${opts.logPrefix} SSE connection exhausted, reporting error and exiting`);
|
|
18209
18262
|
try {
|
|
18210
18263
|
await fetchWithTimeout(
|
|
18211
|
-
`${tandemUrl}
|
|
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:
|
|
18217
|
-
message:
|
|
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
|
-
|
|
18225
|
-
describeFetchError(reportErr,
|
|
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
|
-
|
|
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
|
|
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}
|
|
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
|
-
|
|
18266
|
-
|
|
18267
|
-
|
|
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
|
-
|
|
18282
|
-
describeFetchError(
|
|
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
|
-
|
|
18291
|
-
|
|
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
|
-
|
|
18375
|
+
`${opts.logPrefix} Awareness update failed:`,
|
|
18305
18376
|
describeFetchError(
|
|
18306
18377
|
err,
|
|
18307
|
-
|
|
18378
|
+
`${API_CHANNEL_AWARENESS} update`,
|
|
18308
18379
|
CHANNEL_AWARENESS_FETCH_TIMEOUT_MS
|
|
18309
18380
|
)
|
|
18310
18381
|
);
|
|
18311
18382
|
});
|
|
18312
|
-
|
|
18313
|
-
|
|
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
|
|
18421
|
+
let raw;
|
|
18347
18422
|
try {
|
|
18348
|
-
|
|
18349
|
-
} catch {
|
|
18423
|
+
raw = JSON.parse(data);
|
|
18424
|
+
} catch (err) {
|
|
18350
18425
|
console.error(
|
|
18351
|
-
|
|
18352
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18369
|
-
if (
|
|
18370
|
-
console.error(
|
|
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
|
|
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(
|
|
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
|
-
|
|
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(
|
|
18404
|
-
|
|
18405
|
-
|
|
18406
|
-
|
|
18407
|
-
|
|
18408
|
-
|
|
18409
|
-
|
|
18410
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}
|
|
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
|
-
|
|
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}
|
|
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,
|
|
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://
|
|
18710
|
+
`[Channel] Invalid TANDEM_URL: "${url}" \u2014 expected format: http://127.0.0.1:3479`
|
|
18581
18711
|
);
|
|
18582
18712
|
return false;
|
|
18583
18713
|
}
|