backeer-opencode 0.8.2 → 0.10.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/CHANGELOG.md +19 -0
- package/dist/backeer-permission.js +227 -284
- package/docs/live-control.md +25 -12
- package/package.json +1 -1
- package/src/delivery-tracker.js +4 -4
- package/src/diagnostics.js +3 -3
- package/src/event-publishers.js +33 -34
- package/src/feedback-session-store.js +3 -14
- package/src/http.js +14 -16
- package/src/lifecycle-controller.js +5 -5
- package/src/lifecycle-publishers.js +8 -9
- package/src/live-bridge-client.js +3 -4
- package/src/live-bridge-config.js +3 -4
- package/src/live-bridge-events.js +6 -9
- package/src/live-bridge.js +53 -76
- package/src/live-control-client.js +84 -38
- package/src/live-control-mcp.js +10 -10
- package/src/live-diagnostics.js +42 -0
- package/src/live-discovery.js +11 -3
- package/src/live-errors.js +4 -0
- package/src/live-managed-mcp.js +16 -10
- package/src/live-profiles.js +6 -6
- package/src/live-question-tools.js +5 -5
- package/src/live-questions.js +4 -5
- package/src/live-session-manager.js +101 -15
- package/src/live-session-scope.js +3 -4
- package/src/permission-controller.js +4 -4
- package/src/plugin.js +3 -0
- package/src/reporter.js +4 -8
- package/src/session-messages.js +3 -3
- package/src/sse.js +7 -7
- package/src/updates.js +1 -1
- package/src/utilities.js +3 -6
|
@@ -1860,8 +1860,7 @@ import { homedir } from "node:os";
|
|
|
1860
1860
|
import { join as join2 } from "node:path";
|
|
1861
1861
|
|
|
1862
1862
|
// src/utilities.js
|
|
1863
|
-
var
|
|
1864
|
-
var limit = (value, length = 1e3) => String(value ?? "").slice(0, length);
|
|
1863
|
+
var formatText = (value) => String(value ?? "");
|
|
1865
1864
|
function enabled(value, fallback = true) {
|
|
1866
1865
|
if (typeof value === "boolean") return value;
|
|
1867
1866
|
if (typeof value !== "string") return fallback;
|
|
@@ -1875,10 +1874,9 @@ function stableHash(value) {
|
|
|
1875
1874
|
}
|
|
1876
1875
|
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
1877
1876
|
}
|
|
1878
|
-
function
|
|
1877
|
+
function remember(map, key, value) {
|
|
1879
1878
|
map.delete(key);
|
|
1880
1879
|
map.set(key, value);
|
|
1881
|
-
while (map.size > MAX_TRACKED_ITEMS) map.delete(map.keys().next().value);
|
|
1882
1880
|
}
|
|
1883
1881
|
|
|
1884
1882
|
// src/reporter.js
|
|
@@ -1905,10 +1903,10 @@ function createOpenCodeReporter(client, levelValue) {
|
|
|
1905
1903
|
const body = {
|
|
1906
1904
|
service: "backeer-opencode",
|
|
1907
1905
|
level: kind === "warning" ? "warn" : kind === "applied" || kind === "success" ? "info" : kind,
|
|
1908
|
-
message:
|
|
1906
|
+
message: formatText(message)
|
|
1909
1907
|
};
|
|
1910
1908
|
if (level === "debug" && detail) {
|
|
1911
|
-
body.extra = { detail:
|
|
1909
|
+
body.extra = { detail: formatText(detail?.message ?? detail) };
|
|
1912
1910
|
}
|
|
1913
1911
|
setTimeout(() => {
|
|
1914
1912
|
void Promise.resolve(method.call(client.app, { body })).catch(() => {
|
|
@@ -1927,7 +1925,6 @@ function createOpenCodeReporter(client, levelValue) {
|
|
|
1927
1925
|
function rememberRecent(map, key, value) {
|
|
1928
1926
|
map.delete(key);
|
|
1929
1927
|
map.set(key, value);
|
|
1930
|
-
while (map.size > 100) map.delete(map.keys().next().value);
|
|
1931
1928
|
}
|
|
1932
1929
|
|
|
1933
1930
|
// src/live-bridge-config.js
|
|
@@ -1941,14 +1938,14 @@ function readCredential(file) {
|
|
|
1941
1938
|
try {
|
|
1942
1939
|
descriptor = openSync(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
1943
1940
|
const stat = fstatSync(descriptor);
|
|
1944
|
-
if (!stat.isFile() ||
|
|
1941
|
+
if (!stat.isFile() || process.platform !== "win32" && ((stat.mode & 63) !== 0 || stat.uid !== process.getuid())) {
|
|
1945
1942
|
throw new Error("invalid_file");
|
|
1946
1943
|
}
|
|
1947
1944
|
const token = readFileSync(descriptor, "utf8").trim();
|
|
1948
|
-
if (!/^[A-Za-z0-9_-]{32,
|
|
1945
|
+
if (!/^[A-Za-z0-9_-]{32,}$/.test(token)) throw new Error("invalid_token");
|
|
1949
1946
|
return token;
|
|
1950
1947
|
} catch {
|
|
1951
|
-
throw new Error("Live control credential must be an owner-only regular file containing a 32
|
|
1948
|
+
throw new Error("Live control credential must be an owner-only regular file containing a token of at least 32 characters");
|
|
1952
1949
|
} finally {
|
|
1953
1950
|
if (descriptor !== void 0) closeSync(descriptor);
|
|
1954
1951
|
}
|
|
@@ -2079,12 +2076,12 @@ function createDeliveryTracker() {
|
|
|
2079
2076
|
return {
|
|
2080
2077
|
begin(key) {
|
|
2081
2078
|
if (states.has(key)) return false;
|
|
2082
|
-
|
|
2079
|
+
remember(states, key, "publishing");
|
|
2083
2080
|
return true;
|
|
2084
2081
|
},
|
|
2085
2082
|
succeed(key) {
|
|
2086
2083
|
if (states.get(key) === "publishing") {
|
|
2087
|
-
|
|
2084
|
+
remember(states, key, "published");
|
|
2088
2085
|
}
|
|
2089
2086
|
},
|
|
2090
2087
|
fail(key) {
|
|
@@ -2101,7 +2098,7 @@ function createCompletionTracker() {
|
|
|
2101
2098
|
let state = states.get(key);
|
|
2102
2099
|
if (!state) {
|
|
2103
2100
|
state = { complete: false, publishing: false, revision: 0 };
|
|
2104
|
-
|
|
2101
|
+
remember(states, key, state);
|
|
2105
2102
|
}
|
|
2106
2103
|
return state;
|
|
2107
2104
|
}
|
|
@@ -2137,8 +2134,89 @@ function createCompletionTracker() {
|
|
|
2137
2134
|
};
|
|
2138
2135
|
}
|
|
2139
2136
|
|
|
2137
|
+
// src/live-bridge-events.js
|
|
2138
|
+
var ID = /^[A-Za-z0-9_-]+$/;
|
|
2139
|
+
var validBridgeID = (value) => typeof value === "string" && ID.test(value);
|
|
2140
|
+
function createBridgeSanitizer(secrets = []) {
|
|
2141
|
+
const known = secrets.filter((value) => typeof value === "string" && value.length > 0);
|
|
2142
|
+
const assistants = /* @__PURE__ */ new Map();
|
|
2143
|
+
function text(value) {
|
|
2144
|
+
if (typeof value !== "string") return "";
|
|
2145
|
+
let result = value;
|
|
2146
|
+
for (const secret of known) result = result.split(secret).join("[REDACTED]");
|
|
2147
|
+
result = result.replace(/-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?(?:-----END [^-]*PRIVATE KEY-----|$)/g, "[REDACTED PRIVATE KEY]").replace(/\bBearer\s+[^\s"'<>]+/gi, "Bearer [REDACTED]").replace(/\b(?:sk|bgi)_[A-Za-z0-9_-]{12,}|\bsk-[A-Za-z0-9_-]{12,}/g, "[REDACTED]").replace(/((?:api[_-]?key|access[_-]?token|secret|password|authorization)\s*[=:]\s*)[^\s,;]+/gi, "$1[REDACTED]");
|
|
2148
|
+
return result;
|
|
2149
|
+
}
|
|
2150
|
+
function content(key, value) {
|
|
2151
|
+
return { [key]: text(value) };
|
|
2152
|
+
}
|
|
2153
|
+
function sanitize(event) {
|
|
2154
|
+
const p = event?.properties ?? event?.data ?? {};
|
|
2155
|
+
const sessionID = p.sessionID ?? p.info?.sessionID ?? p.part?.sessionID;
|
|
2156
|
+
if (!validBridgeID(sessionID) || sessionID === "global") return void 0;
|
|
2157
|
+
const base = { type: event.type, sessionID, data: {} };
|
|
2158
|
+
if (event.type === "message.updated") {
|
|
2159
|
+
const info = p.info;
|
|
2160
|
+
if (!validBridgeID(info?.id)) return void 0;
|
|
2161
|
+
if (info.role === "assistant") {
|
|
2162
|
+
assistants.set(info.id, sessionID);
|
|
2163
|
+
}
|
|
2164
|
+
return info.role === "assistant" ? { ...base, data: {
|
|
2165
|
+
messageID: info.id,
|
|
2166
|
+
completed: !!info.time?.completed,
|
|
2167
|
+
failed: !!info.error
|
|
2168
|
+
} } : void 0;
|
|
2169
|
+
}
|
|
2170
|
+
if (event.type === "message.part.updated") {
|
|
2171
|
+
const part = p.part;
|
|
2172
|
+
if (assistants.get(part?.messageID) !== sessionID) return void 0;
|
|
2173
|
+
if (part.type === "text") return { ...base, type: "assistant.text", data: {
|
|
2174
|
+
messageID: part.messageID,
|
|
2175
|
+
...content("text", part.text),
|
|
2176
|
+
replace: true
|
|
2177
|
+
} };
|
|
2178
|
+
if (part.type !== "tool") return void 0;
|
|
2179
|
+
const state = part.state ?? {};
|
|
2180
|
+
return { ...base, type: "tool.status", data: {
|
|
2181
|
+
tool: text(part.tool, 80),
|
|
2182
|
+
status: text(state.status, 40),
|
|
2183
|
+
...state.status === "completed" ? content("output", state.output) : {},
|
|
2184
|
+
...state.status === "error" ? { failed: true } : {}
|
|
2185
|
+
} };
|
|
2186
|
+
}
|
|
2187
|
+
if (event.type === "session.text.ended") return { ...base, type: "assistant.text", data: {
|
|
2188
|
+
messageID: validBridgeID(p.assistantMessageID) ? p.assistantMessageID : void 0,
|
|
2189
|
+
...content("text", p.text),
|
|
2190
|
+
replace: true
|
|
2191
|
+
} };
|
|
2192
|
+
if (["session.tool.called", "session.tool.success", "session.tool.failed"].includes(event.type)) {
|
|
2193
|
+
return { ...base, type: "tool.status", data: {
|
|
2194
|
+
status: event.type.split(".").at(-1),
|
|
2195
|
+
...event.type === "session.tool.success" ? content(
|
|
2196
|
+
"output",
|
|
2197
|
+
(p.content ?? []).filter((item) => item?.type === "text").map((item) => item.text).join("\n")
|
|
2198
|
+
) : {}
|
|
2199
|
+
} };
|
|
2200
|
+
}
|
|
2201
|
+
if (["permission.asked", "question.asked", "question.v2.asked"].includes(event.type)) {
|
|
2202
|
+
return { ...base, type: "approval.waiting", data: {
|
|
2203
|
+
kind: event.type.split(".")[0],
|
|
2204
|
+
...event.type !== "permission.asked" && validBridgeID(p.id ?? p.requestID) ? { questionId: p.id ?? p.requestID } : {}
|
|
2205
|
+
} };
|
|
2206
|
+
}
|
|
2207
|
+
if (["permission.replied", "question.replied", "question.v2.replied", "question.rejected", "question.v2.rejected"].includes(event.type)) {
|
|
2208
|
+
return { ...base, type: "approval.settled", data: { kind: event.type.split(".")[0] } };
|
|
2209
|
+
}
|
|
2210
|
+
if (event.type === "session.status") return { ...base, data: {
|
|
2211
|
+
status: ["idle", "busy", "retry"].includes(p.status?.type) ? p.status.type : "unknown"
|
|
2212
|
+
} };
|
|
2213
|
+
if (["session.idle", "session.error", "session.deleted"].includes(event.type)) return base;
|
|
2214
|
+
return void 0;
|
|
2215
|
+
}
|
|
2216
|
+
return { sanitize, text, clear: () => assistants.clear() };
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2140
2219
|
// src/http.js
|
|
2141
|
-
var RETRY_DELAYS_MS = [0, 500, 1500];
|
|
2142
2220
|
var HttpError = class extends Error {
|
|
2143
2221
|
constructor(message, status, responseBody = "") {
|
|
2144
2222
|
super(message);
|
|
@@ -2169,16 +2247,16 @@ function sleep(milliseconds, signal2) {
|
|
|
2169
2247
|
});
|
|
2170
2248
|
}
|
|
2171
2249
|
async function postJsonWithRetry(config, url, body, options = {}) {
|
|
2172
|
-
const retryDelays = options.retryDelays
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
if (
|
|
2250
|
+
const retryDelays = options.retryDelays;
|
|
2251
|
+
const signal2 = options.signal ?? config.signal;
|
|
2252
|
+
for (let attempt = 0; retryDelays === void 0 || attempt < retryDelays.length; attempt++) {
|
|
2253
|
+
const retryDelay = retryDelays?.[attempt] ?? (attempt === 0 ? 0 : 3e3);
|
|
2254
|
+
if (retryDelay > 0) await (options.sleepImplementation ?? sleep)(retryDelay, signal2);
|
|
2255
|
+
if (signal2?.aborted) throw new DOMException("Aborted", "AbortError");
|
|
2178
2256
|
const controller = new AbortController();
|
|
2179
2257
|
const abort = () => controller.abort();
|
|
2180
2258
|
const timeout = setTimeout(abort, options.timeoutMs ?? 5e3);
|
|
2181
|
-
|
|
2259
|
+
signal2?.addEventListener("abort", abort, { once: true });
|
|
2182
2260
|
try {
|
|
2183
2261
|
const response = await fetch(url, {
|
|
2184
2262
|
method: "POST",
|
|
@@ -2190,22 +2268,22 @@ async function postJsonWithRetry(config, url, body, options = {}) {
|
|
|
2190
2268
|
body: JSON.stringify(body),
|
|
2191
2269
|
signal: controller.signal
|
|
2192
2270
|
});
|
|
2193
|
-
const responseBody =
|
|
2271
|
+
const responseBody = await response.text();
|
|
2194
2272
|
if (response.ok) return { status: response.status, responseBody };
|
|
2195
|
-
if (!isRetryable(response.status) || attempt === retryDelays.length - 1) {
|
|
2273
|
+
if (!isRetryable(response.status) || retryDelays !== void 0 && attempt === retryDelays.length - 1) {
|
|
2196
2274
|
throw new HttpError(
|
|
2197
2275
|
`HTTP request failed with status ${response.status}`,
|
|
2198
2276
|
response.status,
|
|
2199
|
-
responseBody
|
|
2277
|
+
createBridgeSanitizer([config.token]).text(responseBody)
|
|
2200
2278
|
);
|
|
2201
2279
|
}
|
|
2202
2280
|
} catch (error) {
|
|
2203
|
-
if (
|
|
2281
|
+
if (signal2?.aborted) throw new DOMException("Aborted", "AbortError");
|
|
2204
2282
|
if (error instanceof HttpError) throw error;
|
|
2205
|
-
if (attempt === retryDelays.length - 1) throw error;
|
|
2283
|
+
if (retryDelays !== void 0 && attempt === retryDelays.length - 1) throw error;
|
|
2206
2284
|
} finally {
|
|
2207
2285
|
clearTimeout(timeout);
|
|
2208
|
-
|
|
2286
|
+
signal2?.removeEventListener("abort", abort);
|
|
2209
2287
|
}
|
|
2210
2288
|
}
|
|
2211
2289
|
throw new Error("HTTP retry loop ended unexpectedly");
|
|
@@ -2228,19 +2306,19 @@ async function publishPermission(config, reporter, request, directory, identity,
|
|
|
2228
2306
|
const permissionType = request.permission ?? request.action ?? "unknown";
|
|
2229
2307
|
const resources = Array.isArray(request.patterns) ? request.patterns.join(", ") : Array.isArray(request.resources) ? request.resources.join(", ") : "";
|
|
2230
2308
|
const payload = [
|
|
2231
|
-
{ key: "Permission", value:
|
|
2232
|
-
{ key: "Session", value:
|
|
2233
|
-
{ key: "Request ID", value:
|
|
2234
|
-
{ key: "Patterns", value:
|
|
2235
|
-
{ key: "Working directory", value:
|
|
2309
|
+
{ key: "Permission", value: formatText(permissionType) },
|
|
2310
|
+
{ key: "Session", value: formatText(identity.sessionID) },
|
|
2311
|
+
{ key: "Request ID", value: formatText(identity.requestID) },
|
|
2312
|
+
{ key: "Patterns", value: formatText(resources) },
|
|
2313
|
+
{ key: "Working directory", value: formatText(directory) },
|
|
2236
2314
|
{ key: "Action protocol", value: ACTION_PROTOCOL },
|
|
2237
2315
|
{ key: "Available decisions", value: "once, always, reject" }
|
|
2238
2316
|
].filter((item) => item.value.length > 0);
|
|
2239
2317
|
const result = await postJsonWithRetry(config, config.eventsEndpoint, {
|
|
2240
2318
|
type: "application",
|
|
2241
2319
|
severity: "warning",
|
|
2242
|
-
title: `OpenCode permission required: ${
|
|
2243
|
-
body: resources ? `OpenCode is waiting for approval to access: ${
|
|
2320
|
+
title: `OpenCode permission required: ${formatText(permissionType)}`,
|
|
2321
|
+
body: resources ? `OpenCode is waiting for approval to access: ${formatText(resources)}` : `OpenCode is waiting for approval for: ${formatText(permissionType)}`,
|
|
2244
2322
|
payload,
|
|
2245
2323
|
externalId: identity.externalId,
|
|
2246
2324
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2263,8 +2341,8 @@ function questionIdentity(request) {
|
|
|
2263
2341
|
function formatQuestionOptions(options) {
|
|
2264
2342
|
if (!Array.isArray(options)) return "";
|
|
2265
2343
|
return options.map((option) => {
|
|
2266
|
-
const label =
|
|
2267
|
-
const description =
|
|
2344
|
+
const label = formatText(option?.label);
|
|
2345
|
+
const description = formatText(option?.description);
|
|
2268
2346
|
if (!label) return "";
|
|
2269
2347
|
return description ? `${label} \u2014 ${description}` : label;
|
|
2270
2348
|
}).filter(Boolean).join(" | ");
|
|
@@ -2277,7 +2355,7 @@ async function resolveSessionTitle(client, directory, sessionID, reporter) {
|
|
|
2277
2355
|
query: { directory },
|
|
2278
2356
|
throwOnError: true
|
|
2279
2357
|
});
|
|
2280
|
-
return
|
|
2358
|
+
return formatText((result?.data ?? result)?.title);
|
|
2281
2359
|
} catch (error) {
|
|
2282
2360
|
reporter.debug("Session title unavailable", error);
|
|
2283
2361
|
return "";
|
|
@@ -2294,43 +2372,43 @@ async function publishQuestion(config, reporter, request, identity, client, dire
|
|
|
2294
2372
|
);
|
|
2295
2373
|
const payload = [
|
|
2296
2374
|
{ key: "OpenCode session", value: sessionTitle },
|
|
2297
|
-
{ key: "Session", value:
|
|
2298
|
-
{ key: "Request ID", value:
|
|
2375
|
+
{ key: "Session", value: formatText(identity.sessionID) },
|
|
2376
|
+
{ key: "Request ID", value: formatText(identity.requestID) },
|
|
2299
2377
|
{ key: "Action protocol", value: QUESTION_ACTION_PROTOCOL },
|
|
2300
2378
|
...questions.flatMap((question, index) => {
|
|
2301
2379
|
const suffix = questions.length > 1 ? ` ${index + 1}` : "";
|
|
2302
2380
|
return [
|
|
2303
|
-
{ key: `Question title${suffix}`, value:
|
|
2304
|
-
{ key: `Question${suffix}`, value:
|
|
2381
|
+
{ key: `Question title${suffix}`, value: formatText(question?.header) },
|
|
2382
|
+
{ key: `Question${suffix}`, value: formatText(question?.question) },
|
|
2305
2383
|
{
|
|
2306
2384
|
key: `Options${suffix}`,
|
|
2307
|
-
value:
|
|
2385
|
+
value: formatText(formatQuestionOptions(question?.options))
|
|
2308
2386
|
}
|
|
2309
2387
|
];
|
|
2310
2388
|
})
|
|
2311
2389
|
].filter((item) => item.value.length > 0);
|
|
2312
2390
|
const firstQuestion = questions[0];
|
|
2313
2391
|
const actionQuestions = questions.map((question) => ({
|
|
2314
|
-
header:
|
|
2315
|
-
question:
|
|
2316
|
-
options: Array.isArray(question?.options) ? question.options.
|
|
2317
|
-
label:
|
|
2318
|
-
description:
|
|
2392
|
+
header: formatText(question?.header),
|
|
2393
|
+
question: formatText(question?.question),
|
|
2394
|
+
options: Array.isArray(question?.options) ? question.options.map((option) => ({
|
|
2395
|
+
label: formatText(option?.label),
|
|
2396
|
+
description: formatText(option?.description)
|
|
2319
2397
|
})).filter((option) => option.label) : [],
|
|
2320
2398
|
multiple: question?.multiple === true,
|
|
2321
2399
|
custom: question?.custom !== false
|
|
2322
2400
|
}));
|
|
2323
2401
|
const body = questions.map((question) => {
|
|
2324
|
-
const header =
|
|
2325
|
-
const text =
|
|
2326
|
-
const options =
|
|
2402
|
+
const header = formatText(question?.header);
|
|
2403
|
+
const text = formatText(question?.question);
|
|
2404
|
+
const options = formatText(formatQuestionOptions(question?.options));
|
|
2327
2405
|
return [header, text, options ? `Options: ${options}` : ""].filter(Boolean).join("\n");
|
|
2328
2406
|
}).join("\n\n");
|
|
2329
2407
|
const result = await postJsonWithRetry(config, config.eventsEndpoint, {
|
|
2330
2408
|
type: "application",
|
|
2331
2409
|
severity: "info",
|
|
2332
|
-
title: `OpenCode question: ${
|
|
2333
|
-
body:
|
|
2410
|
+
title: `OpenCode question: ${formatText(firstQuestion?.header) || "Question"}`,
|
|
2411
|
+
body: formatText(body),
|
|
2334
2412
|
payload,
|
|
2335
2413
|
externalId: identity.externalId,
|
|
2336
2414
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2359,19 +2437,19 @@ async function publishTodoCompletion(config, reporter, request, eventID, client,
|
|
|
2359
2437
|
request.sessionID,
|
|
2360
2438
|
reporter
|
|
2361
2439
|
);
|
|
2362
|
-
const correlation = typeof eventID === "string" && eventID.length > 0 ?
|
|
2440
|
+
const correlation = typeof eventID === "string" && eventID.length > 0 ? formatText(eventID) : stableHash(JSON.stringify(todos));
|
|
2363
2441
|
const externalId = `opencode-todo:${request.sessionID}:${correlation}`;
|
|
2364
|
-
const visibleTodos = todos
|
|
2442
|
+
const visibleTodos = todos;
|
|
2365
2443
|
const payload = [
|
|
2366
2444
|
{ key: "OpenCode session", value: sessionTitle },
|
|
2367
2445
|
{ key: "Completed tasks", value: String(todos.length) },
|
|
2368
2446
|
...visibleTodos.flatMap((todo, index) => [
|
|
2369
|
-
{ key: `Task ${index + 1}`, value:
|
|
2370
|
-
{ key: `Priority ${index + 1}`, value:
|
|
2447
|
+
{ key: `Task ${index + 1}`, value: formatText(todo?.content) },
|
|
2448
|
+
{ key: `Priority ${index + 1}`, value: formatText(todo?.priority) }
|
|
2371
2449
|
]),
|
|
2372
2450
|
...todos.length > visibleTodos.length ? [{ key: "Additional tasks", value: String(todos.length - visibleTodos.length) }] : []
|
|
2373
2451
|
].filter((item) => item.value.length > 0);
|
|
2374
|
-
const summary = todos.
|
|
2452
|
+
const summary = todos.map((todo) => formatText(todo?.content)).filter(Boolean).join(" \u2022 ");
|
|
2375
2453
|
const location = sessionTitle ? ` in ${sessionTitle}` : "";
|
|
2376
2454
|
const body = [
|
|
2377
2455
|
`All ${todos.length} OpenCode tasks completed${location}.`,
|
|
@@ -2381,7 +2459,7 @@ async function publishTodoCompletion(config, reporter, request, eventID, client,
|
|
|
2381
2459
|
type: "application",
|
|
2382
2460
|
severity: "info",
|
|
2383
2461
|
title: "OpenCode todo list completed",
|
|
2384
|
-
body:
|
|
2462
|
+
body: formatText(body),
|
|
2385
2463
|
payload,
|
|
2386
2464
|
externalId,
|
|
2387
2465
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2408,7 +2486,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2408
2486
|
import { statSync } from "node:fs";
|
|
2409
2487
|
|
|
2410
2488
|
// src/updates.js
|
|
2411
|
-
var PLUGIN_VERSION = "0.
|
|
2489
|
+
var PLUGIN_VERSION = "0.10.0";
|
|
2412
2490
|
var PACKAGE_NAME = "backeer-opencode";
|
|
2413
2491
|
var NPM_REGISTRY_URL = "https://registry.npmjs.org";
|
|
2414
2492
|
var CHECK_TIMEOUT_MS = 3e3;
|
|
@@ -2564,7 +2642,7 @@ async function probeSource(config, fetchImplementation = globalThis.fetch) {
|
|
|
2564
2642
|
headers: { Authorization: `Bearer ${config.token}` },
|
|
2565
2643
|
signal: controller.signal
|
|
2566
2644
|
});
|
|
2567
|
-
const responseBody =
|
|
2645
|
+
const responseBody = formatText(await response.text());
|
|
2568
2646
|
if (!response.ok) {
|
|
2569
2647
|
throw new HttpError(
|
|
2570
2648
|
`Gateway source check failed with status ${response.status}`,
|
|
@@ -2611,7 +2689,7 @@ async function buildDoctorReport(config, { fetchImplementation = globalThis.fetc
|
|
|
2611
2689
|
text: result?.capabilities?.apns === true ? "Gateway APNs delivery is enabled" : "Gateway APNs delivery is disabled"
|
|
2612
2690
|
});
|
|
2613
2691
|
} catch (error) {
|
|
2614
|
-
const detail = error instanceof HttpError && error.status === 401 ? "Source authentication rejected (HTTP 401)" : error?.name === "AbortError" ? "Gateway source check timed out" : `Gateway source check failed: ${
|
|
2692
|
+
const detail = error instanceof HttpError && error.status === 401 ? "Source authentication rejected (HTTP 401)" : error?.name === "AbortError" ? "Gateway source check timed out" : `Gateway source check failed: ${formatText(error?.message || error)}`;
|
|
2615
2693
|
checks.push({ ok: false, text: detail });
|
|
2616
2694
|
}
|
|
2617
2695
|
}
|
|
@@ -2648,7 +2726,6 @@ async function publishTestNotification(config, reporter) {
|
|
|
2648
2726
|
}
|
|
2649
2727
|
|
|
2650
2728
|
// src/feedback-session-store.js
|
|
2651
|
-
var MAX_QUEUED_MESSAGES = 20;
|
|
2652
2729
|
var FeedbackSessionStore = class {
|
|
2653
2730
|
constructor({ send, reporter, flushDelayMs = 150 }) {
|
|
2654
2731
|
this.send = send;
|
|
@@ -2666,10 +2743,9 @@ var FeedbackSessionStore = class {
|
|
|
2666
2743
|
blockers: /* @__PURE__ */ new Set(),
|
|
2667
2744
|
queue: [],
|
|
2668
2745
|
queued: /* @__PURE__ */ new Set(),
|
|
2669
|
-
queueOverflowReported: false,
|
|
2670
2746
|
flushTimer: void 0
|
|
2671
2747
|
};
|
|
2672
|
-
|
|
2748
|
+
remember(this.sessions, sessionID, state);
|
|
2673
2749
|
}
|
|
2674
2750
|
return state;
|
|
2675
2751
|
}
|
|
@@ -2704,16 +2780,8 @@ var FeedbackSessionStore = class {
|
|
|
2704
2780
|
const state = this.stateFor(sessionID);
|
|
2705
2781
|
const key = `${sessionID}:${kind}:${message}`;
|
|
2706
2782
|
if (state.queued.has(key)) return;
|
|
2707
|
-
state.queue.push({ kind, message:
|
|
2783
|
+
state.queue.push({ kind, message: formatText(message), key });
|
|
2708
2784
|
state.queued.add(key);
|
|
2709
|
-
if (state.queue.length > MAX_QUEUED_MESSAGES) {
|
|
2710
|
-
const removed = state.queue.shift();
|
|
2711
|
-
state.queued.delete(removed.key);
|
|
2712
|
-
if (!state.queueOverflowReported) {
|
|
2713
|
-
state.queueOverflowReported = true;
|
|
2714
|
-
this.reporter.warning("Conversation feedback queue full \u2014 oldest message discarded");
|
|
2715
|
-
}
|
|
2716
|
-
}
|
|
2717
2785
|
this.scheduleFlush(sessionID, state);
|
|
2718
2786
|
}
|
|
2719
2787
|
setIdle(sessionID, idle) {
|
|
@@ -2826,7 +2894,7 @@ async function sendSessionMessage(client, sessionID, text) {
|
|
|
2826
2894
|
}
|
|
2827
2895
|
function formatBackeerFeedback(kind, message) {
|
|
2828
2896
|
const symbol = FEEDBACK_SYMBOLS[kind] ?? FEEDBACK_SYMBOLS.info;
|
|
2829
|
-
return `\u25A3 Backeer | ${symbol} ${
|
|
2897
|
+
return `\u25A3 Backeer | ${symbol} ${formatText(message)}`;
|
|
2830
2898
|
}
|
|
2831
2899
|
function formatBackeerFeedbackBatch(items) {
|
|
2832
2900
|
if (items.length === 1) {
|
|
@@ -2836,7 +2904,7 @@ function formatBackeerFeedbackBatch(items) {
|
|
|
2836
2904
|
"\u25A3 Backeer",
|
|
2837
2905
|
...items.map((item) => {
|
|
2838
2906
|
const symbol = FEEDBACK_SYMBOLS[item.kind] ?? FEEDBACK_SYMBOLS.info;
|
|
2839
|
-
return `${symbol} ${
|
|
2907
|
+
return `${symbol} ${formatText(item.message)}`;
|
|
2840
2908
|
})
|
|
2841
2909
|
].join("\n");
|
|
2842
2910
|
}
|
|
@@ -3084,17 +3152,17 @@ var URL_WITH_QUERY = /\bhttps?:\/\/[^\s?#]+(?:\?[^\s#]*)?(?:#[^\s]*)?/gi;
|
|
|
3084
3152
|
var WINDOWS_PATH = /\b[A-Za-z]:\\(?:[^\\\s]+\\)+[^\s]*/g;
|
|
3085
3153
|
var POSIX_PATH = /(^|\s)\/(?:[^/\s]+\/)+[^\s]*/g;
|
|
3086
3154
|
function sanitizeErrorMessage(value) {
|
|
3087
|
-
return
|
|
3155
|
+
return formatText(value).replace(CONTROL_CHARACTERS, " ").replace(BEARER_TOKEN, "Bearer [redacted]").replace(SECRET_ASSIGNMENT, "$1=[redacted]").replace(URL_WITH_QUERY, (value2) => {
|
|
3088
3156
|
try {
|
|
3089
3157
|
const url = new URL(value2);
|
|
3090
3158
|
return `${url.origin}${url.pathname}`;
|
|
3091
3159
|
} catch {
|
|
3092
3160
|
return "[url redacted]";
|
|
3093
3161
|
}
|
|
3094
|
-
}).replace(WINDOWS_PATH, "[path redacted]").replace(POSIX_PATH, "$1[path redacted]").replace(/\s+/g, " ").trim()
|
|
3162
|
+
}).replace(WINDOWS_PATH, "[path redacted]").replace(POSIX_PATH, "$1[path redacted]").replace(/\s+/g, " ").trim();
|
|
3095
3163
|
}
|
|
3096
3164
|
function safeErrorDetails(error) {
|
|
3097
|
-
const name =
|
|
3165
|
+
const name = formatText(error?.name || "OpenCodeError");
|
|
3098
3166
|
const message = sanitizeErrorMessage(error?.data?.message ?? error?.message);
|
|
3099
3167
|
const statusCode = Number.isInteger(error?.data?.statusCode) ? String(error.data.statusCode) : "";
|
|
3100
3168
|
return { name, message, statusCode };
|
|
@@ -3107,7 +3175,7 @@ async function resolveSessionTitle2(client, directory, sessionID, reporter) {
|
|
|
3107
3175
|
query: { directory },
|
|
3108
3176
|
throwOnError: true
|
|
3109
3177
|
});
|
|
3110
|
-
return
|
|
3178
|
+
return formatText((result?.data ?? result)?.title);
|
|
3111
3179
|
} catch (error) {
|
|
3112
3180
|
reporter.debug("Session title unavailable", error);
|
|
3113
3181
|
return "";
|
|
@@ -3214,7 +3282,7 @@ async function publishSessionError({
|
|
|
3214
3282
|
// Backeer/iOS currently support info, warning and critical severities.
|
|
3215
3283
|
severity: "critical",
|
|
3216
3284
|
title: "OpenCode task failed",
|
|
3217
|
-
body:
|
|
3285
|
+
body: formatText(body),
|
|
3218
3286
|
payload: [
|
|
3219
3287
|
{ key: "OpenCode session", value: sessionTitle },
|
|
3220
3288
|
{ key: "Outcome", value: "Failed" },
|
|
@@ -3226,7 +3294,7 @@ async function publishSessionError({
|
|
|
3226
3294
|
}, "Task failure sent");
|
|
3227
3295
|
}
|
|
3228
3296
|
async function publishOpenCodeUpdate({ config, reporter, version }) {
|
|
3229
|
-
const safeVersion =
|
|
3297
|
+
const safeVersion = formatText(version);
|
|
3230
3298
|
await publish(config, reporter, void 0, void 0, {
|
|
3231
3299
|
severity: "info",
|
|
3232
3300
|
title: "OpenCode update available",
|
|
@@ -3236,8 +3304,8 @@ async function publishOpenCodeUpdate({ config, reporter, version }) {
|
|
|
3236
3304
|
}, "OpenCode update sent");
|
|
3237
3305
|
}
|
|
3238
3306
|
function errorCorrelation(event, sessionState) {
|
|
3239
|
-
if (typeof event?.id === "string" && event.id) return
|
|
3240
|
-
if (sessionState?.lastAssistant?.id) return
|
|
3307
|
+
if (typeof event?.id === "string" && event.id) return formatText(event.id);
|
|
3308
|
+
if (sessionState?.lastAssistant?.id) return formatText(sessionState.lastAssistant.id);
|
|
3241
3309
|
const details = safeErrorDetails(event?.properties?.error);
|
|
3242
3310
|
return stableHash(`${details.name}:${details.statusCode}:${details.message}`);
|
|
3243
3311
|
}
|
|
@@ -3277,7 +3345,7 @@ function createLifecycleController({
|
|
|
3277
3345
|
lastAssistant: void 0,
|
|
3278
3346
|
completedMessageID: ""
|
|
3279
3347
|
};
|
|
3280
|
-
|
|
3348
|
+
remember(sessions, sessionID, state);
|
|
3281
3349
|
}
|
|
3282
3350
|
return state;
|
|
3283
3351
|
}
|
|
@@ -3312,7 +3380,7 @@ function createLifecycleController({
|
|
|
3312
3380
|
client,
|
|
3313
3381
|
directory,
|
|
3314
3382
|
sessionID,
|
|
3315
|
-
messageID:
|
|
3383
|
+
messageID: formatText(assistant.id),
|
|
3316
3384
|
durationMs,
|
|
3317
3385
|
conversation
|
|
3318
3386
|
}),
|
|
@@ -3359,7 +3427,7 @@ function createLifecycleController({
|
|
|
3359
3427
|
client,
|
|
3360
3428
|
directory,
|
|
3361
3429
|
sessionID,
|
|
3362
|
-
correlation:
|
|
3430
|
+
correlation: formatText(correlation),
|
|
3363
3431
|
attempt: status.attempt,
|
|
3364
3432
|
next: status.next,
|
|
3365
3433
|
conversation
|
|
@@ -3412,7 +3480,7 @@ function createLifecycleController({
|
|
|
3412
3480
|
const version = event.properties?.version;
|
|
3413
3481
|
if (typeof version !== "string" || !version.trim()) return;
|
|
3414
3482
|
if (publishedUpdates.has(version)) return;
|
|
3415
|
-
|
|
3483
|
+
remember(publishedUpdates, version, true);
|
|
3416
3484
|
dispatch(
|
|
3417
3485
|
"OpenCode update could not be sent",
|
|
3418
3486
|
sendUpdate({ config, reporter, version: version.trim() })
|
|
@@ -3428,7 +3496,7 @@ function createLifecycleController({
|
|
|
3428
3496
|
|
|
3429
3497
|
// src/live-bridge.js
|
|
3430
3498
|
import { createServer } from "node:http";
|
|
3431
|
-
import { randomUUID as randomUUID3, timingSafeEqual } from "node:crypto";
|
|
3499
|
+
import { randomUUID as randomUUID3, timingSafeEqual, createHash as createHash2 } from "node:crypto";
|
|
3432
3500
|
import { resolve as resolve2 } from "node:path";
|
|
3433
3501
|
|
|
3434
3502
|
// src/permission-command.js
|
|
@@ -3622,7 +3690,7 @@ async function openCommandStream(config, state, signal2, fetchImplementation) {
|
|
|
3622
3690
|
signal: signal2
|
|
3623
3691
|
});
|
|
3624
3692
|
if (!response.ok) {
|
|
3625
|
-
const responseBody =
|
|
3693
|
+
const responseBody = formatText(await response.text());
|
|
3626
3694
|
throw new HttpError(
|
|
3627
3695
|
`Backeer command stream failed with status ${response.status}`,
|
|
3628
3696
|
response.status,
|
|
@@ -3654,7 +3722,7 @@ async function consumeConnection({
|
|
|
3654
3722
|
}
|
|
3655
3723
|
}
|
|
3656
3724
|
function streamErrorState(error, lastLoggedError, reconnectDelay, reporter) {
|
|
3657
|
-
const message = error instanceof HttpError ? `${error.message}: ${error.responseBody}` :
|
|
3725
|
+
const message = error instanceof HttpError ? `${error.message}: ${error.responseBody}` : formatText(error);
|
|
3658
3726
|
if (message !== lastLoggedError) {
|
|
3659
3727
|
const authenticationFailure = error instanceof HttpError && [401, 403].includes(error.status);
|
|
3660
3728
|
reporter.error(
|
|
@@ -3664,7 +3732,7 @@ function streamErrorState(error, lastLoggedError, reconnectDelay, reporter) {
|
|
|
3664
3732
|
}
|
|
3665
3733
|
return {
|
|
3666
3734
|
lastLoggedError: message,
|
|
3667
|
-
reconnectDelay: error instanceof HttpError && [401, 403, 404].includes(error.status) ?
|
|
3735
|
+
reconnectDelay: error instanceof HttpError && [401, 403, 404].includes(error.status) ? 3e3 : reconnectDelay
|
|
3668
3736
|
};
|
|
3669
3737
|
}
|
|
3670
3738
|
async function runCommandStream(config, reporter, {
|
|
@@ -3677,7 +3745,7 @@ async function runCommandStream(config, reporter, {
|
|
|
3677
3745
|
stableConnectionMs = 5e3
|
|
3678
3746
|
}) {
|
|
3679
3747
|
const state = { lastEventID: "", framesReceived: 0 };
|
|
3680
|
-
let reconnectDelay =
|
|
3748
|
+
let reconnectDelay = 3e3;
|
|
3681
3749
|
let lastLoggedError = "";
|
|
3682
3750
|
while (!signal2.aborted && hasPending()) {
|
|
3683
3751
|
try {
|
|
@@ -3696,7 +3764,7 @@ async function runCommandStream(config, reporter, {
|
|
|
3696
3764
|
now,
|
|
3697
3765
|
stableConnectionMs,
|
|
3698
3766
|
onStable: () => {
|
|
3699
|
-
reconnectDelay =
|
|
3767
|
+
reconnectDelay = 3e3;
|
|
3700
3768
|
lastLoggedError = "";
|
|
3701
3769
|
}
|
|
3702
3770
|
});
|
|
@@ -3708,7 +3776,7 @@ async function runCommandStream(config, reporter, {
|
|
|
3708
3776
|
}
|
|
3709
3777
|
if (!signal2.aborted && hasPending()) {
|
|
3710
3778
|
await sleepImplementation(reconnectDelay, signal2);
|
|
3711
|
-
reconnectDelay =
|
|
3779
|
+
reconnectDelay = 3e3;
|
|
3712
3780
|
}
|
|
3713
3781
|
}
|
|
3714
3782
|
}
|
|
@@ -3796,7 +3864,7 @@ async function replyToQuestion(client, pending, response) {
|
|
|
3796
3864
|
}
|
|
3797
3865
|
async function postQuestionState(config, eventID, path, body = {}) {
|
|
3798
3866
|
const url = new URL(`/v1/events/${encodeURIComponent(eventID)}/${path}`, config.origin);
|
|
3799
|
-
await postJsonWithRetry(config, url, body
|
|
3867
|
+
await postJsonWithRetry(config, url, body);
|
|
3800
3868
|
}
|
|
3801
3869
|
function createPermissionController({
|
|
3802
3870
|
config,
|
|
@@ -3852,7 +3920,7 @@ function createPermissionController({
|
|
|
3852
3920
|
async function handleCommand(command) {
|
|
3853
3921
|
if (!command || typeof command !== "object") return;
|
|
3854
3922
|
if (!["opencode_permission_reply", "opencode_question_reply"].includes(command.type)) return;
|
|
3855
|
-
if (typeof command.id !== "string"
|
|
3923
|
+
if (typeof command.id !== "string") return;
|
|
3856
3924
|
const previousAcknowledgement = completedCommands.get(command.id);
|
|
3857
3925
|
if (previousAcknowledgement) {
|
|
3858
3926
|
await acknowledgeCommand(config, command.id, previousAcknowledgement).catch((error) => reporter.warning("Decision receipt retry failed", error));
|
|
@@ -3902,7 +3970,7 @@ function createPermissionController({
|
|
|
3902
3970
|
externalId: command.externalId,
|
|
3903
3971
|
processedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3904
3972
|
};
|
|
3905
|
-
|
|
3973
|
+
remember(completedCommands, command.id, acknowledgement);
|
|
3906
3974
|
if (isQuestion) {
|
|
3907
3975
|
reportQuestionAcknowledgement(acknowledgement, pending, reporter, conversation);
|
|
3908
3976
|
} else {
|
|
@@ -4009,8 +4077,8 @@ async function commonDirectory(directory) {
|
|
|
4009
4077
|
return realpath(stdout.trim());
|
|
4010
4078
|
}
|
|
4011
4079
|
function approvedWorktreeSessions(value) {
|
|
4012
|
-
if (!Array.isArray(value)
|
|
4013
|
-
return value.filter((item) => /^[A-Za-z0-9_-]
|
|
4080
|
+
if (!Array.isArray(value)) return [];
|
|
4081
|
+
return value.filter((item) => /^[A-Za-z0-9_-]+$/.test(item?.sessionID) && item.sessionID !== "global" && typeof item.directory === "string" && isAbsolute2(item.directory));
|
|
4014
4082
|
}
|
|
4015
4083
|
function createSessionScope(directory, { resolveCommon = commonDirectory, approvedSessions = [] } = {}) {
|
|
4016
4084
|
const observed = /* @__PURE__ */ new Map();
|
|
@@ -4021,7 +4089,6 @@ function createSessionScope(directory, { resolveCommon = commonDirectory, approv
|
|
|
4021
4089
|
if (location === directory) {
|
|
4022
4090
|
if (info.id) {
|
|
4023
4091
|
observed.set(info.id, info.projectID);
|
|
4024
|
-
if (observed.size > 256) observed.delete(observed.keys().next().value);
|
|
4025
4092
|
}
|
|
4026
4093
|
return true;
|
|
4027
4094
|
}
|
|
@@ -4047,24 +4114,23 @@ function createV2LiveClient(ctx) {
|
|
|
4047
4114
|
if (disposed) return info;
|
|
4048
4115
|
await ownsSession(info);
|
|
4049
4116
|
known.set(id, info);
|
|
4050
|
-
if (known.size > 100) known.delete(known.keys().next().value);
|
|
4051
4117
|
return info;
|
|
4052
4118
|
}
|
|
4053
4119
|
return {
|
|
4054
4120
|
async list() {
|
|
4055
4121
|
const refreshed = [];
|
|
4056
4122
|
if (typeof ctx.session.list === "function") {
|
|
4057
|
-
const result = await ctx.session.list({ directory: ctx.location.directory,
|
|
4123
|
+
const result = await ctx.session.list({ directory: ctx.location.directory, order: "desc" });
|
|
4058
4124
|
const values = Array.isArray(result) ? result : result?.data;
|
|
4059
4125
|
if (!Array.isArray(values)) throw new Error("Invalid native session list");
|
|
4060
|
-
for (const info of values
|
|
4126
|
+
for (const info of values) {
|
|
4061
4127
|
if (await ownsSession(info)) {
|
|
4062
4128
|
refreshed.push(info);
|
|
4063
4129
|
known.set(info.id, info);
|
|
4064
4130
|
}
|
|
4065
4131
|
}
|
|
4066
4132
|
}
|
|
4067
|
-
for (const id of [...known.keys()]
|
|
4133
|
+
for (const id of [...known.keys()]) {
|
|
4068
4134
|
if (refreshed.some((info) => info.id === id)) continue;
|
|
4069
4135
|
try {
|
|
4070
4136
|
refreshed.push(await get(id));
|
|
@@ -4166,100 +4232,13 @@ function createLiveConnectionNotifier(config, reporter) {
|
|
|
4166
4232
|
};
|
|
4167
4233
|
}
|
|
4168
4234
|
|
|
4169
|
-
// src/live-bridge-events.js
|
|
4170
|
-
var ID = /^[A-Za-z0-9_-]{1,160}$/;
|
|
4171
|
-
var validBridgeID = (value) => typeof value === "string" && ID.test(value);
|
|
4172
|
-
function createBridgeSanitizer(secrets = []) {
|
|
4173
|
-
const known = secrets.filter((value) => typeof value === "string" && value.length > 0);
|
|
4174
|
-
const assistants = /* @__PURE__ */ new Map();
|
|
4175
|
-
function clipped(value, max = 12e3) {
|
|
4176
|
-
if (typeof value !== "string") return { value: "", truncated: false };
|
|
4177
|
-
let result = value.slice(0, 64e3);
|
|
4178
|
-
for (const secret of known) result = result.split(secret).join("[REDACTED]");
|
|
4179
|
-
result = result.replace(/-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?(?:-----END [^-]*PRIVATE KEY-----|$)/g, "[REDACTED PRIVATE KEY]").replace(/\bBearer\s+[^\s"'<>]+/gi, "Bearer [REDACTED]").replace(/\b(?:sk|bgi)_[A-Za-z0-9_-]{12,}|\bsk-[A-Za-z0-9_-]{12,}/g, "[REDACTED]").replace(/((?:api[_-]?key|access[_-]?token|secret|password|authorization)\s*[=:]\s*)[^\s,;]+/gi, "$1[REDACTED]");
|
|
4180
|
-
return { value: result.slice(0, max), truncated: value.length > 64e3 || result.length > max };
|
|
4181
|
-
}
|
|
4182
|
-
function text(value, max) {
|
|
4183
|
-
return clipped(value, max).value;
|
|
4184
|
-
}
|
|
4185
|
-
function content(key, value) {
|
|
4186
|
-
const result = clipped(value);
|
|
4187
|
-
return { [key]: result.value, ...result.truncated ? { truncated: true } : {} };
|
|
4188
|
-
}
|
|
4189
|
-
function sanitize(event) {
|
|
4190
|
-
const p = event?.properties ?? event?.data ?? {};
|
|
4191
|
-
const sessionID = p.sessionID ?? p.info?.sessionID ?? p.part?.sessionID;
|
|
4192
|
-
if (!validBridgeID(sessionID) || sessionID === "global") return void 0;
|
|
4193
|
-
const base = { type: event.type, sessionID, data: {} };
|
|
4194
|
-
if (event.type === "message.updated") {
|
|
4195
|
-
const info = p.info;
|
|
4196
|
-
if (!validBridgeID(info?.id)) return void 0;
|
|
4197
|
-
if (info.role === "assistant") {
|
|
4198
|
-
assistants.set(info.id, sessionID);
|
|
4199
|
-
if (assistants.size > 256) assistants.delete(assistants.keys().next().value);
|
|
4200
|
-
}
|
|
4201
|
-
return info.role === "assistant" ? { ...base, data: {
|
|
4202
|
-
messageID: info.id,
|
|
4203
|
-
completed: !!info.time?.completed,
|
|
4204
|
-
failed: !!info.error
|
|
4205
|
-
} } : void 0;
|
|
4206
|
-
}
|
|
4207
|
-
if (event.type === "message.part.updated") {
|
|
4208
|
-
const part = p.part;
|
|
4209
|
-
if (assistants.get(part?.messageID) !== sessionID) return void 0;
|
|
4210
|
-
if (part.type === "text") return { ...base, type: "assistant.text", data: {
|
|
4211
|
-
messageID: part.messageID,
|
|
4212
|
-
...content("text", part.text),
|
|
4213
|
-
replace: true
|
|
4214
|
-
} };
|
|
4215
|
-
if (part.type !== "tool") return void 0;
|
|
4216
|
-
const state = part.state ?? {};
|
|
4217
|
-
return { ...base, type: "tool.status", data: {
|
|
4218
|
-
tool: text(part.tool, 80),
|
|
4219
|
-
status: text(state.status, 40),
|
|
4220
|
-
...state.status === "completed" ? content("output", state.output) : {},
|
|
4221
|
-
...state.status === "error" ? { failed: true } : {}
|
|
4222
|
-
} };
|
|
4223
|
-
}
|
|
4224
|
-
if (event.type === "session.text.ended") return { ...base, type: "assistant.text", data: {
|
|
4225
|
-
messageID: validBridgeID(p.assistantMessageID) ? p.assistantMessageID : void 0,
|
|
4226
|
-
...content("text", p.text),
|
|
4227
|
-
replace: true
|
|
4228
|
-
} };
|
|
4229
|
-
if (["session.tool.called", "session.tool.success", "session.tool.failed"].includes(event.type)) {
|
|
4230
|
-
return { ...base, type: "tool.status", data: {
|
|
4231
|
-
status: event.type.split(".").at(-1),
|
|
4232
|
-
...event.type === "session.tool.success" ? content(
|
|
4233
|
-
"output",
|
|
4234
|
-
(p.content ?? []).filter((item) => item?.type === "text").map((item) => item.text).join("\n")
|
|
4235
|
-
) : {}
|
|
4236
|
-
} };
|
|
4237
|
-
}
|
|
4238
|
-
if (["permission.asked", "question.asked", "question.v2.asked"].includes(event.type)) {
|
|
4239
|
-
return { ...base, type: "approval.waiting", data: {
|
|
4240
|
-
kind: event.type.split(".")[0],
|
|
4241
|
-
...event.type !== "permission.asked" && validBridgeID(p.id ?? p.requestID) ? { questionId: p.id ?? p.requestID } : {}
|
|
4242
|
-
} };
|
|
4243
|
-
}
|
|
4244
|
-
if (["permission.replied", "question.replied", "question.v2.replied", "question.rejected", "question.v2.rejected"].includes(event.type)) {
|
|
4245
|
-
return { ...base, type: "approval.settled", data: { kind: event.type.split(".")[0] } };
|
|
4246
|
-
}
|
|
4247
|
-
if (event.type === "session.status") return { ...base, data: {
|
|
4248
|
-
status: ["idle", "busy", "retry"].includes(p.status?.type) ? p.status.type : "unknown"
|
|
4249
|
-
} };
|
|
4250
|
-
if (["session.idle", "session.error", "session.deleted"].includes(event.type)) return base;
|
|
4251
|
-
return void 0;
|
|
4252
|
-
}
|
|
4253
|
-
return { sanitize, text, clear: () => assistants.clear() };
|
|
4254
|
-
}
|
|
4255
|
-
|
|
4256
4235
|
// src/live-questions.js
|
|
4257
4236
|
import { createHash } from "node:crypto";
|
|
4258
4237
|
var invalid = (code = "invalid_question_answer", status = 422) => Object.assign(new Error(code), { code, status });
|
|
4259
4238
|
function questionSnapshot(pending, text) {
|
|
4260
|
-
if (!validBridgeID(pending?.id) || !validBridgeID(pending?.sessionID) || pending.sessionID === "global" || !Array.isArray(pending.questions) || !pending.questions.length
|
|
4239
|
+
if (!validBridgeID(pending?.id) || !validBridgeID(pending?.sessionID) || pending.sessionID === "global" || !Array.isArray(pending.questions) || !pending.questions.length) throw invalid("unsupported_question");
|
|
4261
4240
|
const questions = pending.questions.map((question, index) => {
|
|
4262
|
-
if (typeof question.question !== "string" || !Array.isArray(question.options) || question.options.
|
|
4241
|
+
if (typeof question.question !== "string" || !Array.isArray(question.options) || question.options.some((option) => typeof option?.label !== "string") || new Set(question.options.map((option) => option.label)).size !== question.options.length) {
|
|
4263
4242
|
throw invalid("unsupported_question");
|
|
4264
4243
|
}
|
|
4265
4244
|
return {
|
|
@@ -4282,14 +4261,13 @@ function questionSnapshot(pending, text) {
|
|
|
4282
4261
|
...pending.nativeRevision ? { nativeRevision: pending.nativeRevision } : {}
|
|
4283
4262
|
})).digest("hex");
|
|
4284
4263
|
const result = { questionId: pending.id, sessionID: pending.sessionID, revision, questions };
|
|
4285
|
-
if (Buffer.byteLength(JSON.stringify(result)) > 3e4) throw invalid("question_too_large");
|
|
4286
4264
|
return result;
|
|
4287
4265
|
}
|
|
4288
4266
|
function resolveQuestionAnswers(pending, answers) {
|
|
4289
4267
|
if (!Array.isArray(answers) || answers.length !== pending.questions.length) throw invalid();
|
|
4290
4268
|
const values = answers.map((answer, index) => {
|
|
4291
4269
|
const question = pending.questions[index];
|
|
4292
|
-
if (!answer || !Array.isArray(answer.choices) ||
|
|
4270
|
+
if (!answer || !Array.isArray(answer.choices) || Object.keys(answer).some((key) => !["choices", "text"].includes(key)) || new Set(answer.choices).size !== answer.choices.length || answer.choices.some((choice) => !Number.isInteger(choice) || choice < 0 || choice >= question.options.length) || answer.text !== void 0 && (typeof answer.text !== "string" || !answer.text.trim())) throw invalid();
|
|
4293
4271
|
if (answer.text !== void 0 && question.custom !== true) throw invalid("custom_not_allowed");
|
|
4294
4272
|
return [
|
|
4295
4273
|
...answer.choices.map((choice) => question.options[choice].label),
|
|
@@ -4302,7 +4280,6 @@ function resolveQuestionAnswers(pending, answers) {
|
|
|
4302
4280
|
}
|
|
4303
4281
|
|
|
4304
4282
|
// src/live-bridge.js
|
|
4305
|
-
var LIMITS = { events: 256, bytes: 2e6, clients: 8, commands: 1024, commandTTL: 864e5, body: 32768, requests: 16 };
|
|
4306
4283
|
function failure(status, code) {
|
|
4307
4284
|
return Object.assign(new Error(code), { status, code });
|
|
4308
4285
|
}
|
|
@@ -4314,10 +4291,7 @@ function json(response, status, value) {
|
|
|
4314
4291
|
}
|
|
4315
4292
|
async function readBody(request) {
|
|
4316
4293
|
const chunks = [];
|
|
4317
|
-
let bytes = 0;
|
|
4318
4294
|
for await (const chunk of request) {
|
|
4319
|
-
bytes += chunk.length;
|
|
4320
|
-
if (bytes > LIMITS.body) throw failure(413, "body_too_large");
|
|
4321
4295
|
chunks.push(chunk);
|
|
4322
4296
|
}
|
|
4323
4297
|
try {
|
|
@@ -4342,45 +4316,44 @@ async function createLiveBridge({ config, client, directory, reporter }) {
|
|
|
4342
4316
|
const sanitizer = createBridgeSanitizer([token, config.token]);
|
|
4343
4317
|
const epoch = randomUUID3();
|
|
4344
4318
|
const events = [], streams = /* @__PURE__ */ new Set(), commands = /* @__PURE__ */ new Map(), states = /* @__PURE__ */ new Map();
|
|
4345
|
-
let sequence = 0,
|
|
4346
|
-
|
|
4347
|
-
const lookupWaiters = /* @__PURE__ */ new Set();
|
|
4319
|
+
let sequence = 0, disposed = false, disposeTask;
|
|
4320
|
+
const lookupWaiters = /* @__PURE__ */ new Set(), lookups = /* @__PURE__ */ new Map();
|
|
4348
4321
|
const cursor = () => `${epoch}:${sequence}`;
|
|
4349
|
-
function lookup(id) {
|
|
4322
|
+
function lookup(id, signal2) {
|
|
4350
4323
|
if (disposed) return Promise.reject(failure(503, "bridge_closed"));
|
|
4351
|
-
|
|
4352
|
-
|
|
4324
|
+
let entry = lookups.get(id);
|
|
4325
|
+
if (!entry) {
|
|
4326
|
+
entry = { waiters: /* @__PURE__ */ new Set() };
|
|
4327
|
+
lookups.set(id, entry);
|
|
4328
|
+
const settled = (ok, value) => {
|
|
4329
|
+
lookups.delete(id);
|
|
4330
|
+
for (const finish of [...entry.waiters]) finish(ok, value);
|
|
4331
|
+
};
|
|
4332
|
+
Promise.resolve().then(() => api.get(id)).then((value) => settled(true, value), (error) => settled(false, error));
|
|
4333
|
+
}
|
|
4353
4334
|
return new Promise((resolve3, reject) => {
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
if (finished) return;
|
|
4357
|
-
finished = true;
|
|
4358
|
-
clearTimeout(timer);
|
|
4335
|
+
const finish = (ok, value) => {
|
|
4336
|
+
if (!entry.waiters.delete(finish)) return;
|
|
4359
4337
|
lookupWaiters.delete(cancel);
|
|
4360
|
-
|
|
4338
|
+
signal2?.removeEventListener("abort", aborted);
|
|
4339
|
+
if (ok) resolve3(value);
|
|
4340
|
+
else reject(value);
|
|
4361
4341
|
};
|
|
4362
|
-
const cancel = () => finish(
|
|
4363
|
-
const
|
|
4342
|
+
const cancel = () => finish(false, failure(503, "bridge_closed"));
|
|
4343
|
+
const aborted = () => finish(false, failure(499, "request_cancelled"));
|
|
4344
|
+
entry.waiters.add(finish);
|
|
4364
4345
|
lookupWaiters.add(cancel);
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
lookups--;
|
|
4368
|
-
finish(resolve3, value);
|
|
4369
|
-
},
|
|
4370
|
-
(error) => {
|
|
4371
|
-
lookups--;
|
|
4372
|
-
finish(reject, error);
|
|
4373
|
-
}
|
|
4374
|
-
);
|
|
4346
|
+
signal2?.addEventListener("abort", aborted, { once: true });
|
|
4347
|
+
if (signal2?.aborted) aborted();
|
|
4375
4348
|
});
|
|
4376
4349
|
}
|
|
4377
|
-
async function session(id) {
|
|
4350
|
+
async function session(id, signal2) {
|
|
4378
4351
|
if (!validBridgeID(id) || id === "global") throw failure(400, "invalid_session");
|
|
4379
4352
|
let info;
|
|
4380
4353
|
try {
|
|
4381
|
-
info = await lookup(id);
|
|
4354
|
+
info = await lookup(id, signal2);
|
|
4382
4355
|
} catch (error) {
|
|
4383
|
-
if (["
|
|
4356
|
+
if (["bridge_closed", "request_cancelled"].includes(error.code)) throw error;
|
|
4384
4357
|
throw failure(404, "session_not_found");
|
|
4385
4358
|
}
|
|
4386
4359
|
if (info?.id !== id || typeof info.time?.archived === "number") {
|
|
@@ -4402,18 +4375,12 @@ async function createLiveBridge({ config, client, directory, reporter }) {
|
|
|
4402
4375
|
const removed = streams.delete(stream);
|
|
4403
4376
|
clearInterval(stream.heartbeat);
|
|
4404
4377
|
stream.queue.length = 0;
|
|
4405
|
-
stream.queuedBytes = 0;
|
|
4406
4378
|
if (removed && !streams.size && !disposed) connectionNotifications.update(false);
|
|
4407
4379
|
}
|
|
4408
4380
|
function write(stream, value) {
|
|
4409
4381
|
if (!streams.has(stream)) return;
|
|
4410
4382
|
if (stream.blocked) {
|
|
4411
4383
|
stream.queue.push(value);
|
|
4412
|
-
stream.queuedBytes += Buffer.byteLength(value);
|
|
4413
|
-
if (stream.queuedBytes > LIMITS.bytes) {
|
|
4414
|
-
removeStream(stream);
|
|
4415
|
-
stream.response.destroy();
|
|
4416
|
-
}
|
|
4417
4384
|
return;
|
|
4418
4385
|
}
|
|
4419
4386
|
if (!stream.response.write(value)) {
|
|
@@ -4433,25 +4400,13 @@ async function createLiveBridge({ config, client, directory, reporter }) {
|
|
|
4433
4400
|
if (safe.type === "session.status") states.set(safe.sessionID, safe.data.status);
|
|
4434
4401
|
if (safe.type === "session.idle") states.set(safe.sessionID, "idle");
|
|
4435
4402
|
if (safe.type === "session.deleted") states.delete(safe.sessionID);
|
|
4436
|
-
if (states.size > 256) states.delete(states.keys().next().value);
|
|
4437
4403
|
const record = { id: `${epoch}:${++sequence}`, ...safe, timestamp: Date.now() };
|
|
4438
|
-
|
|
4439
|
-
event: backeer.event
|
|
4440
|
-
data: ${JSON.stringify(record)}
|
|
4441
|
-
|
|
4442
|
-
`;
|
|
4443
|
-
if (Buffer.byteLength(frame) > 32768) {
|
|
4444
|
-
record.data = { truncated: true };
|
|
4445
|
-
frame = `id: ${record.id}
|
|
4404
|
+
const frame = `id: ${record.id}
|
|
4446
4405
|
event: backeer.event
|
|
4447
4406
|
data: ${JSON.stringify(record)}
|
|
4448
4407
|
|
|
4449
4408
|
`;
|
|
4450
|
-
}
|
|
4451
|
-
const size = Buffer.byteLength(frame);
|
|
4452
|
-
events.push({ sequence, sessionID: record.sessionID, frame, size });
|
|
4453
|
-
bytes += size;
|
|
4454
|
-
while (events.length > LIMITS.events || bytes > LIMITS.bytes) bytes -= events.shift().size;
|
|
4409
|
+
events.push({ sequence, sessionID: record.sessionID, frame });
|
|
4455
4410
|
for (const stream of streams) if (!stream.sessionID || stream.sessionID === record.sessionID) write(stream, frame);
|
|
4456
4411
|
}
|
|
4457
4412
|
function authorized(request) {
|
|
@@ -4462,7 +4417,7 @@ data: ${JSON.stringify(record)}
|
|
|
4462
4417
|
}
|
|
4463
4418
|
function parseCursor(value) {
|
|
4464
4419
|
if (!value) return { after: 0 };
|
|
4465
|
-
const match = /^([a-f0-9-]{36}):(\d
|
|
4420
|
+
const match = /^([a-f0-9-]{36}):(\d+)$/.exec(value);
|
|
4466
4421
|
if (!match) throw failure(400, "invalid_cursor");
|
|
4467
4422
|
if (match[1] !== epoch) return { gap: "stream_restarted" };
|
|
4468
4423
|
const after = Number(match[2]);
|
|
@@ -4471,9 +4426,6 @@ data: ${JSON.stringify(record)}
|
|
|
4471
4426
|
return { after };
|
|
4472
4427
|
}
|
|
4473
4428
|
function cachedCommand(requestId, sessionID, signature) {
|
|
4474
|
-
for (const [id, command] of commands) {
|
|
4475
|
-
if (command.settled && Date.now() - command.created >= LIMITS.commandTTL) commands.delete(id);
|
|
4476
|
-
}
|
|
4477
4429
|
const existing = commands.get(requestId);
|
|
4478
4430
|
if (existing && (existing.sessionID !== sessionID || existing.signature !== signature)) {
|
|
4479
4431
|
throw failure(409, "request_id_conflict");
|
|
@@ -4483,7 +4435,6 @@ data: ${JSON.stringify(record)}
|
|
|
4483
4435
|
async function execute(requestId, sessionID, signature, operation, questionId) {
|
|
4484
4436
|
let command = cachedCommand(requestId, sessionID, signature);
|
|
4485
4437
|
if (!command) {
|
|
4486
|
-
if (commands.size >= LIMITS.commands) throw failure(429, "command_window_full");
|
|
4487
4438
|
if (questionId && [...commands.values()].some((item) => item.sessionID === sessionID && item.questionId === questionId)) {
|
|
4488
4439
|
throw failure(409, "question_reply_already_submitted");
|
|
4489
4440
|
}
|
|
@@ -4491,8 +4442,6 @@ data: ${JSON.stringify(record)}
|
|
|
4491
4442
|
sessionID,
|
|
4492
4443
|
signature,
|
|
4493
4444
|
questionId,
|
|
4494
|
-
created: Date.now(),
|
|
4495
|
-
settled: false,
|
|
4496
4445
|
result: {
|
|
4497
4446
|
requestId,
|
|
4498
4447
|
sessionID,
|
|
@@ -4501,9 +4450,7 @@ data: ${JSON.stringify(record)}
|
|
|
4501
4450
|
}
|
|
4502
4451
|
};
|
|
4503
4452
|
commands.set(requestId, command);
|
|
4504
|
-
command.task = Promise.resolve().then(operation).
|
|
4505
|
-
command.settled = true;
|
|
4506
|
-
});
|
|
4453
|
+
command.task = Promise.resolve().then(operation).then(() => void 0);
|
|
4507
4454
|
}
|
|
4508
4455
|
await command.task;
|
|
4509
4456
|
return command.result;
|
|
@@ -4513,7 +4460,6 @@ data: ${JSON.stringify(record)}
|
|
|
4513
4460
|
const pending = await api.questions(sessionID);
|
|
4514
4461
|
if (!Array.isArray(pending)) throw failure(502, "invalid_question_list");
|
|
4515
4462
|
const own = pending.filter((item) => item.sessionID === sessionID);
|
|
4516
|
-
if (own.length > 16) throw failure(413, "too_many_questions");
|
|
4517
4463
|
return own;
|
|
4518
4464
|
}
|
|
4519
4465
|
async function route(request, response) {
|
|
@@ -4521,10 +4467,9 @@ data: ${JSON.stringify(record)}
|
|
|
4521
4467
|
if (request.headers["x-backeer-project"] !== void 0 && request.headers["x-backeer-project"] !== encodeURIComponent(directory)) {
|
|
4522
4468
|
return json(response, 409, { error: "project_mismatch" });
|
|
4523
4469
|
}
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
timeout.unref();
|
|
4470
|
+
const controller = new AbortController();
|
|
4471
|
+
const closed = () => controller.abort();
|
|
4472
|
+
response.once("close", closed);
|
|
4528
4473
|
try {
|
|
4529
4474
|
const url = new URL(request.url, "http://127.0.0.1");
|
|
4530
4475
|
if (url.username || url.password) throw failure(400, "invalid_url");
|
|
@@ -4533,7 +4478,7 @@ data: ${JSON.stringify(record)}
|
|
|
4533
4478
|
const values = Array.isArray(result2) ? result2 : result2?.data;
|
|
4534
4479
|
if (!Array.isArray(values)) throw failure(502, "invalid_session_list");
|
|
4535
4480
|
const own = [];
|
|
4536
|
-
for (const info of values
|
|
4481
|
+
for (const info of values) {
|
|
4537
4482
|
if (!validBridgeID(info?.id) || typeof info.time?.archived === "number") continue;
|
|
4538
4483
|
if (api.ownsSession ? await api.ownsSession(info) : (info.location?.directory ?? info.directory) === directory) own.push(info);
|
|
4539
4484
|
}
|
|
@@ -4562,11 +4507,9 @@ data: ${JSON.stringify(record)}
|
|
|
4562
4507
|
}
|
|
4563
4508
|
if (request.method === "GET" && url.pathname === "/v1/events") {
|
|
4564
4509
|
const selected = url.searchParams.get("sessionID") || void 0;
|
|
4565
|
-
if (selected) await session(selected);
|
|
4510
|
+
if (selected) await session(selected, controller.signal);
|
|
4566
4511
|
const parsed = parseCursor(url.searchParams.get("after") ?? request.headers["last-event-id"]);
|
|
4567
4512
|
if (response.headersSent) return;
|
|
4568
|
-
if (streams.size >= LIMITS.clients) throw failure(429, "too_many_streams");
|
|
4569
|
-
clearTimeout(timeout);
|
|
4570
4513
|
response.writeHead(200, {
|
|
4571
4514
|
"content-type": "text/event-stream",
|
|
4572
4515
|
"cache-control": "no-cache, no-transform",
|
|
@@ -4585,8 +4528,7 @@ data: ${JSON.stringify({ reason: parsed.gap, cursor: cursor() })}
|
|
|
4585
4528
|
sessionID: selected,
|
|
4586
4529
|
heartbeat: void 0,
|
|
4587
4530
|
blocked: false,
|
|
4588
|
-
queue: []
|
|
4589
|
-
queuedBytes: 0
|
|
4531
|
+
queue: []
|
|
4590
4532
|
};
|
|
4591
4533
|
streams.add(stream);
|
|
4592
4534
|
connectionNotifications.update(true);
|
|
@@ -4597,7 +4539,6 @@ data: ${JSON.stringify({ reason: parsed.gap, cursor: cursor() })}
|
|
|
4597
4539
|
stream.blocked = false;
|
|
4598
4540
|
while (streams.has(stream) && !stream.blocked && stream.queue.length) {
|
|
4599
4541
|
const next = stream.queue.shift();
|
|
4600
|
-
stream.queuedBytes -= Buffer.byteLength(next);
|
|
4601
4542
|
write(stream, next);
|
|
4602
4543
|
}
|
|
4603
4544
|
});
|
|
@@ -4615,13 +4556,12 @@ retry: ${LIVE_RECONNECT_MS}
|
|
|
4615
4556
|
}
|
|
4616
4557
|
return;
|
|
4617
4558
|
}
|
|
4618
|
-
const match = /^\/v1\/sessions\/([A-Za-z0-9_-]
|
|
4559
|
+
const match = /^\/v1\/sessions\/([A-Za-z0-9_-]+)(\/instructions|\/questions(?:\/([A-Za-z0-9_-]+)\/answers)?)?$/.exec(url.pathname);
|
|
4619
4560
|
if (!match) throw failure(404, "route_not_found");
|
|
4620
|
-
if (request.method === "GET" && !match[2]) return json(response, 200, await session(match[1]));
|
|
4561
|
+
if (request.method === "GET" && !match[2]) return json(response, 200, await session(match[1], controller.signal));
|
|
4621
4562
|
if (request.method === "GET" && match[2] === "/questions") {
|
|
4622
|
-
await session(match[1]);
|
|
4563
|
+
await session(match[1], controller.signal);
|
|
4623
4564
|
const result2 = { questions: (await questions(match[1])).map((item) => questionSnapshot(item, sanitizer.text)) };
|
|
4624
|
-
if (Buffer.byteLength(JSON.stringify(result2)) > 24e4) throw failure(413, "questions_too_large");
|
|
4625
4565
|
return json(response, 200, result2);
|
|
4626
4566
|
}
|
|
4627
4567
|
if (request.method !== "POST" || !match[2]) throw failure(405, "method_not_allowed");
|
|
@@ -4629,8 +4569,8 @@ retry: ${LIVE_RECONNECT_MS}
|
|
|
4629
4569
|
const body = await readBody(request);
|
|
4630
4570
|
if (match[3]) {
|
|
4631
4571
|
if (!body || !validBridgeID(body.requestId) || !/^[a-f0-9]{64}$/.test(body.revision) || Object.keys(body).some((key) => !["requestId", "revision", "answers"].includes(key))) throw failure(400, "invalid_question_answer");
|
|
4632
|
-
await session(match[1]);
|
|
4633
|
-
const signature2 = JSON.stringify({ questionId: match[3], revision: body.revision, answers: body.answers });
|
|
4572
|
+
await session(match[1], controller.signal);
|
|
4573
|
+
const signature2 = createHash2("sha256").update(JSON.stringify({ questionId: match[3], revision: body.revision, answers: body.answers })).digest("hex");
|
|
4634
4574
|
const existing2 = cachedCommand(body.requestId, match[1], signature2);
|
|
4635
4575
|
if (existing2) {
|
|
4636
4576
|
await existing2.task;
|
|
@@ -4651,26 +4591,25 @@ retry: ${LIVE_RECONNECT_MS}
|
|
|
4651
4591
|
));
|
|
4652
4592
|
}
|
|
4653
4593
|
if (match[2] !== "/instructions") throw failure(405, "method_not_allowed");
|
|
4654
|
-
if (!body || !validBridgeID(body.requestId) || typeof body.prompt !== "string" || !body.prompt.trim() ||
|
|
4655
|
-
await session(match[1]);
|
|
4594
|
+
if (!body || !validBridgeID(body.requestId) || typeof body.prompt !== "string" || !body.prompt.trim() || Object.keys(body).some((key) => !["requestId", "prompt"].includes(key))) throw failure(400, "invalid_instruction");
|
|
4595
|
+
await session(match[1], controller.signal);
|
|
4656
4596
|
if (response.headersSent || disposed) return;
|
|
4657
|
-
const signature = JSON.stringify({ prompt: body.prompt });
|
|
4597
|
+
const signature = createHash2("sha256").update(JSON.stringify({ prompt: body.prompt })).digest("hex");
|
|
4658
4598
|
const existing = cachedCommand(body.requestId, match[1], signature);
|
|
4659
4599
|
const result = await execute(body.requestId, match[1], signature, () => api.prompt(match[1], body.prompt));
|
|
4660
4600
|
return json(response, existing ? 200 : 202, result);
|
|
4661
4601
|
} catch (error) {
|
|
4662
4602
|
json(response, error.status ?? 502, { error: error.code ?? "opencode_request_failed" });
|
|
4663
4603
|
} finally {
|
|
4664
|
-
|
|
4665
|
-
clearTimeout(timeout);
|
|
4604
|
+
response.removeListener("close", closed);
|
|
4666
4605
|
}
|
|
4667
4606
|
}
|
|
4668
4607
|
const server = createServer((request, response) => {
|
|
4669
4608
|
void route(request, response);
|
|
4670
4609
|
});
|
|
4671
|
-
server.requestTimeout =
|
|
4672
|
-
server.headersTimeout =
|
|
4673
|
-
server.keepAliveTimeout =
|
|
4610
|
+
server.requestTimeout = 0;
|
|
4611
|
+
server.headersTimeout = 0;
|
|
4612
|
+
server.keepAliveTimeout = 0;
|
|
4674
4613
|
await new Promise((resolve3, reject) => {
|
|
4675
4614
|
server.once("error", reject);
|
|
4676
4615
|
server.listen(options.port ?? 43871, "127.0.0.1", () => {
|
|
@@ -4698,6 +4637,7 @@ retry: ${LIVE_RECONNECT_MS}
|
|
|
4698
4637
|
events.length = 0;
|
|
4699
4638
|
states.clear();
|
|
4700
4639
|
commands.clear();
|
|
4640
|
+
lookups.clear();
|
|
4701
4641
|
sanitizer.clear();
|
|
4702
4642
|
api.dispose?.();
|
|
4703
4643
|
});
|
|
@@ -4719,6 +4659,8 @@ async function BackeerPlugin({ client, directory, feedback, v2 = false }, option
|
|
|
4719
4659
|
bootstrapReporter.error("Plugin disabled \u2014 invalid configuration", error);
|
|
4720
4660
|
return {};
|
|
4721
4661
|
}
|
|
4662
|
+
const shutdown = new AbortController();
|
|
4663
|
+
config.signal = shutdown.signal;
|
|
4722
4664
|
const reporter = createOpenCodeReporter(client, config.logLevel);
|
|
4723
4665
|
const conversation = feedback ?? createConversationFeedback({ client, reporter });
|
|
4724
4666
|
if (config.liveBridge.error) reporter.error("Live control disabled", config.liveBridge.error);
|
|
@@ -4878,6 +4820,7 @@ async function BackeerPlugin({ client, directory, feedback, v2 = false }, option
|
|
|
4878
4820
|
await Promise.all([handleNotificationEvent(event), liveBridge.handle(event)]);
|
|
4879
4821
|
},
|
|
4880
4822
|
dispose: async () => {
|
|
4823
|
+
shutdown.abort();
|
|
4881
4824
|
await liveBridge.dispose();
|
|
4882
4825
|
await permissionController.dispose();
|
|
4883
4826
|
lifecycleController.dispose();
|
|
@@ -5461,7 +5404,7 @@ async function getFormApi(ctx, sessionID, { makeClient = exports_client.make, ow
|
|
|
5461
5404
|
}
|
|
5462
5405
|
|
|
5463
5406
|
// src/opencode-v2-client.js
|
|
5464
|
-
import { createHash as
|
|
5407
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
5465
5408
|
async function nativeReply(operation) {
|
|
5466
5409
|
try {
|
|
5467
5410
|
return await operation();
|
|
@@ -5487,7 +5430,7 @@ function questionProperties(form) {
|
|
|
5487
5430
|
return {
|
|
5488
5431
|
id: form.id,
|
|
5489
5432
|
sessionID: form.sessionID,
|
|
5490
|
-
nativeRevision:
|
|
5433
|
+
nativeRevision: createHash3("sha256").update(JSON.stringify({
|
|
5491
5434
|
id: form.id,
|
|
5492
5435
|
sessionID: form.sessionID,
|
|
5493
5436
|
title: form.title,
|