@khalilgharbaoui/opencode-claude-code-plugin 0.33.2 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -1
- package/dist/index.d.ts +22 -0
- package/dist/index.js +2030 -1861
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/skills/claude-code-plugin/SKILL.md +4 -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,2106 @@ 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);
|
|
3508
|
+
}
|
|
3509
|
+
async function waitForPendingMcpServers(first, budgetMs) {
|
|
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
|
+
await new Promise((resolve6) => setTimeout(resolve6, MCP_CONNECT_POLL_MS));
|
|
3517
|
+
const next = await fetchRuntimeMcpStatus();
|
|
3518
|
+
if (!next) break;
|
|
3519
|
+
current = next;
|
|
3520
|
+
if (pendingMcpServers(current).length === 0) break;
|
|
3521
|
+
}
|
|
3522
|
+
const stillPending = pendingMcpServers(current);
|
|
3523
|
+
log.info("waited for opencode MCP servers to finish connecting", {
|
|
3524
|
+
waitedFor,
|
|
3525
|
+
stillPending,
|
|
3526
|
+
budgetMs,
|
|
3527
|
+
elapsedMs: Date.now() - started
|
|
3542
3528
|
});
|
|
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;
|
|
3529
|
+
return current;
|
|
3530
|
+
}
|
|
3531
|
+
async function fetchOpencodeToolList(provider, model, directory) {
|
|
3532
|
+
const client = opencodeClient;
|
|
3533
|
+
if (!client?.tool?.list) return void 0;
|
|
3534
|
+
try {
|
|
3535
|
+
const res = await client.tool.list({
|
|
3536
|
+
query: { provider, model, ...directory ? { directory } : {} }
|
|
3537
|
+
});
|
|
3538
|
+
const data = res.data;
|
|
3539
|
+
if (!Array.isArray(data)) return void 0;
|
|
3540
|
+
const out = [];
|
|
3541
|
+
for (const entry of data) {
|
|
3542
|
+
if (!entry || typeof entry !== "object") continue;
|
|
3543
|
+
const e = entry;
|
|
3544
|
+
const id = typeof e.id === "string" ? e.id : null;
|
|
3545
|
+
const description = typeof e.description === "string" ? e.description : "";
|
|
3546
|
+
const parameters = e.parameters && typeof e.parameters === "object" ? e.parameters : {};
|
|
3547
|
+
if (!id) continue;
|
|
3548
|
+
out.push({ id, description, parameters });
|
|
3615
3549
|
}
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3550
|
+
return out;
|
|
3551
|
+
} catch (err) {
|
|
3552
|
+
log.warn("failed to fetch opencode tool list", {
|
|
3553
|
+
provider,
|
|
3554
|
+
model,
|
|
3555
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3556
|
+
});
|
|
3557
|
+
return void 0;
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
async function fetchSessionReplies(sessionID) {
|
|
3561
|
+
if (!sessionID || sessionID === "default") return void 0;
|
|
3562
|
+
const client = opencodeClient;
|
|
3563
|
+
if (!client?.session?.messages) return void 0;
|
|
3564
|
+
try {
|
|
3565
|
+
const res = await client.session.messages({ path: { id: sessionID } });
|
|
3566
|
+
const data = res.data;
|
|
3567
|
+
if (!Array.isArray(data)) return void 0;
|
|
3568
|
+
const out = [];
|
|
3569
|
+
for (const entry of data) {
|
|
3570
|
+
if (!entry || typeof entry !== "object") continue;
|
|
3571
|
+
const info = entry.info;
|
|
3572
|
+
if (!info || typeof info !== "object") continue;
|
|
3573
|
+
const record = info;
|
|
3574
|
+
const parts = entry.parts;
|
|
3575
|
+
const text = Array.isArray(parts) ? parts.flatMap((part) => {
|
|
3576
|
+
if (!part || typeof part !== "object") return [];
|
|
3577
|
+
const p = part;
|
|
3578
|
+
if (p.type !== "text" || typeof p.text !== "string") return [];
|
|
3579
|
+
return p.synthetic === true ? [] : [p.text];
|
|
3580
|
+
}).join("\n").trim() : "";
|
|
3581
|
+
const error = record.error;
|
|
3582
|
+
const message = error && typeof error === "object" ? typeof error.data?.message === "string" ? error.data.message : typeof error.name === "string" ? error.name : void 0 : void 0;
|
|
3583
|
+
out.push({
|
|
3584
|
+
role: typeof record.role === "string" ? record.role : "",
|
|
3585
|
+
completed: typeof record.time?.completed === "number",
|
|
3586
|
+
error: message,
|
|
3587
|
+
text
|
|
3588
|
+
});
|
|
3621
3589
|
}
|
|
3622
|
-
|
|
3590
|
+
return out;
|
|
3591
|
+
} catch (err) {
|
|
3592
|
+
log.warn("failed to fetch opencode session messages", {
|
|
3593
|
+
sessionID,
|
|
3594
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3595
|
+
});
|
|
3596
|
+
return void 0;
|
|
3597
|
+
}
|
|
3598
|
+
}
|
|
3599
|
+
async function abortSession(sessionID) {
|
|
3600
|
+
if (!sessionID || sessionID === "default") return false;
|
|
3601
|
+
const client = opencodeClient;
|
|
3602
|
+
if (!client?.session?.abort) return false;
|
|
3603
|
+
try {
|
|
3604
|
+
const res = await client.session.abort({ path: { id: sessionID } });
|
|
3605
|
+
return res.data !== false;
|
|
3606
|
+
} catch (err) {
|
|
3607
|
+
log.warn("failed to abort opencode session", {
|
|
3608
|
+
sessionID,
|
|
3609
|
+
error: err instanceof Error ? err.message : String(err)
|
|
3610
|
+
});
|
|
3611
|
+
return false;
|
|
3612
|
+
}
|
|
3623
3613
|
}
|
|
3624
3614
|
|
|
3625
|
-
// src/
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3615
|
+
// src/background-tasks.ts
|
|
3616
|
+
var TASK_STATUS_TOOL_NAME = "task_status";
|
|
3617
|
+
var TASK_CANCEL_TOOL_NAME = "task_cancel";
|
|
3618
|
+
var MAX_LEDGERS = 32;
|
|
3619
|
+
var MAX_COLLECTED_PER_SESSION = 64;
|
|
3620
|
+
var collected = /* @__PURE__ */ new Map();
|
|
3621
|
+
var cancelled = /* @__PURE__ */ new Map();
|
|
3622
|
+
function ledgerIn(ledgers2, sessionKey2) {
|
|
3623
|
+
let ids = ledgers2.get(sessionKey2);
|
|
3624
|
+
if (!ids) {
|
|
3625
|
+
ids = /* @__PURE__ */ new Set();
|
|
3626
|
+
ledgers2.set(sessionKey2, ids);
|
|
3627
|
+
while (ledgers2.size > MAX_LEDGERS) {
|
|
3628
|
+
const oldest = ledgers2.keys().next();
|
|
3629
|
+
if (oldest.done) break;
|
|
3630
|
+
ledgers2.delete(oldest.value);
|
|
3631
|
+
}
|
|
3630
3632
|
}
|
|
3631
|
-
return
|
|
3633
|
+
return ids;
|
|
3632
3634
|
}
|
|
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;
|
|
3635
|
+
function mark(ledgers2, sessionKey2, taskId) {
|
|
3636
|
+
const ids = ledgerIn(ledgers2, sessionKey2);
|
|
3637
|
+
ids.add(taskId);
|
|
3638
|
+
while (ids.size > MAX_COLLECTED_PER_SESSION) {
|
|
3639
|
+
const oldest = ids.values().next();
|
|
3640
|
+
if (oldest.done) break;
|
|
3641
|
+
ids.delete(oldest.value);
|
|
3643
3642
|
}
|
|
3644
3643
|
}
|
|
3645
|
-
function
|
|
3646
|
-
|
|
3647
|
-
const dropped = ap.unattendedDropped ?? 0;
|
|
3648
|
-
ap.unattendedLines = [];
|
|
3649
|
-
ap.unattendedDropped = 0;
|
|
3650
|
-
return { lines, dropped };
|
|
3644
|
+
function markCollected(sessionKey2, taskId) {
|
|
3645
|
+
mark(collected, sessionKey2, taskId);
|
|
3651
3646
|
}
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
ap.lastStderr = ((ap.lastStderr ?? "") + chunk).slice(-STDERR_RETAIN_BYTES);
|
|
3647
|
+
function hasCollectedBackgroundTask(sessionKey2, taskId) {
|
|
3648
|
+
return collected.get(sessionKey2)?.has(taskId) === true;
|
|
3655
3649
|
}
|
|
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}` : "");
|
|
3650
|
+
function clearBackgroundTasks(sessionKey2) {
|
|
3651
|
+
collected.delete(sessionKey2);
|
|
3652
|
+
cancelled.delete(sessionKey2);
|
|
3663
3653
|
}
|
|
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;
|
|
3654
|
+
var lastGate;
|
|
3655
|
+
function recordBackgroundSubagentGate(gate, now = Date.now()) {
|
|
3656
|
+
lastGate = { ...gate, at: now };
|
|
3671
3657
|
}
|
|
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);
|
|
3658
|
+
function snapshotBackgroundSubagentGate() {
|
|
3659
|
+
return lastGate ? { ...lastGate } : void 0;
|
|
3681
3660
|
}
|
|
3682
|
-
function
|
|
3683
|
-
|
|
3661
|
+
function snapshotBackgroundTasks() {
|
|
3662
|
+
const keys = /* @__PURE__ */ new Set([...collected.keys(), ...cancelled.keys()]);
|
|
3663
|
+
return [...keys].map((sessionKey2) => ({
|
|
3664
|
+
sessionKey: sessionKey2,
|
|
3665
|
+
collected: [...collected.get(sessionKey2) ?? []],
|
|
3666
|
+
cancelled: [...cancelled.get(sessionKey2) ?? []]
|
|
3667
|
+
}));
|
|
3684
3668
|
}
|
|
3685
|
-
function
|
|
3686
|
-
|
|
3669
|
+
function readTaskId(input) {
|
|
3670
|
+
const raw = input.task_id ?? input.taskId ?? input.sessionID;
|
|
3671
|
+
if (typeof raw !== "string") return null;
|
|
3672
|
+
const id = raw.trim();
|
|
3673
|
+
return id.length > 0 ? id : null;
|
|
3687
3674
|
}
|
|
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";
|
|
3675
|
+
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.';
|
|
3676
|
+
async function guardParent(taskId, callerSessionId) {
|
|
3677
|
+
if (!callerSessionId || callerSessionId === "default") {
|
|
3678
|
+
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
3679
|
}
|
|
3710
|
-
|
|
3680
|
+
const parent = await fetchSessionParentId(taskId);
|
|
3681
|
+
if (parent === callerSessionId) return null;
|
|
3682
|
+
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
3683
|
}
|
|
3712
|
-
function
|
|
3713
|
-
|
|
3714
|
-
if (
|
|
3715
|
-
|
|
3716
|
-
|
|
3684
|
+
function isBackgroundTaskRunning(runState, replies) {
|
|
3685
|
+
if (runState === "busy") return true;
|
|
3686
|
+
if (runState === "idle") return false;
|
|
3687
|
+
const reply = lastAssistantReply(replies);
|
|
3688
|
+
return reply !== void 0 && !reply.completed && reply.error === void 0;
|
|
3689
|
+
}
|
|
3690
|
+
function lastAssistantReply(replies) {
|
|
3691
|
+
for (let index = replies.length - 1; index >= 0; index--) {
|
|
3692
|
+
const reply = replies[index];
|
|
3693
|
+
if (reply.role === "assistant") return reply;
|
|
3717
3694
|
}
|
|
3695
|
+
return void 0;
|
|
3718
3696
|
}
|
|
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);
|
|
3697
|
+
async function collectBackgroundTask(input, options) {
|
|
3698
|
+
const taskId = readTaskId(input);
|
|
3699
|
+
if (!taskId) return { kind: "error", message: MISSING_TASK_ID };
|
|
3700
|
+
const refused = await guardParent(taskId, options.callerSessionId);
|
|
3701
|
+
if (refused) return { kind: "error", message: refused };
|
|
3702
|
+
const replies = await fetchSessionReplies(taskId);
|
|
3703
|
+
if (replies === void 0) {
|
|
3704
|
+
return {
|
|
3705
|
+
kind: "error",
|
|
3706
|
+
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.`
|
|
3707
|
+
};
|
|
3737
3708
|
}
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3709
|
+
const runState = await fetchSessionRunState(taskId);
|
|
3710
|
+
if (isBackgroundTaskRunning(runState, replies)) {
|
|
3711
|
+
return {
|
|
3712
|
+
kind: "text",
|
|
3713
|
+
text: `<task id="${taskId}" state="running">
|
|
3714
|
+
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.
|
|
3715
|
+
</task>`
|
|
3716
|
+
};
|
|
3746
3717
|
}
|
|
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);
|
|
3718
|
+
const reply = lastAssistantReply(replies);
|
|
3719
|
+
if (!reply) {
|
|
3720
|
+
return {
|
|
3721
|
+
kind: "text",
|
|
3722
|
+
text: `<task id="${taskId}" state="unknown">
|
|
3723
|
+
opencode has no assistant reply for this task yet and it is not running. It may still be starting up.
|
|
3724
|
+
</task>`
|
|
3771
3725
|
};
|
|
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
3726
|
}
|
|
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);
|
|
3727
|
+
if (reply.error) {
|
|
3728
|
+
markCollected(options.sessionKey, taskId);
|
|
3729
|
+
return {
|
|
3730
|
+
kind: "text",
|
|
3731
|
+
isError: true,
|
|
3732
|
+
text: `<task id="${taskId}" state="error">
|
|
3733
|
+
${reply.error}
|
|
3734
|
+
</task>`
|
|
3735
|
+
};
|
|
3801
3736
|
}
|
|
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);
|
|
3737
|
+
if (hasCollectedBackgroundTask(options.sessionKey, taskId)) {
|
|
3738
|
+
return {
|
|
3739
|
+
kind: "text",
|
|
3740
|
+
text: `<task id="${taskId}" state="completed">
|
|
3741
|
+
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.
|
|
3742
|
+
</task>`
|
|
3743
|
+
};
|
|
3815
3744
|
}
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3745
|
+
markCollected(options.sessionKey, taskId);
|
|
3746
|
+
log.info("collected background subagent result", {
|
|
3747
|
+
sessionKey: options.sessionKey,
|
|
3748
|
+
taskId,
|
|
3749
|
+
textLength: reply.text.length
|
|
3750
|
+
});
|
|
3751
|
+
return {
|
|
3752
|
+
kind: "text",
|
|
3753
|
+
text: `<task id="${taskId}" state="completed">
|
|
3754
|
+
<task_result>
|
|
3755
|
+
${reply.text || "(the subagent produced no text)"}
|
|
3756
|
+
</task_result>
|
|
3757
|
+
</task>`
|
|
3758
|
+
};
|
|
3821
3759
|
}
|
|
3822
|
-
function
|
|
3823
|
-
|
|
3824
|
-
if (
|
|
3825
|
-
|
|
3760
|
+
async function cancelBackgroundTask(input, options) {
|
|
3761
|
+
const taskId = readTaskId(input);
|
|
3762
|
+
if (!taskId) return { kind: "error", message: MISSING_TASK_ID };
|
|
3763
|
+
const refused = await guardParent(taskId, options.callerSessionId);
|
|
3764
|
+
if (refused) return { kind: "error", message: refused };
|
|
3765
|
+
const running = isBackgroundTaskRunning(
|
|
3766
|
+
await fetchSessionRunState(taskId),
|
|
3767
|
+
await fetchSessionReplies(taskId) ?? []
|
|
3768
|
+
);
|
|
3769
|
+
const aborted = await abortSession(taskId);
|
|
3770
|
+
if (!aborted) {
|
|
3771
|
+
return {
|
|
3772
|
+
kind: "error",
|
|
3773
|
+
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.`
|
|
3774
|
+
};
|
|
3826
3775
|
}
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
}
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
}, timeoutMs);
|
|
3840
|
-
timer.unref();
|
|
3841
|
-
idleEvictionTimers.set(key, timer);
|
|
3776
|
+
collected.get(options.sessionKey)?.delete(taskId);
|
|
3777
|
+
mark(cancelled, options.sessionKey, taskId);
|
|
3778
|
+
log.info("cancelled background subagent", {
|
|
3779
|
+
sessionKey: options.sessionKey,
|
|
3780
|
+
taskId,
|
|
3781
|
+
wasRunning: running
|
|
3782
|
+
});
|
|
3783
|
+
return {
|
|
3784
|
+
kind: "text",
|
|
3785
|
+
text: `<task id="${taskId}" state="cancelled">
|
|
3786
|
+
` + (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>"
|
|
3787
|
+
};
|
|
3842
3788
|
}
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3789
|
+
|
|
3790
|
+
// src/permission-presets.ts
|
|
3791
|
+
var READ_ONLY_DISALLOWED_CLI_TOOLS = [
|
|
3792
|
+
"Bash",
|
|
3793
|
+
"Write",
|
|
3794
|
+
"Edit",
|
|
3795
|
+
"NotebookEdit",
|
|
3796
|
+
"REPL",
|
|
3797
|
+
"JavaScript",
|
|
3798
|
+
"WebFetch"
|
|
3799
|
+
];
|
|
3800
|
+
var READ_ONLY_DENIED_PROXY_TOOLS = /* @__PURE__ */ new Set([
|
|
3801
|
+
"bash",
|
|
3802
|
+
"write",
|
|
3803
|
+
"edit",
|
|
3804
|
+
"webfetch",
|
|
3805
|
+
"task",
|
|
3806
|
+
"task_batch"
|
|
3807
|
+
]);
|
|
3808
|
+
function dedupe(names) {
|
|
3809
|
+
const out = [];
|
|
3810
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3811
|
+
for (const name of names) {
|
|
3812
|
+
const trimmed = String(name).trim();
|
|
3813
|
+
if (!trimmed || seen.has(trimmed)) continue;
|
|
3814
|
+
seen.add(trimmed);
|
|
3815
|
+
out.push(trimmed);
|
|
3851
3816
|
}
|
|
3852
|
-
return
|
|
3817
|
+
return out;
|
|
3853
3818
|
}
|
|
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);
|
|
3819
|
+
function resolveReadOnly(settings, defaultProxyTools) {
|
|
3820
|
+
const overridden = [];
|
|
3821
|
+
if (settings.permissionMode !== void 0) {
|
|
3822
|
+
overridden.push(
|
|
3823
|
+
`permissionMode: "${settings.permissionMode}" is dropped; read-only sends --restricted instead, which is stricter than every mode except plan and incompatible with bypassPermissions`
|
|
3824
|
+
);
|
|
3869
3825
|
}
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3826
|
+
if (settings.skipPermissions === true) {
|
|
3827
|
+
overridden.push(
|
|
3828
|
+
'skipPermissions: forced to false; the CLI exits with "bypassPermissions not supported in restricted mode" when both are passed'
|
|
3829
|
+
);
|
|
3830
|
+
}
|
|
3831
|
+
if (settings.controlRequestBehavior === "allow") {
|
|
3832
|
+
overridden.push(
|
|
3833
|
+
'controlRequestBehavior: forced to "deny"; read-only answers every permission request the same way the CLI does under --permission-prompts none'
|
|
3834
|
+
);
|
|
3835
|
+
}
|
|
3836
|
+
if (settings.controlRequestToolBehaviors !== void 0) {
|
|
3837
|
+
overridden.push(
|
|
3838
|
+
"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"
|
|
3839
|
+
);
|
|
3840
|
+
}
|
|
3841
|
+
const requested = settings.proxyTools ?? defaultProxyTools;
|
|
3842
|
+
const proxyTools = dedupe(requested).filter(
|
|
3843
|
+
(name) => !READ_ONLY_DENIED_PROXY_TOOLS.has(name.toLowerCase())
|
|
3844
|
+
);
|
|
3845
|
+
const dropped = dedupe(requested).filter(
|
|
3846
|
+
(name) => READ_ONLY_DENIED_PROXY_TOOLS.has(name.toLowerCase())
|
|
3847
|
+
);
|
|
3848
|
+
if (dropped.length > 0) {
|
|
3849
|
+
overridden.push(
|
|
3850
|
+
`proxyTools: dropped ${dropped.join(", ")}; those execute in opencode, where no Claude CLI flag reaches them`
|
|
3851
|
+
);
|
|
3852
|
+
}
|
|
3853
|
+
return {
|
|
3854
|
+
preset: "read-only",
|
|
3855
|
+
permissionMode: READ_ONLY_PERMISSION_MODE,
|
|
3856
|
+
skipPermissions: false,
|
|
3857
|
+
proxyTools,
|
|
3858
|
+
extraDisallowedTools: dedupe([
|
|
3859
|
+
...READ_ONLY_DISALLOWED_CLI_TOOLS,
|
|
3860
|
+
...settings.extraDisallowedTools ?? []
|
|
3861
|
+
]),
|
|
3862
|
+
controlRequestBehavior: "deny",
|
|
3863
|
+
controlRequestToolBehaviors: void 0,
|
|
3864
|
+
overridden
|
|
3865
|
+
};
|
|
3878
3866
|
}
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
if (
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
killAllActiveProcesses();
|
|
3885
|
-
});
|
|
3886
|
-
return true;
|
|
3867
|
+
function resolvePermissionPreset(settings, defaultProxyTools) {
|
|
3868
|
+
const preset = settings.permissionPreset;
|
|
3869
|
+
if (preset === void 0) return null;
|
|
3870
|
+
if (preset === "read-only") return resolveReadOnly(settings, defaultProxyTools);
|
|
3871
|
+
return { unknown: String(preset) };
|
|
3887
3872
|
}
|
|
3888
|
-
function
|
|
3889
|
-
|
|
3890
|
-
ap?.proc.kill();
|
|
3873
|
+
function isUnknownPreset(resolved) {
|
|
3874
|
+
return resolved !== null && "unknown" in resolved;
|
|
3891
3875
|
}
|
|
3892
|
-
|
|
3893
|
-
|
|
3876
|
+
var NO_PERMISSION_PRESET = "none";
|
|
3877
|
+
function summarizePermissionPreset(provider, options, defaultProxyTools = DEFAULT_PROXY_TOOL_NAMES) {
|
|
3878
|
+
const settings = options !== null && typeof options === "object" && !Array.isArray(options) ? options : {};
|
|
3879
|
+
const resolved = resolvePermissionPreset(settings, defaultProxyTools);
|
|
3880
|
+
if (resolved === null) {
|
|
3881
|
+
return { provider, preset: NO_PERMISSION_PRESET, applied: false, overrides: [] };
|
|
3882
|
+
}
|
|
3883
|
+
if (isUnknownPreset(resolved)) {
|
|
3884
|
+
return { provider, preset: resolved.unknown, applied: false, overrides: [] };
|
|
3885
|
+
}
|
|
3886
|
+
return {
|
|
3887
|
+
provider,
|
|
3888
|
+
preset: resolved.preset,
|
|
3889
|
+
applied: true,
|
|
3890
|
+
overrides: resolved.overridden
|
|
3891
|
+
};
|
|
3894
3892
|
}
|
|
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
|
-
});
|
|
3893
|
+
function isReadOnlyPermissionMode(mode) {
|
|
3894
|
+
return mode === READ_ONLY_PERMISSION_MODE;
|
|
3908
3895
|
}
|
|
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;
|
|
3896
|
+
|
|
3897
|
+
// src/side-question.ts
|
|
3898
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
3899
|
+
var MAX_HISTORY_EXCHANGES = 20;
|
|
3900
|
+
var SIDE_QUESTION_USAGE = "Usage: /btw <question>. Ask a side question about the current conversation without adding it to the main context.";
|
|
3901
|
+
var pendingProcesses = /* @__PURE__ */ new WeakSet();
|
|
3902
|
+
function isRecord2(value) {
|
|
3903
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3929
3904
|
}
|
|
3930
|
-
|
|
3931
|
-
|
|
3905
|
+
var SYSTEM_REMINDER_BLOCK = /<system-reminder>[\s\S]*?<\/system-reminder>/g;
|
|
3906
|
+
function parseSideQuestionContent(content) {
|
|
3907
|
+
let text;
|
|
3908
|
+
if (typeof content === "string") {
|
|
3909
|
+
text = content;
|
|
3910
|
+
} else if (Array.isArray(content)) {
|
|
3911
|
+
const parts = [];
|
|
3912
|
+
for (const part of content) {
|
|
3913
|
+
if (!isRecord2(part) || part.type !== "text" || typeof part.text !== "string") return null;
|
|
3914
|
+
parts.push(part.text);
|
|
3915
|
+
}
|
|
3916
|
+
text = parts.join("\n");
|
|
3917
|
+
} else {
|
|
3918
|
+
return null;
|
|
3919
|
+
}
|
|
3920
|
+
const match = /^\/btw(?:\s+([\s\S]*))?$/.exec(text.replace(SYSTEM_REMINDER_BLOCK, "").trim());
|
|
3921
|
+
return match ? { question: (match[1] ?? "").trim() } : null;
|
|
3932
3922
|
}
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
return getPendingProxyCalls(key).length > 0 || hasExitPlanModeQuestions(key);
|
|
3923
|
+
function parseSideQuestion(prompt) {
|
|
3924
|
+
const latest = prompt.at(-1);
|
|
3925
|
+
return latest?.role === "user" ? parseSideQuestionContent(latest.content) : null;
|
|
3937
3926
|
}
|
|
3938
|
-
function
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
size: claudeSessions.size,
|
|
3945
|
-
cap: MAX_CLAUDE_SESSION_ENTRIES
|
|
3946
|
-
});
|
|
3947
|
-
deleteClaudeSessionId(key);
|
|
3927
|
+
function assistantText(content) {
|
|
3928
|
+
if (typeof content === "string") return content.trim();
|
|
3929
|
+
if (!Array.isArray(content)) return "";
|
|
3930
|
+
const parts = [];
|
|
3931
|
+
for (const part of content) {
|
|
3932
|
+
if (isRecord2(part) && part.type === "text" && typeof part.text === "string") parts.push(part.text);
|
|
3948
3933
|
}
|
|
3934
|
+
return parts.join("\n").trim();
|
|
3949
3935
|
}
|
|
3950
|
-
function
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3936
|
+
function collectSideQuestionHistory(prompt) {
|
|
3937
|
+
const history = [];
|
|
3938
|
+
for (let index = 0; index < prompt.length - 1; index++) {
|
|
3939
|
+
const message = prompt[index];
|
|
3940
|
+
if (message.role !== "user") continue;
|
|
3941
|
+
const aside = parseSideQuestionContent(message.content);
|
|
3942
|
+
if (!aside?.question) continue;
|
|
3943
|
+
const reply = prompt[index + 1];
|
|
3944
|
+
if (reply.role !== "assistant") continue;
|
|
3945
|
+
const response = assistantText(reply.content);
|
|
3946
|
+
if (!response || response === SIDE_QUESTION_USAGE) continue;
|
|
3947
|
+
history.push({ question: aside.question, response });
|
|
3948
|
+
}
|
|
3949
|
+
return history.slice(-MAX_HISTORY_EXCHANGES);
|
|
3954
3950
|
}
|
|
3955
|
-
function
|
|
3956
|
-
|
|
3957
|
-
clearAccountFailoverQuestions(key);
|
|
3958
|
-
const claudeSessionId = claudeSessions.get(key);
|
|
3959
|
-
if (claudeSessionId) clearLedger(claudeSessionId);
|
|
3960
|
-
claudeSessions.delete(key);
|
|
3951
|
+
function isSideQuestionPending(activeProcess) {
|
|
3952
|
+
return pendingProcesses.has(activeProcess.proc);
|
|
3961
3953
|
}
|
|
3962
|
-
function
|
|
3963
|
-
|
|
3954
|
+
function dispatchSideQuestionResponse(activeProcess, line) {
|
|
3955
|
+
if (!pendingProcesses.has(activeProcess.proc)) return false;
|
|
3956
|
+
let message;
|
|
3957
|
+
try {
|
|
3958
|
+
message = JSON.parse(line);
|
|
3959
|
+
} catch {
|
|
3960
|
+
return false;
|
|
3961
|
+
}
|
|
3962
|
+
if (!isRecord2(message) || message.type !== "control_response") return false;
|
|
3963
|
+
const response = message.response;
|
|
3964
|
+
if (!isRecord2(response) || typeof response.request_id !== "string") return false;
|
|
3965
|
+
return activeProcess.lineEmitter.emit(`side-question:${response.request_id}`, response);
|
|
3964
3966
|
}
|
|
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
|
-
}
|
|
3967
|
+
async function requestSideQuestion(activeProcess, question, options) {
|
|
3968
|
+
question = question.trim();
|
|
3969
|
+
if (!question) return { response: SIDE_QUESTION_USAGE, synthetic: true };
|
|
3970
|
+
options.abortSignal?.throwIfAborted();
|
|
3971
|
+
const { proc, lineEmitter } = activeProcess;
|
|
3972
|
+
if (options.interactive || !proc.stdout) {
|
|
3973
|
+
throw new Error("/btw requires the headless Claude Code transport; interactive sessions are not supported.");
|
|
3983
3974
|
}
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
deleteClaudeSessionId(key);
|
|
3987
|
-
clearCompression(key);
|
|
3988
|
-
clearBackgroundTasks(key);
|
|
3975
|
+
if (!cliSupportsSideQuestion(options.cliVersion)) {
|
|
3976
|
+
throw new Error("/btw requires Claude Code CLI 2.1.258 or newer (the oldest verified version).");
|
|
3989
3977
|
}
|
|
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;
|
|
3978
|
+
if (pendingProcesses.has(proc)) {
|
|
3979
|
+
throw new Error("Wait for the current /btw to finish before asking another.");
|
|
3980
|
+
}
|
|
3981
|
+
const stdin = proc.stdin;
|
|
3982
|
+
if (proc.killed || proc.exitCode != null || proc.signalCode != null || !stdin || stdin.destroyed || stdin.writableEnded || !stdin.writable) {
|
|
3983
|
+
throw new Error("/btw requires a live Claude Code session with writable stdin.");
|
|
3984
|
+
}
|
|
3985
|
+
const timeoutMs = options.timeoutMs ?? 12e4;
|
|
3986
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2147483647) {
|
|
3987
|
+
throw new Error("/btw timeoutMs must be a positive 32-bit integer.");
|
|
3988
|
+
}
|
|
3989
|
+
const requestId = randomUUID2();
|
|
3990
|
+
const request = JSON.stringify({
|
|
3991
|
+
type: "control_request",
|
|
3992
|
+
request_id: requestId,
|
|
3993
|
+
request: {
|
|
3994
|
+
subtype: "side_question",
|
|
3995
|
+
question,
|
|
3996
|
+
...options.history === void 0 ? {} : { history: options.history }
|
|
4029
3997
|
}
|
|
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
3998
|
});
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
3999
|
+
pendingProcesses.add(proc);
|
|
4000
|
+
return new Promise((resolve6, reject) => {
|
|
4001
|
+
const event = `side-question:${requestId}`;
|
|
4002
|
+
let settled = false;
|
|
4003
|
+
let sent = false;
|
|
4004
|
+
let cancelPending = false;
|
|
4005
|
+
const cleanup = () => {
|
|
4006
|
+
clearTimeout(timer);
|
|
4007
|
+
lineEmitter.off(event, onResponse);
|
|
4008
|
+
lineEmitter.off("close", onClose);
|
|
4009
|
+
lineEmitter.off("error", onError);
|
|
4010
|
+
proc.off("exit", onClose);
|
|
4011
|
+
proc.off("close", onClose);
|
|
4012
|
+
proc.off("error", onError);
|
|
4013
|
+
if (!cancelPending) stdin.off("error", onError);
|
|
4014
|
+
options.abortSignal?.removeEventListener("abort", onAbort);
|
|
4015
|
+
pendingProcesses.delete(proc);
|
|
4016
|
+
};
|
|
4017
|
+
const fail = (error, cancel = false) => {
|
|
4018
|
+
if (settled) return;
|
|
4019
|
+
settled = true;
|
|
4020
|
+
if (cancel && sent && !stdin.destroyed && !stdin.writableEnded && stdin.writable) {
|
|
4021
|
+
try {
|
|
4022
|
+
cancelPending = true;
|
|
4023
|
+
stdin.write(
|
|
4024
|
+
JSON.stringify({ type: "control_cancel_request", request_id: requestId }) + "\n",
|
|
4025
|
+
() => {
|
|
4026
|
+
queueMicrotask(() => stdin.off("error", onError));
|
|
4027
|
+
}
|
|
4028
|
+
);
|
|
4029
|
+
} catch {
|
|
4030
|
+
cancelPending = false;
|
|
4031
|
+
}
|
|
4065
4032
|
}
|
|
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
|
-
});
|
|
4033
|
+
cleanup();
|
|
4034
|
+
reject(error);
|
|
4035
|
+
};
|
|
4036
|
+
const onClose = () => fail(new Error("Claude Code closed before answering /btw."));
|
|
4037
|
+
const onError = (error) => fail(error);
|
|
4038
|
+
const onAbort = () => fail(
|
|
4039
|
+
options.abortSignal?.reason ?? new DOMException("/btw was aborted.", "AbortError"),
|
|
4040
|
+
true
|
|
4041
|
+
);
|
|
4042
|
+
const onResponse = (response) => {
|
|
4043
|
+
if (settled || response.request_id !== requestId) return;
|
|
4044
|
+
if (response.subtype === "error") {
|
|
4045
|
+
fail(new Error(typeof response.error === "string" ? response.error : "Claude Code rejected /btw."));
|
|
4046
|
+
return;
|
|
4047
|
+
}
|
|
4048
|
+
const result = response.response;
|
|
4049
|
+
if (response.subtype !== "success" || !isRecord2(result) || typeof result.response !== "string" || typeof result.synthetic !== "boolean") {
|
|
4050
|
+
fail(new Error("Claude Code returned an invalid /btw response."));
|
|
4051
|
+
return;
|
|
4091
4052
|
}
|
|
4053
|
+
settled = true;
|
|
4054
|
+
cleanup();
|
|
4055
|
+
resolve6({ response: result.response, synthetic: result.synthetic });
|
|
4056
|
+
};
|
|
4057
|
+
const timer = setTimeout(() => {
|
|
4058
|
+
fail(new Error(`/btw timed out after ${timeoutMs}ms.`), true);
|
|
4059
|
+
}, timeoutMs);
|
|
4060
|
+
lineEmitter.on(event, onResponse);
|
|
4061
|
+
lineEmitter.on("close", onClose);
|
|
4062
|
+
lineEmitter.on("error", onError);
|
|
4063
|
+
proc.on("exit", onClose);
|
|
4064
|
+
proc.on("close", onClose);
|
|
4065
|
+
proc.on("error", onError);
|
|
4066
|
+
stdin.on("error", onError);
|
|
4067
|
+
options.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
4068
|
+
if (options.abortSignal?.aborted) {
|
|
4069
|
+
onAbort();
|
|
4070
|
+
return;
|
|
4071
|
+
}
|
|
4072
|
+
try {
|
|
4073
|
+
sent = true;
|
|
4074
|
+
stdin.write(request + "\n");
|
|
4075
|
+
} catch (error) {
|
|
4076
|
+
fail(error);
|
|
4092
4077
|
}
|
|
4093
4078
|
});
|
|
4094
|
-
return ap;
|
|
4095
4079
|
}
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4080
|
+
|
|
4081
|
+
// src/session-manager.ts
|
|
4082
|
+
function findActiveProcessBySessionId(sessionID) {
|
|
4083
|
+
let found;
|
|
4084
|
+
for (const ap of activeProcesses.values()) {
|
|
4085
|
+
if (ap.opencodeSessionID === sessionID) found = ap;
|
|
4099
4086
|
}
|
|
4100
|
-
|
|
4101
|
-
if (!sid) return cliArgs;
|
|
4102
|
-
return [...cliArgs, "--resume", sid];
|
|
4087
|
+
return found;
|
|
4103
4088
|
}
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
const
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4089
|
+
var UNATTENDED_LINE_CAP = 500;
|
|
4090
|
+
var UNATTENDED_BYTE_CAP = 2 * 1024 * 1024;
|
|
4091
|
+
function bufferUnattendedLine(ap, line) {
|
|
4092
|
+
const lines = ap.unattendedLines ??= [];
|
|
4093
|
+
lines.push(line);
|
|
4094
|
+
let bytes = 0;
|
|
4095
|
+
for (const kept of lines) bytes += Buffer.byteLength(kept);
|
|
4096
|
+
while (lines.length > 0 && (lines.length > UNATTENDED_LINE_CAP || bytes > UNATTENDED_BYTE_CAP)) {
|
|
4097
|
+
bytes -= Buffer.byteLength(lines.shift());
|
|
4098
|
+
ap.unattendedDropped = (ap.unattendedDropped ?? 0) + 1;
|
|
4113
4099
|
}
|
|
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
4100
|
}
|
|
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;
|
|
4219
|
-
}
|
|
4220
|
-
function sessionKey(cwd, modelId) {
|
|
4221
|
-
return `${cwd}::${modelId}`;
|
|
4222
|
-
}
|
|
4223
|
-
function describeSessionKey(key) {
|
|
4224
|
-
const parts = key.split("::");
|
|
4225
|
-
return {
|
|
4226
|
-
cwd: parts[0] ?? "unknown",
|
|
4227
|
-
model: parts[1] ?? "unknown",
|
|
4228
|
-
session: parts[3] ?? "unknown",
|
|
4229
|
-
compaction: parts[2] === "compaction"
|
|
4230
|
-
};
|
|
4101
|
+
function takeUnattendedLines(ap) {
|
|
4102
|
+
const lines = ap.unattendedLines ?? [];
|
|
4103
|
+
const dropped = ap.unattendedDropped ?? 0;
|
|
4104
|
+
ap.unattendedLines = [];
|
|
4105
|
+
ap.unattendedDropped = 0;
|
|
4106
|
+
return { lines, dropped };
|
|
4231
4107
|
}
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
const described = describeSessionKey(key);
|
|
4236
|
-
const lastStderr = ap.lastStderr;
|
|
4237
|
-
out.push({
|
|
4238
|
-
sessionKey: key,
|
|
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;
|
|
4108
|
+
var STDERR_RETAIN_BYTES = 2 * 1024;
|
|
4109
|
+
function retainStderr(ap, chunk) {
|
|
4110
|
+
ap.lastStderr = ((ap.lastStderr ?? "") + chunk).slice(-STDERR_RETAIN_BYTES);
|
|
4252
4111
|
}
|
|
4112
|
+
function describeChildCrash(exitCode, signal, lastStderr) {
|
|
4113
|
+
const how = signal ? `was killed by ${signal}` : typeof exitCode === "number" ? `exited with code ${exitCode}` : "closed its output";
|
|
4114
|
+
const tail = lastStderr?.trim();
|
|
4115
|
+
return `The Claude Code CLI ${how} before finishing this turn (no result was emitted), so the answer above may be incomplete.` + (tail ? `
|
|
4253
4116
|
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
name = "BtwHandledError";
|
|
4257
|
-
constructor(message = "/btw was handled by the claude-code plugin; nothing to add to this conversation.") {
|
|
4258
|
-
super(message);
|
|
4259
|
-
}
|
|
4260
|
-
};
|
|
4261
|
-
var BTW_COMMAND_DESCRIPTION = "Ask a side question in the live Claude Code session without changing its context";
|
|
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);
|
|
4277
|
-
}
|
|
4278
|
-
pendingAnswers.delete(sessionID);
|
|
4279
|
-
while (pendingAnswers.size >= PENDING_ANSWER_CAP) {
|
|
4280
|
-
const oldest = pendingAnswers.keys().next().value;
|
|
4281
|
-
if (oldest === void 0) break;
|
|
4282
|
-
pendingAnswers.delete(oldest);
|
|
4283
|
-
}
|
|
4284
|
-
pendingAnswers.set(sessionID, { question: question.trim(), answer, at: now });
|
|
4285
|
-
}
|
|
4286
|
-
function takeSideQuestionAnswer(sessionID, question, now = Date.now()) {
|
|
4287
|
-
const entry = pendingAnswers.get(sessionID);
|
|
4288
|
-
if (!entry) return void 0;
|
|
4289
|
-
pendingAnswers.delete(sessionID);
|
|
4290
|
-
if (!question.trim().startsWith(entry.question) || now - entry.at > PENDING_ANSWER_TTL_MS) return void 0;
|
|
4291
|
-
return entry.answer;
|
|
4292
|
-
}
|
|
4293
|
-
var INLINE_ASIDE_MARKER = "\u258C **btw:**";
|
|
4294
|
-
var LEGACY_INLINE_ASIDE_MARKERS = ["> **btw:**"];
|
|
4295
|
-
function barEveryLine(text) {
|
|
4296
|
-
return text.split("\n").map((line) => line.trim() === "" ? "\u258C" : `\u258C ${line}`).join("\n");
|
|
4117
|
+
Last stderr from the CLI:
|
|
4118
|
+
${tail}` : "");
|
|
4297
4119
|
}
|
|
4298
|
-
|
|
4299
|
-
|
|
4120
|
+
var activeProcesses = /* @__PURE__ */ new Map();
|
|
4121
|
+
var claudeSessions = /* @__PURE__ */ new Map();
|
|
4122
|
+
var idleEvictionTimers = /* @__PURE__ */ new Map();
|
|
4123
|
+
var MAX_IDLE_TIMEOUT_MS = 2147483647;
|
|
4124
|
+
var DEFAULT_IDLE_PROCESS_TIMEOUT_MS = 0;
|
|
4125
|
+
function resolveIdleProcessTimeoutMs(configured) {
|
|
4126
|
+
return configured === void 0 ? DEFAULT_IDLE_PROCESS_TIMEOUT_MS : configured;
|
|
4300
4127
|
}
|
|
4301
|
-
|
|
4302
|
-
|
|
4128
|
+
var MAX_ACTIVE_PROCESSES = 16;
|
|
4129
|
+
var PROCESS_EXIT_TIMEOUT_MS = 1500;
|
|
4130
|
+
var PROCESS_FORCE_EXIT_TIMEOUT_MS = 500;
|
|
4131
|
+
var CHILD_EXITED_MESSAGE = "Claude CLI subprocess closed before pending tool calls were resolved";
|
|
4132
|
+
function envFlagEnabled(value) {
|
|
4133
|
+
if (value === void 0) return false;
|
|
4134
|
+
const normalized = value.trim().toLowerCase();
|
|
4135
|
+
if (!normalized) return false;
|
|
4136
|
+
return !["0", "false", "no", "off"].includes(normalized);
|
|
4303
4137
|
}
|
|
4304
|
-
function
|
|
4305
|
-
return
|
|
4306
|
-
${asideHeader(question)}
|
|
4307
|
-
\u258C
|
|
4308
|
-
${barEveryLine(answer.trim())}
|
|
4309
|
-
`;
|
|
4138
|
+
function isClaudeThinkingDisabled() {
|
|
4139
|
+
return envFlagEnabled(process.env.CLAUDE_CODE_DISABLE_THINKING) || envFlagEnabled(process.env.CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING);
|
|
4310
4140
|
}
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
return `
|
|
4314
|
-
${asideHeader(question)}
|
|
4315
|
-
\u258C ${INLINE_ASIDE_SENT_NOTE}
|
|
4316
|
-
`;
|
|
4141
|
+
function cliEffortLevel(effort) {
|
|
4142
|
+
return effort === "minimal" ? "low" : effort;
|
|
4317
4143
|
}
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4144
|
+
function claudeSpawnEnv(opts) {
|
|
4145
|
+
const env = {
|
|
4146
|
+
...process.env,
|
|
4147
|
+
TERM: "xterm-256color",
|
|
4148
|
+
// Pin the child to the binary whose version we detected, and keep it off
|
|
4149
|
+
// non-essential network calls. Fills gaps only, so an explicit shell value
|
|
4150
|
+
// survives: see `cliHygieneEnv` for why the version has to hold still.
|
|
4151
|
+
...cliHygieneEnv()
|
|
4323
4152
|
};
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
const sink = asideSinks.get(sessionID);
|
|
4327
|
-
if (!sink) return false;
|
|
4328
|
-
try {
|
|
4329
|
-
return sink(text);
|
|
4330
|
-
} catch (error) {
|
|
4331
|
-
log.debug("btw: could not write the aside into the running turn", { sessionID, error: errorText(error) });
|
|
4332
|
-
return false;
|
|
4153
|
+
if (opts?.effort) {
|
|
4154
|
+
env.CLAUDE_CODE_EFFORT_LEVEL = cliEffortLevel(opts.effort);
|
|
4333
4155
|
}
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
try {
|
|
4337
|
-
void client?.tui?.showToast?.({ body })?.catch((error) => {
|
|
4338
|
-
log.debug("btw toast failed", { error: errorText(error) });
|
|
4339
|
-
});
|
|
4340
|
-
} catch (error) {
|
|
4341
|
-
log.debug("btw toast failed", { error: errorText(error) });
|
|
4156
|
+
if (opts?.promptCacheTtl) {
|
|
4157
|
+
env.CLAUDE_CODE_PROMPT_CACHE_TTL = opts.promptCacheTtl;
|
|
4342
4158
|
}
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
}
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
if (!status) return "unknown";
|
|
4350
|
-
try {
|
|
4351
|
-
const result = await status.call(client.session);
|
|
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";
|
|
4159
|
+
if (opts?.ignoreAnthropicApiKey) {
|
|
4160
|
+
delete env.ANTHROPIC_API_KEY;
|
|
4161
|
+
delete env.ANTHROPIC_AUTH_TOKEN;
|
|
4162
|
+
}
|
|
4163
|
+
if (!isClaudeThinkingDisabled() && process.env.CLAUDE_CODE_SHOW_THINKING_SUMMARIES === void 0) {
|
|
4164
|
+
env.CLAUDE_CODE_SHOW_THINKING_SUMMARIES = "1";
|
|
4357
4165
|
}
|
|
4166
|
+
return env;
|
|
4358
4167
|
}
|
|
4359
|
-
|
|
4360
|
-
const
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
if (options.stop?.()) return true;
|
|
4365
|
-
if (await sessionStatus(client, sessionID) !== "busy") return true;
|
|
4366
|
-
if (Date.now() - started >= timeoutMs) return false;
|
|
4367
|
-
await new Promise((resolve6) => setTimeout(resolve6, pollMs));
|
|
4168
|
+
function touch(key) {
|
|
4169
|
+
const existing = activeProcesses.get(key);
|
|
4170
|
+
if (existing) {
|
|
4171
|
+
activeProcesses.delete(key);
|
|
4172
|
+
activeProcesses.set(key, existing);
|
|
4368
4173
|
}
|
|
4369
4174
|
}
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
await new Promise((resolve6) => setTimeout(resolve6, pollMs));
|
|
4379
|
-
}
|
|
4380
|
-
}
|
|
4381
|
-
async function waitForAsideProcess(client, sessionID, options = {}) {
|
|
4382
|
-
const pollMs = options.pollMs ?? IDLE_POLL_MS;
|
|
4383
|
-
const settleMs = options.settleMs ?? BUSY_SETTLE_MS;
|
|
4384
|
-
const spawnWaitMs = options.spawnWaitMs ?? SPAWN_WAIT_MAX_MS;
|
|
4385
|
-
const started = Date.now();
|
|
4386
|
-
for (; ; ) {
|
|
4387
|
-
const active = findActiveProcessBySessionId(sessionID);
|
|
4388
|
-
if (active) return active;
|
|
4389
|
-
const busy = await sessionStatus(client, sessionID) === "busy";
|
|
4390
|
-
const waitedMs = Date.now() - started;
|
|
4391
|
-
if (!busy && waitedMs >= settleMs) {
|
|
4392
|
-
log.info("btw: no live claude process for session, leaving it to the turn", { sessionID, waitedMs });
|
|
4393
|
-
return void 0;
|
|
4175
|
+
function evictIfNeeded() {
|
|
4176
|
+
while (activeProcesses.size >= MAX_ACTIVE_PROCESSES) {
|
|
4177
|
+
let victimKey;
|
|
4178
|
+
for (const [key, ap] of activeProcesses) {
|
|
4179
|
+
if (!isTurnInFlight(ap)) {
|
|
4180
|
+
victimKey = key;
|
|
4181
|
+
break;
|
|
4182
|
+
}
|
|
4394
4183
|
}
|
|
4395
|
-
if (
|
|
4396
|
-
log.warn("
|
|
4397
|
-
|
|
4184
|
+
if (!victimKey) {
|
|
4185
|
+
log.warn("every claude process is mid-turn; skipping LRU eviction", {
|
|
4186
|
+
active: activeProcesses.size,
|
|
4187
|
+
cap: MAX_ACTIVE_PROCESSES
|
|
4188
|
+
});
|
|
4189
|
+
return;
|
|
4398
4190
|
}
|
|
4399
|
-
|
|
4191
|
+
log.info("evicting LRU claude process", { sessionKey: victimKey });
|
|
4192
|
+
deleteActiveProcess(victimKey);
|
|
4400
4193
|
}
|
|
4401
4194
|
}
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
if (status === "unknown") return isProcessBusy(active);
|
|
4410
|
-
if (Date.now() - started >= settleMs) return false;
|
|
4411
|
-
await new Promise((resolve6) => setTimeout(resolve6, pollMs));
|
|
4195
|
+
var TURN_INTERRUPT_TIMEOUT_MS = 5e3;
|
|
4196
|
+
function isTerminalResultLine(line) {
|
|
4197
|
+
if (!line.includes('"result"')) return false;
|
|
4198
|
+
try {
|
|
4199
|
+
return JSON.parse(line).type === "result";
|
|
4200
|
+
} catch {
|
|
4201
|
+
return false;
|
|
4412
4202
|
}
|
|
4413
4203
|
}
|
|
4414
|
-
function
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
return String(error);
|
|
4204
|
+
function settleTurn(ap) {
|
|
4205
|
+
ap.turnInFlight = false;
|
|
4206
|
+
const waiters = ap.turnIdleWaiters ?? [];
|
|
4207
|
+
ap.turnIdleWaiters = [];
|
|
4208
|
+
for (const wake of waiters) wake();
|
|
4420
4209
|
}
|
|
4421
|
-
function
|
|
4422
|
-
|
|
4210
|
+
function noteTurnStarted(ap) {
|
|
4211
|
+
if (ap.asideTransport?.interactive) return;
|
|
4212
|
+
ap.turnInFlight = true;
|
|
4423
4213
|
}
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
if (
|
|
4427
|
-
try {
|
|
4428
|
-
const result = await messages.call(client.session, { path: { id: sessionID } });
|
|
4429
|
-
const prompt = [];
|
|
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
|
-
}
|
|
4214
|
+
function noteTurnLine(ap, line) {
|
|
4215
|
+
if (!ap.turnInFlight) return;
|
|
4216
|
+
if (isTerminalResultLine(line)) settleTurn(ap);
|
|
4441
4217
|
}
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
if (active) log.info("btw: process has no aside transport, leaving it to the turn", { sessionID: input.sessionID });
|
|
4452
|
-
return;
|
|
4453
|
-
}
|
|
4454
|
-
let busy = false;
|
|
4455
|
-
let inlineDone = false;
|
|
4456
|
-
let markInlineDelivered = () => {
|
|
4457
|
-
};
|
|
4458
|
-
const inlineDelivered = new Promise((resolve6) => {
|
|
4459
|
-
markInlineDelivered = () => {
|
|
4460
|
-
inlineDone = true;
|
|
4461
|
-
resolve6("inline");
|
|
4218
|
+
function isTurnInFlight(ap) {
|
|
4219
|
+
return ap.turnInFlight === true;
|
|
4220
|
+
}
|
|
4221
|
+
function awaitTurnIdle(ap, timeoutMs) {
|
|
4222
|
+
if (!ap.turnInFlight) return Promise.resolve(true);
|
|
4223
|
+
return new Promise((resolve6) => {
|
|
4224
|
+
const wake = () => {
|
|
4225
|
+
clearTimeout(timer);
|
|
4226
|
+
resolve6(true);
|
|
4462
4227
|
};
|
|
4228
|
+
const timer = setTimeout(() => {
|
|
4229
|
+
const waiters = ap.turnIdleWaiters ?? [];
|
|
4230
|
+
const at = waiters.indexOf(wake);
|
|
4231
|
+
if (at >= 0) waiters.splice(at, 1);
|
|
4232
|
+
resolve6(false);
|
|
4233
|
+
}, timeoutMs);
|
|
4234
|
+
(ap.turnIdleWaiters ??= []).push(wake);
|
|
4463
4235
|
});
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
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
|
-
);
|
|
4236
|
+
}
|
|
4237
|
+
function interruptTurn(ap, timeoutMs = TURN_INTERRUPT_TIMEOUT_MS) {
|
|
4238
|
+
if (!ap.turnInFlight) return Promise.resolve(true);
|
|
4239
|
+
const stdin = ap.proc.stdin;
|
|
4240
|
+
if (ap.asideTransport?.interactive || !stdin || !stdin.writable) {
|
|
4241
|
+
log.notice("cannot interrupt this transport; waiting for the turn to end");
|
|
4242
|
+
return awaitTurnIdle(ap, timeoutMs);
|
|
4512
4243
|
}
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
log.
|
|
4523
|
-
|
|
4524
|
-
waitedMs: Date.now() - started
|
|
4244
|
+
try {
|
|
4245
|
+
stdin.write(
|
|
4246
|
+
JSON.stringify({
|
|
4247
|
+
type: "control_request",
|
|
4248
|
+
request_id: randomUUID3(),
|
|
4249
|
+
request: { subtype: "interrupt" }
|
|
4250
|
+
}) + "\n"
|
|
4251
|
+
);
|
|
4252
|
+
} catch (error) {
|
|
4253
|
+
log.warn("failed to write interrupt control request", {
|
|
4254
|
+
error: error instanceof Error ? error.message : String(error)
|
|
4525
4255
|
});
|
|
4526
|
-
|
|
4527
|
-
}
|
|
4528
|
-
log.info("btw: turn over, releasing the /btw message", {
|
|
4529
|
-
sessionID: input.sessionID,
|
|
4530
|
-
idle: outcome === "idle",
|
|
4531
|
-
waitedMs: Date.now() - started
|
|
4532
|
-
});
|
|
4533
|
-
if (outcome === "timeout") {
|
|
4534
|
-
showToast(client, { title: "btw", message: BTW_TURN_TOO_LONG_MESSAGE, variant: "warning", duration: 8e3 });
|
|
4535
|
-
throw new BtwHandledError(BTW_TURN_TOO_LONG_MESSAGE);
|
|
4256
|
+
return Promise.resolve(false);
|
|
4536
4257
|
}
|
|
4258
|
+
return awaitTurnIdle(ap, timeoutMs);
|
|
4537
4259
|
}
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
return PLAN_USAGE_ARGUMENTS.has(argument.trim().toLowerCase());
|
|
4260
|
+
function cancelIdleProcessEviction(key) {
|
|
4261
|
+
const timer = idleEvictionTimers.get(key);
|
|
4262
|
+
if (!timer) return;
|
|
4263
|
+
clearTimeout(timer);
|
|
4264
|
+
idleEvictionTimers.delete(key);
|
|
4544
4265
|
}
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4266
|
+
function getActiveProcess(key) {
|
|
4267
|
+
const ap = activeProcesses.get(key);
|
|
4268
|
+
if (ap) {
|
|
4269
|
+
cancelIdleProcessEviction(key);
|
|
4270
|
+
touch(key);
|
|
4271
|
+
}
|
|
4272
|
+
return ap;
|
|
4549
4273
|
}
|
|
4550
|
-
function
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
if (
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
return { status: "failed", error: text || "the CLI reported an error" };
|
|
4274
|
+
function setActiveProcess(key, ap) {
|
|
4275
|
+
cancelIdleProcessEviction(key);
|
|
4276
|
+
activeProcesses.set(key, ap);
|
|
4277
|
+
}
|
|
4278
|
+
function scheduleIdleProcessEviction(key, timeoutMs) {
|
|
4279
|
+
cancelIdleProcessEviction(key);
|
|
4280
|
+
if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_IDLE_TIMEOUT_MS) {
|
|
4281
|
+
return;
|
|
4282
|
+
}
|
|
4283
|
+
const scheduledProcess = activeProcesses.get(key);
|
|
4284
|
+
if (!scheduledProcess) return;
|
|
4285
|
+
const timer = setTimeout(() => {
|
|
4286
|
+
idleEvictionTimers.delete(key);
|
|
4287
|
+
if (activeProcesses.get(key) !== scheduledProcess) return;
|
|
4288
|
+
if (isTurnInFlight(scheduledProcess)) {
|
|
4289
|
+
log.info("idle timer found a turn in flight; re-arming", { sessionKey: key, timeoutMs });
|
|
4290
|
+
scheduleIdleProcessEviction(key, timeoutMs);
|
|
4291
|
+
return;
|
|
4569
4292
|
}
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4293
|
+
log.info("evicting idle claude process", { sessionKey: key, timeoutMs });
|
|
4294
|
+
deleteActiveProcess(key);
|
|
4295
|
+
}, timeoutMs);
|
|
4296
|
+
timer.unref();
|
|
4297
|
+
idleEvictionTimers.set(key, timer);
|
|
4298
|
+
}
|
|
4299
|
+
function detachActiveProcess(key) {
|
|
4300
|
+
cancelIdleProcessEviction(key);
|
|
4301
|
+
const ap = activeProcesses.get(key);
|
|
4302
|
+
if (!ap) return void 0;
|
|
4303
|
+
activeProcesses.delete(key);
|
|
4304
|
+
if (ap.proxyServer) {
|
|
4305
|
+
void ap.proxyServer.close();
|
|
4306
|
+
rejectAllPendingProxyCallsForSession(key, new Error(SERVER_CLOSED_MESSAGE));
|
|
4307
|
+
}
|
|
4308
|
+
return ap;
|
|
4309
|
+
}
|
|
4310
|
+
function deleteActiveProcessesForSession(sessionID) {
|
|
4311
|
+
if (!sessionID || sessionID === "default") return [];
|
|
4312
|
+
const released = [];
|
|
4313
|
+
for (const [key, ap] of [...activeProcesses]) {
|
|
4314
|
+
const owned = ap.opencodeSessionID === sessionID || describeSessionKey(key).session === sessionID;
|
|
4315
|
+
if (!owned) continue;
|
|
4316
|
+
log.info("releasing claude process for deleted session", { sessionKey: key, sessionID });
|
|
4317
|
+
void deleteActiveProcessAndWait(key);
|
|
4318
|
+
released.push(key);
|
|
4319
|
+
}
|
|
4320
|
+
for (const key of [...claudeSessions.keys()]) {
|
|
4321
|
+
if (describeSessionKey(key).session !== sessionID) continue;
|
|
4322
|
+
deleteClaudeSessionId(key);
|
|
4323
|
+
clearCompression(key);
|
|
4324
|
+
if (!released.includes(key)) released.push(key);
|
|
4325
|
+
}
|
|
4326
|
+
for (const key of released) clearBackgroundTasks(key);
|
|
4327
|
+
return released;
|
|
4328
|
+
}
|
|
4329
|
+
function killAllActiveProcesses() {
|
|
4330
|
+
const keys = [...activeProcesses.keys()];
|
|
4331
|
+
for (const key of keys) deleteActiveProcess(key);
|
|
4332
|
+
for (const key of keys) clearBackgroundTasks(key);
|
|
4333
|
+
return keys;
|
|
4334
|
+
}
|
|
4335
|
+
var processExitCleanupWired = false;
|
|
4336
|
+
function ensureProcessExitCleanup() {
|
|
4337
|
+
if (processExitCleanupWired) return false;
|
|
4338
|
+
processExitCleanupWired = true;
|
|
4339
|
+
process.once("exit", () => {
|
|
4340
|
+
killAllActiveProcesses();
|
|
4341
|
+
});
|
|
4342
|
+
return true;
|
|
4343
|
+
}
|
|
4344
|
+
function deleteActiveProcess(key) {
|
|
4345
|
+
const ap = detachActiveProcess(key);
|
|
4346
|
+
ap?.proc.kill();
|
|
4347
|
+
}
|
|
4348
|
+
function hasProcessExited(proc) {
|
|
4349
|
+
return proc.exitCode !== null || proc.signalCode !== null;
|
|
4350
|
+
}
|
|
4351
|
+
function waitForProcessExit(proc, timeoutMs) {
|
|
4352
|
+
if (hasProcessExited(proc)) return Promise.resolve(true);
|
|
4353
|
+
return new Promise((resolve6) => {
|
|
4354
|
+
const onExit = () => {
|
|
4355
|
+
clearTimeout(timer);
|
|
4356
|
+
resolve6(true);
|
|
4576
4357
|
};
|
|
4358
|
+
const timer = setTimeout(() => {
|
|
4359
|
+
proc.off("exit", onExit);
|
|
4360
|
+
resolve6(hasProcessExited(proc));
|
|
4361
|
+
}, timeoutMs);
|
|
4362
|
+
proc.once("exit", onExit);
|
|
4363
|
+
});
|
|
4364
|
+
}
|
|
4365
|
+
async function deleteActiveProcessAndWait(key, options = {}) {
|
|
4366
|
+
const ap = detachActiveProcess(key);
|
|
4367
|
+
if (!ap || hasProcessExited(ap.proc)) return true;
|
|
4368
|
+
const gracefulExit = waitForProcessExit(
|
|
4369
|
+
ap.proc,
|
|
4370
|
+
options.exitTimeoutMs ?? PROCESS_EXIT_TIMEOUT_MS
|
|
4371
|
+
);
|
|
4372
|
+
ap.proc.kill();
|
|
4373
|
+
if (await gracefulExit) return true;
|
|
4374
|
+
const forcedExit = waitForProcessExit(
|
|
4375
|
+
ap.proc,
|
|
4376
|
+
options.forceExitTimeoutMs ?? PROCESS_FORCE_EXIT_TIMEOUT_MS
|
|
4377
|
+
);
|
|
4378
|
+
ap.proc.kill("SIGKILL");
|
|
4379
|
+
if (await forcedExit) return true;
|
|
4380
|
+
log.warn("claude process did not exit; starting a fresh session", {
|
|
4381
|
+
sessionKey: key
|
|
4382
|
+
});
|
|
4383
|
+
deleteClaudeSessionId(key);
|
|
4384
|
+
return false;
|
|
4385
|
+
}
|
|
4386
|
+
function getClaudeSessionId(key) {
|
|
4387
|
+
return claudeSessions.get(key);
|
|
4388
|
+
}
|
|
4389
|
+
var MAX_CLAUDE_SESSION_ENTRIES = 64;
|
|
4390
|
+
function claudeSessionIsBusy(key) {
|
|
4391
|
+
if (activeProcesses.has(key)) return true;
|
|
4392
|
+
return getPendingProxyCalls(key).length > 0 || hasExitPlanModeQuestions(key);
|
|
4393
|
+
}
|
|
4394
|
+
function capClaudeSessions() {
|
|
4395
|
+
for (const key of [...claudeSessions.keys()]) {
|
|
4396
|
+
if (claudeSessions.size <= MAX_CLAUDE_SESSION_ENTRIES) return;
|
|
4397
|
+
if (claudeSessionIsBusy(key)) continue;
|
|
4398
|
+
log.info("claude session cap reached; releasing an idle session", {
|
|
4399
|
+
sessionKey: key,
|
|
4400
|
+
size: claudeSessions.size,
|
|
4401
|
+
cap: MAX_CLAUDE_SESSION_ENTRIES
|
|
4402
|
+
});
|
|
4403
|
+
deleteClaudeSessionId(key);
|
|
4577
4404
|
}
|
|
4578
|
-
return { status: "failed", error: "no result object in the CLI's reply" };
|
|
4579
4405
|
}
|
|
4580
|
-
function
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4406
|
+
function setClaudeSessionId(key, sessionId) {
|
|
4407
|
+
claudeSessions.delete(key);
|
|
4408
|
+
claudeSessions.set(key, sessionId);
|
|
4409
|
+
capClaudeSessions();
|
|
4410
|
+
}
|
|
4411
|
+
function deleteClaudeSessionId(key) {
|
|
4412
|
+
clearExitPlanModeQuestions(key);
|
|
4413
|
+
clearAccountFailoverQuestions(key);
|
|
4414
|
+
const claudeSessionId = claudeSessions.get(key);
|
|
4415
|
+
if (claudeSessionId) clearLedger(claudeSessionId);
|
|
4416
|
+
claudeSessions.delete(key);
|
|
4417
|
+
}
|
|
4418
|
+
function effortSessionKey(baseKey, effort) {
|
|
4419
|
+
return effort ? `${baseKey}::effort=${effort}` : baseKey;
|
|
4420
|
+
}
|
|
4421
|
+
function invalidateOtherEffortSessions(baseKey, effort) {
|
|
4422
|
+
const levels = [
|
|
4423
|
+
void 0,
|
|
4424
|
+
"minimal",
|
|
4425
|
+
"low",
|
|
4426
|
+
"medium",
|
|
4427
|
+
"high",
|
|
4428
|
+
"xhigh",
|
|
4429
|
+
"max"
|
|
4430
|
+
];
|
|
4431
|
+
const staleKeys = levels.filter((level) => level !== effort).map((level) => effortSessionKey(baseKey, level));
|
|
4432
|
+
for (const key of staleKeys) {
|
|
4433
|
+
const active = activeProcesses.get(key);
|
|
4434
|
+
if (getPendingProxyCalls(key).length || hasExitPlanModeQuestions(key) || active?.pendingProxyCompletions?.size || active && (active.lineEmitter.listenerCount("line") > 0 || isSideQuestionPending(active))) {
|
|
4435
|
+
throw new Error(
|
|
4436
|
+
"Cannot change reasoning effort while the previous effort session has pending work. Finish that work at its original effort first."
|
|
4437
|
+
);
|
|
4438
|
+
}
|
|
4439
|
+
}
|
|
4440
|
+
for (const key of staleKeys) {
|
|
4441
|
+
deleteActiveProcess(key);
|
|
4442
|
+
deleteClaudeSessionId(key);
|
|
4443
|
+
clearCompression(key);
|
|
4444
|
+
clearBackgroundTasks(key);
|
|
4445
|
+
}
|
|
4446
|
+
}
|
|
4447
|
+
function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcpHash, systemPromptFile, ignoreAnthropicApiKey, effort, promptCacheTtl) {
|
|
4448
|
+
evictIfNeeded();
|
|
4449
|
+
log.info("spawning new claude process", {
|
|
4450
|
+
cliPath,
|
|
4451
|
+
cliArgs,
|
|
4452
|
+
cwd,
|
|
4453
|
+
sessionKey: sessionKey2,
|
|
4454
|
+
effort,
|
|
4455
|
+
promptCacheTtl
|
|
4456
|
+
});
|
|
4457
|
+
const proc = spawn(cliPath, cliArgs, {
|
|
4458
|
+
cwd,
|
|
4459
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
4460
|
+
env: claudeSpawnEnv({ ignoreAnthropicApiKey, effort, promptCacheTtl }),
|
|
4461
|
+
shell: process.platform === "win32"
|
|
4462
|
+
});
|
|
4463
|
+
const lineEmitter = new EventEmitter3();
|
|
4464
|
+
const ap = {
|
|
4465
|
+
proc,
|
|
4466
|
+
lineEmitter,
|
|
4467
|
+
proxyServer: proxyServer ?? null,
|
|
4468
|
+
mcpHash,
|
|
4469
|
+
systemPromptFile,
|
|
4470
|
+
effort,
|
|
4471
|
+
promptCacheTtl,
|
|
4472
|
+
startedAt: Date.now(),
|
|
4473
|
+
cliPath,
|
|
4474
|
+
cliArgs: [...cliArgs],
|
|
4475
|
+
unattendedLines: [],
|
|
4476
|
+
unattendedDropped: 0
|
|
4477
|
+
};
|
|
4478
|
+
const rl = createInterface({ input: proc.stdout });
|
|
4479
|
+
rl.on("line", (line) => {
|
|
4480
|
+
if (dispatchSideQuestionResponse(ap, line)) return;
|
|
4481
|
+
noteTurnLine(ap, line);
|
|
4482
|
+
if (lineEmitter.listenerCount("line") === 0) {
|
|
4483
|
+
bufferUnattendedLine(ap, line);
|
|
4484
|
+
return;
|
|
4485
|
+
}
|
|
4486
|
+
lineEmitter.emit("line", line);
|
|
4487
|
+
});
|
|
4488
|
+
rl.on("close", () => {
|
|
4489
|
+
settleTurn(ap);
|
|
4490
|
+
lineEmitter.emit("close");
|
|
4491
|
+
});
|
|
4492
|
+
cancelIdleProcessEviction(sessionKey2);
|
|
4493
|
+
activeProcesses.set(sessionKey2, ap);
|
|
4494
|
+
proc.on("error", (err) => {
|
|
4495
|
+
log.error("claude process error", { sessionKey: sessionKey2, error: err.message });
|
|
4496
|
+
});
|
|
4497
|
+
proc.stdin?.on("error", (err) => {
|
|
4498
|
+
log.warn("claude process stdin error", {
|
|
4499
|
+
sessionKey: sessionKey2,
|
|
4500
|
+
code: err.code,
|
|
4501
|
+
error: err.message
|
|
4502
|
+
});
|
|
4503
|
+
settleTurn(ap);
|
|
4504
|
+
});
|
|
4505
|
+
proc.on("exit", (code, signal) => {
|
|
4506
|
+
log.info("claude process exited", { code, signal, sessionKey: sessionKey2 });
|
|
4507
|
+
void proxyServer?.close();
|
|
4508
|
+
if (systemPromptFile) {
|
|
4509
|
+
void unlink(systemPromptFile).catch(() => {
|
|
4510
|
+
});
|
|
4511
|
+
}
|
|
4512
|
+
const ownsSessionKey = activeProcesses.get(sessionKey2) === ap;
|
|
4513
|
+
if (ownsSessionKey) {
|
|
4514
|
+
cancelIdleProcessEviction(sessionKey2);
|
|
4515
|
+
activeProcesses.delete(sessionKey2);
|
|
4516
|
+
if (getPendingProxyCalls(sessionKey2).length > 0) {
|
|
4517
|
+
rejectAllPendingProxyCallsForSession(
|
|
4518
|
+
sessionKey2,
|
|
4519
|
+
new Error(CHILD_EXITED_MESSAGE)
|
|
4520
|
+
);
|
|
4592
4521
|
}
|
|
4593
|
-
|
|
4522
|
+
}
|
|
4523
|
+
if (ownsSessionKey && code !== 0 && code !== null) {
|
|
4524
|
+
log.info("process exited with error, clearing session", {
|
|
4525
|
+
code,
|
|
4526
|
+
sessionKey: sessionKey2
|
|
4527
|
+
});
|
|
4528
|
+
claudeSessions.delete(sessionKey2);
|
|
4529
|
+
}
|
|
4530
|
+
});
|
|
4531
|
+
proc.stderr?.on("data", (data) => {
|
|
4532
|
+
const stderr = data.toString();
|
|
4533
|
+
log.debug("stderr", { data: stderr.slice(0, 200) });
|
|
4534
|
+
retainStderr(ap, stderr);
|
|
4535
|
+
reportUnrecognizedModel(stderr);
|
|
4536
|
+
if (stderr.includes("No conversation found") || stderr.includes("Session ID") && (stderr.includes("already in use") || stderr.includes("not found") || stderr.includes("invalid"))) {
|
|
4537
|
+
if (activeProcesses.get(sessionKey2) === ap) {
|
|
4538
|
+
log.warn("claude session ID error, clearing session", {
|
|
4539
|
+
sessionKey: sessionKey2,
|
|
4540
|
+
error: stderr.slice(0, 200)
|
|
4541
|
+
});
|
|
4542
|
+
claudeSessions.delete(sessionKey2);
|
|
4543
|
+
} else {
|
|
4544
|
+
log.debug("ignoring session ID error from stale claude process", {
|
|
4545
|
+
sessionKey: sessionKey2
|
|
4546
|
+
});
|
|
4547
|
+
}
|
|
4548
|
+
}
|
|
4594
4549
|
});
|
|
4550
|
+
return ap;
|
|
4595
4551
|
}
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4552
|
+
function appendResumeIfNeeded(sessionKey2, cliArgs) {
|
|
4553
|
+
if (cliArgs.includes("--resume") || cliArgs.includes("--session-id")) {
|
|
4554
|
+
return cliArgs;
|
|
4555
|
+
}
|
|
4556
|
+
const sid = claudeSessions.get(sessionKey2);
|
|
4557
|
+
if (!sid) return cliArgs;
|
|
4558
|
+
return [...cliArgs, "--resume", sid];
|
|
4559
|
+
}
|
|
4560
|
+
function respawnActiveProcess(sessionKey2, cliPath, cliArgs, cwd, ignoreAnthropicApiKey) {
|
|
4561
|
+
const old = activeProcesses.get(sessionKey2);
|
|
4562
|
+
if (!old) return void 0;
|
|
4563
|
+
const turnWasInFlight = isTurnInFlight(old);
|
|
4564
|
+
activeProcesses.delete(sessionKey2);
|
|
4565
|
+
old.proc.removeAllListeners("exit");
|
|
4599
4566
|
try {
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4567
|
+
old.proc.kill();
|
|
4568
|
+
} catch {
|
|
4569
|
+
}
|
|
4570
|
+
const replacement = spawnClaudeProcess(
|
|
4571
|
+
cliPath,
|
|
4572
|
+
appendResumeIfNeeded(sessionKey2, old.cliArgs ?? cliArgs),
|
|
4573
|
+
cwd,
|
|
4574
|
+
sessionKey2,
|
|
4575
|
+
old.proxyServer,
|
|
4576
|
+
old.mcpHash,
|
|
4577
|
+
old.systemPromptFile,
|
|
4578
|
+
ignoreAnthropicApiKey,
|
|
4579
|
+
old.effort,
|
|
4580
|
+
old.promptCacheTtl
|
|
4581
|
+
);
|
|
4582
|
+
replacement.mcpServers = old.mcpServers;
|
|
4583
|
+
replacement.pendingProxyCompletions = old.pendingProxyCompletions;
|
|
4584
|
+
delete old.pendingProxyCompletions;
|
|
4585
|
+
if (turnWasInFlight) noteTurnStarted(replacement);
|
|
4586
|
+
return replacement;
|
|
4587
|
+
}
|
|
4588
|
+
function buildCliArgs(opts) {
|
|
4589
|
+
const {
|
|
4590
|
+
sessionKey: sessionKey2,
|
|
4591
|
+
skipPermissions,
|
|
4592
|
+
includeSessionId = true,
|
|
4593
|
+
model,
|
|
4594
|
+
permissionMode,
|
|
4595
|
+
mcpConfig,
|
|
4596
|
+
strictMcpConfig,
|
|
4597
|
+
disallowedTools,
|
|
4598
|
+
appendSystemPromptFile,
|
|
4599
|
+
pluginDirs,
|
|
4600
|
+
thinking,
|
|
4601
|
+
thinkingDisplay,
|
|
4602
|
+
fastMode,
|
|
4603
|
+
cliVersion
|
|
4604
|
+
} = opts;
|
|
4605
|
+
const args = [
|
|
4606
|
+
"--print",
|
|
4607
|
+
"--output-format",
|
|
4608
|
+
"stream-json",
|
|
4609
|
+
"--input-format",
|
|
4610
|
+
"stream-json",
|
|
4611
|
+
"--include-partial-messages",
|
|
4612
|
+
"--verbose"
|
|
4613
|
+
];
|
|
4614
|
+
if (model) {
|
|
4615
|
+
args.push("--model", model);
|
|
4616
|
+
}
|
|
4617
|
+
const readOnly = isReadOnlyPermissionMode(permissionMode);
|
|
4618
|
+
if (readOnly) {
|
|
4619
|
+
if (cliSupportsRestricted(cliVersion ?? null)) {
|
|
4620
|
+
args.push("--restricted");
|
|
4621
|
+
} else {
|
|
4622
|
+
log.warn(
|
|
4623
|
+
'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.',
|
|
4624
|
+
{ cliVersion: cliVersion?.raw ?? "unknown" }
|
|
4625
|
+
);
|
|
4626
|
+
}
|
|
4627
|
+
if (cliSupportsPermissionPrompts(cliVersion ?? null)) {
|
|
4628
|
+
args.push("--permission-prompts", "none");
|
|
4629
|
+
} else {
|
|
4630
|
+
log.warn(
|
|
4631
|
+
`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.`,
|
|
4632
|
+
{ cliVersion: cliVersion?.raw ?? "unknown" }
|
|
4633
|
+
);
|
|
4612
4634
|
}
|
|
4613
|
-
|
|
4614
|
-
|
|
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 };
|
|
4618
|
-
}
|
|
4619
|
-
}
|
|
4620
|
-
|
|
4621
|
-
// src/startup-diagnostics.ts
|
|
4622
|
-
import { execFile as execFile3 } from "child_process";
|
|
4623
|
-
import * as fs4 from "fs";
|
|
4624
|
-
import * as path5 from "path";
|
|
4625
|
-
import { promisify as promisify2 } from "util";
|
|
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) {
|
|
4640
|
-
try {
|
|
4641
|
-
return fs3.statSync(p).isFile();
|
|
4642
|
-
} catch {
|
|
4643
|
-
return false;
|
|
4635
|
+
} else if (permissionMode) {
|
|
4636
|
+
args.push("--permission-mode", permissionMode);
|
|
4644
4637
|
}
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
return false;
|
|
4638
|
+
if (includeSessionId) {
|
|
4639
|
+
const sessionId = claudeSessions.get(sessionKey2);
|
|
4640
|
+
if (sessionId && !activeProcesses.has(sessionKey2)) {
|
|
4641
|
+
args.push("--resume", sessionId);
|
|
4642
|
+
}
|
|
4651
4643
|
}
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
const parsed = parseJsonc(raw, errors, { allowTrailingComma: true });
|
|
4658
|
-
if (errors.length > 0) {
|
|
4659
|
-
const first = errors[0];
|
|
4660
|
-
throw new Error(
|
|
4661
|
-
`${printParseErrorCode(first.error)} at offset ${first.offset}`
|
|
4662
|
-
);
|
|
4644
|
+
if (mcpConfig) {
|
|
4645
|
+
const configs = Array.isArray(mcpConfig) ? mcpConfig : [mcpConfig];
|
|
4646
|
+
const filtered = configs.filter((c) => typeof c === "string" && c.length > 0);
|
|
4647
|
+
if (filtered.length > 0) {
|
|
4648
|
+
args.push("--mcp-config", ...filtered);
|
|
4663
4649
|
}
|
|
4664
|
-
return parsed;
|
|
4665
|
-
} catch (e) {
|
|
4666
|
-
log.warn("failed to parse opencode config", {
|
|
4667
|
-
file,
|
|
4668
|
-
error: e instanceof Error ? e.message : String(e)
|
|
4669
|
-
});
|
|
4670
|
-
return null;
|
|
4671
4650
|
}
|
|
4651
|
+
if (strictMcpConfig) {
|
|
4652
|
+
args.push("--strict-mcp-config");
|
|
4653
|
+
}
|
|
4654
|
+
if (disallowedTools && disallowedTools.length > 0) {
|
|
4655
|
+
args.push("--disallowedTools", ...disallowedTools);
|
|
4656
|
+
}
|
|
4657
|
+
if (thinking && cliSupportsThinking(cliVersion ?? null)) {
|
|
4658
|
+
args.push("--thinking", thinking);
|
|
4659
|
+
}
|
|
4660
|
+
if (thinkingDisplay && cliSupportsThinkingDisplay(cliVersion ?? null)) {
|
|
4661
|
+
args.push("--thinking-display", thinkingDisplay);
|
|
4662
|
+
}
|
|
4663
|
+
if (appendSystemPromptFile) {
|
|
4664
|
+
args.push("--append-system-prompt-file", appendSystemPromptFile);
|
|
4665
|
+
}
|
|
4666
|
+
for (const dir of pluginDirs ?? []) {
|
|
4667
|
+
args.push("--plugin-dir", dir);
|
|
4668
|
+
}
|
|
4669
|
+
if (fastMode && cliSupportsFastMode(cliVersion ?? null)) {
|
|
4670
|
+
args.push("--settings", JSON.stringify({ fastMode: true }));
|
|
4671
|
+
}
|
|
4672
|
+
if (skipPermissions && permissionMode !== "plan" && !readOnly) {
|
|
4673
|
+
args.push("--dangerously-skip-permissions");
|
|
4674
|
+
}
|
|
4675
|
+
return args;
|
|
4672
4676
|
}
|
|
4673
|
-
function
|
|
4674
|
-
return
|
|
4677
|
+
function sessionKey(cwd, modelId) {
|
|
4678
|
+
return `${cwd}::${modelId}`;
|
|
4675
4679
|
}
|
|
4676
|
-
function
|
|
4677
|
-
const
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
out[k] = v;
|
|
4685
|
-
}
|
|
4686
|
-
}
|
|
4687
|
-
return out;
|
|
4680
|
+
function describeSessionKey(key) {
|
|
4681
|
+
const parts = key.split("::");
|
|
4682
|
+
return {
|
|
4683
|
+
cwd: parts[0] ?? "unknown",
|
|
4684
|
+
model: parts[1] ?? "unknown",
|
|
4685
|
+
session: parts[3] ?? "unknown",
|
|
4686
|
+
compaction: parts[2] === "compaction"
|
|
4687
|
+
};
|
|
4688
4688
|
}
|
|
4689
|
-
function
|
|
4689
|
+
function snapshotActiveProcesses(now = Date.now()) {
|
|
4690
4690
|
const out = [];
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4691
|
+
for (const [key, ap] of activeProcesses) {
|
|
4692
|
+
const described = describeSessionKey(key);
|
|
4693
|
+
const lastStderr = ap.lastStderr;
|
|
4694
|
+
out.push({
|
|
4695
|
+
sessionKey: key,
|
|
4696
|
+
session: ap.opencodeSessionID ?? described.session,
|
|
4697
|
+
model: described.model,
|
|
4698
|
+
compaction: described.compaction,
|
|
4699
|
+
pid: ap.proc.pid,
|
|
4700
|
+
inFlight: ap.turnInFlight === true,
|
|
4701
|
+
ageMs: ap.startedAt === void 0 ? void 0 : Math.max(0, now - ap.startedAt),
|
|
4702
|
+
effort: ap.effort,
|
|
4703
|
+
attached: ap.lineEmitter.listenerCount("line") > 0,
|
|
4704
|
+
proxyUrl: ap.proxyServer?.url,
|
|
4705
|
+
lastStderr: typeof lastStderr === "string" ? lastStderr : void 0
|
|
4706
|
+
});
|
|
4701
4707
|
}
|
|
4702
4708
|
return out;
|
|
4703
4709
|
}
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4710
|
+
|
|
4711
|
+
// src/btw-command.ts
|
|
4712
|
+
var BtwHandledError = class extends Error {
|
|
4713
|
+
name = "BtwHandledError";
|
|
4714
|
+
constructor(message = "/btw was handled by the claude-code plugin; nothing to add to this conversation.") {
|
|
4715
|
+
super(message);
|
|
4716
|
+
}
|
|
4717
|
+
};
|
|
4718
|
+
var BTW_COMMAND_DESCRIPTION = "Ask a side question in the live Claude Code session without changing its context";
|
|
4719
|
+
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.";
|
|
4720
|
+
var BTW_INLINE_HANDLED_MESSAGE = "/btw was answered inside the running turn; nothing to add to this conversation.";
|
|
4721
|
+
var BTW_TURN_TOO_LONG_MESSAGE = "/btw gave up waiting for this turn to end. Ask again once it is over.";
|
|
4722
|
+
var IDLE_POLL_MS = 500;
|
|
4723
|
+
var IDLE_WAIT_MAX_MS = 30 * 6e4;
|
|
4724
|
+
var BUSY_SETTLE_MS = 1500;
|
|
4725
|
+
var SPAWN_WAIT_MAX_MS = 3e4;
|
|
4726
|
+
var INLINE_POLL_MS = 200;
|
|
4727
|
+
var INLINE_WAIT_MAX_MS = 2e4;
|
|
4728
|
+
var PENDING_ANSWER_TTL_MS = 10 * 6e4;
|
|
4729
|
+
var PENDING_ANSWER_CAP = 32;
|
|
4730
|
+
var pendingAnswers = /* @__PURE__ */ new Map();
|
|
4731
|
+
function rememberSideQuestionAnswer(sessionID, question, answer, now = Date.now()) {
|
|
4732
|
+
for (const [id, entry] of pendingAnswers) {
|
|
4733
|
+
if (now - entry.at > PENDING_ANSWER_TTL_MS) pendingAnswers.delete(id);
|
|
4734
|
+
}
|
|
4735
|
+
pendingAnswers.delete(sessionID);
|
|
4736
|
+
while (pendingAnswers.size >= PENDING_ANSWER_CAP) {
|
|
4737
|
+
const oldest = pendingAnswers.keys().next().value;
|
|
4738
|
+
if (oldest === void 0) break;
|
|
4739
|
+
pendingAnswers.delete(oldest);
|
|
4717
4740
|
}
|
|
4741
|
+
pendingAnswers.set(sessionID, { question: question.trim(), answer, at: now });
|
|
4718
4742
|
}
|
|
4719
|
-
function
|
|
4720
|
-
const
|
|
4721
|
-
|
|
4743
|
+
function takeSideQuestionAnswer(sessionID, question, now = Date.now()) {
|
|
4744
|
+
const entry = pendingAnswers.get(sessionID);
|
|
4745
|
+
if (!entry) return void 0;
|
|
4746
|
+
pendingAnswers.delete(sessionID);
|
|
4747
|
+
if (!question.trim().startsWith(entry.question) || now - entry.at > PENDING_ANSWER_TTL_MS) return void 0;
|
|
4748
|
+
return entry.answer;
|
|
4722
4749
|
}
|
|
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;
|
|
4750
|
+
var INLINE_ASIDE_MARKER = "\u258C **btw:**";
|
|
4751
|
+
var LEGACY_INLINE_ASIDE_MARKERS = ["> **btw:**"];
|
|
4752
|
+
function barEveryLine(text) {
|
|
4753
|
+
return text.split("\n").map((line) => line.trim() === "" ? "\u258C" : `\u258C ${line}`).join("\n");
|
|
4733
4754
|
}
|
|
4734
|
-
function
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4755
|
+
function oneLine(question) {
|
|
4756
|
+
return question.replace(/\s+/g, " ").trim();
|
|
4757
|
+
}
|
|
4758
|
+
function asideHeader(question) {
|
|
4759
|
+
return `${INLINE_ASIDE_MARKER} ${oneLine(question)}`;
|
|
4760
|
+
}
|
|
4761
|
+
function formatInlineAside(question, answer) {
|
|
4762
|
+
return `
|
|
4763
|
+
${asideHeader(question)}
|
|
4764
|
+
\u258C
|
|
4765
|
+
${barEveryLine(answer.trim())}
|
|
4766
|
+
`;
|
|
4767
|
+
}
|
|
4768
|
+
var INLINE_ASIDE_SENT_NOTE = "*sent to Claude on the side*";
|
|
4769
|
+
function formatInlineAsideAsk(question) {
|
|
4770
|
+
return `
|
|
4771
|
+
${asideHeader(question)}
|
|
4772
|
+
\u258C ${INLINE_ASIDE_SENT_NOTE}
|
|
4773
|
+
`;
|
|
4774
|
+
}
|
|
4775
|
+
var asideSinks = /* @__PURE__ */ new Map();
|
|
4776
|
+
function registerAsideSink(sessionID, sink) {
|
|
4777
|
+
asideSinks.set(sessionID, sink);
|
|
4778
|
+
return () => {
|
|
4779
|
+
if (asideSinks.get(sessionID) === sink) asideSinks.delete(sessionID);
|
|
4780
|
+
};
|
|
4781
|
+
}
|
|
4782
|
+
function emitAsideInline(sessionID, text) {
|
|
4783
|
+
const sink = asideSinks.get(sessionID);
|
|
4784
|
+
if (!sink) return false;
|
|
4785
|
+
try {
|
|
4786
|
+
return sink(text);
|
|
4787
|
+
} catch (error) {
|
|
4788
|
+
log.debug("btw: could not write the aside into the running turn", { sessionID, error: errorText(error) });
|
|
4789
|
+
return false;
|
|
4741
4790
|
}
|
|
4742
|
-
return merged;
|
|
4743
4791
|
}
|
|
4744
|
-
function
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
dirs.push(abs);
|
|
4752
|
-
}
|
|
4753
|
-
};
|
|
4754
|
-
for (const dir of walkUp({
|
|
4755
|
-
start: cwd,
|
|
4756
|
-
stop: worktree,
|
|
4757
|
-
targets: [".opencode"],
|
|
4758
|
-
predicate: dirExists
|
|
4759
|
-
})) {
|
|
4760
|
-
push(dir);
|
|
4792
|
+
function showToast(client, body) {
|
|
4793
|
+
try {
|
|
4794
|
+
void client?.tui?.showToast?.({ body })?.catch((error) => {
|
|
4795
|
+
log.debug("btw toast failed", { error: errorText(error) });
|
|
4796
|
+
});
|
|
4797
|
+
} catch (error) {
|
|
4798
|
+
log.debug("btw toast failed", { error: errorText(error) });
|
|
4761
4799
|
}
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4800
|
+
}
|
|
4801
|
+
function isProcessBusy(active) {
|
|
4802
|
+
return active.lineEmitter.listenerCount("line") > 0;
|
|
4803
|
+
}
|
|
4804
|
+
async function sessionStatus(client, sessionID) {
|
|
4805
|
+
const status = client?.session?.status;
|
|
4806
|
+
if (!status) return "unknown";
|
|
4807
|
+
try {
|
|
4808
|
+
const result = await status.call(client.session);
|
|
4809
|
+
const entry = result.data?.[sessionID];
|
|
4810
|
+
return entry && entry.type !== "idle" ? "busy" : "idle";
|
|
4811
|
+
} catch (error) {
|
|
4812
|
+
log.debug("btw: could not read session status", { sessionID, error: errorText(error) });
|
|
4813
|
+
return "unknown";
|
|
4766
4814
|
}
|
|
4767
|
-
const envDir = process.env.OPENCODE_CONFIG_DIR;
|
|
4768
|
-
if (envDir && dirExists(envDir)) push(envDir);
|
|
4769
|
-
return dirs;
|
|
4770
4815
|
}
|
|
4771
|
-
function
|
|
4772
|
-
const
|
|
4773
|
-
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4816
|
+
async function waitForSessionIdle(client, sessionID, options = {}) {
|
|
4817
|
+
const pollMs = options.pollMs ?? IDLE_POLL_MS;
|
|
4818
|
+
const timeoutMs = options.timeoutMs ?? IDLE_WAIT_MAX_MS;
|
|
4819
|
+
const started = Date.now();
|
|
4820
|
+
for (; ; ) {
|
|
4821
|
+
if (options.stop?.()) return true;
|
|
4822
|
+
if (await sessionStatus(client, sessionID) !== "busy") return true;
|
|
4823
|
+
if (Date.now() - started >= timeoutMs) return false;
|
|
4824
|
+
await new Promise((resolve6) => setTimeout(resolve6, pollMs));
|
|
4779
4825
|
}
|
|
4780
|
-
return out;
|
|
4781
4826
|
}
|
|
4782
|
-
function
|
|
4783
|
-
|
|
4784
|
-
const
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
if (
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
const out = {
|
|
4792
|
-
type: "stdio",
|
|
4793
|
-
command: String(cmd[0])
|
|
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;
|
|
4827
|
+
async function deliverAsideInline(client, sessionID, text, options = {}) {
|
|
4828
|
+
const pollMs = options.inlinePollMs ?? INLINE_POLL_MS;
|
|
4829
|
+
const timeoutMs = options.inlineWaitMs ?? INLINE_WAIT_MAX_MS;
|
|
4830
|
+
const started = Date.now();
|
|
4831
|
+
for (; ; ) {
|
|
4832
|
+
if (emitAsideInline(sessionID, text)) return true;
|
|
4833
|
+
if (Date.now() - started >= timeoutMs) return false;
|
|
4834
|
+
if (await sessionStatus(client, sessionID) !== "busy") return false;
|
|
4835
|
+
await new Promise((resolve6) => setTimeout(resolve6, pollMs));
|
|
4802
4836
|
}
|
|
4803
|
-
|
|
4804
|
-
|
|
4805
|
-
|
|
4806
|
-
|
|
4837
|
+
}
|
|
4838
|
+
async function waitForAsideProcess(client, sessionID, options = {}) {
|
|
4839
|
+
const pollMs = options.pollMs ?? IDLE_POLL_MS;
|
|
4840
|
+
const settleMs = options.settleMs ?? BUSY_SETTLE_MS;
|
|
4841
|
+
const spawnWaitMs = options.spawnWaitMs ?? SPAWN_WAIT_MAX_MS;
|
|
4842
|
+
const started = Date.now();
|
|
4843
|
+
for (; ; ) {
|
|
4844
|
+
const active = findActiveProcessBySessionId(sessionID);
|
|
4845
|
+
if (active) return active;
|
|
4846
|
+
const busy = await sessionStatus(client, sessionID) === "busy";
|
|
4847
|
+
const waitedMs = Date.now() - started;
|
|
4848
|
+
if (!busy && waitedMs >= settleMs) {
|
|
4849
|
+
log.info("btw: no live claude process for session, leaving it to the turn", { sessionID, waitedMs });
|
|
4850
|
+
return void 0;
|
|
4807
4851
|
}
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
};
|
|
4812
|
-
if (spec.headers && typeof spec.headers === "object") {
|
|
4813
|
-
out.headers = substituteEnvPlaceholders(
|
|
4814
|
-
spec.headers
|
|
4815
|
-
);
|
|
4852
|
+
if (busy && waitedMs >= spawnWaitMs) {
|
|
4853
|
+
log.warn("btw: a turn is running but no claude process appeared for it", { sessionID, waitedMs });
|
|
4854
|
+
return void 0;
|
|
4816
4855
|
}
|
|
4817
|
-
|
|
4856
|
+
await new Promise((resolve6) => setTimeout(resolve6, pollMs));
|
|
4818
4857
|
}
|
|
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
4858
|
}
|
|
4841
|
-
function
|
|
4842
|
-
const
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4859
|
+
async function settleSessionBusy(client, sessionID, active, options = {}) {
|
|
4860
|
+
const pollMs = options.pollMs ?? IDLE_POLL_MS;
|
|
4861
|
+
const settleMs = options.settleMs ?? BUSY_SETTLE_MS;
|
|
4862
|
+
const started = Date.now();
|
|
4863
|
+
for (; ; ) {
|
|
4864
|
+
const status = await sessionStatus(client, sessionID);
|
|
4865
|
+
if (status === "busy") return true;
|
|
4866
|
+
if (status === "unknown") return isProcessBusy(active);
|
|
4867
|
+
if (Date.now() - started >= settleMs) return false;
|
|
4868
|
+
await new Promise((resolve6) => setTimeout(resolve6, pollMs));
|
|
4848
4869
|
}
|
|
4849
|
-
return merged;
|
|
4850
4870
|
}
|
|
4851
|
-
function
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
const existing = out[name];
|
|
4856
|
-
if (existing && typeof existing === "object") {
|
|
4857
|
-
out[name] = deepMerge(
|
|
4858
|
-
existing,
|
|
4859
|
-
spec
|
|
4860
|
-
);
|
|
4861
|
-
} else {
|
|
4862
|
-
out[name] = spec;
|
|
4863
|
-
}
|
|
4871
|
+
function errorText(error) {
|
|
4872
|
+
if (error instanceof Error) return error.message;
|
|
4873
|
+
if (error && typeof error === "object" && "message" in error && typeof error.message === "string") {
|
|
4874
|
+
return error.message;
|
|
4864
4875
|
}
|
|
4865
|
-
return
|
|
4876
|
+
return String(error);
|
|
4866
4877
|
}
|
|
4867
|
-
function
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
servers[name] = translated;
|
|
4881
|
-
bridgedServerNames.push(name);
|
|
4878
|
+
function isTextPart(part) {
|
|
4879
|
+
return part !== null && typeof part === "object" && part.type === "text" && typeof part.text === "string";
|
|
4880
|
+
}
|
|
4881
|
+
async function fetchAsideHistory(client, sessionID, question) {
|
|
4882
|
+
const messages = client?.session?.messages;
|
|
4883
|
+
if (!messages) return [];
|
|
4884
|
+
try {
|
|
4885
|
+
const result = await messages.call(client.session, { path: { id: sessionID } });
|
|
4886
|
+
const prompt = [];
|
|
4887
|
+
for (const message of result.data ?? []) {
|
|
4888
|
+
const role = message.info?.role;
|
|
4889
|
+
if (role !== "user" && role !== "assistant") continue;
|
|
4890
|
+
prompt.push({ role, content: (message.parts ?? []).filter(isTextPart) });
|
|
4882
4891
|
}
|
|
4892
|
+
prompt.push({ role: "user", content: `/btw ${question}` });
|
|
4893
|
+
return collectSideQuestionHistory(prompt);
|
|
4894
|
+
} catch (error) {
|
|
4895
|
+
log.debug("btw: could not read aside history", { sessionID, error: errorText(error) });
|
|
4896
|
+
return [];
|
|
4883
4897
|
}
|
|
4884
|
-
return finishBridge({
|
|
4885
|
-
servers,
|
|
4886
|
-
bridgedServerNames,
|
|
4887
|
-
allEnabledServerNames,
|
|
4888
|
-
hash,
|
|
4889
|
-
excludeServers
|
|
4890
|
-
});
|
|
4891
4898
|
}
|
|
4892
|
-
function
|
|
4893
|
-
const
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
if (explicitConfig && fileExists(explicitConfig)) {
|
|
4898
|
-
const parsed = readAndParse(explicitConfig);
|
|
4899
|
-
if (parsed) layers.push(parsed);
|
|
4899
|
+
async function handleBtwCommand(client, input, options = {}) {
|
|
4900
|
+
const question = input.arguments.trim();
|
|
4901
|
+
if (!question) {
|
|
4902
|
+
showToast(client, { title: "btw", message: SIDE_QUESTION_USAGE, variant: "warning", duration: 6e3 });
|
|
4903
|
+
throw new BtwHandledError("/btw needs a question.");
|
|
4900
4904
|
}
|
|
4901
|
-
const
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4905
|
+
const active = await waitForAsideProcess(client, input.sessionID, options);
|
|
4906
|
+
const transport = active?.asideTransport;
|
|
4907
|
+
if (!active || !transport) {
|
|
4908
|
+
if (active) log.info("btw: process has no aside transport, leaving it to the turn", { sessionID: input.sessionID });
|
|
4909
|
+
return;
|
|
4910
|
+
}
|
|
4911
|
+
let busy = false;
|
|
4912
|
+
let inlineDone = false;
|
|
4913
|
+
let markInlineDelivered = () => {
|
|
4914
|
+
};
|
|
4915
|
+
const inlineDelivered = new Promise((resolve6) => {
|
|
4916
|
+
markInlineDelivered = () => {
|
|
4917
|
+
inlineDone = true;
|
|
4918
|
+
resolve6("inline");
|
|
4919
|
+
};
|
|
4906
4920
|
});
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4921
|
+
if (isSideQuestionPending(active)) {
|
|
4922
|
+
busy = await settleSessionBusy(client, input.sessionID, active, options);
|
|
4923
|
+
log.info("btw: an aside is already in flight, leaving this one to the turn", { sessionID: input.sessionID, busy });
|
|
4924
|
+
} else {
|
|
4925
|
+
const settling = settleSessionBusy(client, input.sessionID, active, options);
|
|
4926
|
+
const history = await fetchAsideHistory(client, input.sessionID, question);
|
|
4927
|
+
const answer = requestSideQuestion(active, question, {
|
|
4928
|
+
cliVersion: await detectCliVersion(transport.cliPath),
|
|
4929
|
+
interactive: transport.interactive,
|
|
4930
|
+
...history.length ? { history } : {}
|
|
4931
|
+
});
|
|
4932
|
+
answer.catch(() => void 0);
|
|
4933
|
+
rememberSideQuestionAnswer(input.sessionID, question, answer);
|
|
4934
|
+
busy = await settling;
|
|
4935
|
+
log.info("btw: aside sent ahead of its message", {
|
|
4936
|
+
sessionID: input.sessionID,
|
|
4937
|
+
busy,
|
|
4938
|
+
questionLength: question.length,
|
|
4939
|
+
history: history.length
|
|
4940
|
+
});
|
|
4941
|
+
const asked = busy ? deliverAsideInline(client, input.sessionID, formatInlineAsideAsk(question), options).catch(() => false) : Promise.resolve(false);
|
|
4942
|
+
answer.then(
|
|
4943
|
+
async (result) => {
|
|
4944
|
+
log.info("btw: early answer arrived", { sessionID: input.sessionID, busy, responseLength: result.response.length });
|
|
4945
|
+
if (!busy || result.synthetic) return;
|
|
4946
|
+
await asked;
|
|
4947
|
+
const inline = await deliverAsideInline(
|
|
4948
|
+
client,
|
|
4949
|
+
input.sessionID,
|
|
4950
|
+
formatInlineAside(question, result.response),
|
|
4951
|
+
options
|
|
4952
|
+
);
|
|
4953
|
+
if (inline) {
|
|
4954
|
+
log.info("btw: answer written into the running turn", { sessionID: input.sessionID });
|
|
4955
|
+
markInlineDelivered();
|
|
4956
|
+
return;
|
|
4957
|
+
}
|
|
4958
|
+
log.info("btw: no open stream for the answer; the held message will carry it", {
|
|
4959
|
+
sessionID: input.sessionID
|
|
4960
|
+
});
|
|
4961
|
+
},
|
|
4962
|
+
(error) => {
|
|
4963
|
+
log.warn("btw: early aside failed; the message will ask again", {
|
|
4964
|
+
sessionID: input.sessionID,
|
|
4965
|
+
error: errorText(error)
|
|
4966
|
+
});
|
|
4967
|
+
}
|
|
4968
|
+
);
|
|
4915
4969
|
}
|
|
4916
|
-
|
|
4917
|
-
|
|
4970
|
+
if (!busy) return;
|
|
4971
|
+
const started = Date.now();
|
|
4972
|
+
const outcome = await Promise.race([
|
|
4973
|
+
inlineDelivered,
|
|
4974
|
+
waitForSessionIdle(client, input.sessionID, { ...options, stop: () => inlineDone }).then(
|
|
4975
|
+
(idle) => idle ? "idle" : "timeout"
|
|
4976
|
+
)
|
|
4977
|
+
]);
|
|
4978
|
+
if (outcome === "inline") {
|
|
4979
|
+
log.info("btw: answered inside the running turn, dropping the /btw message", {
|
|
4980
|
+
sessionID: input.sessionID,
|
|
4981
|
+
waitedMs: Date.now() - started
|
|
4982
|
+
});
|
|
4983
|
+
throw new BtwHandledError(BTW_INLINE_HANDLED_MESSAGE);
|
|
4918
4984
|
}
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
}
|
|
4985
|
+
log.info("btw: turn over, releasing the /btw message", {
|
|
4986
|
+
sessionID: input.sessionID,
|
|
4987
|
+
idle: outcome === "idle",
|
|
4988
|
+
waitedMs: Date.now() - started
|
|
4989
|
+
});
|
|
4990
|
+
if (outcome === "timeout") {
|
|
4991
|
+
showToast(client, { title: "btw", message: BTW_TURN_TOO_LONG_MESSAGE, variant: "warning", duration: 8e3 });
|
|
4992
|
+
throw new BtwHandledError(BTW_TURN_TOO_LONG_MESSAGE);
|
|
4928
4993
|
}
|
|
4929
|
-
return layers;
|
|
4930
4994
|
}
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4995
|
+
|
|
4996
|
+
// src/plan-usage.ts
|
|
4997
|
+
import { execFile as execFile2 } from "child_process";
|
|
4998
|
+
var PLAN_USAGE_ARGUMENTS = /* @__PURE__ */ new Set(["usage", "cost", "stats", "limits"]);
|
|
4999
|
+
function wantsPlanUsage(argument) {
|
|
5000
|
+
return PLAN_USAGE_ARGUMENTS.has(argument.trim().toLowerCase());
|
|
4935
5001
|
}
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
merged = hostApi === "v2" ? { ...merged, ...servers } : mergeMcp(merged, servers);
|
|
4941
|
-
}
|
|
4942
|
-
if (runtimeStatus) {
|
|
4943
|
-
for (const name of Object.keys(merged)) {
|
|
4944
|
-
const status = runtimeStatus[name];
|
|
4945
|
-
if (status === void 0) continue;
|
|
4946
|
-
const existing = merged[name];
|
|
4947
|
-
const base = existing && typeof existing === "object" ? existing : {};
|
|
4948
|
-
merged[name] = { ...base, enabled: status === "connected" };
|
|
4949
|
-
}
|
|
4950
|
-
}
|
|
4951
|
-
const enabledServerNames = [];
|
|
4952
|
-
for (const [name, spec] of Object.entries(merged)) {
|
|
4953
|
-
if (!spec || typeof spec !== "object") continue;
|
|
4954
|
-
const enabled = spec.enabled;
|
|
4955
|
-
if (enabled === false) continue;
|
|
4956
|
-
enabledServerNames.push(name);
|
|
4957
|
-
}
|
|
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 };
|
|
5002
|
+
var PLAN_USAGE_MAX_CHARS = 4e3;
|
|
5003
|
+
function truncate(text) {
|
|
5004
|
+
return text.length <= PLAN_USAGE_MAX_CHARS ? text : `${text.slice(0, PLAN_USAGE_MAX_CHARS)}
|
|
5005
|
+
[truncated]`;
|
|
4961
5006
|
}
|
|
4962
|
-
function
|
|
4963
|
-
const
|
|
4964
|
-
if (
|
|
4965
|
-
|
|
4966
|
-
|
|
5007
|
+
function parsePlanUsage(stdout) {
|
|
5008
|
+
const trimmed = stdout.trim();
|
|
5009
|
+
if (!trimmed) return { status: "failed", error: "the CLI printed nothing" };
|
|
5010
|
+
const candidates = trimmed.split("\n").reverse();
|
|
5011
|
+
for (const line of candidates) {
|
|
5012
|
+
const start = line.indexOf("{");
|
|
5013
|
+
if (start === -1) continue;
|
|
5014
|
+
let parsed;
|
|
5015
|
+
try {
|
|
5016
|
+
parsed = JSON.parse(line.slice(start));
|
|
5017
|
+
} catch {
|
|
5018
|
+
continue;
|
|
5019
|
+
}
|
|
5020
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) continue;
|
|
5021
|
+
const record = parsed;
|
|
5022
|
+
if (record.type !== void 0 && record.type !== "result") continue;
|
|
5023
|
+
const text = typeof record.result === "string" ? record.result.trim() : "";
|
|
5024
|
+
if (record.is_error === true) {
|
|
5025
|
+
return { status: "failed", error: text || "the CLI reported an error" };
|
|
5026
|
+
}
|
|
5027
|
+
if (!text) continue;
|
|
4967
5028
|
return {
|
|
4968
|
-
|
|
4969
|
-
|
|
4970
|
-
|
|
4971
|
-
|
|
5029
|
+
status: "ok",
|
|
5030
|
+
text: truncate(text),
|
|
5031
|
+
costUsd: typeof record.total_cost_usd === "number" ? record.total_cost_usd : 0,
|
|
5032
|
+
numTurns: typeof record.num_turns === "number" ? record.num_turns : 0
|
|
4972
5033
|
};
|
|
4973
5034
|
}
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
5035
|
+
return { status: "failed", error: "no result object in the CLI's reply" };
|
|
5036
|
+
}
|
|
5037
|
+
function runCli(cliPath, args, timeoutMs, env) {
|
|
5038
|
+
return new Promise((resolve6, reject) => {
|
|
5039
|
+
execFile2(
|
|
5040
|
+
cliPath,
|
|
5041
|
+
args,
|
|
5042
|
+
// `killSignal` so a CLI wedged on a hook is actually gone, and a generous
|
|
5043
|
+
// buffer because the reply is prose of unbounded length.
|
|
5044
|
+
{ timeout: timeoutMs, killSignal: "SIGKILL", maxBuffer: 4 * 1024 * 1024, env },
|
|
5045
|
+
(error, stdout) => {
|
|
5046
|
+
if (stdout && stdout.trim()) return resolve6(stdout);
|
|
5047
|
+
if (error) return reject(error);
|
|
5048
|
+
resolve6(stdout ?? "");
|
|
5049
|
+
}
|
|
5050
|
+
);
|
|
5051
|
+
});
|
|
5052
|
+
}
|
|
5053
|
+
async function fetchPlanUsage(cliPath, options = {}) {
|
|
5054
|
+
const timeoutMs = options.timeoutMs ?? 2e4;
|
|
5055
|
+
const run = options.runImpl ?? runCli;
|
|
4980
5056
|
try {
|
|
4981
|
-
|
|
4982
|
-
|
|
5057
|
+
const env = claudeSpawnEnv({ ignoreAnthropicApiKey: options.ignoreAnthropicApiKey });
|
|
5058
|
+
const stdout = await run(
|
|
5059
|
+
cliPath,
|
|
5060
|
+
["-p", "/cost", "--output-format", "json"],
|
|
5061
|
+
timeoutMs,
|
|
5062
|
+
env
|
|
5063
|
+
);
|
|
5064
|
+
const usage = parsePlanUsage(stdout);
|
|
5065
|
+
if (usage.status === "ok") {
|
|
5066
|
+
log.info("read claude plan usage", { costUsd: usage.costUsd, numTurns: usage.numTurns });
|
|
5067
|
+
} else if (usage.status === "failed") {
|
|
5068
|
+
log.debug("could not read claude plan usage", { error: usage.error });
|
|
4983
5069
|
}
|
|
4984
|
-
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
});
|
|
4988
|
-
return
|
|
5070
|
+
return usage;
|
|
5071
|
+
} catch (error) {
|
|
5072
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5073
|
+
log.debug("could not read claude plan usage", { error: message });
|
|
5074
|
+
return { status: "failed", error: message };
|
|
4989
5075
|
}
|
|
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
5076
|
}
|
|
5003
5077
|
|
|
5004
5078
|
// src/startup-diagnostics.ts
|
|
5079
|
+
import { execFile as execFile3 } from "child_process";
|
|
5080
|
+
import * as fs4 from "fs";
|
|
5081
|
+
import * as path5 from "path";
|
|
5082
|
+
import { promisify as promisify2 } from "util";
|
|
5083
|
+
import { fileURLToPath } from "url";
|
|
5005
5084
|
var cachedPluginVersion;
|
|
5006
5085
|
function pluginVersion() {
|
|
5007
5086
|
if (cachedPluginVersion) return cachedPluginVersion;
|
|
@@ -8302,6 +8381,77 @@ async function ensureProxyServer(config, tools, sessionKeyForCalls, interceptCom
|
|
|
8302
8381
|
return srv;
|
|
8303
8382
|
}
|
|
8304
8383
|
|
|
8384
|
+
// src/mcp-hot-reload.ts
|
|
8385
|
+
var DEFAULT_MCP_HOT_RELOAD_COOLDOWN_MS = 6e4;
|
|
8386
|
+
var MAX_COOLDOWN_ENTRIES = 64;
|
|
8387
|
+
var lastReloadAt = /* @__PURE__ */ new Map();
|
|
8388
|
+
function resolveMcpHotReloadCooldownMs() {
|
|
8389
|
+
const raw = process.env.CLAUDE_CODE_MCP_HOT_RELOAD_COOLDOWN_MS;
|
|
8390
|
+
if (raw === void 0) return DEFAULT_MCP_HOT_RELOAD_COOLDOWN_MS;
|
|
8391
|
+
const parsed = Number(raw);
|
|
8392
|
+
if (!Number.isFinite(parsed) || parsed < 0) {
|
|
8393
|
+
return DEFAULT_MCP_HOT_RELOAD_COOLDOWN_MS;
|
|
8394
|
+
}
|
|
8395
|
+
return parsed;
|
|
8396
|
+
}
|
|
8397
|
+
function noteMcpHotReload(sessionKey2, now = Date.now()) {
|
|
8398
|
+
lastReloadAt.delete(sessionKey2);
|
|
8399
|
+
lastReloadAt.set(sessionKey2, now);
|
|
8400
|
+
while (lastReloadAt.size > MAX_COOLDOWN_ENTRIES) {
|
|
8401
|
+
const oldest = lastReloadAt.keys().next();
|
|
8402
|
+
if (oldest.done) break;
|
|
8403
|
+
lastReloadAt.delete(oldest.value);
|
|
8404
|
+
}
|
|
8405
|
+
}
|
|
8406
|
+
function difference(a, b) {
|
|
8407
|
+
const other = new Set(b);
|
|
8408
|
+
return a.filter((name) => !other.has(name)).sort();
|
|
8409
|
+
}
|
|
8410
|
+
function decideMcpHotReload(input) {
|
|
8411
|
+
const previousServers = input.previousServers ?? [];
|
|
8412
|
+
const joined = difference(input.currentServers, previousServers);
|
|
8413
|
+
const left = difference(previousServers, input.currentServers);
|
|
8414
|
+
const decide = (verdict) => ({
|
|
8415
|
+
verdict,
|
|
8416
|
+
reload: verdict === "reload",
|
|
8417
|
+
joined,
|
|
8418
|
+
left
|
|
8419
|
+
});
|
|
8420
|
+
if (!input.enabled) return decide("off");
|
|
8421
|
+
if (input.compactionMode) return decide("skipped-compaction");
|
|
8422
|
+
if (!input.hasActiveProcess) return decide("no-process");
|
|
8423
|
+
const previousHash = input.previousHash ?? null;
|
|
8424
|
+
if (previousHash === input.currentHash) return decide("unchanged");
|
|
8425
|
+
if (input.interactive) return decide("skipped-interactive");
|
|
8426
|
+
if (input.pendingProxyCalls > 0) return decide("deferred-proxy-calls");
|
|
8427
|
+
if (input.turnInFlight) return decide("deferred-turn-in-flight");
|
|
8428
|
+
if (input.planQuestionPending) return decide("deferred-plan-question");
|
|
8429
|
+
const cooldownMs = input.cooldownMs ?? resolveMcpHotReloadCooldownMs();
|
|
8430
|
+
const last = lastReloadAt.get(input.sessionKey);
|
|
8431
|
+
if (cooldownMs > 0 && last !== void 0) {
|
|
8432
|
+
const now = input.now ?? Date.now();
|
|
8433
|
+
if (now - last < cooldownMs) return decide("skipped-cooldown");
|
|
8434
|
+
}
|
|
8435
|
+
return decide("reload");
|
|
8436
|
+
}
|
|
8437
|
+
function logMcpHotReloadDecision(decision, context) {
|
|
8438
|
+
if (decision.verdict === "off" || decision.verdict === "no-process" || decision.verdict === "skipped-compaction" || decision.verdict === "unchanged") {
|
|
8439
|
+
return;
|
|
8440
|
+
}
|
|
8441
|
+
const details = {
|
|
8442
|
+
sk: context.sessionKey,
|
|
8443
|
+
joined: decision.joined,
|
|
8444
|
+
left: decision.left,
|
|
8445
|
+
previousHash: context.previousHash ?? null,
|
|
8446
|
+
currentHash: context.currentHash
|
|
8447
|
+
};
|
|
8448
|
+
if (decision.reload) {
|
|
8449
|
+
log.info("opencode MCP servers changed, respawning claude", details);
|
|
8450
|
+
return;
|
|
8451
|
+
}
|
|
8452
|
+
log.info(`opencode MCP servers changed; holding off (${decision.verdict})`, details);
|
|
8453
|
+
}
|
|
8454
|
+
|
|
8305
8455
|
// src/title.ts
|
|
8306
8456
|
function isTitleRequest(config, scope, options) {
|
|
8307
8457
|
if (scope === "no-tools") return true;
|
|
@@ -10490,7 +10640,15 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10490
10640
|
(m) => m.result !== null
|
|
10491
10641
|
);
|
|
10492
10642
|
const [runtimeStatus, cliVersion] = await Promise.all([
|
|
10493
|
-
compactionMode ? Promise.resolve(void 0) : getRuntimeMcpStatus(
|
|
10643
|
+
compactionMode ? Promise.resolve(void 0) : getRuntimeMcpStatus({
|
|
10644
|
+
// Give a server opencode is still starting a bounded moment to
|
|
10645
|
+
// reach a decision before the spawn is planned without it. Only
|
|
10646
|
+
// opencode 2 ever reports `pending`, so on 1.x this is one status
|
|
10647
|
+
// call exactly as before.
|
|
10648
|
+
waitForPendingMs: resolveMcpConnectWaitMs(
|
|
10649
|
+
this.config.mcpConnectWaitMs
|
|
10650
|
+
)
|
|
10651
|
+
}),
|
|
10494
10652
|
detectCliVersion(cliPath)
|
|
10495
10653
|
]);
|
|
10496
10654
|
const failoverAccounts = failoverCandidates(
|
|
@@ -10556,24 +10714,30 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10556
10714
|
if (!compactionMode && state.activeProcess && self.config.hotReloadMcp !== false && self.config.bridgeOpencodeMcp !== false) {
|
|
10557
10715
|
const probe = self.effectiveMcpConfig(cwd, void 0, runtimeStatus);
|
|
10558
10716
|
const previousHash = state.activeProcess.mcpHash ?? null;
|
|
10559
|
-
|
|
10560
|
-
|
|
10561
|
-
|
|
10562
|
-
|
|
10563
|
-
|
|
10564
|
-
|
|
10565
|
-
|
|
10566
|
-
|
|
10567
|
-
|
|
10568
|
-
|
|
10569
|
-
|
|
10570
|
-
|
|
10571
|
-
|
|
10572
|
-
|
|
10573
|
-
|
|
10574
|
-
|
|
10575
|
-
|
|
10576
|
-
|
|
10717
|
+
const decision = decideMcpHotReload({
|
|
10718
|
+
sessionKey: sk,
|
|
10719
|
+
enabled: true,
|
|
10720
|
+
hasActiveProcess: true,
|
|
10721
|
+
compactionMode,
|
|
10722
|
+
interactive: !!useInteractive,
|
|
10723
|
+
turnInFlight: isTurnInFlight(state.activeProcess),
|
|
10724
|
+
pendingProxyCalls: previousPendingProxyCalls.length,
|
|
10725
|
+
planQuestionPending: hasExitPlanModeQuestions(sk),
|
|
10726
|
+
previousHash,
|
|
10727
|
+
currentHash: probe.bridgedHash,
|
|
10728
|
+
previousServers: state.activeProcess.mcpServers,
|
|
10729
|
+
currentServers: probe.allEnabledServerNames
|
|
10730
|
+
});
|
|
10731
|
+
logMcpHotReloadDecision(decision, {
|
|
10732
|
+
sessionKey: sk,
|
|
10733
|
+
previousHash,
|
|
10734
|
+
currentHash: probe.bridgedHash
|
|
10735
|
+
});
|
|
10736
|
+
if (decision.reload) {
|
|
10737
|
+
noteMcpHotReload(sk);
|
|
10738
|
+
await deleteActiveProcessAndWait(sk);
|
|
10739
|
+
state.activeProcess = void 0;
|
|
10740
|
+
state.proxyServer = null;
|
|
10577
10741
|
}
|
|
10578
10742
|
}
|
|
10579
10743
|
if (useInteractive && !compactionMode) {
|
|
@@ -10633,6 +10797,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10633
10797
|
effort: reasoningEffort
|
|
10634
10798
|
});
|
|
10635
10799
|
ap.mcpHash = mcp.bridgedHash;
|
|
10800
|
+
ap.mcpServers = mcp.allEnabledServerNames;
|
|
10636
10801
|
setActiveProcess(sk, ap);
|
|
10637
10802
|
state.proc = ap.proc;
|
|
10638
10803
|
state.lineEmitter = ap.lineEmitter;
|
|
@@ -10648,6 +10813,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10648
10813
|
let spawnSystemPromptFile;
|
|
10649
10814
|
let spawnProxyServer = null;
|
|
10650
10815
|
let spawnMcpHash = null;
|
|
10816
|
+
let spawnMcpServers = [];
|
|
10651
10817
|
if (compactionMode) {
|
|
10652
10818
|
state.cliArgs = buildCliArgs({
|
|
10653
10819
|
sessionKey: sk,
|
|
@@ -10821,6 +10987,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10821
10987
|
spawnSystemPromptFile = systemPromptFile;
|
|
10822
10988
|
spawnProxyServer = state.proxyServer;
|
|
10823
10989
|
spawnMcpHash = mcp.bridgedHash;
|
|
10990
|
+
spawnMcpServers = mcp.allEnabledServerNames;
|
|
10824
10991
|
}
|
|
10825
10992
|
if (state.activeProcess && !compactionMode) {
|
|
10826
10993
|
state.proc = state.activeProcess.proc;
|
|
@@ -10839,6 +11006,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10839
11006
|
reasoningEffort,
|
|
10840
11007
|
promptCacheTtl
|
|
10841
11008
|
);
|
|
11009
|
+
ap.mcpServers = spawnMcpServers;
|
|
10842
11010
|
state.proc = ap.proc;
|
|
10843
11011
|
state.lineEmitter = ap.lineEmitter;
|
|
10844
11012
|
state.activeProcess = ap;
|
|
@@ -12085,6 +12253,7 @@ function createClaudeCode(settings = {}) {
|
|
|
12085
12253
|
planModeQuestion: settings.planModeQuestion ?? false,
|
|
12086
12254
|
webSearch: settings.webSearch,
|
|
12087
12255
|
hotReloadMcp: settings.hotReloadMcp ?? true,
|
|
12256
|
+
mcpConnectWaitMs: settings.mcpConnectWaitMs,
|
|
12088
12257
|
proxyOpencodeMcpTools: settings.proxyOpencodeMcpTools === true,
|
|
12089
12258
|
multiStepContinuation: settings.multiStepContinuation ?? true,
|
|
12090
12259
|
autoContinueIncompleteTurns: settings.autoContinueIncompleteTurns ?? "smart",
|