@khalilgharbaoui/opencode-claude-code-plugin 0.33.2 → 0.34.1
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/README.md +21 -2
- package/dist/index.d.ts +22 -0
- package/dist/index.js +2216 -1919
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/skills/claude-code-plugin/SKILL.md +5 -2
package/dist/index.js
CHANGED
|
@@ -1306,6 +1306,34 @@ import { execFile } from "child_process";
|
|
|
1306
1306
|
import { promisify } from "util";
|
|
1307
1307
|
var execFileAsync = promisify(execFile);
|
|
1308
1308
|
var cache = /* @__PURE__ */ new Map();
|
|
1309
|
+
var PROBE_TIMEOUT_MS = 5e3;
|
|
1310
|
+
var probeTimeoutMs = PROBE_TIMEOUT_MS;
|
|
1311
|
+
var MAX_DEADLINE_REPROBES = 2;
|
|
1312
|
+
var deadlineReprobes = /* @__PURE__ */ new Map();
|
|
1313
|
+
function killedByDeadline(error) {
|
|
1314
|
+
const failure = error;
|
|
1315
|
+
return failure?.killed === true && typeof failure.signal === "string" && typeof failure.code !== "string";
|
|
1316
|
+
}
|
|
1317
|
+
function forgetDeadlineKill(entries, key, probe, killed) {
|
|
1318
|
+
void probe.then(() => {
|
|
1319
|
+
if (entries.get(key) !== probe) return;
|
|
1320
|
+
if (!killed()) {
|
|
1321
|
+
deadlineReprobes.delete(key);
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
const reprobes = deadlineReprobes.get(key) ?? 0;
|
|
1325
|
+
if (reprobes >= MAX_DEADLINE_REPROBES) {
|
|
1326
|
+
log.warn("claude kept missing the probe deadline; keeping the conservative answer for this process", {
|
|
1327
|
+
key: key.replace("\0", " "),
|
|
1328
|
+
deadlineMs: probeTimeoutMs,
|
|
1329
|
+
attempts: reprobes + 1
|
|
1330
|
+
});
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
deadlineReprobes.set(key, reprobes + 1);
|
|
1334
|
+
entries.delete(key);
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1309
1337
|
var CLI_HYGIENE_ENV_VARS = [
|
|
1310
1338
|
"DISABLE_AUTOUPDATER",
|
|
1311
1339
|
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC"
|
|
@@ -1320,10 +1348,11 @@ function cliHygieneEnv(inherited = process.env) {
|
|
|
1320
1348
|
function detectCliVersion(cliPath) {
|
|
1321
1349
|
const cached = cache.get(cliPath);
|
|
1322
1350
|
if (cached) return cached;
|
|
1351
|
+
let deadlineKill = false;
|
|
1323
1352
|
const promise = (async () => {
|
|
1324
1353
|
try {
|
|
1325
1354
|
const { stdout } = await execFileAsync(cliPath, ["--version"], {
|
|
1326
|
-
timeout:
|
|
1355
|
+
timeout: probeTimeoutMs
|
|
1327
1356
|
});
|
|
1328
1357
|
const match = /(\d+)\.(\d+)\.(\d+)/.exec(stdout.trim());
|
|
1329
1358
|
if (!match) {
|
|
@@ -1345,14 +1374,17 @@ function detectCliVersion(cliPath) {
|
|
|
1345
1374
|
}
|
|
1346
1375
|
return v;
|
|
1347
1376
|
} catch (err) {
|
|
1377
|
+
deadlineKill = killedByDeadline(err);
|
|
1348
1378
|
log.warn("failed to detect claude cli version", {
|
|
1349
1379
|
cliPath,
|
|
1350
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1380
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1381
|
+
deadlineKill
|
|
1351
1382
|
});
|
|
1352
1383
|
return null;
|
|
1353
1384
|
}
|
|
1354
1385
|
})();
|
|
1355
1386
|
cache.set(cliPath, promise);
|
|
1387
|
+
forgetDeadlineKill(cache, cliPath, promise, () => deadlineKill);
|
|
1356
1388
|
return promise;
|
|
1357
1389
|
}
|
|
1358
1390
|
function gte(v, target) {
|
|
@@ -1389,10 +1421,11 @@ function detectCliSupportsFlag(cliPath, flag) {
|
|
|
1389
1421
|
const key = `${cliPath}\0${flag}`;
|
|
1390
1422
|
const cached = flagSupport.get(key);
|
|
1391
1423
|
if (cached) return cached;
|
|
1424
|
+
let deadlineKill = false;
|
|
1392
1425
|
const promise = (async () => {
|
|
1393
1426
|
try {
|
|
1394
1427
|
const execution = execFileAsync(cliPath, ["--help"], {
|
|
1395
|
-
timeout:
|
|
1428
|
+
timeout: probeTimeoutMs,
|
|
1396
1429
|
killSignal: "SIGKILL",
|
|
1397
1430
|
maxBuffer: 4 * 1024 * 1024
|
|
1398
1431
|
});
|
|
@@ -1400,15 +1433,18 @@ function detectCliSupportsFlag(cliPath, flag) {
|
|
|
1400
1433
|
const { stdout } = await execution;
|
|
1401
1434
|
return stdout.includes(flag);
|
|
1402
1435
|
} catch (err) {
|
|
1436
|
+
deadlineKill = killedByDeadline(err);
|
|
1403
1437
|
log.warn("failed to probe claude cli flag support", {
|
|
1404
1438
|
cliPath,
|
|
1405
1439
|
flag,
|
|
1406
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1440
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1441
|
+
deadlineKill
|
|
1407
1442
|
});
|
|
1408
1443
|
return false;
|
|
1409
1444
|
}
|
|
1410
1445
|
})();
|
|
1411
1446
|
flagSupport.set(key, promise);
|
|
1447
|
+
forgetDeadlineKill(flagSupport, key, promise, () => deadlineKill);
|
|
1412
1448
|
return promise;
|
|
1413
1449
|
}
|
|
1414
1450
|
|
|
@@ -2945,2063 +2981,2109 @@ function clearCompression(sessionKey2) {
|
|
|
2945
2981
|
compressions.delete(sessionKey2);
|
|
2946
2982
|
}
|
|
2947
2983
|
|
|
2948
|
-
// src/
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
var
|
|
2959
|
-
function
|
|
2960
|
-
opencodeProjectDirectory = dir;
|
|
2961
|
-
}
|
|
2962
|
-
function getOpencodeProjectDirectory() {
|
|
2963
|
-
return opencodeProjectDirectory;
|
|
2964
|
-
}
|
|
2965
|
-
function isUsableDirectory(d) {
|
|
2966
|
-
return typeof d === "string" && d.length > 1 && d !== "/";
|
|
2967
|
-
}
|
|
2968
|
-
function resolveSpawnCwdFrom(configured, live, captured, sessionDir) {
|
|
2969
|
-
if (configured) return configured;
|
|
2970
|
-
if (isUsableDirectory(sessionDir)) return sessionDir;
|
|
2971
|
-
if (isUsableDirectory(live)) return live;
|
|
2972
|
-
return captured ?? live;
|
|
2973
|
-
}
|
|
2974
|
-
async function resolveSpawnCwdForSession(configured, sessionID) {
|
|
2975
|
-
if (configured) return configured;
|
|
2976
|
-
const sessionDir = sessionID ? await fetchSessionDirectory(sessionID) : void 0;
|
|
2977
|
-
return resolveSpawnCwdFrom(
|
|
2978
|
-
configured,
|
|
2979
|
-
process.cwd(),
|
|
2980
|
-
opencodeProjectDirectory,
|
|
2981
|
-
sessionDir
|
|
2982
|
-
);
|
|
2983
|
-
}
|
|
2984
|
-
async function fetchSessionDirectory(sessionID) {
|
|
2985
|
-
if (!sessionID || sessionID === "default") return void 0;
|
|
2986
|
-
const client = opencodeClient;
|
|
2987
|
-
if (!client?.session?.get) return void 0;
|
|
2984
|
+
// src/mcp-bridge.ts
|
|
2985
|
+
import * as fs3 from "fs";
|
|
2986
|
+
import * as path4 from "path";
|
|
2987
|
+
import * as os2 from "os";
|
|
2988
|
+
import * as crypto2 from "crypto";
|
|
2989
|
+
import {
|
|
2990
|
+
parse as parseJsonc,
|
|
2991
|
+
printParseErrorCode
|
|
2992
|
+
} from "jsonc-parser";
|
|
2993
|
+
var FILE_NAMES = ["opencode.jsonc", "opencode.json", "config.json"];
|
|
2994
|
+
var PROJECT_FILE_NAMES = ["opencode.json", "opencode.jsonc"];
|
|
2995
|
+
function fileExists(p) {
|
|
2988
2996
|
try {
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
const dir = data.directory;
|
|
2993
|
-
return isUsableDirectory(dir) ? dir : void 0;
|
|
2994
|
-
} catch (err) {
|
|
2995
|
-
log.warn("failed to fetch opencode session directory", {
|
|
2996
|
-
sessionID,
|
|
2997
|
-
error: err instanceof Error ? err.message : String(err)
|
|
2998
|
-
});
|
|
2999
|
-
return void 0;
|
|
2997
|
+
return fs3.statSync(p).isFile();
|
|
2998
|
+
} catch {
|
|
2999
|
+
return false;
|
|
3000
3000
|
}
|
|
3001
3001
|
}
|
|
3002
|
-
|
|
3003
|
-
if (!sessionID || sessionID === "default") return void 0;
|
|
3004
|
-
const client = opencodeClient;
|
|
3005
|
-
if (!client?.session?.get) return void 0;
|
|
3002
|
+
function dirExists(p) {
|
|
3006
3003
|
try {
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
const parentID = data.parentID;
|
|
3011
|
-
return typeof parentID === "string" && parentID.length > 0 ? parentID : void 0;
|
|
3012
|
-
} catch (err) {
|
|
3013
|
-
log.warn("failed to fetch opencode session parent", {
|
|
3014
|
-
sessionID,
|
|
3015
|
-
error: err instanceof Error ? err.message : String(err)
|
|
3016
|
-
});
|
|
3017
|
-
return void 0;
|
|
3004
|
+
return fs3.statSync(p).isDirectory();
|
|
3005
|
+
} catch {
|
|
3006
|
+
return false;
|
|
3018
3007
|
}
|
|
3019
3008
|
}
|
|
3020
|
-
|
|
3021
|
-
if (!sessionID || sessionID === "default") return "unknown";
|
|
3022
|
-
const status = opencodeClient?.session?.status;
|
|
3023
|
-
if (!status) return "unknown";
|
|
3009
|
+
function readAndParse(file) {
|
|
3024
3010
|
try {
|
|
3025
|
-
const
|
|
3026
|
-
const
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3011
|
+
const raw = fs3.readFileSync(file, "utf8");
|
|
3012
|
+
const errors = [];
|
|
3013
|
+
const parsed = parseJsonc(raw, errors, { allowTrailingComma: true });
|
|
3014
|
+
if (errors.length > 0) {
|
|
3015
|
+
const first = errors[0];
|
|
3016
|
+
throw new Error(
|
|
3017
|
+
`${printParseErrorCode(first.error)} at offset ${first.offset}`
|
|
3018
|
+
);
|
|
3019
|
+
}
|
|
3020
|
+
return parsed;
|
|
3021
|
+
} catch (e) {
|
|
3022
|
+
log.warn("failed to parse opencode config", {
|
|
3023
|
+
file,
|
|
3024
|
+
error: e instanceof Error ? e.message : String(e)
|
|
3036
3025
|
});
|
|
3037
|
-
return
|
|
3026
|
+
return null;
|
|
3038
3027
|
}
|
|
3039
3028
|
}
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
const windowMs = options.windowMs ?? 1500;
|
|
3043
|
-
const started = Date.now();
|
|
3044
|
-
let last = "unknown";
|
|
3045
|
-
for (; ; ) {
|
|
3046
|
-
const state = await fetchSessionRunState(sessionID);
|
|
3047
|
-
if (state === "busy") return "busy";
|
|
3048
|
-
if (state !== "unknown") last = state;
|
|
3049
|
-
if (Date.now() - started >= windowMs) return last;
|
|
3050
|
-
await new Promise((resolve6) => setTimeout(resolve6, pollMs));
|
|
3051
|
-
}
|
|
3029
|
+
function isPlainObject(x) {
|
|
3030
|
+
return typeof x === "object" && x !== null && !Array.isArray(x);
|
|
3052
3031
|
}
|
|
3053
|
-
|
|
3054
|
-
const
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
const
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
if (entry && typeof entry === "object") {
|
|
3063
|
-
const status = entry.status;
|
|
3064
|
-
if (typeof status === "string") out[name] = status;
|
|
3065
|
-
}
|
|
3032
|
+
function deepMerge(target, source) {
|
|
3033
|
+
const out = { ...target };
|
|
3034
|
+
for (const [k, v] of Object.entries(source)) {
|
|
3035
|
+
if (v === void 0) continue;
|
|
3036
|
+
const existing = out[k];
|
|
3037
|
+
if (isPlainObject(existing) && isPlainObject(v)) {
|
|
3038
|
+
out[k] = deepMerge(existing, v);
|
|
3039
|
+
} else {
|
|
3040
|
+
out[k] = v;
|
|
3066
3041
|
}
|
|
3067
|
-
return out;
|
|
3068
|
-
} catch (err) {
|
|
3069
|
-
log.warn("failed to fetch opencode MCP runtime status", {
|
|
3070
|
-
error: err instanceof Error ? err.message : String(err)
|
|
3071
|
-
});
|
|
3072
|
-
return void 0;
|
|
3073
3042
|
}
|
|
3043
|
+
return out;
|
|
3074
3044
|
}
|
|
3075
|
-
|
|
3076
|
-
const
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
const
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
const data = res.data;
|
|
3083
|
-
if (!Array.isArray(data)) return void 0;
|
|
3084
|
-
const out = [];
|
|
3085
|
-
for (const entry of data) {
|
|
3086
|
-
if (!entry || typeof entry !== "object") continue;
|
|
3087
|
-
const e = entry;
|
|
3088
|
-
const id = typeof e.id === "string" ? e.id : null;
|
|
3089
|
-
const description = typeof e.description === "string" ? e.description : "";
|
|
3090
|
-
const parameters = e.parameters && typeof e.parameters === "object" ? e.parameters : {};
|
|
3091
|
-
if (!id) continue;
|
|
3092
|
-
out.push({ id, description, parameters });
|
|
3045
|
+
function walkUp(opts) {
|
|
3046
|
+
const out = [];
|
|
3047
|
+
let current = path4.resolve(opts.start);
|
|
3048
|
+
while (true) {
|
|
3049
|
+
for (const target of opts.targets) {
|
|
3050
|
+
const candidate = path4.join(current, target);
|
|
3051
|
+
if (opts.predicate(candidate)) out.push(candidate);
|
|
3093
3052
|
}
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
model,
|
|
3099
|
-
error: err instanceof Error ? err.message : String(err)
|
|
3100
|
-
});
|
|
3101
|
-
return void 0;
|
|
3053
|
+
if (opts.stop && current === path4.resolve(opts.stop)) break;
|
|
3054
|
+
const parent = path4.dirname(current);
|
|
3055
|
+
if (parent === current) break;
|
|
3056
|
+
current = parent;
|
|
3102
3057
|
}
|
|
3058
|
+
return out;
|
|
3103
3059
|
}
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
const
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
for (const entry of data) {
|
|
3114
|
-
if (!entry || typeof entry !== "object") continue;
|
|
3115
|
-
const info = entry.info;
|
|
3116
|
-
if (!info || typeof info !== "object") continue;
|
|
3117
|
-
const record = info;
|
|
3118
|
-
const parts = entry.parts;
|
|
3119
|
-
const text = Array.isArray(parts) ? parts.flatMap((part) => {
|
|
3120
|
-
if (!part || typeof part !== "object") return [];
|
|
3121
|
-
const p = part;
|
|
3122
|
-
if (p.type !== "text" || typeof p.text !== "string") return [];
|
|
3123
|
-
return p.synthetic === true ? [] : [p.text];
|
|
3124
|
-
}).join("\n").trim() : "";
|
|
3125
|
-
const error = record.error;
|
|
3126
|
-
const message = error && typeof error === "object" ? typeof error.data?.message === "string" ? error.data.message : typeof error.name === "string" ? error.name : void 0 : void 0;
|
|
3127
|
-
out.push({
|
|
3128
|
-
role: typeof record.role === "string" ? record.role : "",
|
|
3129
|
-
completed: typeof record.time?.completed === "number",
|
|
3130
|
-
error: message,
|
|
3131
|
-
text
|
|
3132
|
-
});
|
|
3060
|
+
function detectWorktree(cwd) {
|
|
3061
|
+
const override = process.env.OPENCODE_WORKTREE;
|
|
3062
|
+
if (override) return path4.resolve(override);
|
|
3063
|
+
let current = path4.resolve(cwd);
|
|
3064
|
+
while (true) {
|
|
3065
|
+
const gitPath = path4.join(current, ".git");
|
|
3066
|
+
try {
|
|
3067
|
+
if (fs3.existsSync(gitPath)) return current;
|
|
3068
|
+
} catch {
|
|
3133
3069
|
}
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
sessionID,
|
|
3138
|
-
error: err instanceof Error ? err.message : String(err)
|
|
3139
|
-
});
|
|
3140
|
-
return void 0;
|
|
3070
|
+
const parent = path4.dirname(current);
|
|
3071
|
+
if (parent === current) return void 0;
|
|
3072
|
+
current = parent;
|
|
3141
3073
|
}
|
|
3142
3074
|
}
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
if (!client?.session?.abort) return false;
|
|
3147
|
-
try {
|
|
3148
|
-
const res = await client.session.abort({ path: { id: sessionID } });
|
|
3149
|
-
return res.data !== false;
|
|
3150
|
-
} catch (err) {
|
|
3151
|
-
log.warn("failed to abort opencode session", {
|
|
3152
|
-
sessionID,
|
|
3153
|
-
error: err instanceof Error ? err.message : String(err)
|
|
3154
|
-
});
|
|
3155
|
-
return false;
|
|
3156
|
-
}
|
|
3075
|
+
function globalConfigDir() {
|
|
3076
|
+
const xdg = process.env.XDG_CONFIG_HOME ?? path4.join(os2.homedir(), ".config");
|
|
3077
|
+
return path4.join(xdg, "opencode");
|
|
3157
3078
|
}
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3166
|
-
function ledgerIn(ledgers2, sessionKey2) {
|
|
3167
|
-
let ids = ledgers2.get(sessionKey2);
|
|
3168
|
-
if (!ids) {
|
|
3169
|
-
ids = /* @__PURE__ */ new Set();
|
|
3170
|
-
ledgers2.set(sessionKey2, ids);
|
|
3171
|
-
while (ledgers2.size > MAX_LEDGERS) {
|
|
3172
|
-
const oldest = ledgers2.keys().next();
|
|
3173
|
-
if (oldest.done) break;
|
|
3174
|
-
ledgers2.delete(oldest.value);
|
|
3175
|
-
}
|
|
3079
|
+
function loadGlobalConfig(hostApi = "v1") {
|
|
3080
|
+
const dir = globalConfigDir();
|
|
3081
|
+
let merged = {};
|
|
3082
|
+
for (const name of FILE_NAMES.slice().reverse()) {
|
|
3083
|
+
const file = path4.join(dir, name);
|
|
3084
|
+
if (!fileExists(file)) continue;
|
|
3085
|
+
const parsed = readAndParse(file);
|
|
3086
|
+
if (parsed) merged = mergeConfigLayer(merged, parsed, hostApi);
|
|
3176
3087
|
}
|
|
3177
|
-
return
|
|
3088
|
+
return merged;
|
|
3178
3089
|
}
|
|
3179
|
-
function
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3090
|
+
function loadProjectFilesInDir(dir, hostApi = "v1") {
|
|
3091
|
+
let merged = {};
|
|
3092
|
+
for (const name of PROJECT_FILE_NAMES) {
|
|
3093
|
+
const file = path4.join(dir, name);
|
|
3094
|
+
if (!fileExists(file)) continue;
|
|
3095
|
+
const parsed = readAndParse(file);
|
|
3096
|
+
if (parsed) merged = mergeConfigLayer(merged, parsed, hostApi);
|
|
3186
3097
|
}
|
|
3098
|
+
return merged;
|
|
3187
3099
|
}
|
|
3188
|
-
function
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
}
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
function snapshotBackgroundTasks() {
|
|
3206
|
-
const keys = /* @__PURE__ */ new Set([...collected.keys(), ...cancelled.keys()]);
|
|
3207
|
-
return [...keys].map((sessionKey2) => ({
|
|
3208
|
-
sessionKey: sessionKey2,
|
|
3209
|
-
collected: [...collected.get(sessionKey2) ?? []],
|
|
3210
|
-
cancelled: [...cancelled.get(sessionKey2) ?? []]
|
|
3211
|
-
}));
|
|
3212
|
-
}
|
|
3213
|
-
function readTaskId(input) {
|
|
3214
|
-
const raw = input.task_id ?? input.taskId ?? input.sessionID;
|
|
3215
|
-
if (typeof raw !== "string") return null;
|
|
3216
|
-
const id = raw.trim();
|
|
3217
|
-
return id.length > 0 ? id : null;
|
|
3218
|
-
}
|
|
3219
|
-
var MISSING_TASK_ID = 'task_id is required: pass the background subagent\'s own opencode session id, from the `<task id="...">` envelope on opencode 1.x or the `sessionID` the background dispatch reported on opencode 2.';
|
|
3220
|
-
async function guardParent(taskId, callerSessionId) {
|
|
3221
|
-
if (!callerSessionId || callerSessionId === "default") {
|
|
3222
|
-
return `Cannot check that task_id ${taskId} belongs to this conversation (this turn has no opencode session id), so it is not inspected or cancelled.`;
|
|
3100
|
+
function dotOpencodeDirs(cwd, worktree) {
|
|
3101
|
+
const dirs = [];
|
|
3102
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3103
|
+
const push = (p) => {
|
|
3104
|
+
const abs = path4.resolve(p);
|
|
3105
|
+
if (!seen.has(abs) && dirExists(abs)) {
|
|
3106
|
+
seen.add(abs);
|
|
3107
|
+
dirs.push(abs);
|
|
3108
|
+
}
|
|
3109
|
+
};
|
|
3110
|
+
for (const dir of walkUp({
|
|
3111
|
+
start: cwd,
|
|
3112
|
+
stop: worktree,
|
|
3113
|
+
targets: [".opencode"],
|
|
3114
|
+
predicate: dirExists
|
|
3115
|
+
})) {
|
|
3116
|
+
push(dir);
|
|
3223
3117
|
}
|
|
3224
|
-
const
|
|
3225
|
-
if (
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
if (
|
|
3231
|
-
|
|
3232
|
-
return reply !== void 0 && !reply.completed && reply.error === void 0;
|
|
3118
|
+
const home = os2.homedir();
|
|
3119
|
+
if (home) {
|
|
3120
|
+
const homeDot = path4.join(home, ".opencode");
|
|
3121
|
+
if (dirExists(homeDot)) push(homeDot);
|
|
3122
|
+
}
|
|
3123
|
+
const envDir = process.env.OPENCODE_CONFIG_DIR;
|
|
3124
|
+
if (envDir && dirExists(envDir)) push(envDir);
|
|
3125
|
+
return dirs;
|
|
3233
3126
|
}
|
|
3234
|
-
function
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
if (
|
|
3127
|
+
function substituteEnvPlaceholders(source) {
|
|
3128
|
+
const out = {};
|
|
3129
|
+
for (const [k, v] of Object.entries(source)) {
|
|
3130
|
+
if (typeof v !== "string") continue;
|
|
3131
|
+
out[k] = v.replace(/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, name) => {
|
|
3132
|
+
const resolved = process.env[name];
|
|
3133
|
+
return typeof resolved === "string" ? resolved : "";
|
|
3134
|
+
});
|
|
3238
3135
|
}
|
|
3239
|
-
return
|
|
3136
|
+
return out;
|
|
3240
3137
|
}
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3138
|
+
function translateServer(name, spec) {
|
|
3139
|
+
if (spec.enabled === false) return null;
|
|
3140
|
+
const type = spec.type;
|
|
3141
|
+
if (type === "local") {
|
|
3142
|
+
const cmd = spec.command;
|
|
3143
|
+
if (!Array.isArray(cmd) || cmd.length === 0) {
|
|
3144
|
+
log.warn("skipping local MCP server with no command", { name });
|
|
3145
|
+
return null;
|
|
3146
|
+
}
|
|
3147
|
+
const out = {
|
|
3148
|
+
type: "stdio",
|
|
3149
|
+
command: String(cmd[0])
|
|
3251
3150
|
};
|
|
3151
|
+
if (cmd.length > 1) out.args = cmd.slice(1).map((s) => String(s));
|
|
3152
|
+
if (spec.environment && typeof spec.environment === "object") {
|
|
3153
|
+
out.env = substituteEnvPlaceholders(
|
|
3154
|
+
spec.environment
|
|
3155
|
+
);
|
|
3156
|
+
}
|
|
3157
|
+
return out;
|
|
3252
3158
|
}
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3159
|
+
if (type === "remote") {
|
|
3160
|
+
if (typeof spec.url !== "string" || !spec.url) {
|
|
3161
|
+
log.warn("skipping remote MCP server with no url", { name });
|
|
3162
|
+
return null;
|
|
3163
|
+
}
|
|
3164
|
+
const out = {
|
|
3165
|
+
type: "http",
|
|
3166
|
+
url: spec.url
|
|
3260
3167
|
};
|
|
3168
|
+
if (spec.headers && typeof spec.headers === "object") {
|
|
3169
|
+
out.headers = substituteEnvPlaceholders(
|
|
3170
|
+
spec.headers
|
|
3171
|
+
);
|
|
3172
|
+
}
|
|
3173
|
+
return out;
|
|
3261
3174
|
}
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3175
|
+
log.warn("skipping MCP server with unknown type", {
|
|
3176
|
+
name,
|
|
3177
|
+
type: type ?? null
|
|
3178
|
+
});
|
|
3179
|
+
return null;
|
|
3180
|
+
}
|
|
3181
|
+
function extractMcpBlock(config) {
|
|
3182
|
+
const mcp = config.mcp;
|
|
3183
|
+
if (!isPlainObject(mcp)) return {};
|
|
3184
|
+
if ("servers" in mcp && !isPlainObject(mcp.servers)) return {};
|
|
3185
|
+
const native = isPlainObject(mcp.servers) && typeof mcp.servers.type !== "string" && typeof mcp.servers.enabled !== "boolean" && typeof mcp.servers.disabled !== "boolean";
|
|
3186
|
+
const block = native ? mcp.servers : mcp;
|
|
3187
|
+
return Object.fromEntries(
|
|
3188
|
+
Object.entries(block).filter(([name, spec]) => isPlainObject(spec) && (native || name !== "timeout" || typeof spec.type === "string")).map(([name, spec]) => {
|
|
3189
|
+
const server2 = spec;
|
|
3190
|
+
return [name, {
|
|
3191
|
+
...server2,
|
|
3192
|
+
...typeof server2.disabled === "boolean" ? { enabled: !server2.disabled } : {}
|
|
3193
|
+
}];
|
|
3194
|
+
})
|
|
3195
|
+
);
|
|
3196
|
+
}
|
|
3197
|
+
function mergeConfigLayer(target, source, hostApi) {
|
|
3198
|
+
const merged = deepMerge(target, source);
|
|
3199
|
+
if (hostApi === "v2" && isPlainObject(source.mcp)) {
|
|
3200
|
+
merged.mcp = {
|
|
3201
|
+
...isPlainObject(merged.mcp) ? merged.mcp : {},
|
|
3202
|
+
servers: { ...extractMcpBlock(target), ...extractMcpBlock(source) }
|
|
3269
3203
|
};
|
|
3270
3204
|
}
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3205
|
+
return merged;
|
|
3206
|
+
}
|
|
3207
|
+
function mergeMcp(target, source) {
|
|
3208
|
+
const out = { ...target };
|
|
3209
|
+
for (const [name, spec] of Object.entries(source)) {
|
|
3210
|
+
if (!spec || typeof spec !== "object") continue;
|
|
3211
|
+
const existing = out[name];
|
|
3212
|
+
if (existing && typeof existing === "object") {
|
|
3213
|
+
out[name] = deepMerge(
|
|
3214
|
+
existing,
|
|
3215
|
+
spec
|
|
3216
|
+
);
|
|
3217
|
+
} else {
|
|
3218
|
+
out[name] = spec;
|
|
3219
|
+
}
|
|
3280
3220
|
}
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3221
|
+
return out;
|
|
3222
|
+
}
|
|
3223
|
+
var MCP_PENDING_STATUS = "pending";
|
|
3224
|
+
function bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers, hostApi = "v1") {
|
|
3225
|
+
const {
|
|
3226
|
+
servers: merged,
|
|
3227
|
+
enabledServerNames: allEnabledServerNames,
|
|
3228
|
+
hash
|
|
3229
|
+
} = mergeOpencodeMcp(cwd, runtimeStatus, hostApi);
|
|
3230
|
+
const servers = {};
|
|
3231
|
+
const bridgedServerNames = [];
|
|
3232
|
+
for (const [name, spec] of Object.entries(merged)) {
|
|
3233
|
+
if (!spec || typeof spec !== "object") continue;
|
|
3234
|
+
if (excludeServers?.has(name)) continue;
|
|
3235
|
+
const translated = translateServer(name, spec);
|
|
3236
|
+
if (translated) {
|
|
3237
|
+
servers[name] = translated;
|
|
3238
|
+
bridgedServerNames.push(name);
|
|
3239
|
+
}
|
|
3288
3240
|
}
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3241
|
+
return finishBridge({
|
|
3242
|
+
servers,
|
|
3243
|
+
bridgedServerNames,
|
|
3244
|
+
allEnabledServerNames,
|
|
3245
|
+
hash,
|
|
3246
|
+
excludeServers
|
|
3294
3247
|
});
|
|
3295
|
-
return {
|
|
3296
|
-
kind: "text",
|
|
3297
|
-
text: `<task id="${taskId}" state="completed">
|
|
3298
|
-
<task_result>
|
|
3299
|
-
${reply.text || "(the subagent produced no text)"}
|
|
3300
|
-
</task_result>
|
|
3301
|
-
</task>`
|
|
3302
|
-
};
|
|
3303
3248
|
}
|
|
3304
|
-
|
|
3305
|
-
const
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
);
|
|
3313
|
-
const aborted = await abortSession(taskId);
|
|
3314
|
-
if (!aborted) {
|
|
3315
|
-
return {
|
|
3316
|
-
kind: "error",
|
|
3317
|
-
message: `Could not cancel background task ${taskId}: opencode did not accept the abort. It may have already finished, or this opencode build does not expose session abort.`
|
|
3318
|
-
};
|
|
3249
|
+
function opencodeConfigLayers(cwd, hostApi = "v1") {
|
|
3250
|
+
const worktree = hostApi === "v2" ? void 0 : detectWorktree(cwd);
|
|
3251
|
+
const layers = [];
|
|
3252
|
+
layers.push(loadGlobalConfig(hostApi));
|
|
3253
|
+
const explicitConfig = process.env.OPENCODE_CONFIG;
|
|
3254
|
+
if (explicitConfig && fileExists(explicitConfig)) {
|
|
3255
|
+
const parsed = readAndParse(explicitConfig);
|
|
3256
|
+
if (parsed) layers.push(parsed);
|
|
3319
3257
|
}
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
wasRunning: running
|
|
3258
|
+
const projectFiles = walkUp({
|
|
3259
|
+
start: cwd,
|
|
3260
|
+
stop: worktree,
|
|
3261
|
+
targets: PROJECT_FILE_NAMES,
|
|
3262
|
+
predicate: fileExists
|
|
3326
3263
|
});
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
var READ_ONLY_DISALLOWED_CLI_TOOLS = [
|
|
3336
|
-
"Bash",
|
|
3337
|
-
"Write",
|
|
3338
|
-
"Edit",
|
|
3339
|
-
"NotebookEdit",
|
|
3340
|
-
"REPL",
|
|
3341
|
-
"JavaScript",
|
|
3342
|
-
"WebFetch"
|
|
3343
|
-
];
|
|
3344
|
-
var READ_ONLY_DENIED_PROXY_TOOLS = /* @__PURE__ */ new Set([
|
|
3345
|
-
"bash",
|
|
3346
|
-
"write",
|
|
3347
|
-
"edit",
|
|
3348
|
-
"webfetch",
|
|
3349
|
-
"task",
|
|
3350
|
-
"task_batch"
|
|
3351
|
-
]);
|
|
3352
|
-
function dedupe(names) {
|
|
3353
|
-
const out = [];
|
|
3354
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3355
|
-
for (const name of names) {
|
|
3356
|
-
const trimmed = String(name).trim();
|
|
3357
|
-
if (!trimmed || seen.has(trimmed)) continue;
|
|
3358
|
-
seen.add(trimmed);
|
|
3359
|
-
out.push(trimmed);
|
|
3264
|
+
const projectDirs = [];
|
|
3265
|
+
const seenProjectDirs = /* @__PURE__ */ new Set();
|
|
3266
|
+
for (const f of projectFiles) {
|
|
3267
|
+
const d = path4.dirname(f);
|
|
3268
|
+
if (!seenProjectDirs.has(d)) {
|
|
3269
|
+
seenProjectDirs.add(d);
|
|
3270
|
+
projectDirs.push(d);
|
|
3271
|
+
}
|
|
3360
3272
|
}
|
|
3361
|
-
|
|
3273
|
+
for (const dir of projectDirs.slice().reverse()) {
|
|
3274
|
+
layers.push(loadProjectFilesInDir(dir, hostApi));
|
|
3275
|
+
}
|
|
3276
|
+
if (hostApi === "v2") {
|
|
3277
|
+
const dirs = walkUp({ start: cwd, targets: [".opencode"], predicate: dirExists }).reverse();
|
|
3278
|
+
const extra = process.env.OPENCODE_CONFIG_DIR;
|
|
3279
|
+
if (extra && dirExists(extra)) layers.push(loadProjectFilesInDir(extra, hostApi));
|
|
3280
|
+
for (const dir of dirs) layers.push(loadProjectFilesInDir(dir, hostApi));
|
|
3281
|
+
} else {
|
|
3282
|
+
for (const dir of dotOpencodeDirs(cwd, worktree)) {
|
|
3283
|
+
layers.push(loadProjectFilesInDir(dir));
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
return layers;
|
|
3362
3287
|
}
|
|
3363
|
-
function
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3288
|
+
function loadMergedOpencodeConfig(cwd, hostApi = "v1") {
|
|
3289
|
+
let merged = {};
|
|
3290
|
+
for (const layer of opencodeConfigLayers(cwd, hostApi)) merged = mergeConfigLayer(merged, layer, hostApi);
|
|
3291
|
+
return merged;
|
|
3292
|
+
}
|
|
3293
|
+
function mergeOpencodeMcp(cwd, runtimeStatus, hostApi = "v1") {
|
|
3294
|
+
let merged = {};
|
|
3295
|
+
for (const layer of opencodeConfigLayers(cwd, hostApi)) {
|
|
3296
|
+
const servers = extractMcpBlock(layer);
|
|
3297
|
+
merged = hostApi === "v2" ? { ...merged, ...servers } : mergeMcp(merged, servers);
|
|
3369
3298
|
}
|
|
3370
|
-
if (
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3299
|
+
if (runtimeStatus) {
|
|
3300
|
+
for (const name of Object.keys(merged)) {
|
|
3301
|
+
const status = runtimeStatus[name];
|
|
3302
|
+
if (status === void 0) continue;
|
|
3303
|
+
if (status === MCP_PENDING_STATUS) continue;
|
|
3304
|
+
const existing = merged[name];
|
|
3305
|
+
const base = existing && typeof existing === "object" ? existing : {};
|
|
3306
|
+
merged[name] = { ...base, enabled: status === "connected" };
|
|
3307
|
+
}
|
|
3374
3308
|
}
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3309
|
+
const enabledServerNames = [];
|
|
3310
|
+
for (const [name, spec] of Object.entries(merged)) {
|
|
3311
|
+
if (!spec || typeof spec !== "object") continue;
|
|
3312
|
+
const enabled = spec.enabled;
|
|
3313
|
+
if (enabled === false) continue;
|
|
3314
|
+
enabledServerNames.push(name);
|
|
3379
3315
|
}
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3316
|
+
const mergedBody = JSON.stringify({ mcpServers: merged }, null, 2);
|
|
3317
|
+
const hash = crypto2.createHash("sha256").update(mergedBody).digest("hex").slice(0, 12);
|
|
3318
|
+
return { servers: merged, enabledServerNames, hash };
|
|
3319
|
+
}
|
|
3320
|
+
function finishBridge(input) {
|
|
3321
|
+
const { servers, bridgedServerNames, allEnabledServerNames, hash, excludeServers } = input;
|
|
3322
|
+
if (Object.keys(servers).length === 0) {
|
|
3323
|
+
const allEnabledServersExcluded = excludeServers && allEnabledServerNames.length > 0 && allEnabledServerNames.every((name) => excludeServers.has(name));
|
|
3324
|
+
if (!allEnabledServersExcluded) return null;
|
|
3325
|
+
return {
|
|
3326
|
+
path: "",
|
|
3327
|
+
hash,
|
|
3328
|
+
serverNames: [],
|
|
3329
|
+
allEnabledServerNames
|
|
3330
|
+
};
|
|
3384
3331
|
}
|
|
3385
|
-
const
|
|
3386
|
-
const
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
(name) => READ_ONLY_DENIED_PROXY_TOOLS.has(name.toLowerCase())
|
|
3332
|
+
const body = JSON.stringify({ mcpServers: servers }, null, 2);
|
|
3333
|
+
const bodyDigest = crypto2.createHash("sha256").update(body).digest("hex").slice(0, 12);
|
|
3334
|
+
const outPath = path4.join(
|
|
3335
|
+
pluginTmpDir(),
|
|
3336
|
+
`mcp-${bodyDigest}.json`
|
|
3391
3337
|
);
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3338
|
+
try {
|
|
3339
|
+
if (!fileExists(outPath)) {
|
|
3340
|
+
fs3.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
|
|
3341
|
+
}
|
|
3342
|
+
} catch (e) {
|
|
3343
|
+
log.warn("failed to write bridged MCP config", {
|
|
3344
|
+
error: e instanceof Error ? e.message : String(e)
|
|
3345
|
+
});
|
|
3346
|
+
return null;
|
|
3396
3347
|
}
|
|
3348
|
+
log.info("bridged opencode MCP config", {
|
|
3349
|
+
target: outPath,
|
|
3350
|
+
hash,
|
|
3351
|
+
servers: bridgedServerNames,
|
|
3352
|
+
excluded: excludeServers ? Array.from(excludeServers) : []
|
|
3353
|
+
});
|
|
3397
3354
|
return {
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
extraDisallowedTools: dedupe([
|
|
3403
|
-
...READ_ONLY_DISALLOWED_CLI_TOOLS,
|
|
3404
|
-
...settings.extraDisallowedTools ?? []
|
|
3405
|
-
]),
|
|
3406
|
-
controlRequestBehavior: "deny",
|
|
3407
|
-
controlRequestToolBehaviors: void 0,
|
|
3408
|
-
overridden
|
|
3355
|
+
path: outPath,
|
|
3356
|
+
hash,
|
|
3357
|
+
serverNames: bridgedServerNames,
|
|
3358
|
+
allEnabledServerNames
|
|
3409
3359
|
};
|
|
3410
3360
|
}
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
3415
|
-
|
|
3361
|
+
|
|
3362
|
+
// src/runtime-status.ts
|
|
3363
|
+
var opencodeClient = null;
|
|
3364
|
+
function setOpencodeClient(client) {
|
|
3365
|
+
if (client && typeof client === "object") {
|
|
3366
|
+
opencodeClient = client;
|
|
3367
|
+
}
|
|
3416
3368
|
}
|
|
3417
|
-
function
|
|
3418
|
-
return
|
|
3369
|
+
function getOpencodeClient() {
|
|
3370
|
+
return opencodeClient;
|
|
3419
3371
|
}
|
|
3420
|
-
var
|
|
3421
|
-
function
|
|
3422
|
-
|
|
3423
|
-
const resolved = resolvePermissionPreset(settings, defaultProxyTools);
|
|
3424
|
-
if (resolved === null) {
|
|
3425
|
-
return { provider, preset: NO_PERMISSION_PRESET, applied: false, overrides: [] };
|
|
3426
|
-
}
|
|
3427
|
-
if (isUnknownPreset(resolved)) {
|
|
3428
|
-
return { provider, preset: resolved.unknown, applied: false, overrides: [] };
|
|
3429
|
-
}
|
|
3430
|
-
return {
|
|
3431
|
-
provider,
|
|
3432
|
-
preset: resolved.preset,
|
|
3433
|
-
applied: true,
|
|
3434
|
-
overrides: resolved.overridden
|
|
3435
|
-
};
|
|
3372
|
+
var opencodeProjectDirectory;
|
|
3373
|
+
function setOpencodeProjectDirectory(dir) {
|
|
3374
|
+
opencodeProjectDirectory = dir;
|
|
3436
3375
|
}
|
|
3437
|
-
function
|
|
3438
|
-
return
|
|
3376
|
+
function getOpencodeProjectDirectory() {
|
|
3377
|
+
return opencodeProjectDirectory;
|
|
3439
3378
|
}
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
import { randomUUID as randomUUID2 } from "crypto";
|
|
3443
|
-
var MAX_HISTORY_EXCHANGES = 20;
|
|
3444
|
-
var SIDE_QUESTION_USAGE = "Usage: /btw <question>. Ask a side question about the current conversation without adding it to the main context.";
|
|
3445
|
-
var pendingProcesses = /* @__PURE__ */ new WeakSet();
|
|
3446
|
-
function isRecord2(value) {
|
|
3447
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3379
|
+
function isUsableDirectory(d) {
|
|
3380
|
+
return typeof d === "string" && d.length > 1 && d !== "/";
|
|
3448
3381
|
}
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
if (
|
|
3453
|
-
|
|
3454
|
-
} else if (Array.isArray(content)) {
|
|
3455
|
-
const parts = [];
|
|
3456
|
-
for (const part of content) {
|
|
3457
|
-
if (!isRecord2(part) || part.type !== "text" || typeof part.text !== "string") return null;
|
|
3458
|
-
parts.push(part.text);
|
|
3459
|
-
}
|
|
3460
|
-
text = parts.join("\n");
|
|
3461
|
-
} else {
|
|
3462
|
-
return null;
|
|
3463
|
-
}
|
|
3464
|
-
const match = /^\/btw(?:\s+([\s\S]*))?$/.exec(text.replace(SYSTEM_REMINDER_BLOCK, "").trim());
|
|
3465
|
-
return match ? { question: (match[1] ?? "").trim() } : null;
|
|
3382
|
+
function resolveSpawnCwdFrom(configured, live, captured, sessionDir) {
|
|
3383
|
+
if (configured) return configured;
|
|
3384
|
+
if (isUsableDirectory(sessionDir)) return sessionDir;
|
|
3385
|
+
if (isUsableDirectory(live)) return live;
|
|
3386
|
+
return captured ?? live;
|
|
3466
3387
|
}
|
|
3467
|
-
function
|
|
3468
|
-
|
|
3469
|
-
|
|
3388
|
+
async function resolveSpawnCwdForSession(configured, sessionID) {
|
|
3389
|
+
if (configured) return configured;
|
|
3390
|
+
const sessionDir = sessionID ? await fetchSessionDirectory(sessionID) : void 0;
|
|
3391
|
+
return resolveSpawnCwdFrom(
|
|
3392
|
+
configured,
|
|
3393
|
+
process.cwd(),
|
|
3394
|
+
opencodeProjectDirectory,
|
|
3395
|
+
sessionDir
|
|
3396
|
+
);
|
|
3470
3397
|
}
|
|
3471
|
-
function
|
|
3472
|
-
if (
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3398
|
+
async function fetchSessionDirectory(sessionID) {
|
|
3399
|
+
if (!sessionID || sessionID === "default") return void 0;
|
|
3400
|
+
const client = opencodeClient;
|
|
3401
|
+
if (!client?.session?.get) return void 0;
|
|
3402
|
+
try {
|
|
3403
|
+
const res = await client.session.get({ path: { id: sessionID } });
|
|
3404
|
+
const data = res.data;
|
|
3405
|
+
if (!data || typeof data !== "object") return void 0;
|
|
3406
|
+
const dir = data.directory;
|
|
3407
|
+
return isUsableDirectory(dir) ? dir : void 0;
|
|
3408
|
+
} catch (err) {
|
|
3409
|
+
log.warn("failed to fetch opencode session directory", {
|
|
3410
|
+
sessionID,
|
|
3411
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3412
|
+
});
|
|
3413
|
+
return void 0;
|
|
3477
3414
|
}
|
|
3478
|
-
return parts.join("\n").trim();
|
|
3479
3415
|
}
|
|
3480
|
-
function
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
const
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3416
|
+
async function fetchSessionParentId(sessionID) {
|
|
3417
|
+
if (!sessionID || sessionID === "default") return void 0;
|
|
3418
|
+
const client = opencodeClient;
|
|
3419
|
+
if (!client?.session?.get) return void 0;
|
|
3420
|
+
try {
|
|
3421
|
+
const res = await client.session.get({ path: { id: sessionID } });
|
|
3422
|
+
const data = res.data;
|
|
3423
|
+
if (!data || typeof data !== "object") return void 0;
|
|
3424
|
+
const parentID = data.parentID;
|
|
3425
|
+
return typeof parentID === "string" && parentID.length > 0 ? parentID : void 0;
|
|
3426
|
+
} catch (err) {
|
|
3427
|
+
log.warn("failed to fetch opencode session parent", {
|
|
3428
|
+
sessionID,
|
|
3429
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3430
|
+
});
|
|
3431
|
+
return void 0;
|
|
3492
3432
|
}
|
|
3493
|
-
return history.slice(-MAX_HISTORY_EXCHANGES);
|
|
3494
|
-
}
|
|
3495
|
-
function isSideQuestionPending(activeProcess) {
|
|
3496
|
-
return pendingProcesses.has(activeProcess.proc);
|
|
3497
3433
|
}
|
|
3498
|
-
function
|
|
3499
|
-
if (!
|
|
3500
|
-
|
|
3434
|
+
async function fetchSessionRunState(sessionID) {
|
|
3435
|
+
if (!sessionID || sessionID === "default") return "unknown";
|
|
3436
|
+
const status = opencodeClient?.session?.status;
|
|
3437
|
+
if (!status) return "unknown";
|
|
3501
3438
|
try {
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
return
|
|
3439
|
+
const res = await status.call(opencodeClient.session);
|
|
3440
|
+
const data = res.data;
|
|
3441
|
+
if (!data || typeof data !== "object") return "unknown";
|
|
3442
|
+
const entry = data[sessionID];
|
|
3443
|
+
if (!entry || typeof entry !== "object") return "idle";
|
|
3444
|
+
const type = entry.type;
|
|
3445
|
+
return type === "idle" ? "idle" : "busy";
|
|
3446
|
+
} catch (err) {
|
|
3447
|
+
log.debug("failed to read opencode session status", {
|
|
3448
|
+
sessionID,
|
|
3449
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3450
|
+
});
|
|
3451
|
+
return "unknown";
|
|
3505
3452
|
}
|
|
3506
|
-
if (!isRecord2(message) || message.type !== "control_response") return false;
|
|
3507
|
-
const response = message.response;
|
|
3508
|
-
if (!isRecord2(response) || typeof response.request_id !== "string") return false;
|
|
3509
|
-
return activeProcess.lineEmitter.emit(`side-question:${response.request_id}`, response);
|
|
3510
3453
|
}
|
|
3511
|
-
async function
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
if (pendingProcesses.has(proc)) {
|
|
3523
|
-
throw new Error("Wait for the current /btw to finish before asking another.");
|
|
3524
|
-
}
|
|
3525
|
-
const stdin = proc.stdin;
|
|
3526
|
-
if (proc.killed || proc.exitCode != null || proc.signalCode != null || !stdin || stdin.destroyed || stdin.writableEnded || !stdin.writable) {
|
|
3527
|
-
throw new Error("/btw requires a live Claude Code session with writable stdin.");
|
|
3454
|
+
async function settleSessionRunState(sessionID, options = {}) {
|
|
3455
|
+
const pollMs = options.pollMs ?? 150;
|
|
3456
|
+
const windowMs = options.windowMs ?? 1500;
|
|
3457
|
+
const started = Date.now();
|
|
3458
|
+
let last = "unknown";
|
|
3459
|
+
for (; ; ) {
|
|
3460
|
+
const state = await fetchSessionRunState(sessionID);
|
|
3461
|
+
if (state === "busy") return "busy";
|
|
3462
|
+
if (state !== "unknown") last = state;
|
|
3463
|
+
if (Date.now() - started >= windowMs) return last;
|
|
3464
|
+
await new Promise((resolve6) => setTimeout(resolve6, pollMs));
|
|
3528
3465
|
}
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3466
|
+
}
|
|
3467
|
+
var DEFAULT_MCP_CONNECT_WAIT_MS = 3e3;
|
|
3468
|
+
var MCP_CONNECT_POLL_MS = 100;
|
|
3469
|
+
function resolveMcpConnectWaitMs(configured) {
|
|
3470
|
+
if (typeof configured !== "number" || !Number.isFinite(configured)) {
|
|
3471
|
+
return DEFAULT_MCP_CONNECT_WAIT_MS;
|
|
3532
3472
|
}
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3473
|
+
if (configured < 0) return DEFAULT_MCP_CONNECT_WAIT_MS;
|
|
3474
|
+
return configured;
|
|
3475
|
+
}
|
|
3476
|
+
function pendingMcpServers(status) {
|
|
3477
|
+
return Object.keys(status).filter(
|
|
3478
|
+
(name) => status[name] === MCP_PENDING_STATUS
|
|
3479
|
+
);
|
|
3480
|
+
}
|
|
3481
|
+
async function fetchRuntimeMcpStatus() {
|
|
3482
|
+
const client = opencodeClient;
|
|
3483
|
+
if (!client?.mcp?.status) return void 0;
|
|
3484
|
+
try {
|
|
3485
|
+
const res = await client.mcp.status();
|
|
3486
|
+
const data = res.data;
|
|
3487
|
+
if (!data || typeof data !== "object") return void 0;
|
|
3488
|
+
const out = {};
|
|
3489
|
+
for (const [name, entry] of Object.entries(data)) {
|
|
3490
|
+
if (entry && typeof entry === "object") {
|
|
3491
|
+
const status = entry.status;
|
|
3492
|
+
if (typeof status === "string") out[name] = status;
|
|
3493
|
+
}
|
|
3541
3494
|
}
|
|
3495
|
+
return out;
|
|
3496
|
+
} catch (err) {
|
|
3497
|
+
log.warn("failed to fetch opencode MCP runtime status", {
|
|
3498
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3499
|
+
});
|
|
3500
|
+
return void 0;
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3503
|
+
async function getRuntimeMcpStatus(options = {}) {
|
|
3504
|
+
const current = await fetchRuntimeMcpStatus();
|
|
3505
|
+
const budgetMs = options.waitForPendingMs ?? 0;
|
|
3506
|
+
if (!current || budgetMs <= 0) return current;
|
|
3507
|
+
return waitForPendingMcpServers(current, budgetMs, options.signal);
|
|
3508
|
+
}
|
|
3509
|
+
async function waitForPendingMcpServers(first, budgetMs, signal) {
|
|
3510
|
+
let current = first;
|
|
3511
|
+
const waitedFor = pendingMcpServers(current);
|
|
3512
|
+
if (waitedFor.length === 0) return current;
|
|
3513
|
+
const started = Date.now();
|
|
3514
|
+
for (; ; ) {
|
|
3515
|
+
if (Date.now() - started >= budgetMs) break;
|
|
3516
|
+
if (signal?.aborted) break;
|
|
3517
|
+
await new Promise((resolve6) => setTimeout(resolve6, MCP_CONNECT_POLL_MS));
|
|
3518
|
+
if (signal?.aborted) break;
|
|
3519
|
+
const next = await fetchRuntimeMcpStatus();
|
|
3520
|
+
if (!next) break;
|
|
3521
|
+
current = next;
|
|
3522
|
+
if (pendingMcpServers(current).length === 0) break;
|
|
3523
|
+
}
|
|
3524
|
+
const stillPending = pendingMcpServers(current);
|
|
3525
|
+
log.info("waited for opencode MCP servers to finish connecting", {
|
|
3526
|
+
waitedFor,
|
|
3527
|
+
stillPending,
|
|
3528
|
+
budgetMs,
|
|
3529
|
+
elapsedMs: Date.now() - started,
|
|
3530
|
+
aborted: signal?.aborted === true
|
|
3542
3531
|
});
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
const
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
settled = true;
|
|
3564
|
-
if (cancel && sent && !stdin.destroyed && !stdin.writableEnded && stdin.writable) {
|
|
3565
|
-
try {
|
|
3566
|
-
cancelPending = true;
|
|
3567
|
-
stdin.write(
|
|
3568
|
-
JSON.stringify({ type: "control_cancel_request", request_id: requestId }) + "\n",
|
|
3569
|
-
() => {
|
|
3570
|
-
queueMicrotask(() => stdin.off("error", onError));
|
|
3571
|
-
}
|
|
3572
|
-
);
|
|
3573
|
-
} catch {
|
|
3574
|
-
cancelPending = false;
|
|
3575
|
-
}
|
|
3576
|
-
}
|
|
3577
|
-
cleanup();
|
|
3578
|
-
reject(error);
|
|
3579
|
-
};
|
|
3580
|
-
const onClose = () => fail(new Error("Claude Code closed before answering /btw."));
|
|
3581
|
-
const onError = (error) => fail(error);
|
|
3582
|
-
const onAbort = () => fail(
|
|
3583
|
-
options.abortSignal?.reason ?? new DOMException("/btw was aborted.", "AbortError"),
|
|
3584
|
-
true
|
|
3585
|
-
);
|
|
3586
|
-
const onResponse = (response) => {
|
|
3587
|
-
if (settled || response.request_id !== requestId) return;
|
|
3588
|
-
if (response.subtype === "error") {
|
|
3589
|
-
fail(new Error(typeof response.error === "string" ? response.error : "Claude Code rejected /btw."));
|
|
3590
|
-
return;
|
|
3591
|
-
}
|
|
3592
|
-
const result = response.response;
|
|
3593
|
-
if (response.subtype !== "success" || !isRecord2(result) || typeof result.response !== "string" || typeof result.synthetic !== "boolean") {
|
|
3594
|
-
fail(new Error("Claude Code returned an invalid /btw response."));
|
|
3595
|
-
return;
|
|
3596
|
-
}
|
|
3597
|
-
settled = true;
|
|
3598
|
-
cleanup();
|
|
3599
|
-
resolve6({ response: result.response, synthetic: result.synthetic });
|
|
3600
|
-
};
|
|
3601
|
-
const timer = setTimeout(() => {
|
|
3602
|
-
fail(new Error(`/btw timed out after ${timeoutMs}ms.`), true);
|
|
3603
|
-
}, timeoutMs);
|
|
3604
|
-
lineEmitter.on(event, onResponse);
|
|
3605
|
-
lineEmitter.on("close", onClose);
|
|
3606
|
-
lineEmitter.on("error", onError);
|
|
3607
|
-
proc.on("exit", onClose);
|
|
3608
|
-
proc.on("close", onClose);
|
|
3609
|
-
proc.on("error", onError);
|
|
3610
|
-
stdin.on("error", onError);
|
|
3611
|
-
options.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
3612
|
-
if (options.abortSignal?.aborted) {
|
|
3613
|
-
onAbort();
|
|
3614
|
-
return;
|
|
3532
|
+
return current;
|
|
3533
|
+
}
|
|
3534
|
+
async function fetchOpencodeToolList(provider, model, directory) {
|
|
3535
|
+
const client = opencodeClient;
|
|
3536
|
+
if (!client?.tool?.list) return void 0;
|
|
3537
|
+
try {
|
|
3538
|
+
const res = await client.tool.list({
|
|
3539
|
+
query: { provider, model, ...directory ? { directory } : {} }
|
|
3540
|
+
});
|
|
3541
|
+
const data = res.data;
|
|
3542
|
+
if (!Array.isArray(data)) return void 0;
|
|
3543
|
+
const out = [];
|
|
3544
|
+
for (const entry of data) {
|
|
3545
|
+
if (!entry || typeof entry !== "object") continue;
|
|
3546
|
+
const e = entry;
|
|
3547
|
+
const id = typeof e.id === "string" ? e.id : null;
|
|
3548
|
+
const description = typeof e.description === "string" ? e.description : "";
|
|
3549
|
+
const parameters = e.parameters && typeof e.parameters === "object" ? e.parameters : {};
|
|
3550
|
+
if (!id) continue;
|
|
3551
|
+
out.push({ id, description, parameters });
|
|
3615
3552
|
}
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3553
|
+
return out;
|
|
3554
|
+
} catch (err) {
|
|
3555
|
+
log.warn("failed to fetch opencode tool list", {
|
|
3556
|
+
provider,
|
|
3557
|
+
model,
|
|
3558
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3559
|
+
});
|
|
3560
|
+
return void 0;
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
async function fetchSessionReplies(sessionID) {
|
|
3564
|
+
if (!sessionID || sessionID === "default") return void 0;
|
|
3565
|
+
const client = opencodeClient;
|
|
3566
|
+
if (!client?.session?.messages) return void 0;
|
|
3567
|
+
try {
|
|
3568
|
+
const res = await client.session.messages({ path: { id: sessionID } });
|
|
3569
|
+
const data = res.data;
|
|
3570
|
+
if (!Array.isArray(data)) return void 0;
|
|
3571
|
+
const out = [];
|
|
3572
|
+
for (const entry of data) {
|
|
3573
|
+
if (!entry || typeof entry !== "object") continue;
|
|
3574
|
+
const info = entry.info;
|
|
3575
|
+
if (!info || typeof info !== "object") continue;
|
|
3576
|
+
const record = info;
|
|
3577
|
+
const parts = entry.parts;
|
|
3578
|
+
const text = Array.isArray(parts) ? parts.flatMap((part) => {
|
|
3579
|
+
if (!part || typeof part !== "object") return [];
|
|
3580
|
+
const p = part;
|
|
3581
|
+
if (p.type !== "text" || typeof p.text !== "string") return [];
|
|
3582
|
+
return p.synthetic === true ? [] : [p.text];
|
|
3583
|
+
}).join("\n").trim() : "";
|
|
3584
|
+
const error = record.error;
|
|
3585
|
+
const message = error && typeof error === "object" ? typeof error.data?.message === "string" ? error.data.message : typeof error.name === "string" ? error.name : void 0 : void 0;
|
|
3586
|
+
out.push({
|
|
3587
|
+
role: typeof record.role === "string" ? record.role : "",
|
|
3588
|
+
completed: typeof record.time?.completed === "number",
|
|
3589
|
+
error: message,
|
|
3590
|
+
text
|
|
3591
|
+
});
|
|
3621
3592
|
}
|
|
3622
|
-
|
|
3593
|
+
return out;
|
|
3594
|
+
} catch (err) {
|
|
3595
|
+
log.warn("failed to fetch opencode session messages", {
|
|
3596
|
+
sessionID,
|
|
3597
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3598
|
+
});
|
|
3599
|
+
return void 0;
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3602
|
+
async function abortSession(sessionID) {
|
|
3603
|
+
if (!sessionID || sessionID === "default") return false;
|
|
3604
|
+
const client = opencodeClient;
|
|
3605
|
+
if (!client?.session?.abort) return false;
|
|
3606
|
+
try {
|
|
3607
|
+
const res = await client.session.abort({ path: { id: sessionID } });
|
|
3608
|
+
return res.data !== false;
|
|
3609
|
+
} catch (err) {
|
|
3610
|
+
log.warn("failed to abort opencode session", {
|
|
3611
|
+
sessionID,
|
|
3612
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3613
|
+
});
|
|
3614
|
+
return false;
|
|
3615
|
+
}
|
|
3623
3616
|
}
|
|
3624
3617
|
|
|
3625
|
-
// src/
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3618
|
+
// src/background-tasks.ts
|
|
3619
|
+
var TASK_STATUS_TOOL_NAME = "task_status";
|
|
3620
|
+
var TASK_CANCEL_TOOL_NAME = "task_cancel";
|
|
3621
|
+
var MAX_LEDGERS = 32;
|
|
3622
|
+
var MAX_COLLECTED_PER_SESSION = 64;
|
|
3623
|
+
var collected = /* @__PURE__ */ new Map();
|
|
3624
|
+
var cancelled = /* @__PURE__ */ new Map();
|
|
3625
|
+
function ledgerIn(ledgers2, sessionKey2) {
|
|
3626
|
+
let ids = ledgers2.get(sessionKey2);
|
|
3627
|
+
if (!ids) {
|
|
3628
|
+
ids = /* @__PURE__ */ new Set();
|
|
3629
|
+
ledgers2.set(sessionKey2, ids);
|
|
3630
|
+
while (ledgers2.size > MAX_LEDGERS) {
|
|
3631
|
+
const oldest = ledgers2.keys().next();
|
|
3632
|
+
if (oldest.done) break;
|
|
3633
|
+
ledgers2.delete(oldest.value);
|
|
3634
|
+
}
|
|
3630
3635
|
}
|
|
3631
|
-
return
|
|
3636
|
+
return ids;
|
|
3632
3637
|
}
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
while (lines.length > 0 && (lines.length > UNATTENDED_LINE_CAP || bytes > UNATTENDED_BYTE_CAP)) {
|
|
3641
|
-
bytes -= Buffer.byteLength(lines.shift());
|
|
3642
|
-
ap.unattendedDropped = (ap.unattendedDropped ?? 0) + 1;
|
|
3638
|
+
function mark(ledgers2, sessionKey2, taskId) {
|
|
3639
|
+
const ids = ledgerIn(ledgers2, sessionKey2);
|
|
3640
|
+
ids.add(taskId);
|
|
3641
|
+
while (ids.size > MAX_COLLECTED_PER_SESSION) {
|
|
3642
|
+
const oldest = ids.values().next();
|
|
3643
|
+
if (oldest.done) break;
|
|
3644
|
+
ids.delete(oldest.value);
|
|
3643
3645
|
}
|
|
3644
3646
|
}
|
|
3645
|
-
function
|
|
3646
|
-
|
|
3647
|
-
const dropped = ap.unattendedDropped ?? 0;
|
|
3648
|
-
ap.unattendedLines = [];
|
|
3649
|
-
ap.unattendedDropped = 0;
|
|
3650
|
-
return { lines, dropped };
|
|
3647
|
+
function markCollected(sessionKey2, taskId) {
|
|
3648
|
+
mark(collected, sessionKey2, taskId);
|
|
3651
3649
|
}
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
ap.lastStderr = ((ap.lastStderr ?? "") + chunk).slice(-STDERR_RETAIN_BYTES);
|
|
3650
|
+
function hasCollectedBackgroundTask(sessionKey2, taskId) {
|
|
3651
|
+
return collected.get(sessionKey2)?.has(taskId) === true;
|
|
3655
3652
|
}
|
|
3656
|
-
function
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
return `The Claude Code CLI ${how} before finishing this turn (no result was emitted), so the answer above may be incomplete.` + (tail ? `
|
|
3660
|
-
|
|
3661
|
-
Last stderr from the CLI:
|
|
3662
|
-
${tail}` : "");
|
|
3653
|
+
function clearBackgroundTasks(sessionKey2) {
|
|
3654
|
+
collected.delete(sessionKey2);
|
|
3655
|
+
cancelled.delete(sessionKey2);
|
|
3663
3656
|
}
|
|
3664
|
-
var
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
var MAX_IDLE_TIMEOUT_MS = 2147483647;
|
|
3668
|
-
var DEFAULT_IDLE_PROCESS_TIMEOUT_MS = 0;
|
|
3669
|
-
function resolveIdleProcessTimeoutMs(configured) {
|
|
3670
|
-
return configured === void 0 ? DEFAULT_IDLE_PROCESS_TIMEOUT_MS : configured;
|
|
3657
|
+
var lastGate;
|
|
3658
|
+
function recordBackgroundSubagentGate(gate, now = Date.now()) {
|
|
3659
|
+
lastGate = { ...gate, at: now };
|
|
3671
3660
|
}
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
var PROCESS_FORCE_EXIT_TIMEOUT_MS = 500;
|
|
3675
|
-
var CHILD_EXITED_MESSAGE = "Claude CLI subprocess closed before pending tool calls were resolved";
|
|
3676
|
-
function envFlagEnabled(value) {
|
|
3677
|
-
if (value === void 0) return false;
|
|
3678
|
-
const normalized = value.trim().toLowerCase();
|
|
3679
|
-
if (!normalized) return false;
|
|
3680
|
-
return !["0", "false", "no", "off"].includes(normalized);
|
|
3661
|
+
function snapshotBackgroundSubagentGate() {
|
|
3662
|
+
return lastGate ? { ...lastGate } : void 0;
|
|
3681
3663
|
}
|
|
3682
|
-
function
|
|
3683
|
-
|
|
3664
|
+
function snapshotBackgroundTasks() {
|
|
3665
|
+
const keys = /* @__PURE__ */ new Set([...collected.keys(), ...cancelled.keys()]);
|
|
3666
|
+
return [...keys].map((sessionKey2) => ({
|
|
3667
|
+
sessionKey: sessionKey2,
|
|
3668
|
+
collected: [...collected.get(sessionKey2) ?? []],
|
|
3669
|
+
cancelled: [...cancelled.get(sessionKey2) ?? []]
|
|
3670
|
+
}));
|
|
3684
3671
|
}
|
|
3685
|
-
function
|
|
3686
|
-
|
|
3672
|
+
function readTaskId(input) {
|
|
3673
|
+
const raw = input.task_id ?? input.taskId ?? input.sessionID;
|
|
3674
|
+
if (typeof raw !== "string") return null;
|
|
3675
|
+
const id = raw.trim();
|
|
3676
|
+
return id.length > 0 ? id : null;
|
|
3687
3677
|
}
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
// Pin the child to the binary whose version we detected, and keep it off
|
|
3693
|
-
// non-essential network calls. Fills gaps only, so an explicit shell value
|
|
3694
|
-
// survives: see `cliHygieneEnv` for why the version has to hold still.
|
|
3695
|
-
...cliHygieneEnv()
|
|
3696
|
-
};
|
|
3697
|
-
if (opts?.effort) {
|
|
3698
|
-
env.CLAUDE_CODE_EFFORT_LEVEL = cliEffortLevel(opts.effort);
|
|
3699
|
-
}
|
|
3700
|
-
if (opts?.promptCacheTtl) {
|
|
3701
|
-
env.CLAUDE_CODE_PROMPT_CACHE_TTL = opts.promptCacheTtl;
|
|
3702
|
-
}
|
|
3703
|
-
if (opts?.ignoreAnthropicApiKey) {
|
|
3704
|
-
delete env.ANTHROPIC_API_KEY;
|
|
3705
|
-
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
3706
|
-
}
|
|
3707
|
-
if (!isClaudeThinkingDisabled() && process.env.CLAUDE_CODE_SHOW_THINKING_SUMMARIES === void 0) {
|
|
3708
|
-
env.CLAUDE_CODE_SHOW_THINKING_SUMMARIES = "1";
|
|
3678
|
+
var MISSING_TASK_ID = 'task_id is required: pass the background subagent\'s own opencode session id, from the `<task id="...">` envelope on opencode 1.x or the `sessionID` the background dispatch reported on opencode 2.';
|
|
3679
|
+
async function guardParent(taskId, callerSessionId) {
|
|
3680
|
+
if (!callerSessionId || callerSessionId === "default") {
|
|
3681
|
+
return `Cannot check that task_id ${taskId} belongs to this conversation (this turn has no opencode session id), so it is not inspected or cancelled.`;
|
|
3709
3682
|
}
|
|
3710
|
-
|
|
3683
|
+
const parent = await fetchSessionParentId(taskId);
|
|
3684
|
+
if (parent === callerSessionId) return null;
|
|
3685
|
+
return `task_id ${taskId} is not a subagent of this conversation, so it cannot be inspected or cancelled from here. Use the session id from a background dispatch this conversation itself made.`;
|
|
3711
3686
|
}
|
|
3712
|
-
function
|
|
3713
|
-
|
|
3714
|
-
if (
|
|
3715
|
-
|
|
3716
|
-
|
|
3687
|
+
function isBackgroundTaskRunning(runState, replies) {
|
|
3688
|
+
if (runState === "busy") return true;
|
|
3689
|
+
if (runState === "idle") return false;
|
|
3690
|
+
const reply = lastAssistantReply(replies);
|
|
3691
|
+
return reply !== void 0 && !reply.completed && reply.error === void 0;
|
|
3692
|
+
}
|
|
3693
|
+
function lastAssistantReply(replies) {
|
|
3694
|
+
for (let index = replies.length - 1; index >= 0; index--) {
|
|
3695
|
+
const reply = replies[index];
|
|
3696
|
+
if (reply.role === "assistant") return reply;
|
|
3717
3697
|
}
|
|
3698
|
+
return void 0;
|
|
3718
3699
|
}
|
|
3719
|
-
function
|
|
3720
|
-
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
active: activeProcesses.size,
|
|
3731
|
-
cap: MAX_ACTIVE_PROCESSES
|
|
3732
|
-
});
|
|
3733
|
-
return;
|
|
3734
|
-
}
|
|
3735
|
-
log.info("evicting LRU claude process", { sessionKey: victimKey });
|
|
3736
|
-
deleteActiveProcess(victimKey);
|
|
3700
|
+
async function collectBackgroundTask(input, options) {
|
|
3701
|
+
const taskId = readTaskId(input);
|
|
3702
|
+
if (!taskId) return { kind: "error", message: MISSING_TASK_ID };
|
|
3703
|
+
const refused = await guardParent(taskId, options.callerSessionId);
|
|
3704
|
+
if (refused) return { kind: "error", message: refused };
|
|
3705
|
+
const replies = await fetchSessionReplies(taskId);
|
|
3706
|
+
if (replies === void 0) {
|
|
3707
|
+
return {
|
|
3708
|
+
kind: "error",
|
|
3709
|
+
message: `Could not read background task ${taskId} from opencode. It may have been deleted, or this opencode build does not expose the session transcript. The completion notification is still the primary delivery path: end your turn and wait for it.`
|
|
3710
|
+
};
|
|
3737
3711
|
}
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3712
|
+
const runState = await fetchSessionRunState(taskId);
|
|
3713
|
+
if (isBackgroundTaskRunning(runState, replies)) {
|
|
3714
|
+
return {
|
|
3715
|
+
kind: "text",
|
|
3716
|
+
text: `<task id="${taskId}" state="running">
|
|
3717
|
+
Still working. Do not call this again in a loop: when it finishes opencode delivers the result to this conversation on its own. End your turn, or work on something that does not overlap it.
|
|
3718
|
+
</task>`
|
|
3719
|
+
};
|
|
3746
3720
|
}
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
function noteTurnStarted(ap) {
|
|
3755
|
-
if (ap.asideTransport?.interactive) return;
|
|
3756
|
-
ap.turnInFlight = true;
|
|
3757
|
-
}
|
|
3758
|
-
function noteTurnLine(ap, line) {
|
|
3759
|
-
if (!ap.turnInFlight) return;
|
|
3760
|
-
if (isTerminalResultLine(line)) settleTurn(ap);
|
|
3761
|
-
}
|
|
3762
|
-
function isTurnInFlight(ap) {
|
|
3763
|
-
return ap.turnInFlight === true;
|
|
3764
|
-
}
|
|
3765
|
-
function awaitTurnIdle(ap, timeoutMs) {
|
|
3766
|
-
if (!ap.turnInFlight) return Promise.resolve(true);
|
|
3767
|
-
return new Promise((resolve6) => {
|
|
3768
|
-
const wake = () => {
|
|
3769
|
-
clearTimeout(timer);
|
|
3770
|
-
resolve6(true);
|
|
3721
|
+
const reply = lastAssistantReply(replies);
|
|
3722
|
+
if (!reply) {
|
|
3723
|
+
return {
|
|
3724
|
+
kind: "text",
|
|
3725
|
+
text: `<task id="${taskId}" state="unknown">
|
|
3726
|
+
opencode has no assistant reply for this task yet and it is not running. It may still be starting up.
|
|
3727
|
+
</task>`
|
|
3771
3728
|
};
|
|
3772
|
-
const timer = setTimeout(() => {
|
|
3773
|
-
const waiters = ap.turnIdleWaiters ?? [];
|
|
3774
|
-
const at = waiters.indexOf(wake);
|
|
3775
|
-
if (at >= 0) waiters.splice(at, 1);
|
|
3776
|
-
resolve6(false);
|
|
3777
|
-
}, timeoutMs);
|
|
3778
|
-
(ap.turnIdleWaiters ??= []).push(wake);
|
|
3779
|
-
});
|
|
3780
|
-
}
|
|
3781
|
-
function interruptTurn(ap, timeoutMs = TURN_INTERRUPT_TIMEOUT_MS) {
|
|
3782
|
-
if (!ap.turnInFlight) return Promise.resolve(true);
|
|
3783
|
-
const stdin = ap.proc.stdin;
|
|
3784
|
-
if (ap.asideTransport?.interactive || !stdin || !stdin.writable) {
|
|
3785
|
-
log.notice("cannot interrupt this transport; waiting for the turn to end");
|
|
3786
|
-
return awaitTurnIdle(ap, timeoutMs);
|
|
3787
3729
|
}
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
log.warn("failed to write interrupt control request", {
|
|
3798
|
-
error: error instanceof Error ? error.message : String(error)
|
|
3799
|
-
});
|
|
3800
|
-
return Promise.resolve(false);
|
|
3730
|
+
if (reply.error) {
|
|
3731
|
+
markCollected(options.sessionKey, taskId);
|
|
3732
|
+
return {
|
|
3733
|
+
kind: "text",
|
|
3734
|
+
isError: true,
|
|
3735
|
+
text: `<task id="${taskId}" state="error">
|
|
3736
|
+
${reply.error}
|
|
3737
|
+
</task>`
|
|
3738
|
+
};
|
|
3801
3739
|
}
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
}
|
|
3810
|
-
function getActiveProcess(key) {
|
|
3811
|
-
const ap = activeProcesses.get(key);
|
|
3812
|
-
if (ap) {
|
|
3813
|
-
cancelIdleProcessEviction(key);
|
|
3814
|
-
touch(key);
|
|
3740
|
+
if (hasCollectedBackgroundTask(options.sessionKey, taskId)) {
|
|
3741
|
+
return {
|
|
3742
|
+
kind: "text",
|
|
3743
|
+
text: `<task id="${taskId}" state="completed">
|
|
3744
|
+
Already delivered to this conversation earlier (by this tool or by opencode's completion notification). Scroll back for the result rather than collecting it again.
|
|
3745
|
+
</task>`
|
|
3746
|
+
};
|
|
3815
3747
|
}
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3748
|
+
markCollected(options.sessionKey, taskId);
|
|
3749
|
+
log.info("collected background subagent result", {
|
|
3750
|
+
sessionKey: options.sessionKey,
|
|
3751
|
+
taskId,
|
|
3752
|
+
textLength: reply.text.length
|
|
3753
|
+
});
|
|
3754
|
+
return {
|
|
3755
|
+
kind: "text",
|
|
3756
|
+
text: `<task id="${taskId}" state="completed">
|
|
3757
|
+
<task_result>
|
|
3758
|
+
${reply.text || "(the subagent produced no text)"}
|
|
3759
|
+
</task_result>
|
|
3760
|
+
</task>`
|
|
3761
|
+
};
|
|
3821
3762
|
}
|
|
3822
|
-
function
|
|
3823
|
-
|
|
3824
|
-
if (
|
|
3825
|
-
|
|
3763
|
+
async function cancelBackgroundTask(input, options) {
|
|
3764
|
+
const taskId = readTaskId(input);
|
|
3765
|
+
if (!taskId) return { kind: "error", message: MISSING_TASK_ID };
|
|
3766
|
+
const refused = await guardParent(taskId, options.callerSessionId);
|
|
3767
|
+
if (refused) return { kind: "error", message: refused };
|
|
3768
|
+
const running = isBackgroundTaskRunning(
|
|
3769
|
+
await fetchSessionRunState(taskId),
|
|
3770
|
+
await fetchSessionReplies(taskId) ?? []
|
|
3771
|
+
);
|
|
3772
|
+
const aborted = await abortSession(taskId);
|
|
3773
|
+
if (!aborted) {
|
|
3774
|
+
return {
|
|
3775
|
+
kind: "error",
|
|
3776
|
+
message: `Could not cancel background task ${taskId}: opencode did not accept the abort. It may have already finished, or this opencode build does not expose session abort.`
|
|
3777
|
+
};
|
|
3826
3778
|
}
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
}
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
}, timeoutMs);
|
|
3840
|
-
timer.unref();
|
|
3841
|
-
idleEvictionTimers.set(key, timer);
|
|
3779
|
+
collected.get(options.sessionKey)?.delete(taskId);
|
|
3780
|
+
mark(cancelled, options.sessionKey, taskId);
|
|
3781
|
+
log.info("cancelled background subagent", {
|
|
3782
|
+
sessionKey: options.sessionKey,
|
|
3783
|
+
taskId,
|
|
3784
|
+
wasRunning: running
|
|
3785
|
+
});
|
|
3786
|
+
return {
|
|
3787
|
+
kind: "text",
|
|
3788
|
+
text: `<task id="${taskId}" state="cancelled">
|
|
3789
|
+
` + (running ? "Stopped. No completion notification will arrive for it." : "It was not running when the cancel was sent, so nothing was interrupted. No completion notification will arrive for it.") + "\n</task>"
|
|
3790
|
+
};
|
|
3842
3791
|
}
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3792
|
+
|
|
3793
|
+
// src/permission-presets.ts
|
|
3794
|
+
var READ_ONLY_DISALLOWED_CLI_TOOLS = [
|
|
3795
|
+
"Bash",
|
|
3796
|
+
"Write",
|
|
3797
|
+
"Edit",
|
|
3798
|
+
"NotebookEdit",
|
|
3799
|
+
"REPL",
|
|
3800
|
+
"JavaScript",
|
|
3801
|
+
"WebFetch"
|
|
3802
|
+
];
|
|
3803
|
+
var READ_ONLY_DENIED_PROXY_TOOLS = /* @__PURE__ */ new Set([
|
|
3804
|
+
"bash",
|
|
3805
|
+
"write",
|
|
3806
|
+
"edit",
|
|
3807
|
+
"webfetch",
|
|
3808
|
+
"task",
|
|
3809
|
+
"task_batch"
|
|
3810
|
+
]);
|
|
3811
|
+
function dedupe(names) {
|
|
3812
|
+
const out = [];
|
|
3813
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3814
|
+
for (const name of names) {
|
|
3815
|
+
const trimmed = String(name).trim();
|
|
3816
|
+
if (!trimmed || seen.has(trimmed)) continue;
|
|
3817
|
+
seen.add(trimmed);
|
|
3818
|
+
out.push(trimmed);
|
|
3851
3819
|
}
|
|
3852
|
-
return
|
|
3820
|
+
return out;
|
|
3853
3821
|
}
|
|
3854
|
-
function
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
log.info("releasing claude process for deleted session", { sessionKey: key, sessionID });
|
|
3861
|
-
void deleteActiveProcessAndWait(key);
|
|
3862
|
-
released.push(key);
|
|
3863
|
-
}
|
|
3864
|
-
for (const key of [...claudeSessions.keys()]) {
|
|
3865
|
-
if (describeSessionKey(key).session !== sessionID) continue;
|
|
3866
|
-
deleteClaudeSessionId(key);
|
|
3867
|
-
clearCompression(key);
|
|
3868
|
-
if (!released.includes(key)) released.push(key);
|
|
3822
|
+
function resolveReadOnly(settings, defaultProxyTools) {
|
|
3823
|
+
const overridden = [];
|
|
3824
|
+
if (settings.permissionMode !== void 0) {
|
|
3825
|
+
overridden.push(
|
|
3826
|
+
`permissionMode: "${settings.permissionMode}" is dropped; read-only sends --restricted instead, which is stricter than every mode except plan and incompatible with bypassPermissions`
|
|
3827
|
+
);
|
|
3869
3828
|
}
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3829
|
+
if (settings.skipPermissions === true) {
|
|
3830
|
+
overridden.push(
|
|
3831
|
+
'skipPermissions: forced to false; the CLI exits with "bypassPermissions not supported in restricted mode" when both are passed'
|
|
3832
|
+
);
|
|
3833
|
+
}
|
|
3834
|
+
if (settings.controlRequestBehavior === "allow") {
|
|
3835
|
+
overridden.push(
|
|
3836
|
+
'controlRequestBehavior: forced to "deny"; read-only answers every permission request the same way the CLI does under --permission-prompts none'
|
|
3837
|
+
);
|
|
3838
|
+
}
|
|
3839
|
+
if (settings.controlRequestToolBehaviors !== void 0) {
|
|
3840
|
+
overridden.push(
|
|
3841
|
+
"controlRequestToolBehaviors: ignored; a per-tool allow would apply only on CLIs older than 2.1.263, so the preset would mean different things on different versions"
|
|
3842
|
+
);
|
|
3843
|
+
}
|
|
3844
|
+
const requested = settings.proxyTools ?? defaultProxyTools;
|
|
3845
|
+
const proxyTools = dedupe(requested).filter(
|
|
3846
|
+
(name) => !READ_ONLY_DENIED_PROXY_TOOLS.has(name.toLowerCase())
|
|
3847
|
+
);
|
|
3848
|
+
const dropped = dedupe(requested).filter(
|
|
3849
|
+
(name) => READ_ONLY_DENIED_PROXY_TOOLS.has(name.toLowerCase())
|
|
3850
|
+
);
|
|
3851
|
+
if (dropped.length > 0) {
|
|
3852
|
+
overridden.push(
|
|
3853
|
+
`proxyTools: dropped ${dropped.join(", ")}; those execute in opencode, where no Claude CLI flag reaches them`
|
|
3854
|
+
);
|
|
3855
|
+
}
|
|
3856
|
+
return {
|
|
3857
|
+
preset: "read-only",
|
|
3858
|
+
permissionMode: READ_ONLY_PERMISSION_MODE,
|
|
3859
|
+
skipPermissions: false,
|
|
3860
|
+
proxyTools,
|
|
3861
|
+
extraDisallowedTools: dedupe([
|
|
3862
|
+
...READ_ONLY_DISALLOWED_CLI_TOOLS,
|
|
3863
|
+
...settings.extraDisallowedTools ?? []
|
|
3864
|
+
]),
|
|
3865
|
+
controlRequestBehavior: "deny",
|
|
3866
|
+
controlRequestToolBehaviors: void 0,
|
|
3867
|
+
overridden
|
|
3868
|
+
};
|
|
3878
3869
|
}
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
if (
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
killAllActiveProcesses();
|
|
3885
|
-
});
|
|
3886
|
-
return true;
|
|
3870
|
+
function resolvePermissionPreset(settings, defaultProxyTools) {
|
|
3871
|
+
const preset = settings.permissionPreset;
|
|
3872
|
+
if (preset === void 0) return null;
|
|
3873
|
+
if (preset === "read-only") return resolveReadOnly(settings, defaultProxyTools);
|
|
3874
|
+
return { unknown: String(preset) };
|
|
3887
3875
|
}
|
|
3888
|
-
function
|
|
3889
|
-
|
|
3890
|
-
ap?.proc.kill();
|
|
3876
|
+
function isUnknownPreset(resolved) {
|
|
3877
|
+
return resolved !== null && "unknown" in resolved;
|
|
3891
3878
|
}
|
|
3892
|
-
|
|
3893
|
-
|
|
3879
|
+
var NO_PERMISSION_PRESET = "none";
|
|
3880
|
+
function summarizePermissionPreset(provider, options, defaultProxyTools = DEFAULT_PROXY_TOOL_NAMES) {
|
|
3881
|
+
const settings = options !== null && typeof options === "object" && !Array.isArray(options) ? options : {};
|
|
3882
|
+
const resolved = resolvePermissionPreset(settings, defaultProxyTools);
|
|
3883
|
+
if (resolved === null) {
|
|
3884
|
+
return { provider, preset: NO_PERMISSION_PRESET, applied: false, overrides: [] };
|
|
3885
|
+
}
|
|
3886
|
+
if (isUnknownPreset(resolved)) {
|
|
3887
|
+
return { provider, preset: resolved.unknown, applied: false, overrides: [] };
|
|
3888
|
+
}
|
|
3889
|
+
return {
|
|
3890
|
+
provider,
|
|
3891
|
+
preset: resolved.preset,
|
|
3892
|
+
applied: true,
|
|
3893
|
+
overrides: resolved.overridden
|
|
3894
|
+
};
|
|
3894
3895
|
}
|
|
3895
|
-
function
|
|
3896
|
-
|
|
3897
|
-
return new Promise((resolve6) => {
|
|
3898
|
-
const onExit = () => {
|
|
3899
|
-
clearTimeout(timer);
|
|
3900
|
-
resolve6(true);
|
|
3901
|
-
};
|
|
3902
|
-
const timer = setTimeout(() => {
|
|
3903
|
-
proc.off("exit", onExit);
|
|
3904
|
-
resolve6(hasProcessExited(proc));
|
|
3905
|
-
}, timeoutMs);
|
|
3906
|
-
proc.once("exit", onExit);
|
|
3907
|
-
});
|
|
3896
|
+
function isReadOnlyPermissionMode(mode) {
|
|
3897
|
+
return mode === READ_ONLY_PERMISSION_MODE;
|
|
3908
3898
|
}
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
if (await gracefulExit) return true;
|
|
3918
|
-
const forcedExit = waitForProcessExit(
|
|
3919
|
-
ap.proc,
|
|
3920
|
-
options.forceExitTimeoutMs ?? PROCESS_FORCE_EXIT_TIMEOUT_MS
|
|
3921
|
-
);
|
|
3922
|
-
ap.proc.kill("SIGKILL");
|
|
3923
|
-
if (await forcedExit) return true;
|
|
3924
|
-
log.warn("claude process did not exit; starting a fresh session", {
|
|
3925
|
-
sessionKey: key
|
|
3926
|
-
});
|
|
3927
|
-
deleteClaudeSessionId(key);
|
|
3928
|
-
return false;
|
|
3899
|
+
|
|
3900
|
+
// src/side-question.ts
|
|
3901
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
3902
|
+
var MAX_HISTORY_EXCHANGES = 20;
|
|
3903
|
+
var SIDE_QUESTION_USAGE = "Usage: /btw <question>. Ask a side question about the current conversation without adding it to the main context.";
|
|
3904
|
+
var pendingProcesses = /* @__PURE__ */ new WeakSet();
|
|
3905
|
+
function isRecord2(value) {
|
|
3906
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3929
3907
|
}
|
|
3930
|
-
|
|
3931
|
-
|
|
3908
|
+
var SYSTEM_REMINDER_BLOCK = /<system-reminder>[\s\S]*?<\/system-reminder>/g;
|
|
3909
|
+
function parseSideQuestionContent(content) {
|
|
3910
|
+
let text;
|
|
3911
|
+
if (typeof content === "string") {
|
|
3912
|
+
text = content;
|
|
3913
|
+
} else if (Array.isArray(content)) {
|
|
3914
|
+
const parts = [];
|
|
3915
|
+
for (const part of content) {
|
|
3916
|
+
if (!isRecord2(part) || part.type !== "text" || typeof part.text !== "string") return null;
|
|
3917
|
+
parts.push(part.text);
|
|
3918
|
+
}
|
|
3919
|
+
text = parts.join("\n");
|
|
3920
|
+
} else {
|
|
3921
|
+
return null;
|
|
3922
|
+
}
|
|
3923
|
+
const match = /^\/btw(?:\s+([\s\S]*))?$/.exec(text.replace(SYSTEM_REMINDER_BLOCK, "").trim());
|
|
3924
|
+
return match ? { question: (match[1] ?? "").trim() } : null;
|
|
3932
3925
|
}
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
return getPendingProxyCalls(key).length > 0 || hasExitPlanModeQuestions(key);
|
|
3926
|
+
function parseSideQuestion(prompt) {
|
|
3927
|
+
const latest = prompt.at(-1);
|
|
3928
|
+
return latest?.role === "user" ? parseSideQuestionContent(latest.content) : null;
|
|
3937
3929
|
}
|
|
3938
|
-
function
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
size: claudeSessions.size,
|
|
3945
|
-
cap: MAX_CLAUDE_SESSION_ENTRIES
|
|
3946
|
-
});
|
|
3947
|
-
deleteClaudeSessionId(key);
|
|
3930
|
+
function assistantText(content) {
|
|
3931
|
+
if (typeof content === "string") return content.trim();
|
|
3932
|
+
if (!Array.isArray(content)) return "";
|
|
3933
|
+
const parts = [];
|
|
3934
|
+
for (const part of content) {
|
|
3935
|
+
if (isRecord2(part) && part.type === "text" && typeof part.text === "string") parts.push(part.text);
|
|
3948
3936
|
}
|
|
3937
|
+
return parts.join("\n").trim();
|
|
3949
3938
|
}
|
|
3950
|
-
function
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3939
|
+
function collectSideQuestionHistory(prompt) {
|
|
3940
|
+
const history = [];
|
|
3941
|
+
for (let index = 0; index < prompt.length - 1; index++) {
|
|
3942
|
+
const message = prompt[index];
|
|
3943
|
+
if (message.role !== "user") continue;
|
|
3944
|
+
const aside = parseSideQuestionContent(message.content);
|
|
3945
|
+
if (!aside?.question) continue;
|
|
3946
|
+
const reply = prompt[index + 1];
|
|
3947
|
+
if (reply.role !== "assistant") continue;
|
|
3948
|
+
const response = assistantText(reply.content);
|
|
3949
|
+
if (!response || response === SIDE_QUESTION_USAGE) continue;
|
|
3950
|
+
history.push({ question: aside.question, response });
|
|
3951
|
+
}
|
|
3952
|
+
return history.slice(-MAX_HISTORY_EXCHANGES);
|
|
3954
3953
|
}
|
|
3955
|
-
function
|
|
3956
|
-
|
|
3957
|
-
clearAccountFailoverQuestions(key);
|
|
3958
|
-
const claudeSessionId = claudeSessions.get(key);
|
|
3959
|
-
if (claudeSessionId) clearLedger(claudeSessionId);
|
|
3960
|
-
claudeSessions.delete(key);
|
|
3954
|
+
function isSideQuestionPending(activeProcess) {
|
|
3955
|
+
return pendingProcesses.has(activeProcess.proc);
|
|
3961
3956
|
}
|
|
3962
|
-
function
|
|
3963
|
-
|
|
3957
|
+
function dispatchSideQuestionResponse(activeProcess, line) {
|
|
3958
|
+
if (!pendingProcesses.has(activeProcess.proc)) return false;
|
|
3959
|
+
let message;
|
|
3960
|
+
try {
|
|
3961
|
+
message = JSON.parse(line);
|
|
3962
|
+
} catch {
|
|
3963
|
+
return false;
|
|
3964
|
+
}
|
|
3965
|
+
if (!isRecord2(message) || message.type !== "control_response") return false;
|
|
3966
|
+
const response = message.response;
|
|
3967
|
+
if (!isRecord2(response) || typeof response.request_id !== "string") return false;
|
|
3968
|
+
return activeProcess.lineEmitter.emit(`side-question:${response.request_id}`, response);
|
|
3964
3969
|
}
|
|
3965
|
-
function
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
"
|
|
3972
|
-
"xhigh",
|
|
3973
|
-
"max"
|
|
3974
|
-
];
|
|
3975
|
-
const staleKeys = levels.filter((level) => level !== effort).map((level) => effortSessionKey(baseKey, level));
|
|
3976
|
-
for (const key of staleKeys) {
|
|
3977
|
-
const active = activeProcesses.get(key);
|
|
3978
|
-
if (getPendingProxyCalls(key).length || hasExitPlanModeQuestions(key) || active?.pendingProxyCompletions?.size || active && (active.lineEmitter.listenerCount("line") > 0 || isSideQuestionPending(active))) {
|
|
3979
|
-
throw new Error(
|
|
3980
|
-
"Cannot change reasoning effort while the previous effort session has pending work. Finish that work at its original effort first."
|
|
3981
|
-
);
|
|
3982
|
-
}
|
|
3970
|
+
async function requestSideQuestion(activeProcess, question, options) {
|
|
3971
|
+
question = question.trim();
|
|
3972
|
+
if (!question) return { response: SIDE_QUESTION_USAGE, synthetic: true };
|
|
3973
|
+
options.abortSignal?.throwIfAborted();
|
|
3974
|
+
const { proc, lineEmitter } = activeProcess;
|
|
3975
|
+
if (options.interactive || !proc.stdout) {
|
|
3976
|
+
throw new Error("/btw requires the headless Claude Code transport; interactive sessions are not supported.");
|
|
3983
3977
|
}
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
deleteClaudeSessionId(key);
|
|
3987
|
-
clearCompression(key);
|
|
3988
|
-
clearBackgroundTasks(key);
|
|
3978
|
+
if (!cliSupportsSideQuestion(options.cliVersion)) {
|
|
3979
|
+
throw new Error("/btw requires Claude Code CLI 2.1.258 or newer (the oldest verified version).");
|
|
3989
3980
|
}
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
}
|
|
4001
|
-
const
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
proc,
|
|
4010
|
-
lineEmitter,
|
|
4011
|
-
proxyServer: proxyServer ?? null,
|
|
4012
|
-
mcpHash,
|
|
4013
|
-
systemPromptFile,
|
|
4014
|
-
effort,
|
|
4015
|
-
promptCacheTtl,
|
|
4016
|
-
startedAt: Date.now(),
|
|
4017
|
-
cliPath,
|
|
4018
|
-
cliArgs: [...cliArgs],
|
|
4019
|
-
unattendedLines: [],
|
|
4020
|
-
unattendedDropped: 0
|
|
4021
|
-
};
|
|
4022
|
-
const rl = createInterface({ input: proc.stdout });
|
|
4023
|
-
rl.on("line", (line) => {
|
|
4024
|
-
if (dispatchSideQuestionResponse(ap, line)) return;
|
|
4025
|
-
noteTurnLine(ap, line);
|
|
4026
|
-
if (lineEmitter.listenerCount("line") === 0) {
|
|
4027
|
-
bufferUnattendedLine(ap, line);
|
|
4028
|
-
return;
|
|
3981
|
+
if (pendingProcesses.has(proc)) {
|
|
3982
|
+
throw new Error("Wait for the current /btw to finish before asking another.");
|
|
3983
|
+
}
|
|
3984
|
+
const stdin = proc.stdin;
|
|
3985
|
+
if (proc.killed || proc.exitCode != null || proc.signalCode != null || !stdin || stdin.destroyed || stdin.writableEnded || !stdin.writable) {
|
|
3986
|
+
throw new Error("/btw requires a live Claude Code session with writable stdin.");
|
|
3987
|
+
}
|
|
3988
|
+
const timeoutMs = options.timeoutMs ?? 12e4;
|
|
3989
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2147483647) {
|
|
3990
|
+
throw new Error("/btw timeoutMs must be a positive 32-bit integer.");
|
|
3991
|
+
}
|
|
3992
|
+
const requestId = randomUUID2();
|
|
3993
|
+
const request = JSON.stringify({
|
|
3994
|
+
type: "control_request",
|
|
3995
|
+
request_id: requestId,
|
|
3996
|
+
request: {
|
|
3997
|
+
subtype: "side_question",
|
|
3998
|
+
question,
|
|
3999
|
+
...options.history === void 0 ? {} : { history: options.history }
|
|
4029
4000
|
}
|
|
4030
|
-
lineEmitter.emit("line", line);
|
|
4031
|
-
});
|
|
4032
|
-
rl.on("close", () => {
|
|
4033
|
-
settleTurn(ap);
|
|
4034
|
-
lineEmitter.emit("close");
|
|
4035
|
-
});
|
|
4036
|
-
cancelIdleProcessEviction(sessionKey2);
|
|
4037
|
-
activeProcesses.set(sessionKey2, ap);
|
|
4038
|
-
proc.on("error", (err) => {
|
|
4039
|
-
log.error("claude process error", { sessionKey: sessionKey2, error: err.message });
|
|
4040
|
-
});
|
|
4041
|
-
proc.stdin?.on("error", (err) => {
|
|
4042
|
-
log.warn("claude process stdin error", {
|
|
4043
|
-
sessionKey: sessionKey2,
|
|
4044
|
-
code: err.code,
|
|
4045
|
-
error: err.message
|
|
4046
|
-
});
|
|
4047
|
-
settleTurn(ap);
|
|
4048
4001
|
});
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4002
|
+
pendingProcesses.add(proc);
|
|
4003
|
+
return new Promise((resolve6, reject) => {
|
|
4004
|
+
const event = `side-question:${requestId}`;
|
|
4005
|
+
let settled = false;
|
|
4006
|
+
let sent = false;
|
|
4007
|
+
let cancelPending = false;
|
|
4008
|
+
const cleanup = () => {
|
|
4009
|
+
clearTimeout(timer);
|
|
4010
|
+
lineEmitter.off(event, onResponse);
|
|
4011
|
+
lineEmitter.off("close", onClose);
|
|
4012
|
+
lineEmitter.off("error", onError);
|
|
4013
|
+
proc.off("exit", onClose);
|
|
4014
|
+
proc.off("close", onClose);
|
|
4015
|
+
proc.off("error", onError);
|
|
4016
|
+
if (!cancelPending) stdin.off("error", onError);
|
|
4017
|
+
options.abortSignal?.removeEventListener("abort", onAbort);
|
|
4018
|
+
pendingProcesses.delete(proc);
|
|
4019
|
+
};
|
|
4020
|
+
const fail = (error, cancel = false) => {
|
|
4021
|
+
if (settled) return;
|
|
4022
|
+
settled = true;
|
|
4023
|
+
if (cancel && sent && !stdin.destroyed && !stdin.writableEnded && stdin.writable) {
|
|
4024
|
+
try {
|
|
4025
|
+
cancelPending = true;
|
|
4026
|
+
stdin.write(
|
|
4027
|
+
JSON.stringify({ type: "control_cancel_request", request_id: requestId }) + "\n",
|
|
4028
|
+
() => {
|
|
4029
|
+
queueMicrotask(() => stdin.off("error", onError));
|
|
4030
|
+
}
|
|
4031
|
+
);
|
|
4032
|
+
} catch {
|
|
4033
|
+
cancelPending = false;
|
|
4034
|
+
}
|
|
4065
4035
|
}
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
});
|
|
4086
|
-
claudeSessions.delete(sessionKey2);
|
|
4087
|
-
} else {
|
|
4088
|
-
log.debug("ignoring session ID error from stale claude process", {
|
|
4089
|
-
sessionKey: sessionKey2
|
|
4090
|
-
});
|
|
4036
|
+
cleanup();
|
|
4037
|
+
reject(error);
|
|
4038
|
+
};
|
|
4039
|
+
const onClose = () => fail(new Error("Claude Code closed before answering /btw."));
|
|
4040
|
+
const onError = (error) => fail(error);
|
|
4041
|
+
const onAbort = () => fail(
|
|
4042
|
+
options.abortSignal?.reason ?? new DOMException("/btw was aborted.", "AbortError"),
|
|
4043
|
+
true
|
|
4044
|
+
);
|
|
4045
|
+
const onResponse = (response) => {
|
|
4046
|
+
if (settled || response.request_id !== requestId) return;
|
|
4047
|
+
if (response.subtype === "error") {
|
|
4048
|
+
fail(new Error(typeof response.error === "string" ? response.error : "Claude Code rejected /btw."));
|
|
4049
|
+
return;
|
|
4050
|
+
}
|
|
4051
|
+
const result = response.response;
|
|
4052
|
+
if (response.subtype !== "success" || !isRecord2(result) || typeof result.response !== "string" || typeof result.synthetic !== "boolean") {
|
|
4053
|
+
fail(new Error("Claude Code returned an invalid /btw response."));
|
|
4054
|
+
return;
|
|
4091
4055
|
}
|
|
4056
|
+
settled = true;
|
|
4057
|
+
cleanup();
|
|
4058
|
+
resolve6({ response: result.response, synthetic: result.synthetic });
|
|
4059
|
+
};
|
|
4060
|
+
const timer = setTimeout(() => {
|
|
4061
|
+
fail(new Error(`/btw timed out after ${timeoutMs}ms.`), true);
|
|
4062
|
+
}, timeoutMs);
|
|
4063
|
+
lineEmitter.on(event, onResponse);
|
|
4064
|
+
lineEmitter.on("close", onClose);
|
|
4065
|
+
lineEmitter.on("error", onError);
|
|
4066
|
+
proc.on("exit", onClose);
|
|
4067
|
+
proc.on("close", onClose);
|
|
4068
|
+
proc.on("error", onError);
|
|
4069
|
+
stdin.on("error", onError);
|
|
4070
|
+
options.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
4071
|
+
if (options.abortSignal?.aborted) {
|
|
4072
|
+
onAbort();
|
|
4073
|
+
return;
|
|
4074
|
+
}
|
|
4075
|
+
try {
|
|
4076
|
+
sent = true;
|
|
4077
|
+
stdin.write(request + "\n");
|
|
4078
|
+
} catch (error) {
|
|
4079
|
+
fail(error);
|
|
4092
4080
|
}
|
|
4093
4081
|
});
|
|
4094
|
-
return ap;
|
|
4095
4082
|
}
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4083
|
+
|
|
4084
|
+
// src/session-manager.ts
|
|
4085
|
+
function findActiveProcessBySessionId(sessionID) {
|
|
4086
|
+
let found;
|
|
4087
|
+
for (const ap of activeProcesses.values()) {
|
|
4088
|
+
if (ap.opencodeSessionID === sessionID) found = ap;
|
|
4099
4089
|
}
|
|
4100
|
-
|
|
4101
|
-
if (!sid) return cliArgs;
|
|
4102
|
-
return [...cliArgs, "--resume", sid];
|
|
4090
|
+
return found;
|
|
4103
4091
|
}
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
const
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4092
|
+
var UNATTENDED_LINE_CAP = 500;
|
|
4093
|
+
var UNATTENDED_BYTE_CAP = 2 * 1024 * 1024;
|
|
4094
|
+
function bufferUnattendedLine(ap, line) {
|
|
4095
|
+
const lines = ap.unattendedLines ??= [];
|
|
4096
|
+
lines.push(line);
|
|
4097
|
+
let bytes = 0;
|
|
4098
|
+
for (const kept of lines) bytes += Buffer.byteLength(kept);
|
|
4099
|
+
while (lines.length > 0 && (lines.length > UNATTENDED_LINE_CAP || bytes > UNATTENDED_BYTE_CAP)) {
|
|
4100
|
+
bytes -= Buffer.byteLength(lines.shift());
|
|
4101
|
+
ap.unattendedDropped = (ap.unattendedDropped ?? 0) + 1;
|
|
4113
4102
|
}
|
|
4114
|
-
const replacement = spawnClaudeProcess(
|
|
4115
|
-
cliPath,
|
|
4116
|
-
appendResumeIfNeeded(sessionKey2, old.cliArgs ?? cliArgs),
|
|
4117
|
-
cwd,
|
|
4118
|
-
sessionKey2,
|
|
4119
|
-
old.proxyServer,
|
|
4120
|
-
old.mcpHash,
|
|
4121
|
-
old.systemPromptFile,
|
|
4122
|
-
ignoreAnthropicApiKey,
|
|
4123
|
-
old.effort,
|
|
4124
|
-
old.promptCacheTtl
|
|
4125
|
-
);
|
|
4126
|
-
replacement.pendingProxyCompletions = old.pendingProxyCompletions;
|
|
4127
|
-
delete old.pendingProxyCompletions;
|
|
4128
|
-
if (turnWasInFlight) noteTurnStarted(replacement);
|
|
4129
|
-
return replacement;
|
|
4130
4103
|
}
|
|
4131
|
-
function
|
|
4132
|
-
const
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
permissionMode,
|
|
4138
|
-
mcpConfig,
|
|
4139
|
-
strictMcpConfig,
|
|
4140
|
-
disallowedTools,
|
|
4141
|
-
appendSystemPromptFile,
|
|
4142
|
-
pluginDirs,
|
|
4143
|
-
thinking,
|
|
4144
|
-
thinkingDisplay,
|
|
4145
|
-
fastMode,
|
|
4146
|
-
cliVersion
|
|
4147
|
-
} = opts;
|
|
4148
|
-
const args = [
|
|
4149
|
-
"--print",
|
|
4150
|
-
"--output-format",
|
|
4151
|
-
"stream-json",
|
|
4152
|
-
"--input-format",
|
|
4153
|
-
"stream-json",
|
|
4154
|
-
"--include-partial-messages",
|
|
4155
|
-
"--verbose"
|
|
4156
|
-
];
|
|
4157
|
-
if (model) {
|
|
4158
|
-
args.push("--model", model);
|
|
4159
|
-
}
|
|
4160
|
-
const readOnly = isReadOnlyPermissionMode(permissionMode);
|
|
4161
|
-
if (readOnly) {
|
|
4162
|
-
if (cliSupportsRestricted(cliVersion ?? null)) {
|
|
4163
|
-
args.push("--restricted");
|
|
4164
|
-
} else {
|
|
4165
|
-
log.warn(
|
|
4166
|
-
'permissionPreset "read-only": this claude CLI has no --restricted (2.1.258+), so file reads are not confined to the working directory. The mutating tools are still refused via --disallowedTools and every permission request is denied.',
|
|
4167
|
-
{ cliVersion: cliVersion?.raw ?? "unknown" }
|
|
4168
|
-
);
|
|
4169
|
-
}
|
|
4170
|
-
if (cliSupportsPermissionPrompts(cliVersion ?? null)) {
|
|
4171
|
-
args.push("--permission-prompts", "none");
|
|
4172
|
-
} else {
|
|
4173
|
-
log.warn(
|
|
4174
|
-
`permissionPreset "read-only": this claude CLI has no --permission-prompts (2.1.263+), so permission requests are denied by the plugin's own can_use_tool handler instead of by the CLI. Same outcome, one layer fewer.`,
|
|
4175
|
-
{ cliVersion: cliVersion?.raw ?? "unknown" }
|
|
4176
|
-
);
|
|
4177
|
-
}
|
|
4178
|
-
} else if (permissionMode) {
|
|
4179
|
-
args.push("--permission-mode", permissionMode);
|
|
4180
|
-
}
|
|
4181
|
-
if (includeSessionId) {
|
|
4182
|
-
const sessionId = claudeSessions.get(sessionKey2);
|
|
4183
|
-
if (sessionId && !activeProcesses.has(sessionKey2)) {
|
|
4184
|
-
args.push("--resume", sessionId);
|
|
4185
|
-
}
|
|
4186
|
-
}
|
|
4187
|
-
if (mcpConfig) {
|
|
4188
|
-
const configs = Array.isArray(mcpConfig) ? mcpConfig : [mcpConfig];
|
|
4189
|
-
const filtered = configs.filter((c) => typeof c === "string" && c.length > 0);
|
|
4190
|
-
if (filtered.length > 0) {
|
|
4191
|
-
args.push("--mcp-config", ...filtered);
|
|
4192
|
-
}
|
|
4193
|
-
}
|
|
4194
|
-
if (strictMcpConfig) {
|
|
4195
|
-
args.push("--strict-mcp-config");
|
|
4196
|
-
}
|
|
4197
|
-
if (disallowedTools && disallowedTools.length > 0) {
|
|
4198
|
-
args.push("--disallowedTools", ...disallowedTools);
|
|
4199
|
-
}
|
|
4200
|
-
if (thinking && cliSupportsThinking(cliVersion ?? null)) {
|
|
4201
|
-
args.push("--thinking", thinking);
|
|
4202
|
-
}
|
|
4203
|
-
if (thinkingDisplay && cliSupportsThinkingDisplay(cliVersion ?? null)) {
|
|
4204
|
-
args.push("--thinking-display", thinkingDisplay);
|
|
4205
|
-
}
|
|
4206
|
-
if (appendSystemPromptFile) {
|
|
4207
|
-
args.push("--append-system-prompt-file", appendSystemPromptFile);
|
|
4208
|
-
}
|
|
4209
|
-
for (const dir of pluginDirs ?? []) {
|
|
4210
|
-
args.push("--plugin-dir", dir);
|
|
4211
|
-
}
|
|
4212
|
-
if (fastMode && cliSupportsFastMode(cliVersion ?? null)) {
|
|
4213
|
-
args.push("--settings", JSON.stringify({ fastMode: true }));
|
|
4214
|
-
}
|
|
4215
|
-
if (skipPermissions && permissionMode !== "plan" && !readOnly) {
|
|
4216
|
-
args.push("--dangerously-skip-permissions");
|
|
4217
|
-
}
|
|
4218
|
-
return args;
|
|
4104
|
+
function takeUnattendedLines(ap) {
|
|
4105
|
+
const lines = ap.unattendedLines ?? [];
|
|
4106
|
+
const dropped = ap.unattendedDropped ?? 0;
|
|
4107
|
+
ap.unattendedLines = [];
|
|
4108
|
+
ap.unattendedDropped = 0;
|
|
4109
|
+
return { lines, dropped };
|
|
4219
4110
|
}
|
|
4220
|
-
|
|
4221
|
-
|
|
4111
|
+
var STDERR_RETAIN_BYTES = 2 * 1024;
|
|
4112
|
+
function retainStderr(ap, chunk) {
|
|
4113
|
+
ap.lastStderr = ((ap.lastStderr ?? "") + chunk).slice(-STDERR_RETAIN_BYTES);
|
|
4222
4114
|
}
|
|
4223
|
-
function
|
|
4224
|
-
const
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
};
|
|
4115
|
+
function describeChildCrash(exitCode, signal, lastStderr) {
|
|
4116
|
+
const how = signal ? `was killed by ${signal}` : typeof exitCode === "number" ? `exited with code ${exitCode}` : "closed its output";
|
|
4117
|
+
const tail = lastStderr?.trim();
|
|
4118
|
+
return `The Claude Code CLI ${how} before finishing this turn (no result was emitted), so the answer above may be incomplete.` + (tail ? `
|
|
4119
|
+
|
|
4120
|
+
Last stderr from the CLI:
|
|
4121
|
+
${tail}` : "");
|
|
4231
4122
|
}
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
session: ap.opencodeSessionID ?? described.session,
|
|
4240
|
-
model: described.model,
|
|
4241
|
-
compaction: described.compaction,
|
|
4242
|
-
pid: ap.proc.pid,
|
|
4243
|
-
inFlight: ap.turnInFlight === true,
|
|
4244
|
-
ageMs: ap.startedAt === void 0 ? void 0 : Math.max(0, now - ap.startedAt),
|
|
4245
|
-
effort: ap.effort,
|
|
4246
|
-
attached: ap.lineEmitter.listenerCount("line") > 0,
|
|
4247
|
-
proxyUrl: ap.proxyServer?.url,
|
|
4248
|
-
lastStderr: typeof lastStderr === "string" ? lastStderr : void 0
|
|
4249
|
-
});
|
|
4250
|
-
}
|
|
4251
|
-
return out;
|
|
4123
|
+
var activeProcesses = /* @__PURE__ */ new Map();
|
|
4124
|
+
var claudeSessions = /* @__PURE__ */ new Map();
|
|
4125
|
+
var idleEvictionTimers = /* @__PURE__ */ new Map();
|
|
4126
|
+
var MAX_IDLE_TIMEOUT_MS = 2147483647;
|
|
4127
|
+
var DEFAULT_IDLE_PROCESS_TIMEOUT_MS = 0;
|
|
4128
|
+
function resolveIdleProcessTimeoutMs(configured) {
|
|
4129
|
+
return configured === void 0 ? DEFAULT_IDLE_PROCESS_TIMEOUT_MS : configured;
|
|
4252
4130
|
}
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
var
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4131
|
+
var MAX_ACTIVE_PROCESSES = 16;
|
|
4132
|
+
var PROCESS_EXIT_TIMEOUT_MS = 1500;
|
|
4133
|
+
var PROCESS_FORCE_EXIT_TIMEOUT_MS = 500;
|
|
4134
|
+
var CHILD_EXITED_MESSAGE = "Claude CLI subprocess closed before pending tool calls were resolved";
|
|
4135
|
+
function envFlagEnabled(value) {
|
|
4136
|
+
if (value === void 0) return false;
|
|
4137
|
+
const normalized = value.trim().toLowerCase();
|
|
4138
|
+
if (!normalized) return false;
|
|
4139
|
+
return !["0", "false", "no", "off"].includes(normalized);
|
|
4140
|
+
}
|
|
4141
|
+
function isClaudeThinkingDisabled() {
|
|
4142
|
+
return envFlagEnabled(process.env.CLAUDE_CODE_DISABLE_THINKING) || envFlagEnabled(process.env.CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING);
|
|
4143
|
+
}
|
|
4144
|
+
function cliEffortLevel(effort) {
|
|
4145
|
+
return effort === "minimal" ? "low" : effort;
|
|
4146
|
+
}
|
|
4147
|
+
function claudeSpawnEnv(opts) {
|
|
4148
|
+
const env = {
|
|
4149
|
+
...process.env,
|
|
4150
|
+
TERM: "xterm-256color",
|
|
4151
|
+
// Pin the child to the binary whose version we detected, and keep it off
|
|
4152
|
+
// non-essential network calls. Fills gaps only, so an explicit shell value
|
|
4153
|
+
// survives: see `cliHygieneEnv` for why the version has to hold still.
|
|
4154
|
+
...cliHygieneEnv()
|
|
4155
|
+
};
|
|
4156
|
+
if (opts?.effort) {
|
|
4157
|
+
env.CLAUDE_CODE_EFFORT_LEVEL = cliEffortLevel(opts.effort);
|
|
4259
4158
|
}
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
var BTW_NO_SESSION_MESSAGE = "/btw needs a live Claude Code session in this conversation. Send a normal message with a Claude Code model first, then ask again.";
|
|
4263
|
-
var BTW_INLINE_HANDLED_MESSAGE = "/btw was answered inside the running turn; nothing to add to this conversation.";
|
|
4264
|
-
var BTW_TURN_TOO_LONG_MESSAGE = "/btw gave up waiting for this turn to end. Ask again once it is over.";
|
|
4265
|
-
var IDLE_POLL_MS = 500;
|
|
4266
|
-
var IDLE_WAIT_MAX_MS = 30 * 6e4;
|
|
4267
|
-
var BUSY_SETTLE_MS = 1500;
|
|
4268
|
-
var SPAWN_WAIT_MAX_MS = 3e4;
|
|
4269
|
-
var INLINE_POLL_MS = 200;
|
|
4270
|
-
var INLINE_WAIT_MAX_MS = 2e4;
|
|
4271
|
-
var PENDING_ANSWER_TTL_MS = 10 * 6e4;
|
|
4272
|
-
var PENDING_ANSWER_CAP = 32;
|
|
4273
|
-
var pendingAnswers = /* @__PURE__ */ new Map();
|
|
4274
|
-
function rememberSideQuestionAnswer(sessionID, question, answer, now = Date.now()) {
|
|
4275
|
-
for (const [id, entry] of pendingAnswers) {
|
|
4276
|
-
if (now - entry.at > PENDING_ANSWER_TTL_MS) pendingAnswers.delete(id);
|
|
4159
|
+
if (opts?.promptCacheTtl) {
|
|
4160
|
+
env.CLAUDE_CODE_PROMPT_CACHE_TTL = opts.promptCacheTtl;
|
|
4277
4161
|
}
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
if (oldest === void 0) break;
|
|
4282
|
-
pendingAnswers.delete(oldest);
|
|
4162
|
+
if (opts?.ignoreAnthropicApiKey) {
|
|
4163
|
+
delete env.ANTHROPIC_API_KEY;
|
|
4164
|
+
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
4283
4165
|
}
|
|
4284
|
-
|
|
4166
|
+
if (!isClaudeThinkingDisabled() && process.env.CLAUDE_CODE_SHOW_THINKING_SUMMARIES === void 0) {
|
|
4167
|
+
env.CLAUDE_CODE_SHOW_THINKING_SUMMARIES = "1";
|
|
4168
|
+
}
|
|
4169
|
+
return env;
|
|
4285
4170
|
}
|
|
4286
|
-
function
|
|
4287
|
-
const
|
|
4288
|
-
if (
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4171
|
+
function touch(key) {
|
|
4172
|
+
const existing = activeProcesses.get(key);
|
|
4173
|
+
if (existing) {
|
|
4174
|
+
activeProcesses.delete(key);
|
|
4175
|
+
activeProcesses.set(key, existing);
|
|
4176
|
+
}
|
|
4177
|
+
}
|
|
4178
|
+
function evictIfNeeded() {
|
|
4179
|
+
while (activeProcesses.size >= MAX_ACTIVE_PROCESSES) {
|
|
4180
|
+
let victimKey;
|
|
4181
|
+
for (const [key, ap] of activeProcesses) {
|
|
4182
|
+
if (!isTurnInFlight(ap)) {
|
|
4183
|
+
victimKey = key;
|
|
4184
|
+
break;
|
|
4185
|
+
}
|
|
4186
|
+
}
|
|
4187
|
+
if (!victimKey) {
|
|
4188
|
+
log.warn("every claude process is mid-turn; skipping LRU eviction", {
|
|
4189
|
+
active: activeProcesses.size,
|
|
4190
|
+
cap: MAX_ACTIVE_PROCESSES
|
|
4191
|
+
});
|
|
4192
|
+
return;
|
|
4193
|
+
}
|
|
4194
|
+
log.info("evicting LRU claude process", { sessionKey: victimKey });
|
|
4195
|
+
deleteActiveProcess(victimKey);
|
|
4196
|
+
}
|
|
4292
4197
|
}
|
|
4293
|
-
var
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4198
|
+
var TURN_INTERRUPT_TIMEOUT_MS = 5e3;
|
|
4199
|
+
function isTerminalResultLine(line) {
|
|
4200
|
+
if (!line.includes('"result"')) return false;
|
|
4201
|
+
try {
|
|
4202
|
+
return JSON.parse(line).type === "result";
|
|
4203
|
+
} catch {
|
|
4204
|
+
return false;
|
|
4205
|
+
}
|
|
4297
4206
|
}
|
|
4298
|
-
function
|
|
4299
|
-
|
|
4207
|
+
function settleTurn(ap) {
|
|
4208
|
+
ap.turnInFlight = false;
|
|
4209
|
+
const waiters = ap.turnIdleWaiters ?? [];
|
|
4210
|
+
ap.turnIdleWaiters = [];
|
|
4211
|
+
for (const wake of waiters) wake();
|
|
4300
4212
|
}
|
|
4301
|
-
function
|
|
4302
|
-
|
|
4213
|
+
function noteTurnStarted(ap) {
|
|
4214
|
+
if (ap.asideTransport?.interactive) return;
|
|
4215
|
+
ap.turnInFlight = true;
|
|
4303
4216
|
}
|
|
4304
|
-
function
|
|
4305
|
-
return
|
|
4306
|
-
|
|
4307
|
-
\u258C
|
|
4308
|
-
${barEveryLine(answer.trim())}
|
|
4309
|
-
`;
|
|
4217
|
+
function noteTurnLine(ap, line) {
|
|
4218
|
+
if (!ap.turnInFlight) return;
|
|
4219
|
+
if (isTerminalResultLine(line)) settleTurn(ap);
|
|
4310
4220
|
}
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
return `
|
|
4314
|
-
${asideHeader(question)}
|
|
4315
|
-
\u258C ${INLINE_ASIDE_SENT_NOTE}
|
|
4316
|
-
`;
|
|
4221
|
+
function isTurnInFlight(ap) {
|
|
4222
|
+
return ap.turnInFlight === true;
|
|
4317
4223
|
}
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4224
|
+
function awaitTurnIdle(ap, timeoutMs) {
|
|
4225
|
+
if (!ap.turnInFlight) return Promise.resolve(true);
|
|
4226
|
+
return new Promise((resolve6) => {
|
|
4227
|
+
const wake = () => {
|
|
4228
|
+
clearTimeout(timer);
|
|
4229
|
+
resolve6(true);
|
|
4230
|
+
};
|
|
4231
|
+
const timer = setTimeout(() => {
|
|
4232
|
+
const waiters = ap.turnIdleWaiters ?? [];
|
|
4233
|
+
const at = waiters.indexOf(wake);
|
|
4234
|
+
if (at >= 0) waiters.splice(at, 1);
|
|
4235
|
+
resolve6(false);
|
|
4236
|
+
}, timeoutMs);
|
|
4237
|
+
(ap.turnIdleWaiters ??= []).push(wake);
|
|
4238
|
+
});
|
|
4324
4239
|
}
|
|
4325
|
-
function
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
log.debug("btw: could not write the aside into the running turn", { sessionID, error: errorText(error) });
|
|
4332
|
-
return false;
|
|
4240
|
+
function interruptTurn(ap, timeoutMs = TURN_INTERRUPT_TIMEOUT_MS) {
|
|
4241
|
+
if (!ap.turnInFlight) return Promise.resolve(true);
|
|
4242
|
+
const stdin = ap.proc.stdin;
|
|
4243
|
+
if (ap.asideTransport?.interactive || !stdin || !stdin.writable) {
|
|
4244
|
+
log.notice("cannot interrupt this transport; waiting for the turn to end");
|
|
4245
|
+
return awaitTurnIdle(ap, timeoutMs);
|
|
4333
4246
|
}
|
|
4334
|
-
}
|
|
4335
|
-
function showToast(client, body) {
|
|
4336
4247
|
try {
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4248
|
+
stdin.write(
|
|
4249
|
+
JSON.stringify({
|
|
4250
|
+
type: "control_request",
|
|
4251
|
+
request_id: randomUUID3(),
|
|
4252
|
+
request: { subtype: "interrupt" }
|
|
4253
|
+
}) + "\n"
|
|
4254
|
+
);
|
|
4340
4255
|
} catch (error) {
|
|
4341
|
-
log.
|
|
4256
|
+
log.warn("failed to write interrupt control request", {
|
|
4257
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4258
|
+
});
|
|
4259
|
+
return Promise.resolve(false);
|
|
4342
4260
|
}
|
|
4261
|
+
return awaitTurnIdle(ap, timeoutMs);
|
|
4343
4262
|
}
|
|
4344
|
-
function
|
|
4345
|
-
|
|
4263
|
+
function cancelIdleProcessEviction(key) {
|
|
4264
|
+
const timer = idleEvictionTimers.get(key);
|
|
4265
|
+
if (!timer) return;
|
|
4266
|
+
clearTimeout(timer);
|
|
4267
|
+
idleEvictionTimers.delete(key);
|
|
4346
4268
|
}
|
|
4347
|
-
|
|
4348
|
-
const
|
|
4349
|
-
if (
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
const entry = result.data?.[sessionID];
|
|
4353
|
-
return entry && entry.type !== "idle" ? "busy" : "idle";
|
|
4354
|
-
} catch (error) {
|
|
4355
|
-
log.debug("btw: could not read session status", { sessionID, error: errorText(error) });
|
|
4356
|
-
return "unknown";
|
|
4269
|
+
function getActiveProcess(key) {
|
|
4270
|
+
const ap = activeProcesses.get(key);
|
|
4271
|
+
if (ap) {
|
|
4272
|
+
cancelIdleProcessEviction(key);
|
|
4273
|
+
touch(key);
|
|
4357
4274
|
}
|
|
4275
|
+
return ap;
|
|
4358
4276
|
}
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
await new Promise((resolve6) => setTimeout(resolve6, pollMs));
|
|
4277
|
+
function setActiveProcess(key, ap) {
|
|
4278
|
+
cancelIdleProcessEviction(key);
|
|
4279
|
+
activeProcesses.set(key, ap);
|
|
4280
|
+
}
|
|
4281
|
+
function scheduleIdleProcessEviction(key, timeoutMs) {
|
|
4282
|
+
cancelIdleProcessEviction(key);
|
|
4283
|
+
if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_IDLE_TIMEOUT_MS) {
|
|
4284
|
+
return;
|
|
4368
4285
|
}
|
|
4286
|
+
const scheduledProcess = activeProcesses.get(key);
|
|
4287
|
+
if (!scheduledProcess) return;
|
|
4288
|
+
const timer = setTimeout(() => {
|
|
4289
|
+
idleEvictionTimers.delete(key);
|
|
4290
|
+
if (activeProcesses.get(key) !== scheduledProcess) return;
|
|
4291
|
+
if (isTurnInFlight(scheduledProcess)) {
|
|
4292
|
+
log.info("idle timer found a turn in flight; re-arming", { sessionKey: key, timeoutMs });
|
|
4293
|
+
scheduleIdleProcessEviction(key, timeoutMs);
|
|
4294
|
+
return;
|
|
4295
|
+
}
|
|
4296
|
+
log.info("evicting idle claude process", { sessionKey: key, timeoutMs });
|
|
4297
|
+
deleteActiveProcess(key);
|
|
4298
|
+
}, timeoutMs);
|
|
4299
|
+
timer.unref();
|
|
4300
|
+
idleEvictionTimers.set(key, timer);
|
|
4369
4301
|
}
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
const
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
await new Promise((resolve6) => setTimeout(resolve6, pollMs));
|
|
4302
|
+
function detachActiveProcess(key) {
|
|
4303
|
+
cancelIdleProcessEviction(key);
|
|
4304
|
+
const ap = activeProcesses.get(key);
|
|
4305
|
+
if (!ap) return void 0;
|
|
4306
|
+
activeProcesses.delete(key);
|
|
4307
|
+
if (ap.proxyServer) {
|
|
4308
|
+
void ap.proxyServer.close();
|
|
4309
|
+
rejectAllPendingProxyCallsForSession(key, new Error(SERVER_CLOSED_MESSAGE));
|
|
4379
4310
|
}
|
|
4311
|
+
return ap;
|
|
4380
4312
|
}
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
const
|
|
4384
|
-
const
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
if (
|
|
4396
|
-
log.warn("btw: a turn is running but no claude process appeared for it", { sessionID, waitedMs });
|
|
4397
|
-
return void 0;
|
|
4398
|
-
}
|
|
4399
|
-
await new Promise((resolve6) => setTimeout(resolve6, pollMs));
|
|
4313
|
+
function deleteActiveProcessesForSession(sessionID) {
|
|
4314
|
+
if (!sessionID || sessionID === "default") return [];
|
|
4315
|
+
const released = [];
|
|
4316
|
+
for (const [key, ap] of [...activeProcesses]) {
|
|
4317
|
+
const owned = ap.opencodeSessionID === sessionID || describeSessionKey(key).session === sessionID;
|
|
4318
|
+
if (!owned) continue;
|
|
4319
|
+
log.info("releasing claude process for deleted session", { sessionKey: key, sessionID });
|
|
4320
|
+
void deleteActiveProcessAndWait(key);
|
|
4321
|
+
released.push(key);
|
|
4322
|
+
}
|
|
4323
|
+
for (const key of [...claudeSessions.keys()]) {
|
|
4324
|
+
if (describeSessionKey(key).session !== sessionID) continue;
|
|
4325
|
+
deleteClaudeSessionId(key);
|
|
4326
|
+
clearCompression(key);
|
|
4327
|
+
if (!released.includes(key)) released.push(key);
|
|
4400
4328
|
}
|
|
4329
|
+
for (const key of released) clearBackgroundTasks(key);
|
|
4330
|
+
return released;
|
|
4331
|
+
}
|
|
4332
|
+
function killAllActiveProcesses() {
|
|
4333
|
+
const keys = [...activeProcesses.keys()];
|
|
4334
|
+
for (const key of keys) deleteActiveProcess(key);
|
|
4335
|
+
for (const key of keys) clearBackgroundTasks(key);
|
|
4336
|
+
return keys;
|
|
4337
|
+
}
|
|
4338
|
+
var processExitCleanupWired = false;
|
|
4339
|
+
function ensureProcessExitCleanup() {
|
|
4340
|
+
if (processExitCleanupWired) return false;
|
|
4341
|
+
processExitCleanupWired = true;
|
|
4342
|
+
process.once("exit", () => {
|
|
4343
|
+
killAllActiveProcesses();
|
|
4344
|
+
});
|
|
4345
|
+
return true;
|
|
4346
|
+
}
|
|
4347
|
+
function deleteActiveProcess(key) {
|
|
4348
|
+
const ap = detachActiveProcess(key);
|
|
4349
|
+
ap?.proc.kill();
|
|
4350
|
+
}
|
|
4351
|
+
function hasProcessExited(proc) {
|
|
4352
|
+
return proc.exitCode !== null || proc.signalCode !== null;
|
|
4353
|
+
}
|
|
4354
|
+
function waitForProcessExit(proc, timeoutMs) {
|
|
4355
|
+
if (hasProcessExited(proc)) return Promise.resolve(true);
|
|
4356
|
+
return new Promise((resolve6) => {
|
|
4357
|
+
const onExit = () => {
|
|
4358
|
+
clearTimeout(timer);
|
|
4359
|
+
resolve6(true);
|
|
4360
|
+
};
|
|
4361
|
+
const timer = setTimeout(() => {
|
|
4362
|
+
proc.off("exit", onExit);
|
|
4363
|
+
resolve6(hasProcessExited(proc));
|
|
4364
|
+
}, timeoutMs);
|
|
4365
|
+
proc.once("exit", onExit);
|
|
4366
|
+
});
|
|
4367
|
+
}
|
|
4368
|
+
async function deleteActiveProcessAndWait(key, options = {}) {
|
|
4369
|
+
const ap = detachActiveProcess(key);
|
|
4370
|
+
if (!ap || hasProcessExited(ap.proc)) return true;
|
|
4371
|
+
const gracefulExit = waitForProcessExit(
|
|
4372
|
+
ap.proc,
|
|
4373
|
+
options.exitTimeoutMs ?? PROCESS_EXIT_TIMEOUT_MS
|
|
4374
|
+
);
|
|
4375
|
+
ap.proc.kill();
|
|
4376
|
+
if (await gracefulExit) return true;
|
|
4377
|
+
const forcedExit = waitForProcessExit(
|
|
4378
|
+
ap.proc,
|
|
4379
|
+
options.forceExitTimeoutMs ?? PROCESS_FORCE_EXIT_TIMEOUT_MS
|
|
4380
|
+
);
|
|
4381
|
+
ap.proc.kill("SIGKILL");
|
|
4382
|
+
if (await forcedExit) return true;
|
|
4383
|
+
log.warn("claude process did not exit; starting a fresh session", {
|
|
4384
|
+
sessionKey: key
|
|
4385
|
+
});
|
|
4386
|
+
deleteClaudeSessionId(key);
|
|
4387
|
+
return false;
|
|
4401
4388
|
}
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
const settleMs = options.settleMs ?? BUSY_SETTLE_MS;
|
|
4405
|
-
const started = Date.now();
|
|
4406
|
-
for (; ; ) {
|
|
4407
|
-
const status = await sessionStatus(client, sessionID);
|
|
4408
|
-
if (status === "busy") return true;
|
|
4409
|
-
if (status === "unknown") return isProcessBusy(active);
|
|
4410
|
-
if (Date.now() - started >= settleMs) return false;
|
|
4411
|
-
await new Promise((resolve6) => setTimeout(resolve6, pollMs));
|
|
4412
|
-
}
|
|
4389
|
+
function getClaudeSessionId(key) {
|
|
4390
|
+
return claudeSessions.get(key);
|
|
4413
4391
|
}
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
if (
|
|
4417
|
-
|
|
4392
|
+
var MAX_CLAUDE_SESSION_ENTRIES = 64;
|
|
4393
|
+
function claudeSessionIsBusy(key) {
|
|
4394
|
+
if (activeProcesses.has(key)) return true;
|
|
4395
|
+
return getPendingProxyCalls(key).length > 0 || hasExitPlanModeQuestions(key);
|
|
4396
|
+
}
|
|
4397
|
+
function capClaudeSessions() {
|
|
4398
|
+
for (const key of [...claudeSessions.keys()]) {
|
|
4399
|
+
if (claudeSessions.size <= MAX_CLAUDE_SESSION_ENTRIES) return;
|
|
4400
|
+
if (claudeSessionIsBusy(key)) continue;
|
|
4401
|
+
log.info("claude session cap reached; releasing an idle session", {
|
|
4402
|
+
sessionKey: key,
|
|
4403
|
+
size: claudeSessions.size,
|
|
4404
|
+
cap: MAX_CLAUDE_SESSION_ENTRIES
|
|
4405
|
+
});
|
|
4406
|
+
deleteClaudeSessionId(key);
|
|
4418
4407
|
}
|
|
4419
|
-
return String(error);
|
|
4420
4408
|
}
|
|
4421
|
-
function
|
|
4422
|
-
|
|
4409
|
+
function setClaudeSessionId(key, sessionId) {
|
|
4410
|
+
claudeSessions.delete(key);
|
|
4411
|
+
claudeSessions.set(key, sessionId);
|
|
4412
|
+
capClaudeSessions();
|
|
4423
4413
|
}
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
for (const message of result.data ?? []) {
|
|
4431
|
-
const role = message.info?.role;
|
|
4432
|
-
if (role !== "user" && role !== "assistant") continue;
|
|
4433
|
-
prompt.push({ role, content: (message.parts ?? []).filter(isTextPart) });
|
|
4434
|
-
}
|
|
4435
|
-
prompt.push({ role: "user", content: `/btw ${question}` });
|
|
4436
|
-
return collectSideQuestionHistory(prompt);
|
|
4437
|
-
} catch (error) {
|
|
4438
|
-
log.debug("btw: could not read aside history", { sessionID, error: errorText(error) });
|
|
4439
|
-
return [];
|
|
4440
|
-
}
|
|
4414
|
+
function deleteClaudeSessionId(key) {
|
|
4415
|
+
clearExitPlanModeQuestions(key);
|
|
4416
|
+
clearAccountFailoverQuestions(key);
|
|
4417
|
+
const claudeSessionId = claudeSessions.get(key);
|
|
4418
|
+
if (claudeSessionId) clearLedger(claudeSessionId);
|
|
4419
|
+
claudeSessions.delete(key);
|
|
4441
4420
|
}
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4421
|
+
function effortSessionKey(baseKey, effort) {
|
|
4422
|
+
return effort ? `${baseKey}::effort=${effort}` : baseKey;
|
|
4423
|
+
}
|
|
4424
|
+
function invalidateOtherEffortSessions(baseKey, effort) {
|
|
4425
|
+
const levels = [
|
|
4426
|
+
void 0,
|
|
4427
|
+
"minimal",
|
|
4428
|
+
"low",
|
|
4429
|
+
"medium",
|
|
4430
|
+
"high",
|
|
4431
|
+
"xhigh",
|
|
4432
|
+
"max"
|
|
4433
|
+
];
|
|
4434
|
+
const staleKeys = levels.filter((level) => level !== effort).map((level) => effortSessionKey(baseKey, level));
|
|
4435
|
+
for (const key of staleKeys) {
|
|
4436
|
+
const active = activeProcesses.get(key);
|
|
4437
|
+
if (getPendingProxyCalls(key).length || hasExitPlanModeQuestions(key) || active?.pendingProxyCompletions?.size || active && (active.lineEmitter.listenerCount("line") > 0 || isSideQuestionPending(active))) {
|
|
4438
|
+
throw new Error(
|
|
4439
|
+
"Cannot change reasoning effort while the previous effort session has pending work. Finish that work at its original effort first."
|
|
4440
|
+
);
|
|
4441
|
+
}
|
|
4447
4442
|
}
|
|
4448
|
-
const
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4443
|
+
for (const key of staleKeys) {
|
|
4444
|
+
deleteActiveProcess(key);
|
|
4445
|
+
deleteClaudeSessionId(key);
|
|
4446
|
+
clearCompression(key);
|
|
4447
|
+
clearBackgroundTasks(key);
|
|
4453
4448
|
}
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4449
|
+
}
|
|
4450
|
+
function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcpHash, systemPromptFile, ignoreAnthropicApiKey, effort, promptCacheTtl) {
|
|
4451
|
+
evictIfNeeded();
|
|
4452
|
+
log.info("spawning new claude process", {
|
|
4453
|
+
cliPath,
|
|
4454
|
+
cliArgs,
|
|
4455
|
+
cwd,
|
|
4456
|
+
sessionKey: sessionKey2,
|
|
4457
|
+
effort,
|
|
4458
|
+
promptCacheTtl
|
|
4459
|
+
});
|
|
4460
|
+
const proc = spawn(cliPath, cliArgs, {
|
|
4461
|
+
cwd,
|
|
4462
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
4463
|
+
env: claudeSpawnEnv({ ignoreAnthropicApiKey, effort, promptCacheTtl }),
|
|
4464
|
+
shell: process.platform === "win32"
|
|
4465
|
+
});
|
|
4466
|
+
const lineEmitter = new EventEmitter3();
|
|
4467
|
+
const ap = {
|
|
4468
|
+
proc,
|
|
4469
|
+
lineEmitter,
|
|
4470
|
+
proxyServer: proxyServer ?? null,
|
|
4471
|
+
mcpHash,
|
|
4472
|
+
systemPromptFile,
|
|
4473
|
+
effort,
|
|
4474
|
+
promptCacheTtl,
|
|
4475
|
+
startedAt: Date.now(),
|
|
4476
|
+
cliPath,
|
|
4477
|
+
cliArgs: [...cliArgs],
|
|
4478
|
+
unattendedLines: [],
|
|
4479
|
+
unattendedDropped: 0
|
|
4457
4480
|
};
|
|
4458
|
-
const
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4481
|
+
const rl = createInterface({ input: proc.stdout });
|
|
4482
|
+
rl.on("line", (line) => {
|
|
4483
|
+
if (dispatchSideQuestionResponse(ap, line)) return;
|
|
4484
|
+
noteTurnLine(ap, line);
|
|
4485
|
+
if (lineEmitter.listenerCount("line") === 0) {
|
|
4486
|
+
bufferUnattendedLine(ap, line);
|
|
4487
|
+
return;
|
|
4488
|
+
}
|
|
4489
|
+
lineEmitter.emit("line", line);
|
|
4463
4490
|
});
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
} else {
|
|
4468
|
-
const settling = settleSessionBusy(client, input.sessionID, active, options);
|
|
4469
|
-
const history = await fetchAsideHistory(client, input.sessionID, question);
|
|
4470
|
-
const answer = requestSideQuestion(active, question, {
|
|
4471
|
-
cliVersion: await detectCliVersion(transport.cliPath),
|
|
4472
|
-
interactive: transport.interactive,
|
|
4473
|
-
...history.length ? { history } : {}
|
|
4474
|
-
});
|
|
4475
|
-
answer.catch(() => void 0);
|
|
4476
|
-
rememberSideQuestionAnswer(input.sessionID, question, answer);
|
|
4477
|
-
busy = await settling;
|
|
4478
|
-
log.info("btw: aside sent ahead of its message", {
|
|
4479
|
-
sessionID: input.sessionID,
|
|
4480
|
-
busy,
|
|
4481
|
-
questionLength: question.length,
|
|
4482
|
-
history: history.length
|
|
4483
|
-
});
|
|
4484
|
-
const asked = busy ? deliverAsideInline(client, input.sessionID, formatInlineAsideAsk(question), options).catch(() => false) : Promise.resolve(false);
|
|
4485
|
-
answer.then(
|
|
4486
|
-
async (result) => {
|
|
4487
|
-
log.info("btw: early answer arrived", { sessionID: input.sessionID, busy, responseLength: result.response.length });
|
|
4488
|
-
if (!busy || result.synthetic) return;
|
|
4489
|
-
await asked;
|
|
4490
|
-
const inline = await deliverAsideInline(
|
|
4491
|
-
client,
|
|
4492
|
-
input.sessionID,
|
|
4493
|
-
formatInlineAside(question, result.response),
|
|
4494
|
-
options
|
|
4495
|
-
);
|
|
4496
|
-
if (inline) {
|
|
4497
|
-
log.info("btw: answer written into the running turn", { sessionID: input.sessionID });
|
|
4498
|
-
markInlineDelivered();
|
|
4499
|
-
return;
|
|
4500
|
-
}
|
|
4501
|
-
log.info("btw: no open stream for the answer; the held message will carry it", {
|
|
4502
|
-
sessionID: input.sessionID
|
|
4503
|
-
});
|
|
4504
|
-
},
|
|
4505
|
-
(error) => {
|
|
4506
|
-
log.warn("btw: early aside failed; the message will ask again", {
|
|
4507
|
-
sessionID: input.sessionID,
|
|
4508
|
-
error: errorText(error)
|
|
4509
|
-
});
|
|
4510
|
-
}
|
|
4511
|
-
);
|
|
4512
|
-
}
|
|
4513
|
-
if (!busy) return;
|
|
4514
|
-
const started = Date.now();
|
|
4515
|
-
const outcome = await Promise.race([
|
|
4516
|
-
inlineDelivered,
|
|
4517
|
-
waitForSessionIdle(client, input.sessionID, { ...options, stop: () => inlineDone }).then(
|
|
4518
|
-
(idle) => idle ? "idle" : "timeout"
|
|
4519
|
-
)
|
|
4520
|
-
]);
|
|
4521
|
-
if (outcome === "inline") {
|
|
4522
|
-
log.info("btw: answered inside the running turn, dropping the /btw message", {
|
|
4523
|
-
sessionID: input.sessionID,
|
|
4524
|
-
waitedMs: Date.now() - started
|
|
4525
|
-
});
|
|
4526
|
-
throw new BtwHandledError(BTW_INLINE_HANDLED_MESSAGE);
|
|
4527
|
-
}
|
|
4528
|
-
log.info("btw: turn over, releasing the /btw message", {
|
|
4529
|
-
sessionID: input.sessionID,
|
|
4530
|
-
idle: outcome === "idle",
|
|
4531
|
-
waitedMs: Date.now() - started
|
|
4491
|
+
rl.on("close", () => {
|
|
4492
|
+
settleTurn(ap);
|
|
4493
|
+
lineEmitter.emit("close");
|
|
4532
4494
|
});
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
}
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
if (!trimmed) return { status: "failed", error: "the CLI printed nothing" };
|
|
4553
|
-
const candidates = trimmed.split("\n").reverse();
|
|
4554
|
-
for (const line of candidates) {
|
|
4555
|
-
const start = line.indexOf("{");
|
|
4556
|
-
if (start === -1) continue;
|
|
4557
|
-
let parsed;
|
|
4558
|
-
try {
|
|
4559
|
-
parsed = JSON.parse(line.slice(start));
|
|
4560
|
-
} catch {
|
|
4561
|
-
continue;
|
|
4495
|
+
cancelIdleProcessEviction(sessionKey2);
|
|
4496
|
+
activeProcesses.set(sessionKey2, ap);
|
|
4497
|
+
proc.on("error", (err) => {
|
|
4498
|
+
log.error("claude process error", { sessionKey: sessionKey2, error: err.message });
|
|
4499
|
+
});
|
|
4500
|
+
proc.stdin?.on("error", (err) => {
|
|
4501
|
+
log.warn("claude process stdin error", {
|
|
4502
|
+
sessionKey: sessionKey2,
|
|
4503
|
+
code: err.code,
|
|
4504
|
+
error: err.message
|
|
4505
|
+
});
|
|
4506
|
+
settleTurn(ap);
|
|
4507
|
+
});
|
|
4508
|
+
proc.on("exit", (code, signal) => {
|
|
4509
|
+
log.info("claude process exited", { code, signal, sessionKey: sessionKey2 });
|
|
4510
|
+
void proxyServer?.close();
|
|
4511
|
+
if (systemPromptFile) {
|
|
4512
|
+
void unlink(systemPromptFile).catch(() => {
|
|
4513
|
+
});
|
|
4562
4514
|
}
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4515
|
+
const ownsSessionKey = activeProcesses.get(sessionKey2) === ap;
|
|
4516
|
+
if (ownsSessionKey) {
|
|
4517
|
+
cancelIdleProcessEviction(sessionKey2);
|
|
4518
|
+
activeProcesses.delete(sessionKey2);
|
|
4519
|
+
if (getPendingProxyCalls(sessionKey2).length > 0) {
|
|
4520
|
+
rejectAllPendingProxyCallsForSession(
|
|
4521
|
+
sessionKey2,
|
|
4522
|
+
new Error(CHILD_EXITED_MESSAGE)
|
|
4523
|
+
);
|
|
4524
|
+
}
|
|
4569
4525
|
}
|
|
4570
|
-
if (
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
}
|
|
4577
|
-
}
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4526
|
+
if (ownsSessionKey && code !== 0 && code !== null) {
|
|
4527
|
+
log.info("process exited with error, clearing session", {
|
|
4528
|
+
code,
|
|
4529
|
+
sessionKey: sessionKey2
|
|
4530
|
+
});
|
|
4531
|
+
claudeSessions.delete(sessionKey2);
|
|
4532
|
+
}
|
|
4533
|
+
});
|
|
4534
|
+
proc.stderr?.on("data", (data) => {
|
|
4535
|
+
const stderr = data.toString();
|
|
4536
|
+
log.debug("stderr", { data: stderr.slice(0, 200) });
|
|
4537
|
+
retainStderr(ap, stderr);
|
|
4538
|
+
reportUnrecognizedModel(stderr);
|
|
4539
|
+
if (stderr.includes("No conversation found") || stderr.includes("Session ID") && (stderr.includes("already in use") || stderr.includes("not found") || stderr.includes("invalid"))) {
|
|
4540
|
+
if (activeProcesses.get(sessionKey2) === ap) {
|
|
4541
|
+
log.warn("claude session ID error, clearing session", {
|
|
4542
|
+
sessionKey: sessionKey2,
|
|
4543
|
+
error: stderr.slice(0, 200)
|
|
4544
|
+
});
|
|
4545
|
+
claudeSessions.delete(sessionKey2);
|
|
4546
|
+
} else {
|
|
4547
|
+
log.debug("ignoring session ID error from stale claude process", {
|
|
4548
|
+
sessionKey: sessionKey2
|
|
4549
|
+
});
|
|
4592
4550
|
}
|
|
4593
|
-
|
|
4551
|
+
}
|
|
4594
4552
|
});
|
|
4553
|
+
return ap;
|
|
4595
4554
|
}
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
try {
|
|
4600
|
-
const env = claudeSpawnEnv({ ignoreAnthropicApiKey: options.ignoreAnthropicApiKey });
|
|
4601
|
-
const stdout = await run(
|
|
4602
|
-
cliPath,
|
|
4603
|
-
["-p", "/cost", "--output-format", "json"],
|
|
4604
|
-
timeoutMs,
|
|
4605
|
-
env
|
|
4606
|
-
);
|
|
4607
|
-
const usage = parsePlanUsage(stdout);
|
|
4608
|
-
if (usage.status === "ok") {
|
|
4609
|
-
log.info("read claude plan usage", { costUsd: usage.costUsd, numTurns: usage.numTurns });
|
|
4610
|
-
} else if (usage.status === "failed") {
|
|
4611
|
-
log.debug("could not read claude plan usage", { error: usage.error });
|
|
4612
|
-
}
|
|
4613
|
-
return usage;
|
|
4614
|
-
} catch (error) {
|
|
4615
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
4616
|
-
log.debug("could not read claude plan usage", { error: message });
|
|
4617
|
-
return { status: "failed", error: message };
|
|
4555
|
+
function appendResumeIfNeeded(sessionKey2, cliArgs) {
|
|
4556
|
+
if (cliArgs.includes("--resume") || cliArgs.includes("--session-id")) {
|
|
4557
|
+
return cliArgs;
|
|
4618
4558
|
}
|
|
4559
|
+
const sid = claudeSessions.get(sessionKey2);
|
|
4560
|
+
if (!sid) return cliArgs;
|
|
4561
|
+
return [...cliArgs, "--resume", sid];
|
|
4619
4562
|
}
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
import { fileURLToPath } from "url";
|
|
4627
|
-
|
|
4628
|
-
// src/mcp-bridge.ts
|
|
4629
|
-
import * as fs3 from "fs";
|
|
4630
|
-
import * as path4 from "path";
|
|
4631
|
-
import * as os2 from "os";
|
|
4632
|
-
import * as crypto2 from "crypto";
|
|
4633
|
-
import {
|
|
4634
|
-
parse as parseJsonc,
|
|
4635
|
-
printParseErrorCode
|
|
4636
|
-
} from "jsonc-parser";
|
|
4637
|
-
var FILE_NAMES = ["opencode.jsonc", "opencode.json", "config.json"];
|
|
4638
|
-
var PROJECT_FILE_NAMES = ["opencode.json", "opencode.jsonc"];
|
|
4639
|
-
function fileExists(p) {
|
|
4563
|
+
function respawnActiveProcess(sessionKey2, cliPath, cliArgs, cwd, ignoreAnthropicApiKey) {
|
|
4564
|
+
const old = activeProcesses.get(sessionKey2);
|
|
4565
|
+
if (!old) return void 0;
|
|
4566
|
+
const turnWasInFlight = isTurnInFlight(old);
|
|
4567
|
+
activeProcesses.delete(sessionKey2);
|
|
4568
|
+
old.proc.removeAllListeners("exit");
|
|
4640
4569
|
try {
|
|
4641
|
-
|
|
4570
|
+
old.proc.kill();
|
|
4642
4571
|
} catch {
|
|
4643
|
-
return false;
|
|
4644
4572
|
}
|
|
4573
|
+
const replacement = spawnClaudeProcess(
|
|
4574
|
+
cliPath,
|
|
4575
|
+
appendResumeIfNeeded(sessionKey2, old.cliArgs ?? cliArgs),
|
|
4576
|
+
cwd,
|
|
4577
|
+
sessionKey2,
|
|
4578
|
+
old.proxyServer,
|
|
4579
|
+
old.mcpHash,
|
|
4580
|
+
old.systemPromptFile,
|
|
4581
|
+
ignoreAnthropicApiKey,
|
|
4582
|
+
old.effort,
|
|
4583
|
+
old.promptCacheTtl
|
|
4584
|
+
);
|
|
4585
|
+
replacement.mcpServers = old.mcpServers;
|
|
4586
|
+
replacement.pendingProxyCompletions = old.pendingProxyCompletions;
|
|
4587
|
+
delete old.pendingProxyCompletions;
|
|
4588
|
+
if (turnWasInFlight) noteTurnStarted(replacement);
|
|
4589
|
+
return replacement;
|
|
4645
4590
|
}
|
|
4646
|
-
function
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4591
|
+
function buildCliArgs(opts) {
|
|
4592
|
+
const {
|
|
4593
|
+
sessionKey: sessionKey2,
|
|
4594
|
+
skipPermissions,
|
|
4595
|
+
includeSessionId = true,
|
|
4596
|
+
model,
|
|
4597
|
+
permissionMode,
|
|
4598
|
+
mcpConfig,
|
|
4599
|
+
strictMcpConfig,
|
|
4600
|
+
disallowedTools,
|
|
4601
|
+
appendSystemPromptFile,
|
|
4602
|
+
pluginDirs,
|
|
4603
|
+
thinking,
|
|
4604
|
+
thinkingDisplay,
|
|
4605
|
+
fastMode,
|
|
4606
|
+
cliVersion
|
|
4607
|
+
} = opts;
|
|
4608
|
+
const args = [
|
|
4609
|
+
"--print",
|
|
4610
|
+
"--output-format",
|
|
4611
|
+
"stream-json",
|
|
4612
|
+
"--input-format",
|
|
4613
|
+
"stream-json",
|
|
4614
|
+
"--include-partial-messages",
|
|
4615
|
+
"--verbose"
|
|
4616
|
+
];
|
|
4617
|
+
if (model) {
|
|
4618
|
+
args.push("--model", model);
|
|
4651
4619
|
}
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
throw new Error(
|
|
4661
|
-
`${printParseErrorCode(first.error)} at offset ${first.offset}`
|
|
4620
|
+
const readOnly = isReadOnlyPermissionMode(permissionMode);
|
|
4621
|
+
if (readOnly) {
|
|
4622
|
+
if (cliSupportsRestricted(cliVersion ?? null)) {
|
|
4623
|
+
args.push("--restricted");
|
|
4624
|
+
} else {
|
|
4625
|
+
log.warn(
|
|
4626
|
+
'permissionPreset "read-only": this claude CLI has no --restricted (2.1.258+), so file reads are not confined to the working directory. The mutating tools are still refused via --disallowedTools and every permission request is denied.',
|
|
4627
|
+
{ cliVersion: cliVersion?.raw ?? "unknown" }
|
|
4662
4628
|
);
|
|
4663
4629
|
}
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4630
|
+
if (cliSupportsPermissionPrompts(cliVersion ?? null)) {
|
|
4631
|
+
args.push("--permission-prompts", "none");
|
|
4632
|
+
} else {
|
|
4633
|
+
log.warn(
|
|
4634
|
+
`permissionPreset "read-only": this claude CLI has no --permission-prompts (2.1.263+), so permission requests are denied by the plugin's own can_use_tool handler instead of by the CLI. Same outcome, one layer fewer.`,
|
|
4635
|
+
{ cliVersion: cliVersion?.raw ?? "unknown" }
|
|
4636
|
+
);
|
|
4637
|
+
}
|
|
4638
|
+
} else if (permissionMode) {
|
|
4639
|
+
args.push("--permission-mode", permissionMode);
|
|
4640
|
+
}
|
|
4641
|
+
if (includeSessionId) {
|
|
4642
|
+
const sessionId = claudeSessions.get(sessionKey2);
|
|
4643
|
+
if (sessionId && !activeProcesses.has(sessionKey2)) {
|
|
4644
|
+
args.push("--resume", sessionId);
|
|
4645
|
+
}
|
|
4646
|
+
}
|
|
4647
|
+
if (mcpConfig) {
|
|
4648
|
+
const configs = Array.isArray(mcpConfig) ? mcpConfig : [mcpConfig];
|
|
4649
|
+
const filtered = configs.filter((c) => typeof c === "string" && c.length > 0);
|
|
4650
|
+
if (filtered.length > 0) {
|
|
4651
|
+
args.push("--mcp-config", ...filtered);
|
|
4652
|
+
}
|
|
4653
|
+
}
|
|
4654
|
+
if (strictMcpConfig) {
|
|
4655
|
+
args.push("--strict-mcp-config");
|
|
4656
|
+
}
|
|
4657
|
+
if (disallowedTools && disallowedTools.length > 0) {
|
|
4658
|
+
args.push("--disallowedTools", ...disallowedTools);
|
|
4659
|
+
}
|
|
4660
|
+
if (thinking && cliSupportsThinking(cliVersion ?? null)) {
|
|
4661
|
+
args.push("--thinking", thinking);
|
|
4662
|
+
}
|
|
4663
|
+
if (thinkingDisplay && cliSupportsThinkingDisplay(cliVersion ?? null)) {
|
|
4664
|
+
args.push("--thinking-display", thinkingDisplay);
|
|
4665
|
+
}
|
|
4666
|
+
if (appendSystemPromptFile) {
|
|
4667
|
+
args.push("--append-system-prompt-file", appendSystemPromptFile);
|
|
4668
|
+
}
|
|
4669
|
+
for (const dir of pluginDirs ?? []) {
|
|
4670
|
+
args.push("--plugin-dir", dir);
|
|
4671
|
+
}
|
|
4672
|
+
if (fastMode && cliSupportsFastMode(cliVersion ?? null)) {
|
|
4673
|
+
args.push("--settings", JSON.stringify({ fastMode: true }));
|
|
4674
|
+
}
|
|
4675
|
+
if (skipPermissions && permissionMode !== "plan" && !readOnly) {
|
|
4676
|
+
args.push("--dangerously-skip-permissions");
|
|
4671
4677
|
}
|
|
4678
|
+
return args;
|
|
4672
4679
|
}
|
|
4673
|
-
function
|
|
4674
|
-
return
|
|
4680
|
+
function sessionKey(cwd, modelId) {
|
|
4681
|
+
return `${cwd}::${modelId}`;
|
|
4675
4682
|
}
|
|
4676
|
-
function
|
|
4677
|
-
const
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
out[k] = v;
|
|
4685
|
-
}
|
|
4686
|
-
}
|
|
4687
|
-
return out;
|
|
4683
|
+
function describeSessionKey(key) {
|
|
4684
|
+
const parts = key.split("::");
|
|
4685
|
+
return {
|
|
4686
|
+
cwd: parts[0] ?? "unknown",
|
|
4687
|
+
model: parts[1] ?? "unknown",
|
|
4688
|
+
session: parts[3] ?? "unknown",
|
|
4689
|
+
compaction: parts[2] === "compaction"
|
|
4690
|
+
};
|
|
4688
4691
|
}
|
|
4689
|
-
function
|
|
4692
|
+
function snapshotActiveProcesses(now = Date.now()) {
|
|
4690
4693
|
const out = [];
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4694
|
+
for (const [key, ap] of activeProcesses) {
|
|
4695
|
+
const described = describeSessionKey(key);
|
|
4696
|
+
const lastStderr = ap.lastStderr;
|
|
4697
|
+
out.push({
|
|
4698
|
+
sessionKey: key,
|
|
4699
|
+
session: ap.opencodeSessionID ?? described.session,
|
|
4700
|
+
model: described.model,
|
|
4701
|
+
compaction: described.compaction,
|
|
4702
|
+
pid: ap.proc.pid,
|
|
4703
|
+
inFlight: ap.turnInFlight === true,
|
|
4704
|
+
ageMs: ap.startedAt === void 0 ? void 0 : Math.max(0, now - ap.startedAt),
|
|
4705
|
+
effort: ap.effort,
|
|
4706
|
+
attached: ap.lineEmitter.listenerCount("line") > 0,
|
|
4707
|
+
proxyUrl: ap.proxyServer?.url,
|
|
4708
|
+
lastStderr: typeof lastStderr === "string" ? lastStderr : void 0
|
|
4709
|
+
});
|
|
4701
4710
|
}
|
|
4702
4711
|
return out;
|
|
4703
4712
|
}
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4713
|
+
|
|
4714
|
+
// src/btw-command.ts
|
|
4715
|
+
var BtwHandledError = class extends Error {
|
|
4716
|
+
name = "BtwHandledError";
|
|
4717
|
+
constructor(message = "/btw was handled by the claude-code plugin; nothing to add to this conversation.") {
|
|
4718
|
+
super(message);
|
|
4719
|
+
}
|
|
4720
|
+
};
|
|
4721
|
+
var BTW_COMMAND_DESCRIPTION = "Ask a side question in the live Claude Code session without changing its context";
|
|
4722
|
+
var BTW_NO_SESSION_MESSAGE = "/btw needs a live Claude Code session in this conversation. Send a normal message with a Claude Code model first, then ask again.";
|
|
4723
|
+
var BTW_INLINE_HANDLED_MESSAGE = "/btw was answered inside the running turn; nothing to add to this conversation.";
|
|
4724
|
+
var BTW_TURN_TOO_LONG_MESSAGE = "/btw gave up waiting for this turn to end. Ask again once it is over.";
|
|
4725
|
+
var IDLE_POLL_MS = 500;
|
|
4726
|
+
var IDLE_WAIT_MAX_MS = 30 * 6e4;
|
|
4727
|
+
var BUSY_SETTLE_MS = 1500;
|
|
4728
|
+
var SPAWN_WAIT_MAX_MS = 3e4;
|
|
4729
|
+
var INLINE_POLL_MS = 200;
|
|
4730
|
+
var INLINE_WAIT_MAX_MS = 2e4;
|
|
4731
|
+
var PENDING_ANSWER_TTL_MS = 10 * 6e4;
|
|
4732
|
+
var PENDING_ANSWER_CAP = 32;
|
|
4733
|
+
var pendingAnswers = /* @__PURE__ */ new Map();
|
|
4734
|
+
function rememberSideQuestionAnswer(sessionID, question, answer, now = Date.now()) {
|
|
4735
|
+
for (const [id, entry] of pendingAnswers) {
|
|
4736
|
+
if (now - entry.at > PENDING_ANSWER_TTL_MS) pendingAnswers.delete(id);
|
|
4737
|
+
}
|
|
4738
|
+
pendingAnswers.delete(sessionID);
|
|
4739
|
+
while (pendingAnswers.size >= PENDING_ANSWER_CAP) {
|
|
4740
|
+
const oldest = pendingAnswers.keys().next().value;
|
|
4741
|
+
if (oldest === void 0) break;
|
|
4742
|
+
pendingAnswers.delete(oldest);
|
|
4717
4743
|
}
|
|
4744
|
+
pendingAnswers.set(sessionID, { question: question.trim(), answer, at: now });
|
|
4718
4745
|
}
|
|
4719
|
-
function
|
|
4720
|
-
const
|
|
4721
|
-
|
|
4746
|
+
function takeSideQuestionAnswer(sessionID, question, now = Date.now()) {
|
|
4747
|
+
const entry = pendingAnswers.get(sessionID);
|
|
4748
|
+
if (!entry) return void 0;
|
|
4749
|
+
pendingAnswers.delete(sessionID);
|
|
4750
|
+
if (!question.trim().startsWith(entry.question) || now - entry.at > PENDING_ANSWER_TTL_MS) return void 0;
|
|
4751
|
+
return entry.answer;
|
|
4722
4752
|
}
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
const file = path4.join(dir, name);
|
|
4728
|
-
if (!fileExists(file)) continue;
|
|
4729
|
-
const parsed = readAndParse(file);
|
|
4730
|
-
if (parsed) merged = mergeConfigLayer(merged, parsed, hostApi);
|
|
4731
|
-
}
|
|
4732
|
-
return merged;
|
|
4753
|
+
var INLINE_ASIDE_MARKER = "\u258C **btw:**";
|
|
4754
|
+
var LEGACY_INLINE_ASIDE_MARKERS = ["> **btw:**"];
|
|
4755
|
+
function barEveryLine(text) {
|
|
4756
|
+
return text.split("\n").map((line) => line.trim() === "" ? "\u258C" : `\u258C ${line}`).join("\n");
|
|
4733
4757
|
}
|
|
4734
|
-
function
|
|
4735
|
-
|
|
4736
|
-
for (const name of PROJECT_FILE_NAMES) {
|
|
4737
|
-
const file = path4.join(dir, name);
|
|
4738
|
-
if (!fileExists(file)) continue;
|
|
4739
|
-
const parsed = readAndParse(file);
|
|
4740
|
-
if (parsed) merged = mergeConfigLayer(merged, parsed, hostApi);
|
|
4741
|
-
}
|
|
4742
|
-
return merged;
|
|
4758
|
+
function oneLine(question) {
|
|
4759
|
+
return question.replace(/\s+/g, " ").trim();
|
|
4743
4760
|
}
|
|
4744
|
-
function
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4761
|
+
function asideHeader(question) {
|
|
4762
|
+
return `${INLINE_ASIDE_MARKER} ${oneLine(question)}`;
|
|
4763
|
+
}
|
|
4764
|
+
function formatInlineAside(question, answer) {
|
|
4765
|
+
return `
|
|
4766
|
+
${asideHeader(question)}
|
|
4767
|
+
\u258C
|
|
4768
|
+
${barEveryLine(answer.trim())}
|
|
4769
|
+
`;
|
|
4770
|
+
}
|
|
4771
|
+
var INLINE_ASIDE_SENT_NOTE = "*sent to Claude on the side*";
|
|
4772
|
+
function formatInlineAsideAsk(question) {
|
|
4773
|
+
return `
|
|
4774
|
+
${asideHeader(question)}
|
|
4775
|
+
\u258C ${INLINE_ASIDE_SENT_NOTE}
|
|
4776
|
+
`;
|
|
4777
|
+
}
|
|
4778
|
+
var asideSinks = /* @__PURE__ */ new Map();
|
|
4779
|
+
function registerAsideSink(sessionID, sink) {
|
|
4780
|
+
asideSinks.set(sessionID, sink);
|
|
4781
|
+
return () => {
|
|
4782
|
+
if (asideSinks.get(sessionID) === sink) asideSinks.delete(sessionID);
|
|
4753
4783
|
};
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
if (home) {
|
|
4764
|
-
const homeDot = path4.join(home, ".opencode");
|
|
4765
|
-
if (dirExists(homeDot)) push(homeDot);
|
|
4784
|
+
}
|
|
4785
|
+
function emitAsideInline(sessionID, text) {
|
|
4786
|
+
const sink = asideSinks.get(sessionID);
|
|
4787
|
+
if (!sink) return false;
|
|
4788
|
+
try {
|
|
4789
|
+
return sink(text);
|
|
4790
|
+
} catch (error) {
|
|
4791
|
+
log.debug("btw: could not write the aside into the running turn", { sessionID, error: errorText(error) });
|
|
4792
|
+
return false;
|
|
4766
4793
|
}
|
|
4767
|
-
const envDir = process.env.OPENCODE_CONFIG_DIR;
|
|
4768
|
-
if (envDir && dirExists(envDir)) push(envDir);
|
|
4769
|
-
return dirs;
|
|
4770
4794
|
}
|
|
4771
|
-
function
|
|
4772
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
out[k] = v.replace(/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, name) => {
|
|
4776
|
-
const resolved = process.env[name];
|
|
4777
|
-
return typeof resolved === "string" ? resolved : "";
|
|
4795
|
+
function showToast(client, body) {
|
|
4796
|
+
try {
|
|
4797
|
+
void client?.tui?.showToast?.({ body })?.catch((error) => {
|
|
4798
|
+
log.debug("btw toast failed", { error: errorText(error) });
|
|
4778
4799
|
});
|
|
4800
|
+
} catch (error) {
|
|
4801
|
+
log.debug("btw toast failed", { error: errorText(error) });
|
|
4779
4802
|
}
|
|
4780
|
-
return out;
|
|
4781
4803
|
}
|
|
4782
|
-
function
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
if (cmd.length > 1) out.args = cmd.slice(1).map((s) => String(s));
|
|
4796
|
-
if (spec.environment && typeof spec.environment === "object") {
|
|
4797
|
-
out.env = substituteEnvPlaceholders(
|
|
4798
|
-
spec.environment
|
|
4799
|
-
);
|
|
4800
|
-
}
|
|
4801
|
-
return out;
|
|
4804
|
+
function isProcessBusy(active) {
|
|
4805
|
+
return active.lineEmitter.listenerCount("line") > 0;
|
|
4806
|
+
}
|
|
4807
|
+
async function sessionStatus(client, sessionID) {
|
|
4808
|
+
const status = client?.session?.status;
|
|
4809
|
+
if (!status) return "unknown";
|
|
4810
|
+
try {
|
|
4811
|
+
const result = await status.call(client.session);
|
|
4812
|
+
const entry = result.data?.[sessionID];
|
|
4813
|
+
return entry && entry.type !== "idle" ? "busy" : "idle";
|
|
4814
|
+
} catch (error) {
|
|
4815
|
+
log.debug("btw: could not read session status", { sessionID, error: errorText(error) });
|
|
4816
|
+
return "unknown";
|
|
4802
4817
|
}
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4807
|
-
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
out.headers = substituteEnvPlaceholders(
|
|
4814
|
-
spec.headers
|
|
4815
|
-
);
|
|
4816
|
-
}
|
|
4817
|
-
return out;
|
|
4818
|
+
}
|
|
4819
|
+
async function waitForSessionIdle(client, sessionID, options = {}) {
|
|
4820
|
+
const pollMs = options.pollMs ?? IDLE_POLL_MS;
|
|
4821
|
+
const timeoutMs = options.timeoutMs ?? IDLE_WAIT_MAX_MS;
|
|
4822
|
+
const started = Date.now();
|
|
4823
|
+
for (; ; ) {
|
|
4824
|
+
if (options.stop?.()) return true;
|
|
4825
|
+
if (await sessionStatus(client, sessionID) !== "busy") return true;
|
|
4826
|
+
if (Date.now() - started >= timeoutMs) return false;
|
|
4827
|
+
await new Promise((resolve6) => setTimeout(resolve6, pollMs));
|
|
4818
4828
|
}
|
|
4819
|
-
log.warn("skipping MCP server with unknown type", {
|
|
4820
|
-
name,
|
|
4821
|
-
type: type ?? null
|
|
4822
|
-
});
|
|
4823
|
-
return null;
|
|
4824
|
-
}
|
|
4825
|
-
function extractMcpBlock(config) {
|
|
4826
|
-
const mcp = config.mcp;
|
|
4827
|
-
if (!isPlainObject(mcp)) return {};
|
|
4828
|
-
if ("servers" in mcp && !isPlainObject(mcp.servers)) return {};
|
|
4829
|
-
const native = isPlainObject(mcp.servers) && typeof mcp.servers.type !== "string" && typeof mcp.servers.enabled !== "boolean" && typeof mcp.servers.disabled !== "boolean";
|
|
4830
|
-
const block = native ? mcp.servers : mcp;
|
|
4831
|
-
return Object.fromEntries(
|
|
4832
|
-
Object.entries(block).filter(([name, spec]) => isPlainObject(spec) && (native || name !== "timeout" || typeof spec.type === "string")).map(([name, spec]) => {
|
|
4833
|
-
const server2 = spec;
|
|
4834
|
-
return [name, {
|
|
4835
|
-
...server2,
|
|
4836
|
-
...typeof server2.disabled === "boolean" ? { enabled: !server2.disabled } : {}
|
|
4837
|
-
}];
|
|
4838
|
-
})
|
|
4839
|
-
);
|
|
4840
4829
|
}
|
|
4841
|
-
function
|
|
4842
|
-
const
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4830
|
+
async function deliverAsideInline(client, sessionID, text, options = {}) {
|
|
4831
|
+
const pollMs = options.inlinePollMs ?? INLINE_POLL_MS;
|
|
4832
|
+
const timeoutMs = options.inlineWaitMs ?? INLINE_WAIT_MAX_MS;
|
|
4833
|
+
const started = Date.now();
|
|
4834
|
+
for (; ; ) {
|
|
4835
|
+
if (emitAsideInline(sessionID, text)) return true;
|
|
4836
|
+
if (Date.now() - started >= timeoutMs) return false;
|
|
4837
|
+
if (await sessionStatus(client, sessionID) !== "busy") return false;
|
|
4838
|
+
await new Promise((resolve6) => setTimeout(resolve6, pollMs));
|
|
4848
4839
|
}
|
|
4849
|
-
return merged;
|
|
4850
4840
|
}
|
|
4851
|
-
function
|
|
4852
|
-
const
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4841
|
+
async function waitForAsideProcess(client, sessionID, options = {}) {
|
|
4842
|
+
const pollMs = options.pollMs ?? IDLE_POLL_MS;
|
|
4843
|
+
const settleMs = options.settleMs ?? BUSY_SETTLE_MS;
|
|
4844
|
+
const spawnWaitMs = options.spawnWaitMs ?? SPAWN_WAIT_MAX_MS;
|
|
4845
|
+
const started = Date.now();
|
|
4846
|
+
for (; ; ) {
|
|
4847
|
+
const active = findActiveProcessBySessionId(sessionID);
|
|
4848
|
+
if (active) return active;
|
|
4849
|
+
const busy = await sessionStatus(client, sessionID) === "busy";
|
|
4850
|
+
const waitedMs = Date.now() - started;
|
|
4851
|
+
if (!busy && waitedMs >= settleMs) {
|
|
4852
|
+
log.info("btw: no live claude process for session, leaving it to the turn", { sessionID, waitedMs });
|
|
4853
|
+
return void 0;
|
|
4854
|
+
}
|
|
4855
|
+
if (busy && waitedMs >= spawnWaitMs) {
|
|
4856
|
+
log.warn("btw: a turn is running but no claude process appeared for it", { sessionID, waitedMs });
|
|
4857
|
+
return void 0;
|
|
4863
4858
|
}
|
|
4859
|
+
await new Promise((resolve6) => setTimeout(resolve6, pollMs));
|
|
4864
4860
|
}
|
|
4865
|
-
return out;
|
|
4866
4861
|
}
|
|
4867
|
-
function
|
|
4868
|
-
const
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
if (excludeServers?.has(name)) continue;
|
|
4878
|
-
const translated = translateServer(name, spec);
|
|
4879
|
-
if (translated) {
|
|
4880
|
-
servers[name] = translated;
|
|
4881
|
-
bridgedServerNames.push(name);
|
|
4882
|
-
}
|
|
4862
|
+
async function settleSessionBusy(client, sessionID, active, options = {}) {
|
|
4863
|
+
const pollMs = options.pollMs ?? IDLE_POLL_MS;
|
|
4864
|
+
const settleMs = options.settleMs ?? BUSY_SETTLE_MS;
|
|
4865
|
+
const started = Date.now();
|
|
4866
|
+
for (; ; ) {
|
|
4867
|
+
const status = await sessionStatus(client, sessionID);
|
|
4868
|
+
if (status === "busy") return true;
|
|
4869
|
+
if (status === "unknown") return isProcessBusy(active);
|
|
4870
|
+
if (Date.now() - started >= settleMs) return false;
|
|
4871
|
+
await new Promise((resolve6) => setTimeout(resolve6, pollMs));
|
|
4883
4872
|
}
|
|
4884
|
-
return finishBridge({
|
|
4885
|
-
servers,
|
|
4886
|
-
bridgedServerNames,
|
|
4887
|
-
allEnabledServerNames,
|
|
4888
|
-
hash,
|
|
4889
|
-
excludeServers
|
|
4890
|
-
});
|
|
4891
4873
|
}
|
|
4892
|
-
function
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
const explicitConfig = process.env.OPENCODE_CONFIG;
|
|
4897
|
-
if (explicitConfig && fileExists(explicitConfig)) {
|
|
4898
|
-
const parsed = readAndParse(explicitConfig);
|
|
4899
|
-
if (parsed) layers.push(parsed);
|
|
4874
|
+
function errorText(error) {
|
|
4875
|
+
if (error instanceof Error) return error.message;
|
|
4876
|
+
if (error && typeof error === "object" && "message" in error && typeof error.message === "string") {
|
|
4877
|
+
return error.message;
|
|
4900
4878
|
}
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
const
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
const
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4879
|
+
return String(error);
|
|
4880
|
+
}
|
|
4881
|
+
function isTextPart(part) {
|
|
4882
|
+
return part !== null && typeof part === "object" && part.type === "text" && typeof part.text === "string";
|
|
4883
|
+
}
|
|
4884
|
+
async function fetchAsideHistory(client, sessionID, question) {
|
|
4885
|
+
const messages = client?.session?.messages;
|
|
4886
|
+
if (!messages) return [];
|
|
4887
|
+
try {
|
|
4888
|
+
const result = await messages.call(client.session, { path: { id: sessionID } });
|
|
4889
|
+
const prompt = [];
|
|
4890
|
+
for (const message of result.data ?? []) {
|
|
4891
|
+
const role = message.info?.role;
|
|
4892
|
+
if (role !== "user" && role !== "assistant") continue;
|
|
4893
|
+
prompt.push({ role, content: (message.parts ?? []).filter(isTextPart) });
|
|
4914
4894
|
}
|
|
4895
|
+
prompt.push({ role: "user", content: `/btw ${question}` });
|
|
4896
|
+
return collectSideQuestionHistory(prompt);
|
|
4897
|
+
} catch (error) {
|
|
4898
|
+
log.debug("btw: could not read aside history", { sessionID, error: errorText(error) });
|
|
4899
|
+
return [];
|
|
4915
4900
|
}
|
|
4916
|
-
|
|
4917
|
-
|
|
4901
|
+
}
|
|
4902
|
+
async function handleBtwCommand(client, input, options = {}) {
|
|
4903
|
+
const question = input.arguments.trim();
|
|
4904
|
+
if (!question) {
|
|
4905
|
+
showToast(client, { title: "btw", message: SIDE_QUESTION_USAGE, variant: "warning", duration: 6e3 });
|
|
4906
|
+
throw new BtwHandledError("/btw needs a question.");
|
|
4918
4907
|
}
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
if (
|
|
4923
|
-
|
|
4924
|
-
} else {
|
|
4925
|
-
for (const dir of dotOpencodeDirs(cwd, worktree)) {
|
|
4926
|
-
layers.push(loadProjectFilesInDir(dir));
|
|
4927
|
-
}
|
|
4908
|
+
const active = await waitForAsideProcess(client, input.sessionID, options);
|
|
4909
|
+
const transport = active?.asideTransport;
|
|
4910
|
+
if (!active || !transport) {
|
|
4911
|
+
if (active) log.info("btw: process has no aside transport, leaving it to the turn", { sessionID: input.sessionID });
|
|
4912
|
+
return;
|
|
4928
4913
|
}
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4914
|
+
let busy = false;
|
|
4915
|
+
let inlineDone = false;
|
|
4916
|
+
let markInlineDelivered = () => {
|
|
4917
|
+
};
|
|
4918
|
+
const inlineDelivered = new Promise((resolve6) => {
|
|
4919
|
+
markInlineDelivered = () => {
|
|
4920
|
+
inlineDone = true;
|
|
4921
|
+
resolve6("inline");
|
|
4922
|
+
};
|
|
4923
|
+
});
|
|
4924
|
+
if (isSideQuestionPending(active)) {
|
|
4925
|
+
busy = await settleSessionBusy(client, input.sessionID, active, options);
|
|
4926
|
+
log.info("btw: an aside is already in flight, leaving this one to the turn", { sessionID: input.sessionID, busy });
|
|
4927
|
+
} else {
|
|
4928
|
+
const settling = settleSessionBusy(client, input.sessionID, active, options);
|
|
4929
|
+
const history = await fetchAsideHistory(client, input.sessionID, question);
|
|
4930
|
+
const answer = requestSideQuestion(active, question, {
|
|
4931
|
+
cliVersion: await detectCliVersion(transport.cliPath),
|
|
4932
|
+
interactive: transport.interactive,
|
|
4933
|
+
...history.length ? { history } : {}
|
|
4934
|
+
});
|
|
4935
|
+
answer.catch(() => void 0);
|
|
4936
|
+
rememberSideQuestionAnswer(input.sessionID, question, answer);
|
|
4937
|
+
busy = await settling;
|
|
4938
|
+
log.info("btw: aside sent ahead of its message", {
|
|
4939
|
+
sessionID: input.sessionID,
|
|
4940
|
+
busy,
|
|
4941
|
+
questionLength: question.length,
|
|
4942
|
+
history: history.length
|
|
4943
|
+
});
|
|
4944
|
+
const asked = busy ? deliverAsideInline(client, input.sessionID, formatInlineAsideAsk(question), options).catch(() => false) : Promise.resolve(false);
|
|
4945
|
+
answer.then(
|
|
4946
|
+
async (result) => {
|
|
4947
|
+
log.info("btw: early answer arrived", { sessionID: input.sessionID, busy, responseLength: result.response.length });
|
|
4948
|
+
if (!busy || result.synthetic) return;
|
|
4949
|
+
await asked;
|
|
4950
|
+
const inline = await deliverAsideInline(
|
|
4951
|
+
client,
|
|
4952
|
+
input.sessionID,
|
|
4953
|
+
formatInlineAside(question, result.response),
|
|
4954
|
+
options
|
|
4955
|
+
);
|
|
4956
|
+
if (inline) {
|
|
4957
|
+
log.info("btw: answer written into the running turn", { sessionID: input.sessionID });
|
|
4958
|
+
markInlineDelivered();
|
|
4959
|
+
return;
|
|
4960
|
+
}
|
|
4961
|
+
log.info("btw: no open stream for the answer; the held message will carry it", {
|
|
4962
|
+
sessionID: input.sessionID
|
|
4963
|
+
});
|
|
4964
|
+
},
|
|
4965
|
+
(error) => {
|
|
4966
|
+
log.warn("btw: early aside failed; the message will ask again", {
|
|
4967
|
+
sessionID: input.sessionID,
|
|
4968
|
+
error: errorText(error)
|
|
4969
|
+
});
|
|
4970
|
+
}
|
|
4971
|
+
);
|
|
4941
4972
|
}
|
|
4942
|
-
if (
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4973
|
+
if (!busy) return;
|
|
4974
|
+
const started = Date.now();
|
|
4975
|
+
const outcome = await Promise.race([
|
|
4976
|
+
inlineDelivered,
|
|
4977
|
+
waitForSessionIdle(client, input.sessionID, { ...options, stop: () => inlineDone }).then(
|
|
4978
|
+
(idle) => idle ? "idle" : "timeout"
|
|
4979
|
+
)
|
|
4980
|
+
]);
|
|
4981
|
+
if (outcome === "inline") {
|
|
4982
|
+
log.info("btw: answered inside the running turn, dropping the /btw message", {
|
|
4983
|
+
sessionID: input.sessionID,
|
|
4984
|
+
waitedMs: Date.now() - started
|
|
4985
|
+
});
|
|
4986
|
+
throw new BtwHandledError(BTW_INLINE_HANDLED_MESSAGE);
|
|
4950
4987
|
}
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
4988
|
+
log.info("btw: turn over, releasing the /btw message", {
|
|
4989
|
+
sessionID: input.sessionID,
|
|
4990
|
+
idle: outcome === "idle",
|
|
4991
|
+
waitedMs: Date.now() - started
|
|
4992
|
+
});
|
|
4993
|
+
if (outcome === "timeout") {
|
|
4994
|
+
showToast(client, { title: "btw", message: BTW_TURN_TOO_LONG_MESSAGE, variant: "warning", duration: 8e3 });
|
|
4995
|
+
throw new BtwHandledError(BTW_TURN_TOO_LONG_MESSAGE);
|
|
4957
4996
|
}
|
|
4958
|
-
const mergedBody = JSON.stringify({ mcpServers: merged }, null, 2);
|
|
4959
|
-
const hash = crypto2.createHash("sha256").update(mergedBody).digest("hex").slice(0, 12);
|
|
4960
|
-
return { servers: merged, enabledServerNames, hash };
|
|
4961
4997
|
}
|
|
4962
|
-
|
|
4963
|
-
|
|
4964
|
-
|
|
4965
|
-
|
|
4966
|
-
|
|
4998
|
+
|
|
4999
|
+
// src/plan-usage.ts
|
|
5000
|
+
import { execFile as execFile2 } from "child_process";
|
|
5001
|
+
var PLAN_USAGE_ARGUMENTS = /* @__PURE__ */ new Set(["usage", "cost", "stats", "limits"]);
|
|
5002
|
+
function wantsPlanUsage(argument) {
|
|
5003
|
+
return PLAN_USAGE_ARGUMENTS.has(argument.trim().toLowerCase());
|
|
5004
|
+
}
|
|
5005
|
+
var PLAN_USAGE_MAX_CHARS = 4e3;
|
|
5006
|
+
function truncate(text) {
|
|
5007
|
+
return text.length <= PLAN_USAGE_MAX_CHARS ? text : `${text.slice(0, PLAN_USAGE_MAX_CHARS)}
|
|
5008
|
+
[truncated]`;
|
|
5009
|
+
}
|
|
5010
|
+
function parsePlanUsage(stdout) {
|
|
5011
|
+
const trimmed = stdout.trim();
|
|
5012
|
+
if (!trimmed) return { status: "failed", error: "the CLI printed nothing" };
|
|
5013
|
+
const candidates = trimmed.split("\n").reverse();
|
|
5014
|
+
for (const line of candidates) {
|
|
5015
|
+
const start = line.indexOf("{");
|
|
5016
|
+
if (start === -1) continue;
|
|
5017
|
+
let parsed;
|
|
5018
|
+
try {
|
|
5019
|
+
parsed = JSON.parse(line.slice(start));
|
|
5020
|
+
} catch {
|
|
5021
|
+
continue;
|
|
5022
|
+
}
|
|
5023
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) continue;
|
|
5024
|
+
const record = parsed;
|
|
5025
|
+
if (record.type !== void 0 && record.type !== "result") continue;
|
|
5026
|
+
const text = typeof record.result === "string" ? record.result.trim() : "";
|
|
5027
|
+
if (record.is_error === true) {
|
|
5028
|
+
return { status: "failed", error: text || "the CLI reported an error" };
|
|
5029
|
+
}
|
|
5030
|
+
if (!text) continue;
|
|
4967
5031
|
return {
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
5032
|
+
status: "ok",
|
|
5033
|
+
text: truncate(text),
|
|
5034
|
+
costUsd: typeof record.total_cost_usd === "number" ? record.total_cost_usd : 0,
|
|
5035
|
+
numTurns: typeof record.num_turns === "number" ? record.num_turns : 0
|
|
4972
5036
|
};
|
|
4973
5037
|
}
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
5038
|
+
return { status: "failed", error: "no result object in the CLI's reply" };
|
|
5039
|
+
}
|
|
5040
|
+
function runCli(cliPath, args, timeoutMs, env) {
|
|
5041
|
+
return new Promise((resolve6, reject) => {
|
|
5042
|
+
execFile2(
|
|
5043
|
+
cliPath,
|
|
5044
|
+
args,
|
|
5045
|
+
// `killSignal` so a CLI wedged on a hook is actually gone, and a generous
|
|
5046
|
+
// buffer because the reply is prose of unbounded length.
|
|
5047
|
+
{ timeout: timeoutMs, killSignal: "SIGKILL", maxBuffer: 4 * 1024 * 1024, env },
|
|
5048
|
+
(error, stdout) => {
|
|
5049
|
+
if (stdout && stdout.trim()) return resolve6(stdout);
|
|
5050
|
+
if (error) return reject(error);
|
|
5051
|
+
resolve6(stdout ?? "");
|
|
5052
|
+
}
|
|
5053
|
+
);
|
|
5054
|
+
});
|
|
5055
|
+
}
|
|
5056
|
+
async function fetchPlanUsage(cliPath, options = {}) {
|
|
5057
|
+
const timeoutMs = options.timeoutMs ?? 2e4;
|
|
5058
|
+
const run = options.runImpl ?? runCli;
|
|
4980
5059
|
try {
|
|
4981
|
-
|
|
4982
|
-
|
|
5060
|
+
const env = claudeSpawnEnv({ ignoreAnthropicApiKey: options.ignoreAnthropicApiKey });
|
|
5061
|
+
const stdout = await run(
|
|
5062
|
+
cliPath,
|
|
5063
|
+
["-p", "/cost", "--output-format", "json"],
|
|
5064
|
+
timeoutMs,
|
|
5065
|
+
env
|
|
5066
|
+
);
|
|
5067
|
+
const usage = parsePlanUsage(stdout);
|
|
5068
|
+
if (usage.status === "ok") {
|
|
5069
|
+
log.info("read claude plan usage", { costUsd: usage.costUsd, numTurns: usage.numTurns });
|
|
5070
|
+
} else if (usage.status === "failed") {
|
|
5071
|
+
log.debug("could not read claude plan usage", { error: usage.error });
|
|
4983
5072
|
}
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
});
|
|
4988
|
-
return
|
|
5073
|
+
return usage;
|
|
5074
|
+
} catch (error) {
|
|
5075
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5076
|
+
log.debug("could not read claude plan usage", { error: message });
|
|
5077
|
+
return { status: "failed", error: message };
|
|
4989
5078
|
}
|
|
4990
|
-
log.info("bridged opencode MCP config", {
|
|
4991
|
-
target: outPath,
|
|
4992
|
-
hash,
|
|
4993
|
-
servers: bridgedServerNames,
|
|
4994
|
-
excluded: excludeServers ? Array.from(excludeServers) : []
|
|
4995
|
-
});
|
|
4996
|
-
return {
|
|
4997
|
-
path: outPath,
|
|
4998
|
-
hash,
|
|
4999
|
-
serverNames: bridgedServerNames,
|
|
5000
|
-
allEnabledServerNames
|
|
5001
|
-
};
|
|
5002
5079
|
}
|
|
5003
5080
|
|
|
5004
5081
|
// src/startup-diagnostics.ts
|
|
5082
|
+
import { execFile as execFile3 } from "child_process";
|
|
5083
|
+
import * as fs4 from "fs";
|
|
5084
|
+
import * as path5 from "path";
|
|
5085
|
+
import { promisify as promisify2 } from "util";
|
|
5086
|
+
import { fileURLToPath } from "url";
|
|
5005
5087
|
var cachedPluginVersion;
|
|
5006
5088
|
function pluginVersion() {
|
|
5007
5089
|
if (cachedPluginVersion) return cachedPluginVersion;
|
|
@@ -8302,6 +8384,77 @@ async function ensureProxyServer(config, tools, sessionKeyForCalls, interceptCom
|
|
|
8302
8384
|
return srv;
|
|
8303
8385
|
}
|
|
8304
8386
|
|
|
8387
|
+
// src/mcp-hot-reload.ts
|
|
8388
|
+
var DEFAULT_MCP_HOT_RELOAD_COOLDOWN_MS = 6e4;
|
|
8389
|
+
var MAX_COOLDOWN_ENTRIES = 64;
|
|
8390
|
+
var lastReloadAt = /* @__PURE__ */ new Map();
|
|
8391
|
+
function resolveMcpHotReloadCooldownMs() {
|
|
8392
|
+
const raw = process.env.CLAUDE_CODE_MCP_HOT_RELOAD_COOLDOWN_MS;
|
|
8393
|
+
if (raw === void 0) return DEFAULT_MCP_HOT_RELOAD_COOLDOWN_MS;
|
|
8394
|
+
const parsed = Number(raw);
|
|
8395
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
8396
|
+
return DEFAULT_MCP_HOT_RELOAD_COOLDOWN_MS;
|
|
8397
|
+
}
|
|
8398
|
+
return parsed;
|
|
8399
|
+
}
|
|
8400
|
+
function noteMcpHotReload(sessionKey2, now = Date.now()) {
|
|
8401
|
+
lastReloadAt.delete(sessionKey2);
|
|
8402
|
+
lastReloadAt.set(sessionKey2, now);
|
|
8403
|
+
while (lastReloadAt.size > MAX_COOLDOWN_ENTRIES) {
|
|
8404
|
+
const oldest = lastReloadAt.keys().next();
|
|
8405
|
+
if (oldest.done) break;
|
|
8406
|
+
lastReloadAt.delete(oldest.value);
|
|
8407
|
+
}
|
|
8408
|
+
}
|
|
8409
|
+
function difference(a, b) {
|
|
8410
|
+
const other = new Set(b);
|
|
8411
|
+
return a.filter((name) => !other.has(name)).sort();
|
|
8412
|
+
}
|
|
8413
|
+
function decideMcpHotReload(input) {
|
|
8414
|
+
const previousServers = input.previousServers ?? [];
|
|
8415
|
+
const joined = difference(input.currentServers, previousServers);
|
|
8416
|
+
const left = difference(previousServers, input.currentServers);
|
|
8417
|
+
const decide = (verdict) => ({
|
|
8418
|
+
verdict,
|
|
8419
|
+
reload: verdict === "reload",
|
|
8420
|
+
joined,
|
|
8421
|
+
left
|
|
8422
|
+
});
|
|
8423
|
+
if (!input.enabled) return decide("off");
|
|
8424
|
+
if (input.compactionMode) return decide("skipped-compaction");
|
|
8425
|
+
if (!input.hasActiveProcess) return decide("no-process");
|
|
8426
|
+
const previousHash = input.previousHash ?? null;
|
|
8427
|
+
if (previousHash === input.currentHash) return decide("unchanged");
|
|
8428
|
+
if (input.interactive) return decide("skipped-interactive");
|
|
8429
|
+
if (input.pendingProxyCalls > 0) return decide("deferred-proxy-calls");
|
|
8430
|
+
if (input.turnInFlight) return decide("deferred-turn-in-flight");
|
|
8431
|
+
if (input.planQuestionPending) return decide("deferred-plan-question");
|
|
8432
|
+
const cooldownMs = input.cooldownMs ?? resolveMcpHotReloadCooldownMs();
|
|
8433
|
+
const last = lastReloadAt.get(input.sessionKey);
|
|
8434
|
+
if (cooldownMs > 0 && last !== void 0) {
|
|
8435
|
+
const now = input.now ?? Date.now();
|
|
8436
|
+
if (now - last < cooldownMs) return decide("skipped-cooldown");
|
|
8437
|
+
}
|
|
8438
|
+
return decide("reload");
|
|
8439
|
+
}
|
|
8440
|
+
function logMcpHotReloadDecision(decision, context) {
|
|
8441
|
+
if (decision.verdict === "off" || decision.verdict === "no-process" || decision.verdict === "skipped-compaction" || decision.verdict === "unchanged") {
|
|
8442
|
+
return;
|
|
8443
|
+
}
|
|
8444
|
+
const details = {
|
|
8445
|
+
sk: context.sessionKey,
|
|
8446
|
+
joined: decision.joined,
|
|
8447
|
+
left: decision.left,
|
|
8448
|
+
previousHash: context.previousHash ?? null,
|
|
8449
|
+
currentHash: context.currentHash
|
|
8450
|
+
};
|
|
8451
|
+
if (decision.reload) {
|
|
8452
|
+
log.info("opencode MCP servers changed, respawning claude", details);
|
|
8453
|
+
return;
|
|
8454
|
+
}
|
|
8455
|
+
log.info(`opencode MCP servers changed; holding off (${decision.verdict})`, details);
|
|
8456
|
+
}
|
|
8457
|
+
|
|
8305
8458
|
// src/title.ts
|
|
8306
8459
|
function isTitleRequest(config, scope, options) {
|
|
8307
8460
|
if (scope === "no-tools") return true;
|
|
@@ -8471,6 +8624,8 @@ function createTurnState(init) {
|
|
|
8471
8624
|
reasoningIds: /* @__PURE__ */ new Map(),
|
|
8472
8625
|
reasoningStarted: /* @__PURE__ */ new Map(),
|
|
8473
8626
|
hadThinkingTextFromStream: false,
|
|
8627
|
+
cliAskedForWork: false,
|
|
8628
|
+
streamStarted: false,
|
|
8474
8629
|
turnCompleted: false,
|
|
8475
8630
|
controllerClosed: false,
|
|
8476
8631
|
cleanedUp: false,
|
|
@@ -8520,6 +8675,59 @@ function createTurnState(init) {
|
|
|
8520
8675
|
return state;
|
|
8521
8676
|
}
|
|
8522
8677
|
|
|
8678
|
+
// src/turn-abort.ts
|
|
8679
|
+
function watchTurnAbort(signal) {
|
|
8680
|
+
let aborted = signal?.aborted === true;
|
|
8681
|
+
let listeners = [];
|
|
8682
|
+
let settle;
|
|
8683
|
+
const whenAborted = new Promise((resolve6) => {
|
|
8684
|
+
settle = resolve6;
|
|
8685
|
+
});
|
|
8686
|
+
const fire = () => {
|
|
8687
|
+
if (aborted && listeners === null) return;
|
|
8688
|
+
aborted = true;
|
|
8689
|
+
const pending = listeners ?? [];
|
|
8690
|
+
listeners = null;
|
|
8691
|
+
settle();
|
|
8692
|
+
for (const fn of pending) {
|
|
8693
|
+
try {
|
|
8694
|
+
fn();
|
|
8695
|
+
} catch (error) {
|
|
8696
|
+
log.warn("turn abort listener threw", {
|
|
8697
|
+
error: error instanceof Error ? error.message : String(error)
|
|
8698
|
+
});
|
|
8699
|
+
}
|
|
8700
|
+
}
|
|
8701
|
+
};
|
|
8702
|
+
const onSignalAbort = () => fire();
|
|
8703
|
+
if (aborted) {
|
|
8704
|
+
listeners = null;
|
|
8705
|
+
settle();
|
|
8706
|
+
} else {
|
|
8707
|
+
signal?.addEventListener("abort", onSignalAbort, { once: true });
|
|
8708
|
+
}
|
|
8709
|
+
return {
|
|
8710
|
+
get aborted() {
|
|
8711
|
+
return aborted;
|
|
8712
|
+
},
|
|
8713
|
+
get reason() {
|
|
8714
|
+
return signal?.reason;
|
|
8715
|
+
},
|
|
8716
|
+
whenAborted,
|
|
8717
|
+
onAbort(fn) {
|
|
8718
|
+
if (aborted) {
|
|
8719
|
+
fn();
|
|
8720
|
+
return;
|
|
8721
|
+
}
|
|
8722
|
+
listeners?.push(fn);
|
|
8723
|
+
},
|
|
8724
|
+
dispose() {
|
|
8725
|
+
listeners = null;
|
|
8726
|
+
signal?.removeEventListener("abort", onSignalAbort);
|
|
8727
|
+
}
|
|
8728
|
+
};
|
|
8729
|
+
}
|
|
8730
|
+
|
|
8523
8731
|
// src/stream-parser.ts
|
|
8524
8732
|
import { generateId as generateId2 } from "@ai-sdk/provider-utils";
|
|
8525
8733
|
|
|
@@ -10194,6 +10402,21 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10194
10402
|
}
|
|
10195
10403
|
async doStreamForHost(options, attempt, mode = "stream") {
|
|
10196
10404
|
const warnings = [];
|
|
10405
|
+
const turnAbort = watchTurnAbort(options.abortSignal);
|
|
10406
|
+
const abortedBeforeWork = (stage) => {
|
|
10407
|
+
log.info("abort before the turn asked claude for work; nothing spawned", {
|
|
10408
|
+
stage,
|
|
10409
|
+
reason: describeAbortReason(turnAbort.reason)
|
|
10410
|
+
});
|
|
10411
|
+
turnAbort.dispose();
|
|
10412
|
+
const aborted = new ReadableStream({
|
|
10413
|
+
start(controller) {
|
|
10414
|
+
controller.enqueue({ type: "stream-start", warnings });
|
|
10415
|
+
controller.close();
|
|
10416
|
+
}
|
|
10417
|
+
});
|
|
10418
|
+
return { stream: aborted, request: { body: { text: "" } } };
|
|
10419
|
+
};
|
|
10197
10420
|
const skipPermissions = this.config.skipPermissions !== false;
|
|
10198
10421
|
const scope = this.requestScope(options);
|
|
10199
10422
|
const affinity = this.sessionAffinity(options);
|
|
@@ -10238,6 +10461,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10238
10461
|
modelId: effectiveModelId
|
|
10239
10462
|
});
|
|
10240
10463
|
let cliPath = failover.cliPath;
|
|
10464
|
+
if (turnAbort.aborted) return abortedBeforeWork("resolving the spawn cwd and account");
|
|
10241
10465
|
const asideTransportRef = { cliPath, interactive: !!useInteractive };
|
|
10242
10466
|
const doctor = !compactionMode && scope !== "no-tools" ? parseDoctorCommand(options.prompt) : null;
|
|
10243
10467
|
if (doctor) {
|
|
@@ -10489,10 +10713,28 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10489
10713
|
const hasMatchedPendingResults = previousPendingProxyMatches.some(
|
|
10490
10714
|
(m) => m.result !== null
|
|
10491
10715
|
);
|
|
10492
|
-
const
|
|
10493
|
-
compactionMode ? Promise.resolve(void 0) : getRuntimeMcpStatus(
|
|
10716
|
+
const prologueWork = Promise.all([
|
|
10717
|
+
compactionMode ? Promise.resolve(void 0) : getRuntimeMcpStatus({
|
|
10718
|
+
// Give a server opencode is still starting a bounded moment to
|
|
10719
|
+
// reach a decision before the spawn is planned without it. Only
|
|
10720
|
+
// opencode 2 ever reports `pending`, so on 1.x this is one status
|
|
10721
|
+
// call exactly as before.
|
|
10722
|
+
waitForPendingMs: resolveMcpConnectWaitMs(
|
|
10723
|
+
this.config.mcpConnectWaitMs
|
|
10724
|
+
),
|
|
10725
|
+
signal: options.abortSignal
|
|
10726
|
+
}),
|
|
10494
10727
|
detectCliVersion(cliPath)
|
|
10495
10728
|
]);
|
|
10729
|
+
prologueWork.catch(() => void 0);
|
|
10730
|
+
const prologue = await Promise.race([
|
|
10731
|
+
prologueWork.then((value) => ({ value })),
|
|
10732
|
+
turnAbort.whenAborted.then(() => void 0)
|
|
10733
|
+
]);
|
|
10734
|
+
if (!prologue) {
|
|
10735
|
+
return abortedBeforeWork("waiting for opencode's MCP status and the CLI version");
|
|
10736
|
+
}
|
|
10737
|
+
const [runtimeStatus, cliVersion] = prologue.value;
|
|
10496
10738
|
const failoverAccounts = failoverCandidates(
|
|
10497
10739
|
this.config.failoverAccounts,
|
|
10498
10740
|
sourceAccount
|
|
@@ -10523,6 +10765,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10523
10765
|
opencodeAgent: this.getOpencodeAgent(options),
|
|
10524
10766
|
providerOptionsKeys: options.providerOptions ? Object.keys(options.providerOptions) : []
|
|
10525
10767
|
});
|
|
10768
|
+
if (turnAbort.aborted) return abortedBeforeWork("planning the spawn");
|
|
10526
10769
|
const stream = new ReadableStream({
|
|
10527
10770
|
start(controller) {
|
|
10528
10771
|
if (compactionMode) {
|
|
@@ -10552,28 +10795,136 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10552
10795
|
});
|
|
10553
10796
|
state.activeProcess = getActiveProcess(sk);
|
|
10554
10797
|
state.proxyServer = state.activeProcess?.proxyServer ?? null;
|
|
10798
|
+
const discardUnattachedProxyServer = () => {
|
|
10799
|
+
const own = state.proxyServer;
|
|
10800
|
+
if (!own || own === state.activeProcess?.proxyServer) return;
|
|
10801
|
+
state.proxyServer = null;
|
|
10802
|
+
void own.close().catch((error) => {
|
|
10803
|
+
log.warn("failed to close the proxy server of an aborted turn", {
|
|
10804
|
+
sessionKey: sk,
|
|
10805
|
+
error: error instanceof Error ? error.message : String(error)
|
|
10806
|
+
});
|
|
10807
|
+
});
|
|
10808
|
+
};
|
|
10809
|
+
const releaseAbandonedProxyCalls = (reason) => {
|
|
10810
|
+
if (state.drainBuffer.length === 0 && getPendingProxyCalls(sk).length === 0) return;
|
|
10811
|
+
rejectAllPendingProxyCallsForSession(sk, new Error(reason));
|
|
10812
|
+
state.drainBuffer.length = 0;
|
|
10813
|
+
};
|
|
10814
|
+
turnAbort.onAbort(() => {
|
|
10815
|
+
state.autoContinueState.aborted = true;
|
|
10816
|
+
if (!state.cliAskedForWork) {
|
|
10817
|
+
if (state.controllerClosed) return;
|
|
10818
|
+
state.controllerClosed = true;
|
|
10819
|
+
log.info("abort while the turn was still being prepared; nothing was sent to claude", {
|
|
10820
|
+
sessionKey: sk,
|
|
10821
|
+
// A process here was spawned by this turn or reused from an
|
|
10822
|
+
// earlier one; either way nothing was asked of it.
|
|
10823
|
+
attachedProcess: !!state.activeProcess,
|
|
10824
|
+
reason: describeAbortReason(turnAbort.reason)
|
|
10825
|
+
});
|
|
10826
|
+
discardUnattachedProxyServer();
|
|
10827
|
+
state.cleanupTurn?.();
|
|
10828
|
+
if (!state.streamStarted) {
|
|
10829
|
+
state.streamStarted = true;
|
|
10830
|
+
controller.enqueue({ type: "stream-start", warnings });
|
|
10831
|
+
}
|
|
10832
|
+
try {
|
|
10833
|
+
controller.close();
|
|
10834
|
+
} catch {
|
|
10835
|
+
}
|
|
10836
|
+
return;
|
|
10837
|
+
}
|
|
10838
|
+
if (state.turnCompleted || state.controllerClosed) {
|
|
10839
|
+
const stoppedProcess = state.activeProcess;
|
|
10840
|
+
if (stoppedProcess && stoppedProcess.lineEmitter.listenerCount("line") === 0 && getPendingProxyCalls(sk).length > 0) {
|
|
10841
|
+
const reason = describeAbortReason(options.abortSignal?.reason);
|
|
10842
|
+
void settleSessionRunState(affinity).then((stopped) => {
|
|
10843
|
+
const stillParked = stoppedProcess.lineEmitter.listenerCount("line") === 0 && getPendingProxyCalls(sk).length > 0;
|
|
10844
|
+
if (stopped === "busy" || !stillParked) {
|
|
10845
|
+
log.debug("abort at a tool boundary while opencode is still running the turn; keeping pending calls", {
|
|
10846
|
+
sk,
|
|
10847
|
+
session: stopped,
|
|
10848
|
+
stillParked,
|
|
10849
|
+
reason
|
|
10850
|
+
});
|
|
10851
|
+
return;
|
|
10852
|
+
}
|
|
10853
|
+
log.info("abort between proxy tool boundaries; releasing pending calls", { sk, reason });
|
|
10854
|
+
void interruptTurn(stoppedProcess).then((idle) => {
|
|
10855
|
+
log.info("interrupt sent for aborted turn", { sk, idle });
|
|
10856
|
+
});
|
|
10857
|
+
releaseAbandonedProxyCalls(
|
|
10858
|
+
"Provider stream was aborted while opencode was running its proxy tool calls"
|
|
10859
|
+
);
|
|
10860
|
+
});
|
|
10861
|
+
}
|
|
10862
|
+
return;
|
|
10863
|
+
}
|
|
10864
|
+
if (state.activeProcess) {
|
|
10865
|
+
void interruptTurn(state.activeProcess).then((idle) => {
|
|
10866
|
+
log.info("interrupt sent for aborted turn", { sk, idle });
|
|
10867
|
+
});
|
|
10868
|
+
}
|
|
10869
|
+
if (!state.hasReceivedContent) {
|
|
10870
|
+
log.info(
|
|
10871
|
+
"abort signal received before content, closing stream immediately",
|
|
10872
|
+
{ cwd }
|
|
10873
|
+
);
|
|
10874
|
+
releaseAbandonedProxyCalls(
|
|
10875
|
+
"Provider stream was aborted before pending proxy calls were emitted"
|
|
10876
|
+
);
|
|
10877
|
+
state.controllerClosed = true;
|
|
10878
|
+
state.cleanupTurn?.();
|
|
10879
|
+
try {
|
|
10880
|
+
controller.close();
|
|
10881
|
+
} catch {
|
|
10882
|
+
}
|
|
10883
|
+
return;
|
|
10884
|
+
}
|
|
10885
|
+
log.info(
|
|
10886
|
+
"abort signal received mid-turn, starting grace period",
|
|
10887
|
+
{ cwd }
|
|
10888
|
+
);
|
|
10889
|
+
releaseAbandonedProxyCalls(
|
|
10890
|
+
"Provider stream was aborted while proxy tool calls were pending"
|
|
10891
|
+
);
|
|
10892
|
+
startResultFallback(state, 5e3);
|
|
10893
|
+
});
|
|
10894
|
+
const stoppedBeforeWork = () => {
|
|
10895
|
+
if (!state.controllerClosed || state.cliAskedForWork) return false;
|
|
10896
|
+
discardUnattachedProxyServer();
|
|
10897
|
+
return true;
|
|
10898
|
+
};
|
|
10555
10899
|
const setup = async () => {
|
|
10900
|
+
if (stoppedBeforeWork()) return;
|
|
10556
10901
|
if (!compactionMode && state.activeProcess && self.config.hotReloadMcp !== false && self.config.bridgeOpencodeMcp !== false) {
|
|
10557
10902
|
const probe = self.effectiveMcpConfig(cwd, void 0, runtimeStatus);
|
|
10558
10903
|
const previousHash = state.activeProcess.mcpHash ?? null;
|
|
10559
|
-
|
|
10560
|
-
|
|
10561
|
-
|
|
10562
|
-
|
|
10563
|
-
|
|
10564
|
-
|
|
10565
|
-
|
|
10566
|
-
|
|
10567
|
-
|
|
10568
|
-
|
|
10569
|
-
|
|
10570
|
-
|
|
10571
|
-
|
|
10572
|
-
|
|
10573
|
-
|
|
10574
|
-
|
|
10575
|
-
|
|
10576
|
-
|
|
10904
|
+
const decision = decideMcpHotReload({
|
|
10905
|
+
sessionKey: sk,
|
|
10906
|
+
enabled: true,
|
|
10907
|
+
hasActiveProcess: true,
|
|
10908
|
+
compactionMode,
|
|
10909
|
+
interactive: !!useInteractive,
|
|
10910
|
+
turnInFlight: isTurnInFlight(state.activeProcess),
|
|
10911
|
+
pendingProxyCalls: previousPendingProxyCalls.length,
|
|
10912
|
+
planQuestionPending: hasExitPlanModeQuestions(sk),
|
|
10913
|
+
previousHash,
|
|
10914
|
+
currentHash: probe.bridgedHash,
|
|
10915
|
+
previousServers: state.activeProcess.mcpServers,
|
|
10916
|
+
currentServers: probe.allEnabledServerNames
|
|
10917
|
+
});
|
|
10918
|
+
logMcpHotReloadDecision(decision, {
|
|
10919
|
+
sessionKey: sk,
|
|
10920
|
+
previousHash,
|
|
10921
|
+
currentHash: probe.bridgedHash
|
|
10922
|
+
});
|
|
10923
|
+
if (decision.reload) {
|
|
10924
|
+
noteMcpHotReload(sk);
|
|
10925
|
+
await deleteActiveProcessAndWait(sk);
|
|
10926
|
+
state.activeProcess = void 0;
|
|
10927
|
+
state.proxyServer = null;
|
|
10577
10928
|
}
|
|
10578
10929
|
}
|
|
10579
10930
|
if (useInteractive && !compactionMode) {
|
|
@@ -10619,6 +10970,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10619
10970
|
enabled: self.config.bridgeOpencodeSkills === true,
|
|
10620
10971
|
...self.skillBridgeSpawn(failover)
|
|
10621
10972
|
});
|
|
10973
|
+
if (stoppedBeforeWork()) return;
|
|
10622
10974
|
const ap = spawnInteractiveProcess({
|
|
10623
10975
|
cwd,
|
|
10624
10976
|
cliPath,
|
|
@@ -10633,6 +10985,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10633
10985
|
effort: reasoningEffort
|
|
10634
10986
|
});
|
|
10635
10987
|
ap.mcpHash = mcp.bridgedHash;
|
|
10988
|
+
ap.mcpServers = mcp.allEnabledServerNames;
|
|
10636
10989
|
setActiveProcess(sk, ap);
|
|
10637
10990
|
state.proc = ap.proc;
|
|
10638
10991
|
state.lineEmitter = ap.lineEmitter;
|
|
@@ -10648,6 +11001,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10648
11001
|
let spawnSystemPromptFile;
|
|
10649
11002
|
let spawnProxyServer = null;
|
|
10650
11003
|
let spawnMcpHash = null;
|
|
11004
|
+
let spawnMcpServers = [];
|
|
10651
11005
|
if (compactionMode) {
|
|
10652
11006
|
state.cliArgs = buildCliArgs({
|
|
10653
11007
|
sessionKey: sk,
|
|
@@ -10821,7 +11175,9 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10821
11175
|
spawnSystemPromptFile = systemPromptFile;
|
|
10822
11176
|
spawnProxyServer = state.proxyServer;
|
|
10823
11177
|
spawnMcpHash = mcp.bridgedHash;
|
|
11178
|
+
spawnMcpServers = mcp.allEnabledServerNames;
|
|
10824
11179
|
}
|
|
11180
|
+
if (stoppedBeforeWork()) return;
|
|
10825
11181
|
if (state.activeProcess && !compactionMode) {
|
|
10826
11182
|
state.proc = state.activeProcess.proc;
|
|
10827
11183
|
state.lineEmitter = state.activeProcess.lineEmitter;
|
|
@@ -10839,6 +11195,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10839
11195
|
reasoningEffort,
|
|
10840
11196
|
promptCacheTtl
|
|
10841
11197
|
);
|
|
11198
|
+
ap.mcpServers = spawnMcpServers;
|
|
10842
11199
|
state.proc = ap.proc;
|
|
10843
11200
|
state.lineEmitter = ap.lineEmitter;
|
|
10844
11201
|
state.activeProcess = ap;
|
|
@@ -10851,6 +11208,8 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10851
11208
|
log.warn("previous turn did not stop in time; this turn may see stale output", { sk });
|
|
10852
11209
|
}
|
|
10853
11210
|
}
|
|
11211
|
+
if (stoppedBeforeWork()) return;
|
|
11212
|
+
state.streamStarted = true;
|
|
10854
11213
|
controller.enqueue({ type: "stream-start", warnings });
|
|
10855
11214
|
if (failoverNote) {
|
|
10856
11215
|
controller.enqueue({
|
|
@@ -11209,72 +11568,8 @@ var ClaudeCodeLanguageModel = class {
|
|
|
11209
11568
|
state.drainTimer = setTimeout(() => drainNow(state), DRAIN_QUIET_MS);
|
|
11210
11569
|
});
|
|
11211
11570
|
state.proc.on("error", procErrorHandler);
|
|
11212
|
-
if (options.abortSignal) {
|
|
11213
|
-
const releaseAbandonedProxyCalls = (reason) => {
|
|
11214
|
-
if (state.drainBuffer.length === 0 && getPendingProxyCalls(sk).length === 0) return;
|
|
11215
|
-
rejectAllPendingProxyCallsForSession(sk, new Error(reason));
|
|
11216
|
-
state.drainBuffer.length = 0;
|
|
11217
|
-
};
|
|
11218
|
-
options.abortSignal.addEventListener("abort", () => {
|
|
11219
|
-
state.autoContinueState.aborted = true;
|
|
11220
|
-
if (state.turnCompleted || state.controllerClosed) {
|
|
11221
|
-
const stoppedProcess = state.activeProcess;
|
|
11222
|
-
if (stoppedProcess && stoppedProcess.lineEmitter.listenerCount("line") === 0 && getPendingProxyCalls(sk).length > 0) {
|
|
11223
|
-
const reason = describeAbortReason(options.abortSignal?.reason);
|
|
11224
|
-
void settleSessionRunState(affinity).then((stopped) => {
|
|
11225
|
-
const stillParked = stoppedProcess.lineEmitter.listenerCount("line") === 0 && getPendingProxyCalls(sk).length > 0;
|
|
11226
|
-
if (stopped === "busy" || !stillParked) {
|
|
11227
|
-
log.debug("abort at a tool boundary while opencode is still running the turn; keeping pending calls", {
|
|
11228
|
-
sk,
|
|
11229
|
-
session: stopped,
|
|
11230
|
-
stillParked,
|
|
11231
|
-
reason
|
|
11232
|
-
});
|
|
11233
|
-
return;
|
|
11234
|
-
}
|
|
11235
|
-
log.info("abort between proxy tool boundaries; releasing pending calls", { sk, reason });
|
|
11236
|
-
void interruptTurn(stoppedProcess).then((idle) => {
|
|
11237
|
-
log.info("interrupt sent for aborted turn", { sk, idle });
|
|
11238
|
-
});
|
|
11239
|
-
releaseAbandonedProxyCalls(
|
|
11240
|
-
"Provider stream was aborted while opencode was running its proxy tool calls"
|
|
11241
|
-
);
|
|
11242
|
-
});
|
|
11243
|
-
}
|
|
11244
|
-
return;
|
|
11245
|
-
}
|
|
11246
|
-
if (state.activeProcess) {
|
|
11247
|
-
void interruptTurn(state.activeProcess).then((idle) => {
|
|
11248
|
-
log.info("interrupt sent for aborted turn", { sk, idle });
|
|
11249
|
-
});
|
|
11250
|
-
}
|
|
11251
|
-
if (!state.hasReceivedContent) {
|
|
11252
|
-
log.info(
|
|
11253
|
-
"abort signal received before content, closing stream immediately",
|
|
11254
|
-
{ cwd }
|
|
11255
|
-
);
|
|
11256
|
-
releaseAbandonedProxyCalls(
|
|
11257
|
-
"Provider stream was aborted before pending proxy calls were emitted"
|
|
11258
|
-
);
|
|
11259
|
-
state.controllerClosed = true;
|
|
11260
|
-
cleanupTurn();
|
|
11261
|
-
try {
|
|
11262
|
-
controller.close();
|
|
11263
|
-
} catch {
|
|
11264
|
-
}
|
|
11265
|
-
return;
|
|
11266
|
-
}
|
|
11267
|
-
log.info(
|
|
11268
|
-
"abort signal received mid-turn, starting grace period",
|
|
11269
|
-
{ cwd }
|
|
11270
|
-
);
|
|
11271
|
-
releaseAbandonedProxyCalls(
|
|
11272
|
-
"Provider stream was aborted while proxy tool calls were pending"
|
|
11273
|
-
);
|
|
11274
|
-
startResultFallback(state, 5e3);
|
|
11275
|
-
});
|
|
11276
|
-
}
|
|
11277
11571
|
if (hasMatchedPendingResults) {
|
|
11572
|
+
state.cliAskedForWork = true;
|
|
11278
11573
|
for (const { call, result } of previousPendingProxyMatches) {
|
|
11279
11574
|
if (result) {
|
|
11280
11575
|
const channelClosed = isPendingProxyCallChannelClosed(call);
|
|
@@ -11332,6 +11627,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
11332
11627
|
);
|
|
11333
11628
|
}
|
|
11334
11629
|
}
|
|
11630
|
+
state.cliAskedForWork = true;
|
|
11335
11631
|
if (state.activeProcess) noteTurnStarted(state.activeProcess);
|
|
11336
11632
|
state.proc.stdin?.write(userMsg + "\n");
|
|
11337
11633
|
log.debug("sent user message", { textLength: userMsg.length });
|
|
@@ -12085,6 +12381,7 @@ function createClaudeCode(settings = {}) {
|
|
|
12085
12381
|
planModeQuestion: settings.planModeQuestion ?? false,
|
|
12086
12382
|
webSearch: settings.webSearch,
|
|
12087
12383
|
hotReloadMcp: settings.hotReloadMcp ?? true,
|
|
12384
|
+
mcpConnectWaitMs: settings.mcpConnectWaitMs,
|
|
12088
12385
|
proxyOpencodeMcpTools: settings.proxyOpencodeMcpTools === true,
|
|
12089
12386
|
multiStepContinuation: settings.multiStepContinuation ?? true,
|
|
12090
12387
|
autoContinueIncompleteTurns: settings.autoContinueIncompleteTurns ?? "smart",
|