backeer-opencode 0.4.0 → 0.5.2
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 +35 -0
- package/README.md +57 -3
- package/bin/backeer-opencode-mcp.js +9 -0
- package/dist/backeer-permission.js +619 -53
- package/docs/live-control.md +80 -0
- package/package.json +10 -6
- package/src/config.js +4 -0
- package/src/live-bridge-client.js +52 -0
- package/src/live-bridge-config.js +48 -0
- package/src/live-bridge-events.js +80 -0
- package/src/live-bridge.js +273 -0
- package/src/live-connection.js +50 -0
- package/src/live-control-client.js +372 -0
- package/src/live-control-mcp.js +79 -0
- package/src/opencode-v2-client.js +2 -0
- package/src/opencode-v2.js +2 -4
- package/src/plugin.js +54 -46
- package/src/updates.js +1 -1
|
@@ -235,7 +235,7 @@ function make3(options) {
|
|
|
235
235
|
};
|
|
236
236
|
const responseError = async (response, descriptor) => {
|
|
237
237
|
if (descriptor.declaredStatuses.includes(response.status))
|
|
238
|
-
throw declared(await
|
|
238
|
+
throw declared(await json2(response));
|
|
239
239
|
try {
|
|
240
240
|
await response.body?.cancel();
|
|
241
241
|
} catch {
|
|
@@ -255,7 +255,7 @@ function make3(options) {
|
|
|
255
255
|
}
|
|
256
256
|
return;
|
|
257
257
|
}
|
|
258
|
-
return await
|
|
258
|
+
return await json2(response);
|
|
259
259
|
};
|
|
260
260
|
const sse = (descriptor, requestOptions) => ({
|
|
261
261
|
async *[Symbol.asyncIterator]() {
|
|
@@ -1615,7 +1615,7 @@ function appendQuery(params, key, value) {
|
|
|
1615
1615
|
}
|
|
1616
1616
|
params.append(key, String(value));
|
|
1617
1617
|
}
|
|
1618
|
-
async function
|
|
1618
|
+
async function json2(response) {
|
|
1619
1619
|
if (!isContentType(response, "application/json") && !response.headers.get("content-type")?.includes("+json")) {
|
|
1620
1620
|
try {
|
|
1621
1621
|
await response.body?.cancel();
|
|
@@ -1855,9 +1855,9 @@ var init_promise = __esm({
|
|
|
1855
1855
|
});
|
|
1856
1856
|
|
|
1857
1857
|
// src/config.js
|
|
1858
|
-
import { readFileSync } from "node:fs";
|
|
1858
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
1859
1859
|
import { homedir } from "node:os";
|
|
1860
|
-
import { join } from "node:path";
|
|
1860
|
+
import { join as join2 } from "node:path";
|
|
1861
1861
|
|
|
1862
1862
|
// src/utilities.js
|
|
1863
1863
|
var MAX_TRACKED_ITEMS = 500;
|
|
@@ -1930,6 +1930,48 @@ function rememberRecent(map, key, value) {
|
|
|
1930
1930
|
while (map.size > 100) map.delete(map.keys().next().value);
|
|
1931
1931
|
}
|
|
1932
1932
|
|
|
1933
|
+
// src/live-bridge-config.js
|
|
1934
|
+
import { constants, closeSync, fstatSync, openSync, readFileSync } from "node:fs";
|
|
1935
|
+
import { isAbsolute, join } from "node:path";
|
|
1936
|
+
function readCredential(file) {
|
|
1937
|
+
if (typeof file !== "string" || !isAbsolute(file)) {
|
|
1938
|
+
throw new Error("Live control requires an absolute credential-file path");
|
|
1939
|
+
}
|
|
1940
|
+
let descriptor;
|
|
1941
|
+
try {
|
|
1942
|
+
descriptor = openSync(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
1943
|
+
const stat = fstatSync(descriptor);
|
|
1944
|
+
if (!stat.isFile() || stat.size > 4096 || process.platform !== "win32" && ((stat.mode & 63) !== 0 || stat.uid !== process.getuid())) {
|
|
1945
|
+
throw new Error("invalid_file");
|
|
1946
|
+
}
|
|
1947
|
+
const token = readFileSync(descriptor, "utf8").trim();
|
|
1948
|
+
if (!/^[A-Za-z0-9_-]{32,256}$/.test(token)) throw new Error("invalid_token");
|
|
1949
|
+
return token;
|
|
1950
|
+
} catch {
|
|
1951
|
+
throw new Error("Live control credential must be an owner-only regular file containing a 32\u2013256 character token");
|
|
1952
|
+
} finally {
|
|
1953
|
+
if (descriptor !== void 0) closeSync(descriptor);
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
function loadLiveBridgeConfig(option, { configDirectory, ingestToken } = {}) {
|
|
1957
|
+
const values = option && typeof option === "object" ? option : {};
|
|
1958
|
+
const enabled2 = (typeof option === "boolean" ? option : values.enabled) ?? process.env.BACKEER_LIVE_ENABLED;
|
|
1959
|
+
if (enabled2 !== true && enabled2 !== "true" && enabled2 !== "1") return { enabled: false };
|
|
1960
|
+
try {
|
|
1961
|
+
const rawPort = values.port ?? process.env.BACKEER_LIVE_PORT ?? 43871;
|
|
1962
|
+
const port = Number(rawPort);
|
|
1963
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
1964
|
+
throw new Error("Live control port must be an integer between 1 and 65535");
|
|
1965
|
+
}
|
|
1966
|
+
const tokenFile = values.tokenFile ?? process.env.BACKEER_LIVE_TOKEN_FILE ?? join(configDirectory, "secrets", "backeer-live-token");
|
|
1967
|
+
const token = readCredential(tokenFile);
|
|
1968
|
+
if (token === ingestToken) throw new Error("Live control requires a credential distinct from the Backeer ingest token");
|
|
1969
|
+
return { enabled: true, token, tokenFile, port };
|
|
1970
|
+
} catch (error) {
|
|
1971
|
+
return { enabled: false, error: error.message };
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1933
1975
|
// src/config.js
|
|
1934
1976
|
var DEFAULT_ORIGIN = "https://external-gateway.backeer.com";
|
|
1935
1977
|
function integer(value, fallback, { minimum = 0, maximum = 86400 } = {}) {
|
|
@@ -1971,13 +2013,13 @@ function loadLifecycleConfig(option) {
|
|
|
1971
2013
|
}
|
|
1972
2014
|
function defaultConfigDirectory() {
|
|
1973
2015
|
const base = process.env.XDG_CONFIG_HOME?.trim();
|
|
1974
|
-
return base ?
|
|
2016
|
+
return base ? join2(base, "opencode") : join2(homedir(), ".config", "opencode");
|
|
1975
2017
|
}
|
|
1976
2018
|
function loadToken(tokenFile) {
|
|
1977
2019
|
const environmentToken = process.env.BACKEER_INGEST_TOKEN?.trim();
|
|
1978
2020
|
if (environmentToken) return { token: environmentToken, source: "environment" };
|
|
1979
2021
|
try {
|
|
1980
|
-
const token =
|
|
2022
|
+
const token = readFileSync2(tokenFile, "utf8").trim();
|
|
1981
2023
|
return { token, source: token ? "file" : "missing" };
|
|
1982
2024
|
} catch {
|
|
1983
2025
|
return { token: "", source: "missing" };
|
|
@@ -1990,7 +2032,7 @@ function loadConfig(options = {}) {
|
|
|
1990
2032
|
if (origin.protocol !== "https:") {
|
|
1991
2033
|
throw new Error("Backeer origin must use HTTPS");
|
|
1992
2034
|
}
|
|
1993
|
-
const tokenFile = options.tokenFile ?? process.env.BACKEER_TOKEN_FILE ??
|
|
2035
|
+
const tokenFile = options.tokenFile ?? process.env.BACKEER_TOKEN_FILE ?? join2(defaultConfigDirectory(), "secrets", "backeer-ingest-token");
|
|
1994
2036
|
const loadedToken = loadToken(tokenFile);
|
|
1995
2037
|
return {
|
|
1996
2038
|
origin,
|
|
@@ -2000,6 +2042,10 @@ function loadConfig(options = {}) {
|
|
|
2000
2042
|
token: loadedToken.token,
|
|
2001
2043
|
tokenSource: loadedToken.source,
|
|
2002
2044
|
tokenFile,
|
|
2045
|
+
liveBridge: loadLiveBridgeConfig(options.liveBridge, {
|
|
2046
|
+
configDirectory: defaultConfigDirectory(),
|
|
2047
|
+
ingestToken: loadedToken.token
|
|
2048
|
+
}),
|
|
2003
2049
|
permissions: enabled(
|
|
2004
2050
|
options.permissions ?? process.env.BACKEER_NOTIFY_PERMISSIONS
|
|
2005
2051
|
),
|
|
@@ -2337,7 +2383,7 @@ async function publishTodoCompletion(config, reporter, request, eventID, client,
|
|
|
2337
2383
|
import {
|
|
2338
2384
|
chmodSync,
|
|
2339
2385
|
mkdirSync,
|
|
2340
|
-
readFileSync as
|
|
2386
|
+
readFileSync as readFileSync3,
|
|
2341
2387
|
renameSync,
|
|
2342
2388
|
statSync as statSync2,
|
|
2343
2389
|
writeFileSync
|
|
@@ -2349,7 +2395,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2349
2395
|
import { statSync } from "node:fs";
|
|
2350
2396
|
|
|
2351
2397
|
// src/updates.js
|
|
2352
|
-
var PLUGIN_VERSION = "0.
|
|
2398
|
+
var PLUGIN_VERSION = "0.5.2";
|
|
2353
2399
|
var PACKAGE_NAME = "backeer-opencode";
|
|
2354
2400
|
var NPM_REGISTRY_URL = "https://registry.npmjs.org";
|
|
2355
2401
|
var CHECK_TIMEOUT_MS = 3e3;
|
|
@@ -2840,7 +2886,7 @@ function rotateTokenFromFile(config, sourcePathValue) {
|
|
|
2840
2886
|
if ((sourceStats.mode & 63) !== 0) {
|
|
2841
2887
|
throw new Error("The token source must not be readable by group or other users");
|
|
2842
2888
|
}
|
|
2843
|
-
const token =
|
|
2889
|
+
const token = readFileSync3(sourcePath, "utf8").trim();
|
|
2844
2890
|
if (!token || /\s/.test(token)) {
|
|
2845
2891
|
throw new Error("The token source must contain exactly one non-empty token");
|
|
2846
2892
|
}
|
|
@@ -3367,6 +3413,517 @@ function createLifecycleController({
|
|
|
3367
3413
|
};
|
|
3368
3414
|
}
|
|
3369
3415
|
|
|
3416
|
+
// src/live-bridge.js
|
|
3417
|
+
import { createServer } from "node:http";
|
|
3418
|
+
import { randomUUID as randomUUID3, timingSafeEqual } from "node:crypto";
|
|
3419
|
+
|
|
3420
|
+
// src/live-bridge-client.js
|
|
3421
|
+
function createV2LiveClient(ctx) {
|
|
3422
|
+
const known = /* @__PURE__ */ new Map();
|
|
3423
|
+
let disposed = false;
|
|
3424
|
+
async function get(id) {
|
|
3425
|
+
const info = await ctx.session.get({ sessionID: id });
|
|
3426
|
+
if (disposed) return info;
|
|
3427
|
+
known.set(id, info);
|
|
3428
|
+
if (known.size > 100) known.delete(known.keys().next().value);
|
|
3429
|
+
return info;
|
|
3430
|
+
}
|
|
3431
|
+
return {
|
|
3432
|
+
async list() {
|
|
3433
|
+
if (typeof ctx.session.list === "function") {
|
|
3434
|
+
return ctx.session.list({ directory: ctx.location.directory, limit: 100, order: "desc" });
|
|
3435
|
+
}
|
|
3436
|
+
const refreshed = [];
|
|
3437
|
+
for (const id of known.keys()) {
|
|
3438
|
+
try {
|
|
3439
|
+
refreshed.push(await get(id));
|
|
3440
|
+
} catch {
|
|
3441
|
+
known.delete(id);
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
return refreshed;
|
|
3445
|
+
},
|
|
3446
|
+
get,
|
|
3447
|
+
prompt: (id, text) => ctx.session.prompt({ sessionID: id, text, delivery: "queue" }),
|
|
3448
|
+
dispose: () => {
|
|
3449
|
+
disposed = true;
|
|
3450
|
+
known.clear();
|
|
3451
|
+
}
|
|
3452
|
+
};
|
|
3453
|
+
}
|
|
3454
|
+
function unwrap(value) {
|
|
3455
|
+
if (value?.error) throw new Error("OpenCode request failed");
|
|
3456
|
+
return value?.data ?? value;
|
|
3457
|
+
}
|
|
3458
|
+
function createV1LiveClient(client, directory) {
|
|
3459
|
+
return {
|
|
3460
|
+
list: async () => unwrap(await client.session.list({ query: { directory }, throwOnError: true })),
|
|
3461
|
+
get: async (id) => unwrap(await client.session.get({ path: { id }, query: { directory }, throwOnError: true })),
|
|
3462
|
+
async prompt(id, text) {
|
|
3463
|
+
if (typeof client.session.promptAsync !== "function") {
|
|
3464
|
+
throw Object.assign(new Error("OpenCode asynchronous prompts are unavailable"), { status: 501 });
|
|
3465
|
+
}
|
|
3466
|
+
return unwrap(await client.session.promptAsync({
|
|
3467
|
+
path: { id },
|
|
3468
|
+
query: { directory },
|
|
3469
|
+
body: { parts: [{ type: "text", text }] },
|
|
3470
|
+
throwOnError: true
|
|
3471
|
+
}));
|
|
3472
|
+
}
|
|
3473
|
+
};
|
|
3474
|
+
}
|
|
3475
|
+
|
|
3476
|
+
// src/live-connection.js
|
|
3477
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
3478
|
+
var LIVE_RECONNECT_MS = 3e3;
|
|
3479
|
+
var LIVE_HEARTBEAT_MS = 15e3;
|
|
3480
|
+
function createLiveConnectionNotifier(config, reporter) {
|
|
3481
|
+
const epoch = randomUUID2();
|
|
3482
|
+
const controller = new AbortController();
|
|
3483
|
+
let last, sequence = 0, pending, task;
|
|
3484
|
+
async function drain() {
|
|
3485
|
+
while (pending && !controller.signal.aborted) {
|
|
3486
|
+
const event = pending;
|
|
3487
|
+
pending = void 0;
|
|
3488
|
+
try {
|
|
3489
|
+
await postJsonWithRetry(config, config.eventsEndpoint, event, { signal: controller.signal });
|
|
3490
|
+
} catch {
|
|
3491
|
+
if (!controller.signal.aborted) reporter?.error("Live connection notification could not be sent");
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
return {
|
|
3496
|
+
update(connected) {
|
|
3497
|
+
if (controller.signal.aborted || connected === last) return;
|
|
3498
|
+
const initial = last === void 0;
|
|
3499
|
+
last = connected;
|
|
3500
|
+
if (initial || !config.token || !config.eventsEndpoint) return;
|
|
3501
|
+
pending = {
|
|
3502
|
+
type: "application",
|
|
3503
|
+
severity: connected ? "info" : "warning",
|
|
3504
|
+
title: connected ? "OpenCode ba\u011Flant\u0131s\u0131 yeniden kuruldu" : "OpenCode ba\u011Flant\u0131s\u0131 kesildi",
|
|
3505
|
+
body: connected ? "Canl\u0131 takip yeniden ba\u011Fland\u0131. Sunucudaki g\xF6rev otomatik olarak yeniden ba\u015Flat\u0131lmad\u0131." : "Canl\u0131 takip ba\u011Flant\u0131s\u0131 kesildi. Yeniden ba\u011Flanma her 3 saniyede denenecek; sunucudaki g\xF6rev durdurulmad\u0131.",
|
|
3506
|
+
payload: [{ key: "Connection", value: connected ? "reconnected" : "disconnected" }],
|
|
3507
|
+
externalId: `opencode:live:${epoch}:${++sequence}`,
|
|
3508
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3509
|
+
notify: true,
|
|
3510
|
+
direction: "in"
|
|
3511
|
+
};
|
|
3512
|
+
task ??= drain().finally(() => {
|
|
3513
|
+
task = void 0;
|
|
3514
|
+
});
|
|
3515
|
+
},
|
|
3516
|
+
async dispose() {
|
|
3517
|
+
controller.abort();
|
|
3518
|
+
pending = void 0;
|
|
3519
|
+
await task;
|
|
3520
|
+
}
|
|
3521
|
+
};
|
|
3522
|
+
}
|
|
3523
|
+
|
|
3524
|
+
// src/live-bridge-events.js
|
|
3525
|
+
var ID = /^[A-Za-z0-9_-]{1,160}$/;
|
|
3526
|
+
var validBridgeID = (value) => typeof value === "string" && ID.test(value);
|
|
3527
|
+
function createBridgeSanitizer(secrets = []) {
|
|
3528
|
+
const known = secrets.filter((value) => typeof value === "string" && value.length > 0);
|
|
3529
|
+
const assistants = /* @__PURE__ */ new Map();
|
|
3530
|
+
function clipped(value, max = 12e3) {
|
|
3531
|
+
if (typeof value !== "string") return { value: "", truncated: false };
|
|
3532
|
+
let result = value.slice(0, 64e3);
|
|
3533
|
+
for (const secret of known) result = result.split(secret).join("[REDACTED]");
|
|
3534
|
+
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]");
|
|
3535
|
+
return { value: result.slice(0, max), truncated: value.length > 64e3 || result.length > max };
|
|
3536
|
+
}
|
|
3537
|
+
function text(value, max) {
|
|
3538
|
+
return clipped(value, max).value;
|
|
3539
|
+
}
|
|
3540
|
+
function content(key, value) {
|
|
3541
|
+
const result = clipped(value);
|
|
3542
|
+
return { [key]: result.value, ...result.truncated ? { truncated: true } : {} };
|
|
3543
|
+
}
|
|
3544
|
+
function sanitize(event) {
|
|
3545
|
+
const p = event?.properties ?? event?.data ?? {};
|
|
3546
|
+
const sessionID = p.sessionID ?? p.info?.sessionID ?? p.part?.sessionID;
|
|
3547
|
+
if (!validBridgeID(sessionID) || sessionID === "global") return void 0;
|
|
3548
|
+
const base = { type: event.type, sessionID, data: {} };
|
|
3549
|
+
if (event.type === "message.updated") {
|
|
3550
|
+
const info = p.info;
|
|
3551
|
+
if (!validBridgeID(info?.id)) return void 0;
|
|
3552
|
+
if (info.role === "assistant") {
|
|
3553
|
+
assistants.set(info.id, sessionID);
|
|
3554
|
+
if (assistants.size > 256) assistants.delete(assistants.keys().next().value);
|
|
3555
|
+
}
|
|
3556
|
+
return info.role === "assistant" ? { ...base, data: {
|
|
3557
|
+
messageID: info.id,
|
|
3558
|
+
completed: !!info.time?.completed,
|
|
3559
|
+
failed: !!info.error
|
|
3560
|
+
} } : void 0;
|
|
3561
|
+
}
|
|
3562
|
+
if (event.type === "message.part.updated") {
|
|
3563
|
+
const part = p.part;
|
|
3564
|
+
if (assistants.get(part?.messageID) !== sessionID) return void 0;
|
|
3565
|
+
if (part.type === "text") return { ...base, type: "assistant.text", data: {
|
|
3566
|
+
messageID: part.messageID,
|
|
3567
|
+
...content("text", part.text),
|
|
3568
|
+
replace: true
|
|
3569
|
+
} };
|
|
3570
|
+
if (part.type !== "tool") return void 0;
|
|
3571
|
+
const state = part.state ?? {};
|
|
3572
|
+
return { ...base, type: "tool.status", data: {
|
|
3573
|
+
tool: text(part.tool, 80),
|
|
3574
|
+
status: text(state.status, 40),
|
|
3575
|
+
...state.status === "completed" ? content("output", state.output) : {},
|
|
3576
|
+
...state.status === "error" ? { failed: true } : {}
|
|
3577
|
+
} };
|
|
3578
|
+
}
|
|
3579
|
+
if (event.type === "session.text.ended") return { ...base, type: "assistant.text", data: {
|
|
3580
|
+
messageID: validBridgeID(p.assistantMessageID) ? p.assistantMessageID : void 0,
|
|
3581
|
+
...content("text", p.text),
|
|
3582
|
+
replace: true
|
|
3583
|
+
} };
|
|
3584
|
+
if (["session.tool.called", "session.tool.success", "session.tool.failed"].includes(event.type)) {
|
|
3585
|
+
return { ...base, type: "tool.status", data: {
|
|
3586
|
+
status: event.type.split(".").at(-1),
|
|
3587
|
+
...event.type === "session.tool.success" ? content(
|
|
3588
|
+
"output",
|
|
3589
|
+
(p.content ?? []).filter((item) => item?.type === "text").map((item) => item.text).join("\n")
|
|
3590
|
+
) : {}
|
|
3591
|
+
} };
|
|
3592
|
+
}
|
|
3593
|
+
if (["permission.asked", "question.asked", "question.v2.asked"].includes(event.type)) {
|
|
3594
|
+
return { ...base, type: "approval.waiting", data: { kind: event.type.split(".")[0] } };
|
|
3595
|
+
}
|
|
3596
|
+
if (["permission.replied", "question.replied", "question.v2.replied", "question.rejected", "question.v2.rejected"].includes(event.type)) {
|
|
3597
|
+
return { ...base, type: "approval.settled", data: { kind: event.type.split(".")[0] } };
|
|
3598
|
+
}
|
|
3599
|
+
if (event.type === "session.status") return { ...base, data: {
|
|
3600
|
+
status: ["idle", "busy", "retry"].includes(p.status?.type) ? p.status.type : "unknown"
|
|
3601
|
+
} };
|
|
3602
|
+
if (["session.idle", "session.error", "session.deleted"].includes(event.type)) return base;
|
|
3603
|
+
return void 0;
|
|
3604
|
+
}
|
|
3605
|
+
return { sanitize, text, clear: () => assistants.clear() };
|
|
3606
|
+
}
|
|
3607
|
+
|
|
3608
|
+
// src/live-bridge.js
|
|
3609
|
+
var LIMITS = { events: 256, bytes: 2e6, clients: 8, commands: 1024, commandTTL: 864e5, body: 32768, requests: 16 };
|
|
3610
|
+
function failure(status, code) {
|
|
3611
|
+
return Object.assign(new Error(code), { status, code });
|
|
3612
|
+
}
|
|
3613
|
+
function json(response, status, value) {
|
|
3614
|
+
if (!response.destroyed && !response.headersSent) {
|
|
3615
|
+
response.writeHead(status, { "content-type": "application/json", "cache-control": "no-store" });
|
|
3616
|
+
response.end(JSON.stringify(value));
|
|
3617
|
+
}
|
|
3618
|
+
}
|
|
3619
|
+
async function readBody(request) {
|
|
3620
|
+
const chunks = [];
|
|
3621
|
+
let bytes = 0;
|
|
3622
|
+
for await (const chunk of request) {
|
|
3623
|
+
bytes += chunk.length;
|
|
3624
|
+
if (bytes > LIMITS.body) throw failure(413, "body_too_large");
|
|
3625
|
+
chunks.push(chunk);
|
|
3626
|
+
}
|
|
3627
|
+
try {
|
|
3628
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
3629
|
+
} catch {
|
|
3630
|
+
throw failure(400, "invalid_json");
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3633
|
+
async function createLiveBridge({ config, client, directory, reporter }) {
|
|
3634
|
+
const options = config.liveBridge;
|
|
3635
|
+
if (!options?.enabled) return { handle() {
|
|
3636
|
+
}, async dispose() {
|
|
3637
|
+
}, address: void 0 };
|
|
3638
|
+
const token = options.token;
|
|
3639
|
+
if (typeof token !== "string" || token.length < 32 || token === config.token) {
|
|
3640
|
+
throw new Error("Live bridge requires a separate credential of at least 32 characters");
|
|
3641
|
+
}
|
|
3642
|
+
const api = client.liveBridge ?? createV1LiveClient(client, directory);
|
|
3643
|
+
const connectionNotifications = createLiveConnectionNotifier(config, reporter);
|
|
3644
|
+
const sanitizer = createBridgeSanitizer([token, config.token]);
|
|
3645
|
+
const epoch = randomUUID3();
|
|
3646
|
+
const events = [], streams = /* @__PURE__ */ new Set(), commands = /* @__PURE__ */ new Map(), states = /* @__PURE__ */ new Map();
|
|
3647
|
+
let sequence = 0, bytes = 0, active = 0, disposed = false, disposeTask;
|
|
3648
|
+
let lookups = 0;
|
|
3649
|
+
const lookupWaiters = /* @__PURE__ */ new Set();
|
|
3650
|
+
const cursor = () => `${epoch}:${sequence}`;
|
|
3651
|
+
function lookup(id) {
|
|
3652
|
+
if (disposed) return Promise.reject(failure(503, "bridge_closed"));
|
|
3653
|
+
if (lookups >= 16) return Promise.reject(failure(429, "session_lookup_busy"));
|
|
3654
|
+
lookups++;
|
|
3655
|
+
return new Promise((resolve2, reject) => {
|
|
3656
|
+
let finished = false;
|
|
3657
|
+
const finish = (complete, value) => {
|
|
3658
|
+
if (finished) return;
|
|
3659
|
+
finished = true;
|
|
3660
|
+
clearTimeout(timer);
|
|
3661
|
+
lookupWaiters.delete(cancel);
|
|
3662
|
+
complete(value);
|
|
3663
|
+
};
|
|
3664
|
+
const cancel = () => finish(reject, failure(503, "bridge_closed"));
|
|
3665
|
+
const timer = setTimeout(() => finish(reject, failure(504, "session_lookup_timed_out")), 1e3);
|
|
3666
|
+
lookupWaiters.add(cancel);
|
|
3667
|
+
Promise.resolve().then(() => api.get(id)).then(
|
|
3668
|
+
(value) => {
|
|
3669
|
+
lookups--;
|
|
3670
|
+
finish(resolve2, value);
|
|
3671
|
+
},
|
|
3672
|
+
(error) => {
|
|
3673
|
+
lookups--;
|
|
3674
|
+
finish(reject, error);
|
|
3675
|
+
}
|
|
3676
|
+
);
|
|
3677
|
+
});
|
|
3678
|
+
}
|
|
3679
|
+
async function session(id) {
|
|
3680
|
+
if (!validBridgeID(id) || id === "global") throw failure(400, "invalid_session");
|
|
3681
|
+
let info;
|
|
3682
|
+
try {
|
|
3683
|
+
info = await lookup(id);
|
|
3684
|
+
} catch (error) {
|
|
3685
|
+
if (["session_lookup_busy", "session_lookup_timed_out", "bridge_closed"].includes(error.code)) throw error;
|
|
3686
|
+
throw failure(404, "session_not_found");
|
|
3687
|
+
}
|
|
3688
|
+
if (info?.id !== id || (info.location?.directory ?? info.directory) !== directory) {
|
|
3689
|
+
throw failure(404, "session_not_found");
|
|
3690
|
+
}
|
|
3691
|
+
return {
|
|
3692
|
+
id,
|
|
3693
|
+
title: sanitizer.text(info.title, 240),
|
|
3694
|
+
...validBridgeID(info.parentID) ? { parentID: info.parentID } : {},
|
|
3695
|
+
status: states.get(id) ?? "unknown"
|
|
3696
|
+
};
|
|
3697
|
+
}
|
|
3698
|
+
function removeStream(stream) {
|
|
3699
|
+
const removed = streams.delete(stream);
|
|
3700
|
+
clearInterval(stream.heartbeat);
|
|
3701
|
+
stream.queue.length = 0;
|
|
3702
|
+
stream.queuedBytes = 0;
|
|
3703
|
+
if (removed && !streams.size && !disposed) connectionNotifications.update(false);
|
|
3704
|
+
}
|
|
3705
|
+
function write(stream, value) {
|
|
3706
|
+
if (!streams.has(stream)) return;
|
|
3707
|
+
if (stream.blocked) {
|
|
3708
|
+
stream.queue.push(value);
|
|
3709
|
+
stream.queuedBytes += Buffer.byteLength(value);
|
|
3710
|
+
if (stream.queuedBytes > LIMITS.bytes) {
|
|
3711
|
+
removeStream(stream);
|
|
3712
|
+
stream.response.destroy();
|
|
3713
|
+
}
|
|
3714
|
+
return;
|
|
3715
|
+
}
|
|
3716
|
+
if (!stream.response.write(value)) {
|
|
3717
|
+
stream.blocked = true;
|
|
3718
|
+
}
|
|
3719
|
+
}
|
|
3720
|
+
async function handle(event) {
|
|
3721
|
+
if (disposed) return;
|
|
3722
|
+
const safe = sanitizer.sanitize(event);
|
|
3723
|
+
if (!safe) return;
|
|
3724
|
+
try {
|
|
3725
|
+
await session(safe.sessionID);
|
|
3726
|
+
} catch {
|
|
3727
|
+
return;
|
|
3728
|
+
}
|
|
3729
|
+
if (disposed) return;
|
|
3730
|
+
if (safe.type === "session.status") states.set(safe.sessionID, safe.data.status);
|
|
3731
|
+
if (safe.type === "session.idle") states.set(safe.sessionID, "idle");
|
|
3732
|
+
if (safe.type === "session.deleted") states.delete(safe.sessionID);
|
|
3733
|
+
if (states.size > 256) states.delete(states.keys().next().value);
|
|
3734
|
+
const record = { id: `${epoch}:${++sequence}`, ...safe, timestamp: Date.now() };
|
|
3735
|
+
let frame = `id: ${record.id}
|
|
3736
|
+
event: backeer.event
|
|
3737
|
+
data: ${JSON.stringify(record)}
|
|
3738
|
+
|
|
3739
|
+
`;
|
|
3740
|
+
if (Buffer.byteLength(frame) > 32768) {
|
|
3741
|
+
record.data = { truncated: true };
|
|
3742
|
+
frame = `id: ${record.id}
|
|
3743
|
+
event: backeer.event
|
|
3744
|
+
data: ${JSON.stringify(record)}
|
|
3745
|
+
|
|
3746
|
+
`;
|
|
3747
|
+
}
|
|
3748
|
+
const size = Buffer.byteLength(frame);
|
|
3749
|
+
events.push({ sequence, sessionID: record.sessionID, frame, size });
|
|
3750
|
+
bytes += size;
|
|
3751
|
+
while (events.length > LIMITS.events || bytes > LIMITS.bytes) bytes -= events.shift().size;
|
|
3752
|
+
for (const stream of streams) if (!stream.sessionID || stream.sessionID === record.sessionID) write(stream, frame);
|
|
3753
|
+
}
|
|
3754
|
+
function authorized(request) {
|
|
3755
|
+
if (request.headers.origin || request.headers["sec-fetch-site"]) return false;
|
|
3756
|
+
const expected = Buffer.from(`Bearer ${token}`);
|
|
3757
|
+
const actual = Buffer.from(request.headers.authorization ?? "");
|
|
3758
|
+
return actual.length === expected.length && timingSafeEqual(actual, expected);
|
|
3759
|
+
}
|
|
3760
|
+
function parseCursor(value) {
|
|
3761
|
+
if (!value) return { after: 0 };
|
|
3762
|
+
const match = /^([a-f0-9-]{36}):(\d{1,16})$/.exec(value);
|
|
3763
|
+
if (!match) throw failure(400, "invalid_cursor");
|
|
3764
|
+
if (match[1] !== epoch) return { gap: "stream_restarted" };
|
|
3765
|
+
const after = Number(match[2]);
|
|
3766
|
+
if (!Number.isSafeInteger(after) || after > sequence) throw failure(400, "invalid_cursor");
|
|
3767
|
+
if (events.length && after < events[0].sequence - 1) return { gap: "cursor_expired" };
|
|
3768
|
+
return { after };
|
|
3769
|
+
}
|
|
3770
|
+
async function route(request, response) {
|
|
3771
|
+
if (!authorized(request)) return json(response, 401, { error: "unauthorized" });
|
|
3772
|
+
if (active >= LIMITS.requests) return json(response, 429, { error: "bridge_busy" });
|
|
3773
|
+
active++;
|
|
3774
|
+
const timeout = setTimeout(() => json(response, 504, { error: "request_timed_out" }), 15e3);
|
|
3775
|
+
timeout.unref();
|
|
3776
|
+
try {
|
|
3777
|
+
const url = new URL(request.url, "http://127.0.0.1");
|
|
3778
|
+
if (url.username || url.password) throw failure(400, "invalid_url");
|
|
3779
|
+
if (request.method === "GET" && url.pathname === "/v1/sessions") {
|
|
3780
|
+
const result = await api.list();
|
|
3781
|
+
const values = Array.isArray(result) ? result : result?.data;
|
|
3782
|
+
if (!Array.isArray(values)) throw failure(502, "invalid_session_list");
|
|
3783
|
+
const own = values.slice(0, 100).filter((info) => validBridgeID(info?.id) && (info.location?.directory ?? info.directory) === directory);
|
|
3784
|
+
return json(response, 200, { sessions: own.map((info) => ({
|
|
3785
|
+
id: info.id,
|
|
3786
|
+
title: sanitizer.text(info.title, 240),
|
|
3787
|
+
status: states.get(info.id) ?? "unknown",
|
|
3788
|
+
...validBridgeID(info.parentID) ? { parentID: info.parentID } : {}
|
|
3789
|
+
})), cursor: cursor() });
|
|
3790
|
+
}
|
|
3791
|
+
if (request.method === "GET" && url.pathname === "/v1/events") {
|
|
3792
|
+
const selected = url.searchParams.get("sessionID") || void 0;
|
|
3793
|
+
if (selected) await session(selected);
|
|
3794
|
+
const parsed = parseCursor(url.searchParams.get("after") ?? request.headers["last-event-id"]);
|
|
3795
|
+
if (response.headersSent) return;
|
|
3796
|
+
if (streams.size >= LIMITS.clients) throw failure(429, "too_many_streams");
|
|
3797
|
+
clearTimeout(timeout);
|
|
3798
|
+
response.writeHead(200, {
|
|
3799
|
+
"content-type": "text/event-stream",
|
|
3800
|
+
"cache-control": "no-cache, no-transform",
|
|
3801
|
+
"x-accel-buffering": "no",
|
|
3802
|
+
connection: "keep-alive"
|
|
3803
|
+
});
|
|
3804
|
+
if (parsed.gap) {
|
|
3805
|
+
response.end(`event: backeer.gap
|
|
3806
|
+
data: ${JSON.stringify({ reason: parsed.gap, cursor: cursor() })}
|
|
3807
|
+
|
|
3808
|
+
`);
|
|
3809
|
+
return;
|
|
3810
|
+
}
|
|
3811
|
+
const stream = {
|
|
3812
|
+
response,
|
|
3813
|
+
sessionID: selected,
|
|
3814
|
+
heartbeat: void 0,
|
|
3815
|
+
blocked: false,
|
|
3816
|
+
queue: [],
|
|
3817
|
+
queuedBytes: 0
|
|
3818
|
+
};
|
|
3819
|
+
streams.add(stream);
|
|
3820
|
+
connectionNotifications.update(true);
|
|
3821
|
+
request.socket.setTimeout(0);
|
|
3822
|
+
request.socket.setKeepAlive(true, LIVE_HEARTBEAT_MS);
|
|
3823
|
+
response.on("close", () => removeStream(stream));
|
|
3824
|
+
response.on("drain", () => {
|
|
3825
|
+
stream.blocked = false;
|
|
3826
|
+
while (streams.has(stream) && !stream.blocked && stream.queue.length) {
|
|
3827
|
+
const next = stream.queue.shift();
|
|
3828
|
+
stream.queuedBytes -= Buffer.byteLength(next);
|
|
3829
|
+
write(stream, next);
|
|
3830
|
+
}
|
|
3831
|
+
});
|
|
3832
|
+
write(stream, `: connected
|
|
3833
|
+
retry: ${LIVE_RECONNECT_MS}
|
|
3834
|
+
|
|
3835
|
+
`);
|
|
3836
|
+
for (const item of events) {
|
|
3837
|
+
if (!streams.has(stream)) break;
|
|
3838
|
+
if (item.sequence > parsed.after && (!selected || selected === item.sessionID)) write(stream, item.frame);
|
|
3839
|
+
}
|
|
3840
|
+
if (streams.has(stream)) {
|
|
3841
|
+
stream.heartbeat = setInterval(() => write(stream, ": heartbeat\n\n"), LIVE_HEARTBEAT_MS);
|
|
3842
|
+
stream.heartbeat.unref();
|
|
3843
|
+
}
|
|
3844
|
+
return;
|
|
3845
|
+
}
|
|
3846
|
+
const match = /^\/v1\/sessions\/([A-Za-z0-9_-]{1,160})(\/instructions)?$/.exec(url.pathname);
|
|
3847
|
+
if (!match) throw failure(404, "route_not_found");
|
|
3848
|
+
if (request.method === "GET" && !match[2]) return json(response, 200, await session(match[1]));
|
|
3849
|
+
if (request.method !== "POST" || !match[2]) throw failure(405, "method_not_allowed");
|
|
3850
|
+
if (!/^application\/json(?:;|$)/i.test(request.headers["content-type"] ?? "")) throw failure(415, "json_required");
|
|
3851
|
+
const body = await readBody(request);
|
|
3852
|
+
if (!body || !validBridgeID(body.requestId) || typeof body.prompt !== "string" || !body.prompt.trim() || body.prompt.length > 16e3 || Object.keys(body).some((key) => !["requestId", "prompt"].includes(key))) throw failure(400, "invalid_instruction");
|
|
3853
|
+
await session(match[1]);
|
|
3854
|
+
if (response.headersSent || disposed) return;
|
|
3855
|
+
for (const [id, command2] of commands) {
|
|
3856
|
+
if (command2.settled && Date.now() - command2.created >= LIMITS.commandTTL) commands.delete(id);
|
|
3857
|
+
}
|
|
3858
|
+
const existing = commands.get(body.requestId);
|
|
3859
|
+
if (existing) {
|
|
3860
|
+
if (existing.sessionID !== match[1] || existing.prompt !== body.prompt) throw failure(409, "request_id_conflict");
|
|
3861
|
+
await existing.task;
|
|
3862
|
+
return json(response, 200, existing.result);
|
|
3863
|
+
}
|
|
3864
|
+
if (commands.size >= LIMITS.commands) throw failure(429, "command_window_full");
|
|
3865
|
+
const command = {
|
|
3866
|
+
sessionID: match[1],
|
|
3867
|
+
prompt: body.prompt,
|
|
3868
|
+
created: Date.now(),
|
|
3869
|
+
settled: false,
|
|
3870
|
+
result: { requestId: body.requestId, sessionID: match[1], status: "accepted" },
|
|
3871
|
+
task: void 0
|
|
3872
|
+
};
|
|
3873
|
+
commands.set(body.requestId, command);
|
|
3874
|
+
command.task = Promise.resolve().then(() => api.prompt(match[1], body.prompt)).finally(() => {
|
|
3875
|
+
command.settled = true;
|
|
3876
|
+
});
|
|
3877
|
+
await command.task;
|
|
3878
|
+
return json(response, 202, command.result);
|
|
3879
|
+
} catch (error) {
|
|
3880
|
+
json(response, error.status ?? 502, { error: error.code ?? "opencode_request_failed" });
|
|
3881
|
+
} finally {
|
|
3882
|
+
active--;
|
|
3883
|
+
clearTimeout(timeout);
|
|
3884
|
+
}
|
|
3885
|
+
}
|
|
3886
|
+
const server = createServer((request, response) => {
|
|
3887
|
+
void route(request, response);
|
|
3888
|
+
});
|
|
3889
|
+
server.requestTimeout = 15e3;
|
|
3890
|
+
server.headersTimeout = 1e4;
|
|
3891
|
+
server.keepAliveTimeout = 5e3;
|
|
3892
|
+
await new Promise((resolve2, reject) => {
|
|
3893
|
+
server.once("error", reject);
|
|
3894
|
+
server.listen(options.port ?? 43871, "127.0.0.1", () => {
|
|
3895
|
+
server.off("error", reject);
|
|
3896
|
+
resolve2();
|
|
3897
|
+
});
|
|
3898
|
+
});
|
|
3899
|
+
server.on("error", () => {
|
|
3900
|
+
});
|
|
3901
|
+
return {
|
|
3902
|
+
handle,
|
|
3903
|
+
address: server.address(),
|
|
3904
|
+
dispose() {
|
|
3905
|
+
disposeTask ??= new Promise((resolve2) => {
|
|
3906
|
+
disposed = true;
|
|
3907
|
+
for (const cancel of lookupWaiters) cancel();
|
|
3908
|
+
for (const stream of streams) {
|
|
3909
|
+
removeStream(stream);
|
|
3910
|
+
stream.response.destroy();
|
|
3911
|
+
}
|
|
3912
|
+
server.close(() => {
|
|
3913
|
+
void connectionNotifications.dispose().then(resolve2);
|
|
3914
|
+
});
|
|
3915
|
+
server.closeAllConnections?.();
|
|
3916
|
+
events.length = 0;
|
|
3917
|
+
states.clear();
|
|
3918
|
+
commands.clear();
|
|
3919
|
+
sanitizer.clear();
|
|
3920
|
+
api.dispose?.();
|
|
3921
|
+
});
|
|
3922
|
+
return disposeTask;
|
|
3923
|
+
}
|
|
3924
|
+
};
|
|
3925
|
+
}
|
|
3926
|
+
|
|
3370
3927
|
// src/permission-command.js
|
|
3371
3928
|
var ALLOWED_DECISIONS = /* @__PURE__ */ new Set(["once", "always", "reject"]);
|
|
3372
3929
|
var DECISION_LABELS = {
|
|
@@ -3941,6 +4498,13 @@ async function BackeerPlugin({ client, directory, feedback, v2 = false }, option
|
|
|
3941
4498
|
}
|
|
3942
4499
|
const reporter = createOpenCodeReporter(client, config.logLevel);
|
|
3943
4500
|
const conversation = feedback ?? createConversationFeedback({ client, reporter });
|
|
4501
|
+
if (config.liveBridge.error) reporter.error("Live control disabled", config.liveBridge.error);
|
|
4502
|
+
const liveBridge = await createLiveBridge({ config, client, directory, reporter }).catch((error) => {
|
|
4503
|
+
reporter.error("Live control unavailable", error);
|
|
4504
|
+
return { async handle() {
|
|
4505
|
+
}, async dispose() {
|
|
4506
|
+
} };
|
|
4507
|
+
});
|
|
3944
4508
|
const permissionController = createPermissionController({
|
|
3945
4509
|
config,
|
|
3946
4510
|
reporter,
|
|
@@ -4048,6 +4612,38 @@ async function BackeerPlugin({ client, directory, feedback, v2 = false }, option
|
|
|
4048
4612
|
conversation.error(identity.sessionID, "Question could not be sent");
|
|
4049
4613
|
});
|
|
4050
4614
|
}
|
|
4615
|
+
async function handleNotificationEvent(event) {
|
|
4616
|
+
if (!config.token) return;
|
|
4617
|
+
conversation.handle(event);
|
|
4618
|
+
lifecycleController.handle(event);
|
|
4619
|
+
if (config.permissions && event.type === "permission.asked") {
|
|
4620
|
+
await handlePermissionAsked(event);
|
|
4621
|
+
return;
|
|
4622
|
+
}
|
|
4623
|
+
if (config.permissions && event.type === "permission.replied") {
|
|
4624
|
+
permissionController.resolveLocally(
|
|
4625
|
+
event.properties.requestID ?? event.properties.id
|
|
4626
|
+
);
|
|
4627
|
+
return;
|
|
4628
|
+
}
|
|
4629
|
+
if (config.todos && event.type === "todo.updated") {
|
|
4630
|
+
await handleTodoUpdated(event);
|
|
4631
|
+
return;
|
|
4632
|
+
}
|
|
4633
|
+
if (config.questions && (event.type === "question.asked" || event.type === "question.v2.asked")) {
|
|
4634
|
+
await handleQuestionAsked(event);
|
|
4635
|
+
return;
|
|
4636
|
+
}
|
|
4637
|
+
if (config.questions && ["question.replied", "question.v2.replied", "question.rejected", "question.v2.rejected"].includes(event.type)) {
|
|
4638
|
+
const requestID = event.properties.requestID ?? event.properties.id;
|
|
4639
|
+
if (typeof requestID === "string") {
|
|
4640
|
+
permissionController.resolveQuestionLocally(
|
|
4641
|
+
requestID,
|
|
4642
|
+
event.type.endsWith("rejected") ? "rejected" : "answered"
|
|
4643
|
+
);
|
|
4644
|
+
}
|
|
4645
|
+
}
|
|
4646
|
+
}
|
|
4051
4647
|
return {
|
|
4052
4648
|
config: async (opencodeConfig) => {
|
|
4053
4649
|
registerBackeerCommand(opencodeConfig);
|
|
@@ -4055,38 +4651,10 @@ async function BackeerPlugin({ client, directory, feedback, v2 = false }, option
|
|
|
4055
4651
|
"command.execute.before": handleBackeerCommand,
|
|
4056
4652
|
"chat.message": handleUpdateNotice,
|
|
4057
4653
|
event: async ({ event }) => {
|
|
4058
|
-
|
|
4059
|
-
conversation.handle(event);
|
|
4060
|
-
lifecycleController.handle(event);
|
|
4061
|
-
if (config.permissions && event.type === "permission.asked") {
|
|
4062
|
-
await handlePermissionAsked(event);
|
|
4063
|
-
return;
|
|
4064
|
-
}
|
|
4065
|
-
if (config.permissions && event.type === "permission.replied") {
|
|
4066
|
-
permissionController.resolveLocally(
|
|
4067
|
-
event.properties.requestID ?? event.properties.id
|
|
4068
|
-
);
|
|
4069
|
-
return;
|
|
4070
|
-
}
|
|
4071
|
-
if (config.todos && event.type === "todo.updated") {
|
|
4072
|
-
await handleTodoUpdated(event);
|
|
4073
|
-
return;
|
|
4074
|
-
}
|
|
4075
|
-
if (config.questions && (event.type === "question.asked" || event.type === "question.v2.asked")) {
|
|
4076
|
-
await handleQuestionAsked(event);
|
|
4077
|
-
return;
|
|
4078
|
-
}
|
|
4079
|
-
if (config.questions && ["question.replied", "question.v2.replied", "question.rejected", "question.v2.rejected"].includes(event.type)) {
|
|
4080
|
-
const requestID = event.properties.requestID ?? event.properties.id;
|
|
4081
|
-
if (typeof requestID === "string") {
|
|
4082
|
-
permissionController.resolveQuestionLocally(
|
|
4083
|
-
requestID,
|
|
4084
|
-
event.type.endsWith("rejected") ? "rejected" : "answered"
|
|
4085
|
-
);
|
|
4086
|
-
}
|
|
4087
|
-
}
|
|
4654
|
+
await Promise.all([handleNotificationEvent(event), liveBridge.handle(event)]);
|
|
4088
4655
|
},
|
|
4089
4656
|
dispose: async () => {
|
|
4657
|
+
await liveBridge.dispose();
|
|
4090
4658
|
await permissionController.dispose();
|
|
4091
4659
|
lifecycleController.dispose();
|
|
4092
4660
|
permissionPublications.clear();
|
|
@@ -4141,11 +4709,11 @@ function spawnServiceContender2(command, args, env) {
|
|
|
4141
4709
|
}
|
|
4142
4710
|
function contenderPool2(timing) {
|
|
4143
4711
|
const contenders = /* @__PURE__ */ new Set();
|
|
4144
|
-
let
|
|
4712
|
+
let failure2;
|
|
4145
4713
|
let spawnDelay = timing.spawnDelay;
|
|
4146
4714
|
let lastSpawn = 0;
|
|
4147
4715
|
return {
|
|
4148
|
-
failure: () =>
|
|
4716
|
+
failure: () => failure2,
|
|
4149
4717
|
serviceAnswered() {
|
|
4150
4718
|
spawnDelay = timing.spawnDelay;
|
|
4151
4719
|
},
|
|
@@ -4156,7 +4724,7 @@ function contenderPool2(timing) {
|
|
|
4156
4724
|
contenders.delete(item);
|
|
4157
4725
|
}
|
|
4158
4726
|
}
|
|
4159
|
-
|
|
4727
|
+
failure2 = void 0;
|
|
4160
4728
|
lastSpawn = 0;
|
|
4161
4729
|
},
|
|
4162
4730
|
recruitNow() {
|
|
@@ -4164,16 +4732,16 @@ function contenderPool2(timing) {
|
|
|
4164
4732
|
},
|
|
4165
4733
|
reap() {
|
|
4166
4734
|
const finished = [...contenders].filter(contenderFinished2);
|
|
4167
|
-
|
|
4735
|
+
failure2 ??= finished.map(contenderFailure).find((error) => error !== void 0);
|
|
4168
4736
|
if (finished.some((item) => item.child.exitCode === 0))
|
|
4169
4737
|
spawnDelay = Math.min(spawnDelay * 2, timing.maxSpawnDelay);
|
|
4170
4738
|
finished.forEach((item) => contenders.delete(item));
|
|
4171
|
-
return contenders.size === 0 ?
|
|
4739
|
+
return contenders.size === 0 ? failure2 : void 0;
|
|
4172
4740
|
},
|
|
4173
4741
|
shouldRecruit(registered2) {
|
|
4174
4742
|
if (lastSpawn === 0 && registered2)
|
|
4175
4743
|
lastSpawn = Date.now();
|
|
4176
|
-
return
|
|
4744
|
+
return failure2 === void 0 && contenders.size < 2 && Date.now() - lastSpawn >= spawnDelay;
|
|
4177
4745
|
},
|
|
4178
4746
|
add(contender) {
|
|
4179
4747
|
contenders.add(contender);
|
|
@@ -4332,7 +4900,7 @@ function isHandoff(value) {
|
|
|
4332
4900
|
|
|
4333
4901
|
// node_modules/@opencode/client/dist/chunks/service-timing-8ye1ye6f.js
|
|
4334
4902
|
import { homedir as homedir2 } from "node:os";
|
|
4335
|
-
import { join as
|
|
4903
|
+
import { join as join3 } from "node:path";
|
|
4336
4904
|
function decide2(service, options) {
|
|
4337
4905
|
if (!matchesVersion2(service.version, options))
|
|
4338
4906
|
return { _tag: "replace", pty: service.state === "ready" ? "handoff" : "clear" };
|
|
@@ -4348,7 +4916,7 @@ function fail(message) {
|
|
|
4348
4916
|
return { _tag: "fail", error: new Error(message) };
|
|
4349
4917
|
}
|
|
4350
4918
|
function fallback2() {
|
|
4351
|
-
return
|
|
4919
|
+
return join3(process.env["XDG_STATE_HOME"] ?? join3(homedir2(), ".local", "state"), "opencode", "service.json");
|
|
4352
4920
|
}
|
|
4353
4921
|
function same2(left, right) {
|
|
4354
4922
|
return left.id === right.id && left.version === right.version && left.url === right.url && left.pid === right.pid;
|
|
@@ -4663,6 +5231,7 @@ function normalizeV2Event(event) {
|
|
|
4663
5231
|
}
|
|
4664
5232
|
function createV2Client(ctx, resolveFormApi = (sessionID) => getFormApi(ctx, sessionID)) {
|
|
4665
5233
|
return {
|
|
5234
|
+
liveBridge: createV2LiveClient(ctx),
|
|
4666
5235
|
app: {
|
|
4667
5236
|
async log({ body }) {
|
|
4668
5237
|
console.error(JSON.stringify(body));
|
|
@@ -4807,13 +5376,11 @@ ${output.parts.map((part) => part.text).join("\n")}`;
|
|
|
4807
5376
|
}
|
|
4808
5377
|
}));
|
|
4809
5378
|
streamTask = (async () => {
|
|
4810
|
-
let delay2 = 1e3;
|
|
4811
5379
|
while (!controller.signal.aborted) {
|
|
4812
5380
|
try {
|
|
4813
5381
|
for await (const event of ctx.event.subscribe({ signal: controller.signal })) {
|
|
4814
5382
|
if (controller.signal.aborted) break;
|
|
4815
5383
|
if (!await client.ownsEvent(event)) continue;
|
|
4816
|
-
delay2 = 1e3;
|
|
4817
5384
|
for (const normalized of normalizeV2Event(event)) {
|
|
4818
5385
|
await hooks.event({ event: normalized });
|
|
4819
5386
|
}
|
|
@@ -4822,8 +5389,7 @@ ${output.parts.map((part) => part.text).join("\n")}`;
|
|
|
4822
5389
|
if (!controller.signal.aborted) reporter.error("OpenCode event stream disconnected", error);
|
|
4823
5390
|
}
|
|
4824
5391
|
if (!controller.signal.aborted) {
|
|
4825
|
-
await waitForReconnect(controller.signal,
|
|
4826
|
-
delay2 = Math.min(delay2 * 2, 3e4);
|
|
5392
|
+
await waitForReconnect(controller.signal, LIVE_RECONNECT_MS);
|
|
4827
5393
|
}
|
|
4828
5394
|
}
|
|
4829
5395
|
})();
|