backeer-opencode 0.8.1 → 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 +25 -0
- package/dist/backeer-permission.js +263 -303
- package/docs/live-control.md +51 -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 +10 -6
- package/src/live-bridge-events.js +6 -9
- package/src/live-bridge.js +58 -77
- 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/opencode-v2-endpoint.js +8 -2
- package/src/permission-controller.js +4 -4
- package/src/plugin.js +5 -1
- 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
|
}
|
|
@@ -1967,8 +1964,19 @@ function loadLiveBridgeConfig(option, { configDirectory, ingestToken } = {}) {
|
|
|
1967
1964
|
const token = readCredential(tokenFile);
|
|
1968
1965
|
if (token === ingestToken) throw new Error("Live control requires a credential distinct from the Backeer ingest token");
|
|
1969
1966
|
const owner = values.owner ?? "server";
|
|
1970
|
-
if (!["server", "tui"].includes(owner)) throw new Error("Live control owner must be server or tui");
|
|
1971
|
-
|
|
1967
|
+
if (!["server", "standalone", "tui"].includes(owner)) throw new Error("Live control owner must be server, standalone or tui");
|
|
1968
|
+
const projectDirectory = values.projectDirectory;
|
|
1969
|
+
if (projectDirectory !== void 0 && (typeof projectDirectory !== "string" || !isAbsolute(projectDirectory))) {
|
|
1970
|
+
throw new Error("Live control projectDirectory must be an absolute path");
|
|
1971
|
+
}
|
|
1972
|
+
return {
|
|
1973
|
+
enabled: true,
|
|
1974
|
+
token,
|
|
1975
|
+
tokenFile,
|
|
1976
|
+
port,
|
|
1977
|
+
owner,
|
|
1978
|
+
...projectDirectory === void 0 ? {} : { projectDirectory }
|
|
1979
|
+
};
|
|
1972
1980
|
} catch (error) {
|
|
1973
1981
|
return { enabled: false, error: error.message };
|
|
1974
1982
|
}
|
|
@@ -2068,12 +2076,12 @@ function createDeliveryTracker() {
|
|
|
2068
2076
|
return {
|
|
2069
2077
|
begin(key) {
|
|
2070
2078
|
if (states.has(key)) return false;
|
|
2071
|
-
|
|
2079
|
+
remember(states, key, "publishing");
|
|
2072
2080
|
return true;
|
|
2073
2081
|
},
|
|
2074
2082
|
succeed(key) {
|
|
2075
2083
|
if (states.get(key) === "publishing") {
|
|
2076
|
-
|
|
2084
|
+
remember(states, key, "published");
|
|
2077
2085
|
}
|
|
2078
2086
|
},
|
|
2079
2087
|
fail(key) {
|
|
@@ -2090,7 +2098,7 @@ function createCompletionTracker() {
|
|
|
2090
2098
|
let state = states.get(key);
|
|
2091
2099
|
if (!state) {
|
|
2092
2100
|
state = { complete: false, publishing: false, revision: 0 };
|
|
2093
|
-
|
|
2101
|
+
remember(states, key, state);
|
|
2094
2102
|
}
|
|
2095
2103
|
return state;
|
|
2096
2104
|
}
|
|
@@ -2126,8 +2134,89 @@ function createCompletionTracker() {
|
|
|
2126
2134
|
};
|
|
2127
2135
|
}
|
|
2128
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
|
+
|
|
2129
2219
|
// src/http.js
|
|
2130
|
-
var RETRY_DELAYS_MS = [0, 500, 1500];
|
|
2131
2220
|
var HttpError = class extends Error {
|
|
2132
2221
|
constructor(message, status, responseBody = "") {
|
|
2133
2222
|
super(message);
|
|
@@ -2143,31 +2232,31 @@ function isAbortError(error) {
|
|
|
2143
2232
|
return error instanceof Error && error.name === "AbortError";
|
|
2144
2233
|
}
|
|
2145
2234
|
function sleep(milliseconds, signal2) {
|
|
2146
|
-
return new Promise((
|
|
2235
|
+
return new Promise((resolve3) => {
|
|
2147
2236
|
if (signal2?.aborted) {
|
|
2148
|
-
|
|
2237
|
+
resolve3();
|
|
2149
2238
|
return;
|
|
2150
2239
|
}
|
|
2151
2240
|
const timeout = setTimeout(done, milliseconds);
|
|
2152
2241
|
function done() {
|
|
2153
2242
|
clearTimeout(timeout);
|
|
2154
2243
|
signal2?.removeEventListener("abort", done);
|
|
2155
|
-
|
|
2244
|
+
resolve3();
|
|
2156
2245
|
}
|
|
2157
2246
|
signal2?.addEventListener("abort", done, { once: true });
|
|
2158
2247
|
});
|
|
2159
2248
|
}
|
|
2160
2249
|
async function postJsonWithRetry(config, url, body, options = {}) {
|
|
2161
|
-
const retryDelays = options.retryDelays
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
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");
|
|
2167
2256
|
const controller = new AbortController();
|
|
2168
2257
|
const abort = () => controller.abort();
|
|
2169
2258
|
const timeout = setTimeout(abort, options.timeoutMs ?? 5e3);
|
|
2170
|
-
|
|
2259
|
+
signal2?.addEventListener("abort", abort, { once: true });
|
|
2171
2260
|
try {
|
|
2172
2261
|
const response = await fetch(url, {
|
|
2173
2262
|
method: "POST",
|
|
@@ -2179,22 +2268,22 @@ async function postJsonWithRetry(config, url, body, options = {}) {
|
|
|
2179
2268
|
body: JSON.stringify(body),
|
|
2180
2269
|
signal: controller.signal
|
|
2181
2270
|
});
|
|
2182
|
-
const responseBody =
|
|
2271
|
+
const responseBody = await response.text();
|
|
2183
2272
|
if (response.ok) return { status: response.status, responseBody };
|
|
2184
|
-
if (!isRetryable(response.status) || attempt === retryDelays.length - 1) {
|
|
2273
|
+
if (!isRetryable(response.status) || retryDelays !== void 0 && attempt === retryDelays.length - 1) {
|
|
2185
2274
|
throw new HttpError(
|
|
2186
2275
|
`HTTP request failed with status ${response.status}`,
|
|
2187
2276
|
response.status,
|
|
2188
|
-
responseBody
|
|
2277
|
+
createBridgeSanitizer([config.token]).text(responseBody)
|
|
2189
2278
|
);
|
|
2190
2279
|
}
|
|
2191
2280
|
} catch (error) {
|
|
2192
|
-
if (
|
|
2281
|
+
if (signal2?.aborted) throw new DOMException("Aborted", "AbortError");
|
|
2193
2282
|
if (error instanceof HttpError) throw error;
|
|
2194
|
-
if (attempt === retryDelays.length - 1) throw error;
|
|
2283
|
+
if (retryDelays !== void 0 && attempt === retryDelays.length - 1) throw error;
|
|
2195
2284
|
} finally {
|
|
2196
2285
|
clearTimeout(timeout);
|
|
2197
|
-
|
|
2286
|
+
signal2?.removeEventListener("abort", abort);
|
|
2198
2287
|
}
|
|
2199
2288
|
}
|
|
2200
2289
|
throw new Error("HTTP retry loop ended unexpectedly");
|
|
@@ -2217,19 +2306,19 @@ async function publishPermission(config, reporter, request, directory, identity,
|
|
|
2217
2306
|
const permissionType = request.permission ?? request.action ?? "unknown";
|
|
2218
2307
|
const resources = Array.isArray(request.patterns) ? request.patterns.join(", ") : Array.isArray(request.resources) ? request.resources.join(", ") : "";
|
|
2219
2308
|
const payload = [
|
|
2220
|
-
{ key: "Permission", value:
|
|
2221
|
-
{ key: "Session", value:
|
|
2222
|
-
{ key: "Request ID", value:
|
|
2223
|
-
{ key: "Patterns", value:
|
|
2224
|
-
{ 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) },
|
|
2225
2314
|
{ key: "Action protocol", value: ACTION_PROTOCOL },
|
|
2226
2315
|
{ key: "Available decisions", value: "once, always, reject" }
|
|
2227
2316
|
].filter((item) => item.value.length > 0);
|
|
2228
2317
|
const result = await postJsonWithRetry(config, config.eventsEndpoint, {
|
|
2229
2318
|
type: "application",
|
|
2230
2319
|
severity: "warning",
|
|
2231
|
-
title: `OpenCode permission required: ${
|
|
2232
|
-
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)}`,
|
|
2233
2322
|
payload,
|
|
2234
2323
|
externalId: identity.externalId,
|
|
2235
2324
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2252,8 +2341,8 @@ function questionIdentity(request) {
|
|
|
2252
2341
|
function formatQuestionOptions(options) {
|
|
2253
2342
|
if (!Array.isArray(options)) return "";
|
|
2254
2343
|
return options.map((option) => {
|
|
2255
|
-
const label =
|
|
2256
|
-
const description =
|
|
2344
|
+
const label = formatText(option?.label);
|
|
2345
|
+
const description = formatText(option?.description);
|
|
2257
2346
|
if (!label) return "";
|
|
2258
2347
|
return description ? `${label} \u2014 ${description}` : label;
|
|
2259
2348
|
}).filter(Boolean).join(" | ");
|
|
@@ -2266,7 +2355,7 @@ async function resolveSessionTitle(client, directory, sessionID, reporter) {
|
|
|
2266
2355
|
query: { directory },
|
|
2267
2356
|
throwOnError: true
|
|
2268
2357
|
});
|
|
2269
|
-
return
|
|
2358
|
+
return formatText((result?.data ?? result)?.title);
|
|
2270
2359
|
} catch (error) {
|
|
2271
2360
|
reporter.debug("Session title unavailable", error);
|
|
2272
2361
|
return "";
|
|
@@ -2283,43 +2372,43 @@ async function publishQuestion(config, reporter, request, identity, client, dire
|
|
|
2283
2372
|
);
|
|
2284
2373
|
const payload = [
|
|
2285
2374
|
{ key: "OpenCode session", value: sessionTitle },
|
|
2286
|
-
{ key: "Session", value:
|
|
2287
|
-
{ key: "Request ID", value:
|
|
2375
|
+
{ key: "Session", value: formatText(identity.sessionID) },
|
|
2376
|
+
{ key: "Request ID", value: formatText(identity.requestID) },
|
|
2288
2377
|
{ key: "Action protocol", value: QUESTION_ACTION_PROTOCOL },
|
|
2289
2378
|
...questions.flatMap((question, index) => {
|
|
2290
2379
|
const suffix = questions.length > 1 ? ` ${index + 1}` : "";
|
|
2291
2380
|
return [
|
|
2292
|
-
{ key: `Question title${suffix}`, value:
|
|
2293
|
-
{ key: `Question${suffix}`, value:
|
|
2381
|
+
{ key: `Question title${suffix}`, value: formatText(question?.header) },
|
|
2382
|
+
{ key: `Question${suffix}`, value: formatText(question?.question) },
|
|
2294
2383
|
{
|
|
2295
2384
|
key: `Options${suffix}`,
|
|
2296
|
-
value:
|
|
2385
|
+
value: formatText(formatQuestionOptions(question?.options))
|
|
2297
2386
|
}
|
|
2298
2387
|
];
|
|
2299
2388
|
})
|
|
2300
2389
|
].filter((item) => item.value.length > 0);
|
|
2301
2390
|
const firstQuestion = questions[0];
|
|
2302
2391
|
const actionQuestions = questions.map((question) => ({
|
|
2303
|
-
header:
|
|
2304
|
-
question:
|
|
2305
|
-
options: Array.isArray(question?.options) ? question.options.
|
|
2306
|
-
label:
|
|
2307
|
-
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)
|
|
2308
2397
|
})).filter((option) => option.label) : [],
|
|
2309
2398
|
multiple: question?.multiple === true,
|
|
2310
2399
|
custom: question?.custom !== false
|
|
2311
2400
|
}));
|
|
2312
2401
|
const body = questions.map((question) => {
|
|
2313
|
-
const header =
|
|
2314
|
-
const text =
|
|
2315
|
-
const options =
|
|
2402
|
+
const header = formatText(question?.header);
|
|
2403
|
+
const text = formatText(question?.question);
|
|
2404
|
+
const options = formatText(formatQuestionOptions(question?.options));
|
|
2316
2405
|
return [header, text, options ? `Options: ${options}` : ""].filter(Boolean).join("\n");
|
|
2317
2406
|
}).join("\n\n");
|
|
2318
2407
|
const result = await postJsonWithRetry(config, config.eventsEndpoint, {
|
|
2319
2408
|
type: "application",
|
|
2320
2409
|
severity: "info",
|
|
2321
|
-
title: `OpenCode question: ${
|
|
2322
|
-
body:
|
|
2410
|
+
title: `OpenCode question: ${formatText(firstQuestion?.header) || "Question"}`,
|
|
2411
|
+
body: formatText(body),
|
|
2323
2412
|
payload,
|
|
2324
2413
|
externalId: identity.externalId,
|
|
2325
2414
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2348,19 +2437,19 @@ async function publishTodoCompletion(config, reporter, request, eventID, client,
|
|
|
2348
2437
|
request.sessionID,
|
|
2349
2438
|
reporter
|
|
2350
2439
|
);
|
|
2351
|
-
const correlation = typeof eventID === "string" && eventID.length > 0 ?
|
|
2440
|
+
const correlation = typeof eventID === "string" && eventID.length > 0 ? formatText(eventID) : stableHash(JSON.stringify(todos));
|
|
2352
2441
|
const externalId = `opencode-todo:${request.sessionID}:${correlation}`;
|
|
2353
|
-
const visibleTodos = todos
|
|
2442
|
+
const visibleTodos = todos;
|
|
2354
2443
|
const payload = [
|
|
2355
2444
|
{ key: "OpenCode session", value: sessionTitle },
|
|
2356
2445
|
{ key: "Completed tasks", value: String(todos.length) },
|
|
2357
2446
|
...visibleTodos.flatMap((todo, index) => [
|
|
2358
|
-
{ key: `Task ${index + 1}`, value:
|
|
2359
|
-
{ key: `Priority ${index + 1}`, value:
|
|
2447
|
+
{ key: `Task ${index + 1}`, value: formatText(todo?.content) },
|
|
2448
|
+
{ key: `Priority ${index + 1}`, value: formatText(todo?.priority) }
|
|
2360
2449
|
]),
|
|
2361
2450
|
...todos.length > visibleTodos.length ? [{ key: "Additional tasks", value: String(todos.length - visibleTodos.length) }] : []
|
|
2362
2451
|
].filter((item) => item.value.length > 0);
|
|
2363
|
-
const summary = todos.
|
|
2452
|
+
const summary = todos.map((todo) => formatText(todo?.content)).filter(Boolean).join(" \u2022 ");
|
|
2364
2453
|
const location = sessionTitle ? ` in ${sessionTitle}` : "";
|
|
2365
2454
|
const body = [
|
|
2366
2455
|
`All ${todos.length} OpenCode tasks completed${location}.`,
|
|
@@ -2370,7 +2459,7 @@ async function publishTodoCompletion(config, reporter, request, eventID, client,
|
|
|
2370
2459
|
type: "application",
|
|
2371
2460
|
severity: "info",
|
|
2372
2461
|
title: "OpenCode todo list completed",
|
|
2373
|
-
body:
|
|
2462
|
+
body: formatText(body),
|
|
2374
2463
|
payload,
|
|
2375
2464
|
externalId,
|
|
2376
2465
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -2397,7 +2486,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2397
2486
|
import { statSync } from "node:fs";
|
|
2398
2487
|
|
|
2399
2488
|
// src/updates.js
|
|
2400
|
-
var PLUGIN_VERSION = "0.
|
|
2489
|
+
var PLUGIN_VERSION = "0.10.0";
|
|
2401
2490
|
var PACKAGE_NAME = "backeer-opencode";
|
|
2402
2491
|
var NPM_REGISTRY_URL = "https://registry.npmjs.org";
|
|
2403
2492
|
var CHECK_TIMEOUT_MS = 3e3;
|
|
@@ -2553,7 +2642,7 @@ async function probeSource(config, fetchImplementation = globalThis.fetch) {
|
|
|
2553
2642
|
headers: { Authorization: `Bearer ${config.token}` },
|
|
2554
2643
|
signal: controller.signal
|
|
2555
2644
|
});
|
|
2556
|
-
const responseBody =
|
|
2645
|
+
const responseBody = formatText(await response.text());
|
|
2557
2646
|
if (!response.ok) {
|
|
2558
2647
|
throw new HttpError(
|
|
2559
2648
|
`Gateway source check failed with status ${response.status}`,
|
|
@@ -2600,7 +2689,7 @@ async function buildDoctorReport(config, { fetchImplementation = globalThis.fetc
|
|
|
2600
2689
|
text: result?.capabilities?.apns === true ? "Gateway APNs delivery is enabled" : "Gateway APNs delivery is disabled"
|
|
2601
2690
|
});
|
|
2602
2691
|
} catch (error) {
|
|
2603
|
-
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)}`;
|
|
2604
2693
|
checks.push({ ok: false, text: detail });
|
|
2605
2694
|
}
|
|
2606
2695
|
}
|
|
@@ -2637,7 +2726,6 @@ async function publishTestNotification(config, reporter) {
|
|
|
2637
2726
|
}
|
|
2638
2727
|
|
|
2639
2728
|
// src/feedback-session-store.js
|
|
2640
|
-
var MAX_QUEUED_MESSAGES = 20;
|
|
2641
2729
|
var FeedbackSessionStore = class {
|
|
2642
2730
|
constructor({ send, reporter, flushDelayMs = 150 }) {
|
|
2643
2731
|
this.send = send;
|
|
@@ -2655,10 +2743,9 @@ var FeedbackSessionStore = class {
|
|
|
2655
2743
|
blockers: /* @__PURE__ */ new Set(),
|
|
2656
2744
|
queue: [],
|
|
2657
2745
|
queued: /* @__PURE__ */ new Set(),
|
|
2658
|
-
queueOverflowReported: false,
|
|
2659
2746
|
flushTimer: void 0
|
|
2660
2747
|
};
|
|
2661
|
-
|
|
2748
|
+
remember(this.sessions, sessionID, state);
|
|
2662
2749
|
}
|
|
2663
2750
|
return state;
|
|
2664
2751
|
}
|
|
@@ -2693,16 +2780,8 @@ var FeedbackSessionStore = class {
|
|
|
2693
2780
|
const state = this.stateFor(sessionID);
|
|
2694
2781
|
const key = `${sessionID}:${kind}:${message}`;
|
|
2695
2782
|
if (state.queued.has(key)) return;
|
|
2696
|
-
state.queue.push({ kind, message:
|
|
2783
|
+
state.queue.push({ kind, message: formatText(message), key });
|
|
2697
2784
|
state.queued.add(key);
|
|
2698
|
-
if (state.queue.length > MAX_QUEUED_MESSAGES) {
|
|
2699
|
-
const removed = state.queue.shift();
|
|
2700
|
-
state.queued.delete(removed.key);
|
|
2701
|
-
if (!state.queueOverflowReported) {
|
|
2702
|
-
state.queueOverflowReported = true;
|
|
2703
|
-
this.reporter.warning("Conversation feedback queue full \u2014 oldest message discarded");
|
|
2704
|
-
}
|
|
2705
|
-
}
|
|
2706
2785
|
this.scheduleFlush(sessionID, state);
|
|
2707
2786
|
}
|
|
2708
2787
|
setIdle(sessionID, idle) {
|
|
@@ -2815,7 +2894,7 @@ async function sendSessionMessage(client, sessionID, text) {
|
|
|
2815
2894
|
}
|
|
2816
2895
|
function formatBackeerFeedback(kind, message) {
|
|
2817
2896
|
const symbol = FEEDBACK_SYMBOLS[kind] ?? FEEDBACK_SYMBOLS.info;
|
|
2818
|
-
return `\u25A3 Backeer | ${symbol} ${
|
|
2897
|
+
return `\u25A3 Backeer | ${symbol} ${formatText(message)}`;
|
|
2819
2898
|
}
|
|
2820
2899
|
function formatBackeerFeedbackBatch(items) {
|
|
2821
2900
|
if (items.length === 1) {
|
|
@@ -2825,7 +2904,7 @@ function formatBackeerFeedbackBatch(items) {
|
|
|
2825
2904
|
"\u25A3 Backeer",
|
|
2826
2905
|
...items.map((item) => {
|
|
2827
2906
|
const symbol = FEEDBACK_SYMBOLS[item.kind] ?? FEEDBACK_SYMBOLS.info;
|
|
2828
|
-
return `${symbol} ${
|
|
2907
|
+
return `${symbol} ${formatText(item.message)}`;
|
|
2829
2908
|
})
|
|
2830
2909
|
].join("\n");
|
|
2831
2910
|
}
|
|
@@ -3073,17 +3152,17 @@ var URL_WITH_QUERY = /\bhttps?:\/\/[^\s?#]+(?:\?[^\s#]*)?(?:#[^\s]*)?/gi;
|
|
|
3073
3152
|
var WINDOWS_PATH = /\b[A-Za-z]:\\(?:[^\\\s]+\\)+[^\s]*/g;
|
|
3074
3153
|
var POSIX_PATH = /(^|\s)\/(?:[^/\s]+\/)+[^\s]*/g;
|
|
3075
3154
|
function sanitizeErrorMessage(value) {
|
|
3076
|
-
return
|
|
3155
|
+
return formatText(value).replace(CONTROL_CHARACTERS, " ").replace(BEARER_TOKEN, "Bearer [redacted]").replace(SECRET_ASSIGNMENT, "$1=[redacted]").replace(URL_WITH_QUERY, (value2) => {
|
|
3077
3156
|
try {
|
|
3078
3157
|
const url = new URL(value2);
|
|
3079
3158
|
return `${url.origin}${url.pathname}`;
|
|
3080
3159
|
} catch {
|
|
3081
3160
|
return "[url redacted]";
|
|
3082
3161
|
}
|
|
3083
|
-
}).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();
|
|
3084
3163
|
}
|
|
3085
3164
|
function safeErrorDetails(error) {
|
|
3086
|
-
const name =
|
|
3165
|
+
const name = formatText(error?.name || "OpenCodeError");
|
|
3087
3166
|
const message = sanitizeErrorMessage(error?.data?.message ?? error?.message);
|
|
3088
3167
|
const statusCode = Number.isInteger(error?.data?.statusCode) ? String(error.data.statusCode) : "";
|
|
3089
3168
|
return { name, message, statusCode };
|
|
@@ -3096,7 +3175,7 @@ async function resolveSessionTitle2(client, directory, sessionID, reporter) {
|
|
|
3096
3175
|
query: { directory },
|
|
3097
3176
|
throwOnError: true
|
|
3098
3177
|
});
|
|
3099
|
-
return
|
|
3178
|
+
return formatText((result?.data ?? result)?.title);
|
|
3100
3179
|
} catch (error) {
|
|
3101
3180
|
reporter.debug("Session title unavailable", error);
|
|
3102
3181
|
return "";
|
|
@@ -3203,7 +3282,7 @@ async function publishSessionError({
|
|
|
3203
3282
|
// Backeer/iOS currently support info, warning and critical severities.
|
|
3204
3283
|
severity: "critical",
|
|
3205
3284
|
title: "OpenCode task failed",
|
|
3206
|
-
body:
|
|
3285
|
+
body: formatText(body),
|
|
3207
3286
|
payload: [
|
|
3208
3287
|
{ key: "OpenCode session", value: sessionTitle },
|
|
3209
3288
|
{ key: "Outcome", value: "Failed" },
|
|
@@ -3215,7 +3294,7 @@ async function publishSessionError({
|
|
|
3215
3294
|
}, "Task failure sent");
|
|
3216
3295
|
}
|
|
3217
3296
|
async function publishOpenCodeUpdate({ config, reporter, version }) {
|
|
3218
|
-
const safeVersion =
|
|
3297
|
+
const safeVersion = formatText(version);
|
|
3219
3298
|
await publish(config, reporter, void 0, void 0, {
|
|
3220
3299
|
severity: "info",
|
|
3221
3300
|
title: "OpenCode update available",
|
|
@@ -3225,8 +3304,8 @@ async function publishOpenCodeUpdate({ config, reporter, version }) {
|
|
|
3225
3304
|
}, "OpenCode update sent");
|
|
3226
3305
|
}
|
|
3227
3306
|
function errorCorrelation(event, sessionState) {
|
|
3228
|
-
if (typeof event?.id === "string" && event.id) return
|
|
3229
|
-
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);
|
|
3230
3309
|
const details = safeErrorDetails(event?.properties?.error);
|
|
3231
3310
|
return stableHash(`${details.name}:${details.statusCode}:${details.message}`);
|
|
3232
3311
|
}
|
|
@@ -3266,7 +3345,7 @@ function createLifecycleController({
|
|
|
3266
3345
|
lastAssistant: void 0,
|
|
3267
3346
|
completedMessageID: ""
|
|
3268
3347
|
};
|
|
3269
|
-
|
|
3348
|
+
remember(sessions, sessionID, state);
|
|
3270
3349
|
}
|
|
3271
3350
|
return state;
|
|
3272
3351
|
}
|
|
@@ -3301,7 +3380,7 @@ function createLifecycleController({
|
|
|
3301
3380
|
client,
|
|
3302
3381
|
directory,
|
|
3303
3382
|
sessionID,
|
|
3304
|
-
messageID:
|
|
3383
|
+
messageID: formatText(assistant.id),
|
|
3305
3384
|
durationMs,
|
|
3306
3385
|
conversation
|
|
3307
3386
|
}),
|
|
@@ -3348,7 +3427,7 @@ function createLifecycleController({
|
|
|
3348
3427
|
client,
|
|
3349
3428
|
directory,
|
|
3350
3429
|
sessionID,
|
|
3351
|
-
correlation:
|
|
3430
|
+
correlation: formatText(correlation),
|
|
3352
3431
|
attempt: status.attempt,
|
|
3353
3432
|
next: status.next,
|
|
3354
3433
|
conversation
|
|
@@ -3401,7 +3480,7 @@ function createLifecycleController({
|
|
|
3401
3480
|
const version = event.properties?.version;
|
|
3402
3481
|
if (typeof version !== "string" || !version.trim()) return;
|
|
3403
3482
|
if (publishedUpdates.has(version)) return;
|
|
3404
|
-
|
|
3483
|
+
remember(publishedUpdates, version, true);
|
|
3405
3484
|
dispatch(
|
|
3406
3485
|
"OpenCode update could not be sent",
|
|
3407
3486
|
sendUpdate({ config, reporter, version: version.trim() })
|
|
@@ -3417,7 +3496,8 @@ function createLifecycleController({
|
|
|
3417
3496
|
|
|
3418
3497
|
// src/live-bridge.js
|
|
3419
3498
|
import { createServer } from "node:http";
|
|
3420
|
-
import { randomUUID as randomUUID3, timingSafeEqual } from "node:crypto";
|
|
3499
|
+
import { randomUUID as randomUUID3, timingSafeEqual, createHash as createHash2 } from "node:crypto";
|
|
3500
|
+
import { resolve as resolve2 } from "node:path";
|
|
3421
3501
|
|
|
3422
3502
|
// src/permission-command.js
|
|
3423
3503
|
var ALLOWED_DECISIONS = /* @__PURE__ */ new Set(["once", "always", "reject"]);
|
|
@@ -3610,7 +3690,7 @@ async function openCommandStream(config, state, signal2, fetchImplementation) {
|
|
|
3610
3690
|
signal: signal2
|
|
3611
3691
|
});
|
|
3612
3692
|
if (!response.ok) {
|
|
3613
|
-
const responseBody =
|
|
3693
|
+
const responseBody = formatText(await response.text());
|
|
3614
3694
|
throw new HttpError(
|
|
3615
3695
|
`Backeer command stream failed with status ${response.status}`,
|
|
3616
3696
|
response.status,
|
|
@@ -3642,7 +3722,7 @@ async function consumeConnection({
|
|
|
3642
3722
|
}
|
|
3643
3723
|
}
|
|
3644
3724
|
function streamErrorState(error, lastLoggedError, reconnectDelay, reporter) {
|
|
3645
|
-
const message = error instanceof HttpError ? `${error.message}: ${error.responseBody}` :
|
|
3725
|
+
const message = error instanceof HttpError ? `${error.message}: ${error.responseBody}` : formatText(error);
|
|
3646
3726
|
if (message !== lastLoggedError) {
|
|
3647
3727
|
const authenticationFailure = error instanceof HttpError && [401, 403].includes(error.status);
|
|
3648
3728
|
reporter.error(
|
|
@@ -3652,7 +3732,7 @@ function streamErrorState(error, lastLoggedError, reconnectDelay, reporter) {
|
|
|
3652
3732
|
}
|
|
3653
3733
|
return {
|
|
3654
3734
|
lastLoggedError: message,
|
|
3655
|
-
reconnectDelay: error instanceof HttpError && [401, 403, 404].includes(error.status) ?
|
|
3735
|
+
reconnectDelay: error instanceof HttpError && [401, 403, 404].includes(error.status) ? 3e3 : reconnectDelay
|
|
3656
3736
|
};
|
|
3657
3737
|
}
|
|
3658
3738
|
async function runCommandStream(config, reporter, {
|
|
@@ -3665,7 +3745,7 @@ async function runCommandStream(config, reporter, {
|
|
|
3665
3745
|
stableConnectionMs = 5e3
|
|
3666
3746
|
}) {
|
|
3667
3747
|
const state = { lastEventID: "", framesReceived: 0 };
|
|
3668
|
-
let reconnectDelay =
|
|
3748
|
+
let reconnectDelay = 3e3;
|
|
3669
3749
|
let lastLoggedError = "";
|
|
3670
3750
|
while (!signal2.aborted && hasPending()) {
|
|
3671
3751
|
try {
|
|
@@ -3684,7 +3764,7 @@ async function runCommandStream(config, reporter, {
|
|
|
3684
3764
|
now,
|
|
3685
3765
|
stableConnectionMs,
|
|
3686
3766
|
onStable: () => {
|
|
3687
|
-
reconnectDelay =
|
|
3767
|
+
reconnectDelay = 3e3;
|
|
3688
3768
|
lastLoggedError = "";
|
|
3689
3769
|
}
|
|
3690
3770
|
});
|
|
@@ -3696,7 +3776,7 @@ async function runCommandStream(config, reporter, {
|
|
|
3696
3776
|
}
|
|
3697
3777
|
if (!signal2.aborted && hasPending()) {
|
|
3698
3778
|
await sleepImplementation(reconnectDelay, signal2);
|
|
3699
|
-
reconnectDelay =
|
|
3779
|
+
reconnectDelay = 3e3;
|
|
3700
3780
|
}
|
|
3701
3781
|
}
|
|
3702
3782
|
}
|
|
@@ -3784,7 +3864,7 @@ async function replyToQuestion(client, pending, response) {
|
|
|
3784
3864
|
}
|
|
3785
3865
|
async function postQuestionState(config, eventID, path, body = {}) {
|
|
3786
3866
|
const url = new URL(`/v1/events/${encodeURIComponent(eventID)}/${path}`, config.origin);
|
|
3787
|
-
await postJsonWithRetry(config, url, body
|
|
3867
|
+
await postJsonWithRetry(config, url, body);
|
|
3788
3868
|
}
|
|
3789
3869
|
function createPermissionController({
|
|
3790
3870
|
config,
|
|
@@ -3840,7 +3920,7 @@ function createPermissionController({
|
|
|
3840
3920
|
async function handleCommand(command) {
|
|
3841
3921
|
if (!command || typeof command !== "object") return;
|
|
3842
3922
|
if (!["opencode_permission_reply", "opencode_question_reply"].includes(command.type)) return;
|
|
3843
|
-
if (typeof command.id !== "string"
|
|
3923
|
+
if (typeof command.id !== "string") return;
|
|
3844
3924
|
const previousAcknowledgement = completedCommands.get(command.id);
|
|
3845
3925
|
if (previousAcknowledgement) {
|
|
3846
3926
|
await acknowledgeCommand(config, command.id, previousAcknowledgement).catch((error) => reporter.warning("Decision receipt retry failed", error));
|
|
@@ -3890,7 +3970,7 @@ function createPermissionController({
|
|
|
3890
3970
|
externalId: command.externalId,
|
|
3891
3971
|
processedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3892
3972
|
};
|
|
3893
|
-
|
|
3973
|
+
remember(completedCommands, command.id, acknowledgement);
|
|
3894
3974
|
if (isQuestion) {
|
|
3895
3975
|
reportQuestionAcknowledgement(acknowledgement, pending, reporter, conversation);
|
|
3896
3976
|
} else {
|
|
@@ -3997,8 +4077,8 @@ async function commonDirectory(directory) {
|
|
|
3997
4077
|
return realpath(stdout.trim());
|
|
3998
4078
|
}
|
|
3999
4079
|
function approvedWorktreeSessions(value) {
|
|
4000
|
-
if (!Array.isArray(value)
|
|
4001
|
-
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));
|
|
4002
4082
|
}
|
|
4003
4083
|
function createSessionScope(directory, { resolveCommon = commonDirectory, approvedSessions = [] } = {}) {
|
|
4004
4084
|
const observed = /* @__PURE__ */ new Map();
|
|
@@ -4009,7 +4089,6 @@ function createSessionScope(directory, { resolveCommon = commonDirectory, approv
|
|
|
4009
4089
|
if (location === directory) {
|
|
4010
4090
|
if (info.id) {
|
|
4011
4091
|
observed.set(info.id, info.projectID);
|
|
4012
|
-
if (observed.size > 256) observed.delete(observed.keys().next().value);
|
|
4013
4092
|
}
|
|
4014
4093
|
return true;
|
|
4015
4094
|
}
|
|
@@ -4035,24 +4114,23 @@ function createV2LiveClient(ctx) {
|
|
|
4035
4114
|
if (disposed) return info;
|
|
4036
4115
|
await ownsSession(info);
|
|
4037
4116
|
known.set(id, info);
|
|
4038
|
-
if (known.size > 100) known.delete(known.keys().next().value);
|
|
4039
4117
|
return info;
|
|
4040
4118
|
}
|
|
4041
4119
|
return {
|
|
4042
4120
|
async list() {
|
|
4043
4121
|
const refreshed = [];
|
|
4044
4122
|
if (typeof ctx.session.list === "function") {
|
|
4045
|
-
const result = await ctx.session.list({ directory: ctx.location.directory,
|
|
4123
|
+
const result = await ctx.session.list({ directory: ctx.location.directory, order: "desc" });
|
|
4046
4124
|
const values = Array.isArray(result) ? result : result?.data;
|
|
4047
4125
|
if (!Array.isArray(values)) throw new Error("Invalid native session list");
|
|
4048
|
-
for (const info of values
|
|
4126
|
+
for (const info of values) {
|
|
4049
4127
|
if (await ownsSession(info)) {
|
|
4050
4128
|
refreshed.push(info);
|
|
4051
4129
|
known.set(info.id, info);
|
|
4052
4130
|
}
|
|
4053
4131
|
}
|
|
4054
4132
|
}
|
|
4055
|
-
for (const id of [...known.keys()]
|
|
4133
|
+
for (const id of [...known.keys()]) {
|
|
4056
4134
|
if (refreshed.some((info) => info.id === id)) continue;
|
|
4057
4135
|
try {
|
|
4058
4136
|
refreshed.push(await get(id));
|
|
@@ -4154,100 +4232,13 @@ function createLiveConnectionNotifier(config, reporter) {
|
|
|
4154
4232
|
};
|
|
4155
4233
|
}
|
|
4156
4234
|
|
|
4157
|
-
// src/live-bridge-events.js
|
|
4158
|
-
var ID = /^[A-Za-z0-9_-]{1,160}$/;
|
|
4159
|
-
var validBridgeID = (value) => typeof value === "string" && ID.test(value);
|
|
4160
|
-
function createBridgeSanitizer(secrets = []) {
|
|
4161
|
-
const known = secrets.filter((value) => typeof value === "string" && value.length > 0);
|
|
4162
|
-
const assistants = /* @__PURE__ */ new Map();
|
|
4163
|
-
function clipped(value, max = 12e3) {
|
|
4164
|
-
if (typeof value !== "string") return { value: "", truncated: false };
|
|
4165
|
-
let result = value.slice(0, 64e3);
|
|
4166
|
-
for (const secret of known) result = result.split(secret).join("[REDACTED]");
|
|
4167
|
-
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]");
|
|
4168
|
-
return { value: result.slice(0, max), truncated: value.length > 64e3 || result.length > max };
|
|
4169
|
-
}
|
|
4170
|
-
function text(value, max) {
|
|
4171
|
-
return clipped(value, max).value;
|
|
4172
|
-
}
|
|
4173
|
-
function content(key, value) {
|
|
4174
|
-
const result = clipped(value);
|
|
4175
|
-
return { [key]: result.value, ...result.truncated ? { truncated: true } : {} };
|
|
4176
|
-
}
|
|
4177
|
-
function sanitize(event) {
|
|
4178
|
-
const p = event?.properties ?? event?.data ?? {};
|
|
4179
|
-
const sessionID = p.sessionID ?? p.info?.sessionID ?? p.part?.sessionID;
|
|
4180
|
-
if (!validBridgeID(sessionID) || sessionID === "global") return void 0;
|
|
4181
|
-
const base = { type: event.type, sessionID, data: {} };
|
|
4182
|
-
if (event.type === "message.updated") {
|
|
4183
|
-
const info = p.info;
|
|
4184
|
-
if (!validBridgeID(info?.id)) return void 0;
|
|
4185
|
-
if (info.role === "assistant") {
|
|
4186
|
-
assistants.set(info.id, sessionID);
|
|
4187
|
-
if (assistants.size > 256) assistants.delete(assistants.keys().next().value);
|
|
4188
|
-
}
|
|
4189
|
-
return info.role === "assistant" ? { ...base, data: {
|
|
4190
|
-
messageID: info.id,
|
|
4191
|
-
completed: !!info.time?.completed,
|
|
4192
|
-
failed: !!info.error
|
|
4193
|
-
} } : void 0;
|
|
4194
|
-
}
|
|
4195
|
-
if (event.type === "message.part.updated") {
|
|
4196
|
-
const part = p.part;
|
|
4197
|
-
if (assistants.get(part?.messageID) !== sessionID) return void 0;
|
|
4198
|
-
if (part.type === "text") return { ...base, type: "assistant.text", data: {
|
|
4199
|
-
messageID: part.messageID,
|
|
4200
|
-
...content("text", part.text),
|
|
4201
|
-
replace: true
|
|
4202
|
-
} };
|
|
4203
|
-
if (part.type !== "tool") return void 0;
|
|
4204
|
-
const state = part.state ?? {};
|
|
4205
|
-
return { ...base, type: "tool.status", data: {
|
|
4206
|
-
tool: text(part.tool, 80),
|
|
4207
|
-
status: text(state.status, 40),
|
|
4208
|
-
...state.status === "completed" ? content("output", state.output) : {},
|
|
4209
|
-
...state.status === "error" ? { failed: true } : {}
|
|
4210
|
-
} };
|
|
4211
|
-
}
|
|
4212
|
-
if (event.type === "session.text.ended") return { ...base, type: "assistant.text", data: {
|
|
4213
|
-
messageID: validBridgeID(p.assistantMessageID) ? p.assistantMessageID : void 0,
|
|
4214
|
-
...content("text", p.text),
|
|
4215
|
-
replace: true
|
|
4216
|
-
} };
|
|
4217
|
-
if (["session.tool.called", "session.tool.success", "session.tool.failed"].includes(event.type)) {
|
|
4218
|
-
return { ...base, type: "tool.status", data: {
|
|
4219
|
-
status: event.type.split(".").at(-1),
|
|
4220
|
-
...event.type === "session.tool.success" ? content(
|
|
4221
|
-
"output",
|
|
4222
|
-
(p.content ?? []).filter((item) => item?.type === "text").map((item) => item.text).join("\n")
|
|
4223
|
-
) : {}
|
|
4224
|
-
} };
|
|
4225
|
-
}
|
|
4226
|
-
if (["permission.asked", "question.asked", "question.v2.asked"].includes(event.type)) {
|
|
4227
|
-
return { ...base, type: "approval.waiting", data: {
|
|
4228
|
-
kind: event.type.split(".")[0],
|
|
4229
|
-
...event.type !== "permission.asked" && validBridgeID(p.id ?? p.requestID) ? { questionId: p.id ?? p.requestID } : {}
|
|
4230
|
-
} };
|
|
4231
|
-
}
|
|
4232
|
-
if (["permission.replied", "question.replied", "question.v2.replied", "question.rejected", "question.v2.rejected"].includes(event.type)) {
|
|
4233
|
-
return { ...base, type: "approval.settled", data: { kind: event.type.split(".")[0] } };
|
|
4234
|
-
}
|
|
4235
|
-
if (event.type === "session.status") return { ...base, data: {
|
|
4236
|
-
status: ["idle", "busy", "retry"].includes(p.status?.type) ? p.status.type : "unknown"
|
|
4237
|
-
} };
|
|
4238
|
-
if (["session.idle", "session.error", "session.deleted"].includes(event.type)) return base;
|
|
4239
|
-
return void 0;
|
|
4240
|
-
}
|
|
4241
|
-
return { sanitize, text, clear: () => assistants.clear() };
|
|
4242
|
-
}
|
|
4243
|
-
|
|
4244
4235
|
// src/live-questions.js
|
|
4245
4236
|
import { createHash } from "node:crypto";
|
|
4246
4237
|
var invalid = (code = "invalid_question_answer", status = 422) => Object.assign(new Error(code), { code, status });
|
|
4247
4238
|
function questionSnapshot(pending, text) {
|
|
4248
|
-
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");
|
|
4249
4240
|
const questions = pending.questions.map((question, index) => {
|
|
4250
|
-
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) {
|
|
4251
4242
|
throw invalid("unsupported_question");
|
|
4252
4243
|
}
|
|
4253
4244
|
return {
|
|
@@ -4270,14 +4261,13 @@ function questionSnapshot(pending, text) {
|
|
|
4270
4261
|
...pending.nativeRevision ? { nativeRevision: pending.nativeRevision } : {}
|
|
4271
4262
|
})).digest("hex");
|
|
4272
4263
|
const result = { questionId: pending.id, sessionID: pending.sessionID, revision, questions };
|
|
4273
|
-
if (Buffer.byteLength(JSON.stringify(result)) > 3e4) throw invalid("question_too_large");
|
|
4274
4264
|
return result;
|
|
4275
4265
|
}
|
|
4276
4266
|
function resolveQuestionAnswers(pending, answers) {
|
|
4277
4267
|
if (!Array.isArray(answers) || answers.length !== pending.questions.length) throw invalid();
|
|
4278
4268
|
const values = answers.map((answer, index) => {
|
|
4279
4269
|
const question = pending.questions[index];
|
|
4280
|
-
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();
|
|
4281
4271
|
if (answer.text !== void 0 && question.custom !== true) throw invalid("custom_not_allowed");
|
|
4282
4272
|
return [
|
|
4283
4273
|
...answer.choices.map((choice) => question.options[choice].label),
|
|
@@ -4290,7 +4280,6 @@ function resolveQuestionAnswers(pending, answers) {
|
|
|
4290
4280
|
}
|
|
4291
4281
|
|
|
4292
4282
|
// src/live-bridge.js
|
|
4293
|
-
var LIMITS = { events: 256, bytes: 2e6, clients: 8, commands: 1024, commandTTL: 864e5, body: 32768, requests: 16 };
|
|
4294
4283
|
function failure(status, code) {
|
|
4295
4284
|
return Object.assign(new Error(code), { status, code });
|
|
4296
4285
|
}
|
|
@@ -4302,10 +4291,7 @@ function json(response, status, value) {
|
|
|
4302
4291
|
}
|
|
4303
4292
|
async function readBody(request) {
|
|
4304
4293
|
const chunks = [];
|
|
4305
|
-
let bytes = 0;
|
|
4306
4294
|
for await (const chunk of request) {
|
|
4307
|
-
bytes += chunk.length;
|
|
4308
|
-
if (bytes > LIMITS.body) throw failure(413, "body_too_large");
|
|
4309
4295
|
chunks.push(chunk);
|
|
4310
4296
|
}
|
|
4311
4297
|
try {
|
|
@@ -4316,9 +4302,11 @@ async function readBody(request) {
|
|
|
4316
4302
|
}
|
|
4317
4303
|
async function createLiveBridge({ config, client, directory, reporter }) {
|
|
4318
4304
|
const options = config.liveBridge;
|
|
4319
|
-
if (!options?.enabled
|
|
4320
|
-
|
|
4321
|
-
|
|
4305
|
+
if (!options?.enabled || options.projectDirectory !== void 0 && resolve2(options.projectDirectory) !== resolve2(directory)) {
|
|
4306
|
+
return { handle() {
|
|
4307
|
+
}, async dispose() {
|
|
4308
|
+
}, address: void 0 };
|
|
4309
|
+
}
|
|
4322
4310
|
const token = options.token;
|
|
4323
4311
|
if (typeof token !== "string" || token.length < 32 || token === config.token) {
|
|
4324
4312
|
throw new Error("Live bridge requires a separate credential of at least 32 characters");
|
|
@@ -4328,45 +4316,44 @@ async function createLiveBridge({ config, client, directory, reporter }) {
|
|
|
4328
4316
|
const sanitizer = createBridgeSanitizer([token, config.token]);
|
|
4329
4317
|
const epoch = randomUUID3();
|
|
4330
4318
|
const events = [], streams = /* @__PURE__ */ new Set(), commands = /* @__PURE__ */ new Map(), states = /* @__PURE__ */ new Map();
|
|
4331
|
-
let sequence = 0,
|
|
4332
|
-
|
|
4333
|
-
const lookupWaiters = /* @__PURE__ */ new Set();
|
|
4319
|
+
let sequence = 0, disposed = false, disposeTask;
|
|
4320
|
+
const lookupWaiters = /* @__PURE__ */ new Set(), lookups = /* @__PURE__ */ new Map();
|
|
4334
4321
|
const cursor = () => `${epoch}:${sequence}`;
|
|
4335
|
-
function lookup(id) {
|
|
4322
|
+
function lookup(id, signal2) {
|
|
4336
4323
|
if (disposed) return Promise.reject(failure(503, "bridge_closed"));
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
const
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
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
|
+
}
|
|
4334
|
+
return new Promise((resolve3, reject) => {
|
|
4335
|
+
const finish = (ok, value) => {
|
|
4336
|
+
if (!entry.waiters.delete(finish)) return;
|
|
4345
4337
|
lookupWaiters.delete(cancel);
|
|
4346
|
-
|
|
4338
|
+
signal2?.removeEventListener("abort", aborted);
|
|
4339
|
+
if (ok) resolve3(value);
|
|
4340
|
+
else reject(value);
|
|
4347
4341
|
};
|
|
4348
|
-
const cancel = () => finish(
|
|
4349
|
-
const
|
|
4342
|
+
const cancel = () => finish(false, failure(503, "bridge_closed"));
|
|
4343
|
+
const aborted = () => finish(false, failure(499, "request_cancelled"));
|
|
4344
|
+
entry.waiters.add(finish);
|
|
4350
4345
|
lookupWaiters.add(cancel);
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
lookups--;
|
|
4354
|
-
finish(resolve2, value);
|
|
4355
|
-
},
|
|
4356
|
-
(error) => {
|
|
4357
|
-
lookups--;
|
|
4358
|
-
finish(reject, error);
|
|
4359
|
-
}
|
|
4360
|
-
);
|
|
4346
|
+
signal2?.addEventListener("abort", aborted, { once: true });
|
|
4347
|
+
if (signal2?.aborted) aborted();
|
|
4361
4348
|
});
|
|
4362
4349
|
}
|
|
4363
|
-
async function session(id) {
|
|
4350
|
+
async function session(id, signal2) {
|
|
4364
4351
|
if (!validBridgeID(id) || id === "global") throw failure(400, "invalid_session");
|
|
4365
4352
|
let info;
|
|
4366
4353
|
try {
|
|
4367
|
-
info = await lookup(id);
|
|
4354
|
+
info = await lookup(id, signal2);
|
|
4368
4355
|
} catch (error) {
|
|
4369
|
-
if (["
|
|
4356
|
+
if (["bridge_closed", "request_cancelled"].includes(error.code)) throw error;
|
|
4370
4357
|
throw failure(404, "session_not_found");
|
|
4371
4358
|
}
|
|
4372
4359
|
if (info?.id !== id || typeof info.time?.archived === "number") {
|
|
@@ -4388,18 +4375,12 @@ async function createLiveBridge({ config, client, directory, reporter }) {
|
|
|
4388
4375
|
const removed = streams.delete(stream);
|
|
4389
4376
|
clearInterval(stream.heartbeat);
|
|
4390
4377
|
stream.queue.length = 0;
|
|
4391
|
-
stream.queuedBytes = 0;
|
|
4392
4378
|
if (removed && !streams.size && !disposed) connectionNotifications.update(false);
|
|
4393
4379
|
}
|
|
4394
4380
|
function write(stream, value) {
|
|
4395
4381
|
if (!streams.has(stream)) return;
|
|
4396
4382
|
if (stream.blocked) {
|
|
4397
4383
|
stream.queue.push(value);
|
|
4398
|
-
stream.queuedBytes += Buffer.byteLength(value);
|
|
4399
|
-
if (stream.queuedBytes > LIMITS.bytes) {
|
|
4400
|
-
removeStream(stream);
|
|
4401
|
-
stream.response.destroy();
|
|
4402
|
-
}
|
|
4403
4384
|
return;
|
|
4404
4385
|
}
|
|
4405
4386
|
if (!stream.response.write(value)) {
|
|
@@ -4419,25 +4400,13 @@ async function createLiveBridge({ config, client, directory, reporter }) {
|
|
|
4419
4400
|
if (safe.type === "session.status") states.set(safe.sessionID, safe.data.status);
|
|
4420
4401
|
if (safe.type === "session.idle") states.set(safe.sessionID, "idle");
|
|
4421
4402
|
if (safe.type === "session.deleted") states.delete(safe.sessionID);
|
|
4422
|
-
if (states.size > 256) states.delete(states.keys().next().value);
|
|
4423
4403
|
const record = { id: `${epoch}:${++sequence}`, ...safe, timestamp: Date.now() };
|
|
4424
|
-
|
|
4425
|
-
event: backeer.event
|
|
4426
|
-
data: ${JSON.stringify(record)}
|
|
4427
|
-
|
|
4428
|
-
`;
|
|
4429
|
-
if (Buffer.byteLength(frame) > 32768) {
|
|
4430
|
-
record.data = { truncated: true };
|
|
4431
|
-
frame = `id: ${record.id}
|
|
4404
|
+
const frame = `id: ${record.id}
|
|
4432
4405
|
event: backeer.event
|
|
4433
4406
|
data: ${JSON.stringify(record)}
|
|
4434
4407
|
|
|
4435
4408
|
`;
|
|
4436
|
-
}
|
|
4437
|
-
const size = Buffer.byteLength(frame);
|
|
4438
|
-
events.push({ sequence, sessionID: record.sessionID, frame, size });
|
|
4439
|
-
bytes += size;
|
|
4440
|
-
while (events.length > LIMITS.events || bytes > LIMITS.bytes) bytes -= events.shift().size;
|
|
4409
|
+
events.push({ sequence, sessionID: record.sessionID, frame });
|
|
4441
4410
|
for (const stream of streams) if (!stream.sessionID || stream.sessionID === record.sessionID) write(stream, frame);
|
|
4442
4411
|
}
|
|
4443
4412
|
function authorized(request) {
|
|
@@ -4448,7 +4417,7 @@ data: ${JSON.stringify(record)}
|
|
|
4448
4417
|
}
|
|
4449
4418
|
function parseCursor(value) {
|
|
4450
4419
|
if (!value) return { after: 0 };
|
|
4451
|
-
const match = /^([a-f0-9-]{36}):(\d
|
|
4420
|
+
const match = /^([a-f0-9-]{36}):(\d+)$/.exec(value);
|
|
4452
4421
|
if (!match) throw failure(400, "invalid_cursor");
|
|
4453
4422
|
if (match[1] !== epoch) return { gap: "stream_restarted" };
|
|
4454
4423
|
const after = Number(match[2]);
|
|
@@ -4457,9 +4426,6 @@ data: ${JSON.stringify(record)}
|
|
|
4457
4426
|
return { after };
|
|
4458
4427
|
}
|
|
4459
4428
|
function cachedCommand(requestId, sessionID, signature) {
|
|
4460
|
-
for (const [id, command] of commands) {
|
|
4461
|
-
if (command.settled && Date.now() - command.created >= LIMITS.commandTTL) commands.delete(id);
|
|
4462
|
-
}
|
|
4463
4429
|
const existing = commands.get(requestId);
|
|
4464
4430
|
if (existing && (existing.sessionID !== sessionID || existing.signature !== signature)) {
|
|
4465
4431
|
throw failure(409, "request_id_conflict");
|
|
@@ -4469,7 +4435,6 @@ data: ${JSON.stringify(record)}
|
|
|
4469
4435
|
async function execute(requestId, sessionID, signature, operation, questionId) {
|
|
4470
4436
|
let command = cachedCommand(requestId, sessionID, signature);
|
|
4471
4437
|
if (!command) {
|
|
4472
|
-
if (commands.size >= LIMITS.commands) throw failure(429, "command_window_full");
|
|
4473
4438
|
if (questionId && [...commands.values()].some((item) => item.sessionID === sessionID && item.questionId === questionId)) {
|
|
4474
4439
|
throw failure(409, "question_reply_already_submitted");
|
|
4475
4440
|
}
|
|
@@ -4477,8 +4442,6 @@ data: ${JSON.stringify(record)}
|
|
|
4477
4442
|
sessionID,
|
|
4478
4443
|
signature,
|
|
4479
4444
|
questionId,
|
|
4480
|
-
created: Date.now(),
|
|
4481
|
-
settled: false,
|
|
4482
4445
|
result: {
|
|
4483
4446
|
requestId,
|
|
4484
4447
|
sessionID,
|
|
@@ -4487,9 +4450,7 @@ data: ${JSON.stringify(record)}
|
|
|
4487
4450
|
}
|
|
4488
4451
|
};
|
|
4489
4452
|
commands.set(requestId, command);
|
|
4490
|
-
command.task = Promise.resolve().then(operation).
|
|
4491
|
-
command.settled = true;
|
|
4492
|
-
});
|
|
4453
|
+
command.task = Promise.resolve().then(operation).then(() => void 0);
|
|
4493
4454
|
}
|
|
4494
4455
|
await command.task;
|
|
4495
4456
|
return command.result;
|
|
@@ -4499,7 +4460,6 @@ data: ${JSON.stringify(record)}
|
|
|
4499
4460
|
const pending = await api.questions(sessionID);
|
|
4500
4461
|
if (!Array.isArray(pending)) throw failure(502, "invalid_question_list");
|
|
4501
4462
|
const own = pending.filter((item) => item.sessionID === sessionID);
|
|
4502
|
-
if (own.length > 16) throw failure(413, "too_many_questions");
|
|
4503
4463
|
return own;
|
|
4504
4464
|
}
|
|
4505
4465
|
async function route(request, response) {
|
|
@@ -4507,10 +4467,9 @@ data: ${JSON.stringify(record)}
|
|
|
4507
4467
|
if (request.headers["x-backeer-project"] !== void 0 && request.headers["x-backeer-project"] !== encodeURIComponent(directory)) {
|
|
4508
4468
|
return json(response, 409, { error: "project_mismatch" });
|
|
4509
4469
|
}
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
timeout.unref();
|
|
4470
|
+
const controller = new AbortController();
|
|
4471
|
+
const closed = () => controller.abort();
|
|
4472
|
+
response.once("close", closed);
|
|
4514
4473
|
try {
|
|
4515
4474
|
const url = new URL(request.url, "http://127.0.0.1");
|
|
4516
4475
|
if (url.username || url.password) throw failure(400, "invalid_url");
|
|
@@ -4519,7 +4478,7 @@ data: ${JSON.stringify(record)}
|
|
|
4519
4478
|
const values = Array.isArray(result2) ? result2 : result2?.data;
|
|
4520
4479
|
if (!Array.isArray(values)) throw failure(502, "invalid_session_list");
|
|
4521
4480
|
const own = [];
|
|
4522
|
-
for (const info of values
|
|
4481
|
+
for (const info of values) {
|
|
4523
4482
|
if (!validBridgeID(info?.id) || typeof info.time?.archived === "number") continue;
|
|
4524
4483
|
if (api.ownsSession ? await api.ownsSession(info) : (info.location?.directory ?? info.directory) === directory) own.push(info);
|
|
4525
4484
|
}
|
|
@@ -4548,11 +4507,9 @@ data: ${JSON.stringify(record)}
|
|
|
4548
4507
|
}
|
|
4549
4508
|
if (request.method === "GET" && url.pathname === "/v1/events") {
|
|
4550
4509
|
const selected = url.searchParams.get("sessionID") || void 0;
|
|
4551
|
-
if (selected) await session(selected);
|
|
4510
|
+
if (selected) await session(selected, controller.signal);
|
|
4552
4511
|
const parsed = parseCursor(url.searchParams.get("after") ?? request.headers["last-event-id"]);
|
|
4553
4512
|
if (response.headersSent) return;
|
|
4554
|
-
if (streams.size >= LIMITS.clients) throw failure(429, "too_many_streams");
|
|
4555
|
-
clearTimeout(timeout);
|
|
4556
4513
|
response.writeHead(200, {
|
|
4557
4514
|
"content-type": "text/event-stream",
|
|
4558
4515
|
"cache-control": "no-cache, no-transform",
|
|
@@ -4571,8 +4528,7 @@ data: ${JSON.stringify({ reason: parsed.gap, cursor: cursor() })}
|
|
|
4571
4528
|
sessionID: selected,
|
|
4572
4529
|
heartbeat: void 0,
|
|
4573
4530
|
blocked: false,
|
|
4574
|
-
queue: []
|
|
4575
|
-
queuedBytes: 0
|
|
4531
|
+
queue: []
|
|
4576
4532
|
};
|
|
4577
4533
|
streams.add(stream);
|
|
4578
4534
|
connectionNotifications.update(true);
|
|
@@ -4583,7 +4539,6 @@ data: ${JSON.stringify({ reason: parsed.gap, cursor: cursor() })}
|
|
|
4583
4539
|
stream.blocked = false;
|
|
4584
4540
|
while (streams.has(stream) && !stream.blocked && stream.queue.length) {
|
|
4585
4541
|
const next = stream.queue.shift();
|
|
4586
|
-
stream.queuedBytes -= Buffer.byteLength(next);
|
|
4587
4542
|
write(stream, next);
|
|
4588
4543
|
}
|
|
4589
4544
|
});
|
|
@@ -4601,13 +4556,12 @@ retry: ${LIVE_RECONNECT_MS}
|
|
|
4601
4556
|
}
|
|
4602
4557
|
return;
|
|
4603
4558
|
}
|
|
4604
|
-
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);
|
|
4605
4560
|
if (!match) throw failure(404, "route_not_found");
|
|
4606
|
-
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));
|
|
4607
4562
|
if (request.method === "GET" && match[2] === "/questions") {
|
|
4608
|
-
await session(match[1]);
|
|
4563
|
+
await session(match[1], controller.signal);
|
|
4609
4564
|
const result2 = { questions: (await questions(match[1])).map((item) => questionSnapshot(item, sanitizer.text)) };
|
|
4610
|
-
if (Buffer.byteLength(JSON.stringify(result2)) > 24e4) throw failure(413, "questions_too_large");
|
|
4611
4565
|
return json(response, 200, result2);
|
|
4612
4566
|
}
|
|
4613
4567
|
if (request.method !== "POST" || !match[2]) throw failure(405, "method_not_allowed");
|
|
@@ -4615,8 +4569,8 @@ retry: ${LIVE_RECONNECT_MS}
|
|
|
4615
4569
|
const body = await readBody(request);
|
|
4616
4570
|
if (match[3]) {
|
|
4617
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");
|
|
4618
|
-
await session(match[1]);
|
|
4619
|
-
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");
|
|
4620
4574
|
const existing2 = cachedCommand(body.requestId, match[1], signature2);
|
|
4621
4575
|
if (existing2) {
|
|
4622
4576
|
await existing2.task;
|
|
@@ -4637,31 +4591,30 @@ retry: ${LIVE_RECONNECT_MS}
|
|
|
4637
4591
|
));
|
|
4638
4592
|
}
|
|
4639
4593
|
if (match[2] !== "/instructions") throw failure(405, "method_not_allowed");
|
|
4640
|
-
if (!body || !validBridgeID(body.requestId) || typeof body.prompt !== "string" || !body.prompt.trim() ||
|
|
4641
|
-
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);
|
|
4642
4596
|
if (response.headersSent || disposed) return;
|
|
4643
|
-
const signature = JSON.stringify({ prompt: body.prompt });
|
|
4597
|
+
const signature = createHash2("sha256").update(JSON.stringify({ prompt: body.prompt })).digest("hex");
|
|
4644
4598
|
const existing = cachedCommand(body.requestId, match[1], signature);
|
|
4645
4599
|
const result = await execute(body.requestId, match[1], signature, () => api.prompt(match[1], body.prompt));
|
|
4646
4600
|
return json(response, existing ? 200 : 202, result);
|
|
4647
4601
|
} catch (error) {
|
|
4648
4602
|
json(response, error.status ?? 502, { error: error.code ?? "opencode_request_failed" });
|
|
4649
4603
|
} finally {
|
|
4650
|
-
|
|
4651
|
-
clearTimeout(timeout);
|
|
4604
|
+
response.removeListener("close", closed);
|
|
4652
4605
|
}
|
|
4653
4606
|
}
|
|
4654
4607
|
const server = createServer((request, response) => {
|
|
4655
4608
|
void route(request, response);
|
|
4656
4609
|
});
|
|
4657
|
-
server.requestTimeout =
|
|
4658
|
-
server.headersTimeout =
|
|
4659
|
-
server.keepAliveTimeout =
|
|
4660
|
-
await new Promise((
|
|
4610
|
+
server.requestTimeout = 0;
|
|
4611
|
+
server.headersTimeout = 0;
|
|
4612
|
+
server.keepAliveTimeout = 0;
|
|
4613
|
+
await new Promise((resolve3, reject) => {
|
|
4661
4614
|
server.once("error", reject);
|
|
4662
4615
|
server.listen(options.port ?? 43871, "127.0.0.1", () => {
|
|
4663
4616
|
server.off("error", reject);
|
|
4664
|
-
|
|
4617
|
+
resolve3();
|
|
4665
4618
|
});
|
|
4666
4619
|
});
|
|
4667
4620
|
server.on("error", () => {
|
|
@@ -4670,7 +4623,7 @@ retry: ${LIVE_RECONNECT_MS}
|
|
|
4670
4623
|
handle,
|
|
4671
4624
|
address: server.address(),
|
|
4672
4625
|
dispose() {
|
|
4673
|
-
disposeTask ??= new Promise((
|
|
4626
|
+
disposeTask ??= new Promise((resolve3) => {
|
|
4674
4627
|
disposed = true;
|
|
4675
4628
|
for (const cancel of lookupWaiters) cancel();
|
|
4676
4629
|
for (const stream of streams) {
|
|
@@ -4678,12 +4631,13 @@ retry: ${LIVE_RECONNECT_MS}
|
|
|
4678
4631
|
stream.response.destroy();
|
|
4679
4632
|
}
|
|
4680
4633
|
server.close(() => {
|
|
4681
|
-
void connectionNotifications.dispose().then(
|
|
4634
|
+
void connectionNotifications.dispose().then(resolve3);
|
|
4682
4635
|
});
|
|
4683
4636
|
server.closeAllConnections?.();
|
|
4684
4637
|
events.length = 0;
|
|
4685
4638
|
states.clear();
|
|
4686
4639
|
commands.clear();
|
|
4640
|
+
lookups.clear();
|
|
4687
4641
|
sanitizer.clear();
|
|
4688
4642
|
api.dispose?.();
|
|
4689
4643
|
});
|
|
@@ -4705,10 +4659,12 @@ async function BackeerPlugin({ client, directory, feedback, v2 = false }, option
|
|
|
4705
4659
|
bootstrapReporter.error("Plugin disabled \u2014 invalid configuration", error);
|
|
4706
4660
|
return {};
|
|
4707
4661
|
}
|
|
4662
|
+
const shutdown = new AbortController();
|
|
4663
|
+
config.signal = shutdown.signal;
|
|
4708
4664
|
const reporter = createOpenCodeReporter(client, config.logLevel);
|
|
4709
4665
|
const conversation = feedback ?? createConversationFeedback({ client, reporter });
|
|
4710
4666
|
if (config.liveBridge.error) reporter.error("Live control disabled", config.liveBridge.error);
|
|
4711
|
-
const bridgeConfig = config.liveBridge.owner === "tui" ? { ...config, liveBridge: { enabled: false } } : config;
|
|
4667
|
+
const bridgeConfig = config.liveBridge.owner === "tui" || config.liveBridge.owner === "standalone" && (!v2 || !process.argv.includes("--stdio")) ? { ...config, liveBridge: { enabled: false } } : config;
|
|
4712
4668
|
const liveBridge = await createLiveBridge({ config: bridgeConfig, client, directory, reporter }).catch((error) => {
|
|
4713
4669
|
reporter.error("Live control unavailable", error);
|
|
4714
4670
|
return { async handle() {
|
|
@@ -4864,6 +4820,7 @@ async function BackeerPlugin({ client, directory, feedback, v2 = false }, option
|
|
|
4864
4820
|
await Promise.all([handleNotificationEvent(event), liveBridge.handle(event)]);
|
|
4865
4821
|
},
|
|
4866
4822
|
dispose: async () => {
|
|
4823
|
+
shutdown.abort();
|
|
4867
4824
|
await liveBridge.dispose();
|
|
4868
4825
|
await permissionController.dispose();
|
|
4869
4826
|
lifecycleController.dispose();
|
|
@@ -5344,7 +5301,7 @@ async function terminate(info, options, timing) {
|
|
|
5344
5301
|
await rm2(options.file ?? fallback2(), { force: true });
|
|
5345
5302
|
}
|
|
5346
5303
|
function delay(milliseconds) {
|
|
5347
|
-
return new Promise((
|
|
5304
|
+
return new Promise((resolve3) => setTimeout(resolve3, milliseconds));
|
|
5348
5305
|
}
|
|
5349
5306
|
var Service = { discover, ensure, stop, headers: headers2 };
|
|
5350
5307
|
|
|
@@ -5382,6 +5339,8 @@ async function privateEndpoint({
|
|
|
5382
5339
|
argv = process.argv,
|
|
5383
5340
|
env = process.env,
|
|
5384
5341
|
pid = process.pid,
|
|
5342
|
+
platform = process.platform,
|
|
5343
|
+
initialEnvironment = () => readFile3("/proc/self/environ", "utf8"),
|
|
5385
5344
|
urls = ownLoopbackUrls,
|
|
5386
5345
|
request = fetch
|
|
5387
5346
|
} = {}) {
|
|
@@ -5389,9 +5348,10 @@ async function privateEndpoint({
|
|
|
5389
5348
|
const flag = argv.indexOf("--port");
|
|
5390
5349
|
const rawPort = flag >= 0 ? argv[flag + 1] : argv.find((arg) => arg.startsWith("--port="))?.slice(7);
|
|
5391
5350
|
const port = typeof rawPort === "string" && /^\d+$/.test(rawPort) ? Number(rawPort) : NaN;
|
|
5392
|
-
const password = env.OPENCODE_PASSWORD;
|
|
5393
5351
|
try {
|
|
5394
|
-
if (!Number.isInteger(port) || port < 0 || port > 65535
|
|
5352
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error();
|
|
5353
|
+
const password = env.OPENCODE_PASSWORD || (platform === "linux" ? (await initialEnvironment()).split("\0").find((value) => value.startsWith("OPENCODE_PASSWORD="))?.slice(18) : void 0);
|
|
5354
|
+
if (!password) throw new Error();
|
|
5395
5355
|
const candidates = port === 0 ? await urls() : [`http://127.0.0.1:${port}`];
|
|
5396
5356
|
for (const url of candidates) {
|
|
5397
5357
|
const endpoint = { url, auth: { type: "basic", username: "opencode", password } };
|
|
@@ -5444,7 +5404,7 @@ async function getFormApi(ctx, sessionID, { makeClient = exports_client.make, ow
|
|
|
5444
5404
|
}
|
|
5445
5405
|
|
|
5446
5406
|
// src/opencode-v2-client.js
|
|
5447
|
-
import { createHash as
|
|
5407
|
+
import { createHash as createHash3 } from "node:crypto";
|
|
5448
5408
|
async function nativeReply(operation) {
|
|
5449
5409
|
try {
|
|
5450
5410
|
return await operation();
|
|
@@ -5470,7 +5430,7 @@ function questionProperties(form) {
|
|
|
5470
5430
|
return {
|
|
5471
5431
|
id: form.id,
|
|
5472
5432
|
sessionID: form.sessionID,
|
|
5473
|
-
nativeRevision:
|
|
5433
|
+
nativeRevision: createHash3("sha256").update(JSON.stringify({
|
|
5474
5434
|
id: form.id,
|
|
5475
5435
|
sessionID: form.sessionID,
|
|
5476
5436
|
title: form.title,
|
|
@@ -5612,11 +5572,11 @@ function createV2Client(ctx, resolveFormApi) {
|
|
|
5612
5572
|
|
|
5613
5573
|
// src/opencode-v2.js
|
|
5614
5574
|
function waitForReconnect(signal2, delay2) {
|
|
5615
|
-
return new Promise((
|
|
5575
|
+
return new Promise((resolve3) => {
|
|
5616
5576
|
const finish = () => {
|
|
5617
5577
|
clearTimeout(timer);
|
|
5618
5578
|
signal2.removeEventListener("abort", finish);
|
|
5619
|
-
|
|
5579
|
+
resolve3();
|
|
5620
5580
|
};
|
|
5621
5581
|
const timer = setTimeout(finish, delay2);
|
|
5622
5582
|
signal2.addEventListener("abort", finish, { once: true });
|