@khalilgharbaoui/opencode-claude-code-plugin 0.27.1 → 0.28.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 +81 -2
- package/dist/index.d.ts +120 -66
- package/dist/index.js +1163 -778
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/skills/claude-code-plugin/SKILL.md +30 -3
package/dist/index.js
CHANGED
|
@@ -647,7 +647,9 @@ while [[ $# -gt 0 ]]; do
|
|
|
647
647
|
done
|
|
648
648
|
|
|
649
649
|
export CLAUDE_CONFIG_DIR=${shellSingleQuote(configDir)}
|
|
650
|
-
|
|
650
|
+
# \${args[@]+...} keeps an empty array legal under \`set -u\` on bash 3.2,
|
|
651
|
+
# which is the bash macOS ships and \`/usr/bin/env bash\` usually finds.
|
|
652
|
+
exec ${shellSingleQuote(baseCliPath)} \${args[@]+"\${args[@]}"}
|
|
651
653
|
`;
|
|
652
654
|
await writeFile(wrapperPath, script, "utf8");
|
|
653
655
|
await chmod(wrapperPath, 493);
|
|
@@ -929,6 +931,13 @@ function formatStreamTimeoutNote(silenceMs) {
|
|
|
929
931
|
${STREAM_TIMEOUT_MARKER} The Claude Code CLI produced output and then went silent for ${seconds}s without finishing the turn, so it was closed without a result. The answer above may be incomplete.
|
|
930
932
|
`;
|
|
931
933
|
}
|
|
934
|
+
var SILENT_TURN_MARKER = "\u258C **no reply:**";
|
|
935
|
+
function formatSilentTurnNote(hadReasoning) {
|
|
936
|
+
const what = hadReasoning ? "Claude Code finished the turn after thinking but never wrote an answer" : "Claude Code finished the turn without writing anything or calling a tool";
|
|
937
|
+
return `
|
|
938
|
+
${SILENT_TURN_MARKER} ${what}, so there is no reply above. Nothing failed and nothing is pending: send the message again, or rephrase it.
|
|
939
|
+
`;
|
|
940
|
+
}
|
|
932
941
|
|
|
933
942
|
// src/plan-mode-question.ts
|
|
934
943
|
var QUESTION_TOOL_NAME = "question";
|
|
@@ -1466,6 +1475,14 @@ function cliSupportsFastMode(v) {
|
|
|
1466
1475
|
if (!v) return false;
|
|
1467
1476
|
return gte(v, { major: 2, minor: 1, patch: 220 });
|
|
1468
1477
|
}
|
|
1478
|
+
function cliSupportsRestricted(v) {
|
|
1479
|
+
if (!v) return false;
|
|
1480
|
+
return gte(v, { major: 2, minor: 1, patch: 258 });
|
|
1481
|
+
}
|
|
1482
|
+
function cliSupportsPermissionPrompts(v) {
|
|
1483
|
+
if (!v) return false;
|
|
1484
|
+
return gte(v, { major: 2, minor: 1, patch: 263 });
|
|
1485
|
+
}
|
|
1469
1486
|
function cliSupportsSideQuestion(v) {
|
|
1470
1487
|
if (!v) return false;
|
|
1471
1488
|
return gte(v, { major: 2, minor: 1, patch: 258 });
|
|
@@ -1520,25 +1537,137 @@ import { EventEmitter } from "events";
|
|
|
1520
1537
|
import * as fs from "fs";
|
|
1521
1538
|
import * as os from "os";
|
|
1522
1539
|
import * as path2 from "path";
|
|
1523
|
-
var
|
|
1540
|
+
var PLUGIN_TMP_PREFIX = "opencode-claude-code-";
|
|
1541
|
+
var PID_TMP_DIR = path2.join(
|
|
1524
1542
|
os.tmpdir(),
|
|
1525
|
-
|
|
1543
|
+
`${PLUGIN_TMP_PREFIX}${process.pid}`
|
|
1526
1544
|
);
|
|
1545
|
+
var currentDir = null;
|
|
1546
|
+
var createdDirs = /* @__PURE__ */ new Set();
|
|
1527
1547
|
var registered = false;
|
|
1528
|
-
function
|
|
1529
|
-
|
|
1530
|
-
|
|
1548
|
+
function currentUid() {
|
|
1549
|
+
return typeof process.getuid === "function" ? process.getuid() : void 0;
|
|
1550
|
+
}
|
|
1551
|
+
function untrustworthyReason(dir) {
|
|
1552
|
+
let stat;
|
|
1553
|
+
try {
|
|
1554
|
+
stat = fs.lstatSync(dir);
|
|
1555
|
+
} catch {
|
|
1556
|
+
return null;
|
|
1557
|
+
}
|
|
1558
|
+
if (stat.isSymbolicLink()) return "path is a symlink";
|
|
1559
|
+
if (!stat.isDirectory()) return "path is not a directory";
|
|
1560
|
+
const uid = currentUid();
|
|
1561
|
+
if (uid !== void 0 && stat.uid !== uid) {
|
|
1562
|
+
return `directory is owned by uid ${stat.uid}, not ${uid}`;
|
|
1563
|
+
}
|
|
1564
|
+
return null;
|
|
1565
|
+
}
|
|
1566
|
+
function ensureDir(dir) {
|
|
1567
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
1568
|
+
try {
|
|
1569
|
+
fs.chmodSync(dir, 448);
|
|
1570
|
+
} catch {
|
|
1531
1571
|
}
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1572
|
+
}
|
|
1573
|
+
function chooseDir() {
|
|
1574
|
+
const reason = untrustworthyReason(PID_TMP_DIR);
|
|
1575
|
+
if (!reason) return PID_TMP_DIR;
|
|
1576
|
+
const fallback = fs.mkdtempSync(path2.join(os.tmpdir(), PLUGIN_TMP_PREFIX));
|
|
1577
|
+
log.warn("refusing the pid-named scratch directory", {
|
|
1578
|
+
path: PID_TMP_DIR,
|
|
1579
|
+
reason,
|
|
1580
|
+
fallback
|
|
1581
|
+
});
|
|
1582
|
+
return fallback;
|
|
1583
|
+
}
|
|
1584
|
+
function registerExitCleanup() {
|
|
1585
|
+
if (registered) return;
|
|
1586
|
+
registered = true;
|
|
1587
|
+
process.on("exit", () => {
|
|
1588
|
+
for (const dir of createdDirs) {
|
|
1535
1589
|
try {
|
|
1536
|
-
fs.rmSync(
|
|
1590
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1537
1591
|
} catch {
|
|
1538
1592
|
}
|
|
1593
|
+
}
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
var PID_DIR_NAME = /^opencode-claude-code-(\d+)$/;
|
|
1597
|
+
function isProcessAlive(pid) {
|
|
1598
|
+
try {
|
|
1599
|
+
process.kill(pid, 0);
|
|
1600
|
+
return true;
|
|
1601
|
+
} catch (err) {
|
|
1602
|
+
return err.code === "EPERM";
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
function sweepStalePluginTmpDirs(keep) {
|
|
1606
|
+
const removed = [];
|
|
1607
|
+
if (process.env.OPENCODE_CLAUDE_CODE_NO_TMP_SWEEP === "1") return removed;
|
|
1608
|
+
const root = os.tmpdir();
|
|
1609
|
+
let entries;
|
|
1610
|
+
try {
|
|
1611
|
+
entries = fs.readdirSync(root);
|
|
1612
|
+
} catch {
|
|
1613
|
+
return removed;
|
|
1614
|
+
}
|
|
1615
|
+
const uid = currentUid();
|
|
1616
|
+
const keepPath = keep ? path2.resolve(keep) : null;
|
|
1617
|
+
for (const entry of entries) {
|
|
1618
|
+
const match = PID_DIR_NAME.exec(entry);
|
|
1619
|
+
if (!match) continue;
|
|
1620
|
+
const pid = Number(match[1]);
|
|
1621
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) continue;
|
|
1622
|
+
if (pid === process.pid) continue;
|
|
1623
|
+
const dir = path2.join(root, entry);
|
|
1624
|
+
if (keepPath && path2.resolve(dir) === keepPath) continue;
|
|
1625
|
+
let stat;
|
|
1626
|
+
try {
|
|
1627
|
+
stat = fs.lstatSync(dir);
|
|
1628
|
+
} catch {
|
|
1629
|
+
continue;
|
|
1630
|
+
}
|
|
1631
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) continue;
|
|
1632
|
+
if (uid !== void 0 && stat.uid !== uid) continue;
|
|
1633
|
+
if (isProcessAlive(pid)) continue;
|
|
1634
|
+
try {
|
|
1635
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
1636
|
+
removed.push(dir);
|
|
1637
|
+
} catch (err) {
|
|
1638
|
+
log.debug("could not remove a stale scratch directory", {
|
|
1639
|
+
dir,
|
|
1640
|
+
error: String(err)
|
|
1641
|
+
});
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
if (removed.length > 0) {
|
|
1645
|
+
log.info("removed stale plugin scratch directories", {
|
|
1646
|
+
count: removed.length,
|
|
1647
|
+
dirs: removed
|
|
1539
1648
|
});
|
|
1540
1649
|
}
|
|
1541
|
-
return
|
|
1650
|
+
return removed;
|
|
1651
|
+
}
|
|
1652
|
+
var swept = false;
|
|
1653
|
+
function sweepOnce(keep) {
|
|
1654
|
+
if (swept) return;
|
|
1655
|
+
swept = true;
|
|
1656
|
+
try {
|
|
1657
|
+
sweepStalePluginTmpDirs(keep);
|
|
1658
|
+
} catch (err) {
|
|
1659
|
+
log.warn("stale scratch directory sweep failed", { error: String(err) });
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
function pluginTmpDir() {
|
|
1663
|
+
if (currentDir === null || untrustworthyReason(currentDir) !== null) {
|
|
1664
|
+
currentDir = chooseDir();
|
|
1665
|
+
}
|
|
1666
|
+
ensureDir(currentDir);
|
|
1667
|
+
createdDirs.add(currentDir);
|
|
1668
|
+
registerExitCleanup();
|
|
1669
|
+
sweepOnce(currentDir);
|
|
1670
|
+
return currentDir;
|
|
1542
1671
|
}
|
|
1543
1672
|
|
|
1544
1673
|
// src/proxy-mcp.ts
|
|
@@ -2120,12 +2249,12 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
2120
2249
|
});
|
|
2121
2250
|
let timer = null;
|
|
2122
2251
|
const result = await new Promise(
|
|
2123
|
-
(
|
|
2252
|
+
(resolve6, reject2) => {
|
|
2124
2253
|
const entry = {
|
|
2125
2254
|
id: callId,
|
|
2126
2255
|
toolName,
|
|
2127
2256
|
input,
|
|
2128
|
-
resolve:
|
|
2257
|
+
resolve: resolve6,
|
|
2129
2258
|
reject: reject2,
|
|
2130
2259
|
channel
|
|
2131
2260
|
};
|
|
@@ -2214,11 +2343,11 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
2214
2343
|
}
|
|
2215
2344
|
}
|
|
2216
2345
|
});
|
|
2217
|
-
await new Promise((
|
|
2346
|
+
await new Promise((resolve6, reject2) => {
|
|
2218
2347
|
server2.once("error", reject2);
|
|
2219
2348
|
server2.listen(0, "127.0.0.1", () => {
|
|
2220
2349
|
server2.off("error", reject2);
|
|
2221
|
-
|
|
2350
|
+
resolve6();
|
|
2222
2351
|
});
|
|
2223
2352
|
});
|
|
2224
2353
|
const addr = server2.address();
|
|
@@ -2275,8 +2404,8 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
2275
2404
|
entry.reject(new Error(SERVER_CLOSED_MESSAGE));
|
|
2276
2405
|
}
|
|
2277
2406
|
pending.clear();
|
|
2278
|
-
await new Promise((
|
|
2279
|
-
server2.close(() =>
|
|
2407
|
+
await new Promise((resolve6) => {
|
|
2408
|
+
server2.close(() => resolve6());
|
|
2280
2409
|
});
|
|
2281
2410
|
if (configFilePath) {
|
|
2282
2411
|
try {
|
|
@@ -2431,10 +2560,10 @@ function resolveMcpProxyToolDefs(options) {
|
|
|
2431
2560
|
return { defs, coveredServers };
|
|
2432
2561
|
}
|
|
2433
2562
|
function readBody(req) {
|
|
2434
|
-
return new Promise((
|
|
2563
|
+
return new Promise((resolve6, reject) => {
|
|
2435
2564
|
const chunks = [];
|
|
2436
2565
|
req.on("data", (chunk) => chunks.push(chunk));
|
|
2437
|
-
req.on("end", () =>
|
|
2566
|
+
req.on("end", () => resolve6(Buffer.concat(chunks).toString("utf8")));
|
|
2438
2567
|
req.on("error", reject);
|
|
2439
2568
|
});
|
|
2440
2569
|
}
|
|
@@ -2742,6 +2871,99 @@ function clearCompression(sessionKey2) {
|
|
|
2742
2871
|
compressions.delete(sessionKey2);
|
|
2743
2872
|
}
|
|
2744
2873
|
|
|
2874
|
+
// src/types.ts
|
|
2875
|
+
var READ_ONLY_PERMISSION_MODE = "read-only";
|
|
2876
|
+
|
|
2877
|
+
// src/permission-presets.ts
|
|
2878
|
+
var READ_ONLY_DISALLOWED_CLI_TOOLS = [
|
|
2879
|
+
"Bash",
|
|
2880
|
+
"Write",
|
|
2881
|
+
"Edit",
|
|
2882
|
+
"NotebookEdit",
|
|
2883
|
+
"REPL",
|
|
2884
|
+
"JavaScript",
|
|
2885
|
+
"WebFetch"
|
|
2886
|
+
];
|
|
2887
|
+
var READ_ONLY_DENIED_PROXY_TOOLS = /* @__PURE__ */ new Set([
|
|
2888
|
+
"bash",
|
|
2889
|
+
"write",
|
|
2890
|
+
"edit",
|
|
2891
|
+
"webfetch",
|
|
2892
|
+
"task",
|
|
2893
|
+
"task_batch"
|
|
2894
|
+
]);
|
|
2895
|
+
function dedupe(names) {
|
|
2896
|
+
const out = [];
|
|
2897
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2898
|
+
for (const name of names) {
|
|
2899
|
+
const trimmed = String(name).trim();
|
|
2900
|
+
if (!trimmed || seen.has(trimmed)) continue;
|
|
2901
|
+
seen.add(trimmed);
|
|
2902
|
+
out.push(trimmed);
|
|
2903
|
+
}
|
|
2904
|
+
return out;
|
|
2905
|
+
}
|
|
2906
|
+
function resolveReadOnly(settings, defaultProxyTools) {
|
|
2907
|
+
const overridden = [];
|
|
2908
|
+
if (settings.permissionMode !== void 0) {
|
|
2909
|
+
overridden.push(
|
|
2910
|
+
`permissionMode: "${settings.permissionMode}" is dropped; read-only sends --restricted instead, which is stricter than every mode except plan and incompatible with bypassPermissions`
|
|
2911
|
+
);
|
|
2912
|
+
}
|
|
2913
|
+
if (settings.skipPermissions === true) {
|
|
2914
|
+
overridden.push(
|
|
2915
|
+
'skipPermissions: forced to false; the CLI exits with "bypassPermissions not supported in restricted mode" when both are passed'
|
|
2916
|
+
);
|
|
2917
|
+
}
|
|
2918
|
+
if (settings.controlRequestBehavior === "allow") {
|
|
2919
|
+
overridden.push(
|
|
2920
|
+
'controlRequestBehavior: forced to "deny"; read-only answers every permission request the same way the CLI does under --permission-prompts none'
|
|
2921
|
+
);
|
|
2922
|
+
}
|
|
2923
|
+
if (settings.controlRequestToolBehaviors !== void 0) {
|
|
2924
|
+
overridden.push(
|
|
2925
|
+
"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"
|
|
2926
|
+
);
|
|
2927
|
+
}
|
|
2928
|
+
const requested = settings.proxyTools ?? defaultProxyTools;
|
|
2929
|
+
const proxyTools = dedupe(requested).filter(
|
|
2930
|
+
(name) => !READ_ONLY_DENIED_PROXY_TOOLS.has(name.toLowerCase())
|
|
2931
|
+
);
|
|
2932
|
+
const dropped = dedupe(requested).filter(
|
|
2933
|
+
(name) => READ_ONLY_DENIED_PROXY_TOOLS.has(name.toLowerCase())
|
|
2934
|
+
);
|
|
2935
|
+
if (dropped.length > 0) {
|
|
2936
|
+
overridden.push(
|
|
2937
|
+
`proxyTools: dropped ${dropped.join(", ")}; those execute in opencode, where no Claude CLI flag reaches them`
|
|
2938
|
+
);
|
|
2939
|
+
}
|
|
2940
|
+
return {
|
|
2941
|
+
preset: "read-only",
|
|
2942
|
+
permissionMode: READ_ONLY_PERMISSION_MODE,
|
|
2943
|
+
skipPermissions: false,
|
|
2944
|
+
proxyTools,
|
|
2945
|
+
extraDisallowedTools: dedupe([
|
|
2946
|
+
...READ_ONLY_DISALLOWED_CLI_TOOLS,
|
|
2947
|
+
...settings.extraDisallowedTools ?? []
|
|
2948
|
+
]),
|
|
2949
|
+
controlRequestBehavior: "deny",
|
|
2950
|
+
controlRequestToolBehaviors: void 0,
|
|
2951
|
+
overridden
|
|
2952
|
+
};
|
|
2953
|
+
}
|
|
2954
|
+
function resolvePermissionPreset(settings, defaultProxyTools) {
|
|
2955
|
+
const preset = settings.permissionPreset;
|
|
2956
|
+
if (preset === void 0) return null;
|
|
2957
|
+
if (preset === "read-only") return resolveReadOnly(settings, defaultProxyTools);
|
|
2958
|
+
return { unknown: String(preset) };
|
|
2959
|
+
}
|
|
2960
|
+
function isUnknownPreset(resolved) {
|
|
2961
|
+
return resolved !== null && "unknown" in resolved;
|
|
2962
|
+
}
|
|
2963
|
+
function isReadOnlyPermissionMode(mode) {
|
|
2964
|
+
return mode === READ_ONLY_PERMISSION_MODE;
|
|
2965
|
+
}
|
|
2966
|
+
|
|
2745
2967
|
// src/side-question.ts
|
|
2746
2968
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
2747
2969
|
var MAX_HISTORY_EXCHANGES = 20;
|
|
@@ -2845,7 +3067,7 @@ async function requestSideQuestion(activeProcess, question, options) {
|
|
|
2845
3067
|
}
|
|
2846
3068
|
});
|
|
2847
3069
|
pendingProcesses.add(proc);
|
|
2848
|
-
return new Promise((
|
|
3070
|
+
return new Promise((resolve6, reject) => {
|
|
2849
3071
|
const event = `side-question:${requestId}`;
|
|
2850
3072
|
let settled = false;
|
|
2851
3073
|
let sent = false;
|
|
@@ -2900,7 +3122,7 @@ async function requestSideQuestion(activeProcess, question, options) {
|
|
|
2900
3122
|
}
|
|
2901
3123
|
settled = true;
|
|
2902
3124
|
cleanup();
|
|
2903
|
-
|
|
3125
|
+
resolve6({ response: result.response, synthetic: result.synthetic });
|
|
2904
3126
|
};
|
|
2905
3127
|
const timer = setTimeout(() => {
|
|
2906
3128
|
fail(new Error(`/btw timed out after ${timeoutMs}ms.`), true);
|
|
@@ -3065,16 +3287,16 @@ function isTurnInFlight(ap) {
|
|
|
3065
3287
|
}
|
|
3066
3288
|
function awaitTurnIdle(ap, timeoutMs) {
|
|
3067
3289
|
if (!ap.turnInFlight) return Promise.resolve(true);
|
|
3068
|
-
return new Promise((
|
|
3290
|
+
return new Promise((resolve6) => {
|
|
3069
3291
|
const wake = () => {
|
|
3070
3292
|
clearTimeout(timer);
|
|
3071
|
-
|
|
3293
|
+
resolve6(true);
|
|
3072
3294
|
};
|
|
3073
3295
|
const timer = setTimeout(() => {
|
|
3074
3296
|
const waiters = ap.turnIdleWaiters ?? [];
|
|
3075
3297
|
const at = waiters.indexOf(wake);
|
|
3076
3298
|
if (at >= 0) waiters.splice(at, 1);
|
|
3077
|
-
|
|
3299
|
+
resolve6(false);
|
|
3078
3300
|
}, timeoutMs);
|
|
3079
3301
|
(ap.turnIdleWaiters ??= []).push(wake);
|
|
3080
3302
|
});
|
|
@@ -3193,14 +3415,14 @@ function hasProcessExited(proc) {
|
|
|
3193
3415
|
}
|
|
3194
3416
|
function waitForProcessExit(proc, timeoutMs) {
|
|
3195
3417
|
if (hasProcessExited(proc)) return Promise.resolve(true);
|
|
3196
|
-
return new Promise((
|
|
3418
|
+
return new Promise((resolve6) => {
|
|
3197
3419
|
const onExit = () => {
|
|
3198
3420
|
clearTimeout(timer);
|
|
3199
|
-
|
|
3421
|
+
resolve6(true);
|
|
3200
3422
|
};
|
|
3201
3423
|
const timer = setTimeout(() => {
|
|
3202
3424
|
proc.off("exit", onExit);
|
|
3203
|
-
|
|
3425
|
+
resolve6(hasProcessExited(proc));
|
|
3204
3426
|
}, timeoutMs);
|
|
3205
3427
|
proc.once("exit", onExit);
|
|
3206
3428
|
});
|
|
@@ -3451,7 +3673,25 @@ function buildCliArgs(opts) {
|
|
|
3451
3673
|
if (model) {
|
|
3452
3674
|
args.push("--model", model);
|
|
3453
3675
|
}
|
|
3454
|
-
|
|
3676
|
+
const readOnly = isReadOnlyPermissionMode(permissionMode);
|
|
3677
|
+
if (readOnly) {
|
|
3678
|
+
if (cliSupportsRestricted(cliVersion ?? null)) {
|
|
3679
|
+
args.push("--restricted");
|
|
3680
|
+
} else {
|
|
3681
|
+
log.warn(
|
|
3682
|
+
'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.',
|
|
3683
|
+
{ cliVersion: cliVersion?.raw ?? "unknown" }
|
|
3684
|
+
);
|
|
3685
|
+
}
|
|
3686
|
+
if (cliSupportsPermissionPrompts(cliVersion ?? null)) {
|
|
3687
|
+
args.push("--permission-prompts", "none");
|
|
3688
|
+
} else {
|
|
3689
|
+
log.warn(
|
|
3690
|
+
`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.`,
|
|
3691
|
+
{ cliVersion: cliVersion?.raw ?? "unknown" }
|
|
3692
|
+
);
|
|
3693
|
+
}
|
|
3694
|
+
} else if (permissionMode) {
|
|
3455
3695
|
args.push("--permission-mode", permissionMode);
|
|
3456
3696
|
}
|
|
3457
3697
|
if (includeSessionId) {
|
|
@@ -3488,7 +3728,7 @@ function buildCliArgs(opts) {
|
|
|
3488
3728
|
if (fastMode && cliSupportsFastMode(cliVersion ?? null)) {
|
|
3489
3729
|
args.push("--settings", JSON.stringify({ fastMode: true }));
|
|
3490
3730
|
}
|
|
3491
|
-
if (skipPermissions && permissionMode !== "plan") {
|
|
3731
|
+
if (skipPermissions && permissionMode !== "plan" && !readOnly) {
|
|
3492
3732
|
args.push("--dangerously-skip-permissions");
|
|
3493
3733
|
}
|
|
3494
3734
|
return args;
|
|
@@ -3640,7 +3880,7 @@ async function waitForSessionIdle(client, sessionID, options = {}) {
|
|
|
3640
3880
|
if (options.stop?.()) return true;
|
|
3641
3881
|
if (await sessionStatus(client, sessionID) !== "busy") return true;
|
|
3642
3882
|
if (Date.now() - started >= timeoutMs) return false;
|
|
3643
|
-
await new Promise((
|
|
3883
|
+
await new Promise((resolve6) => setTimeout(resolve6, pollMs));
|
|
3644
3884
|
}
|
|
3645
3885
|
}
|
|
3646
3886
|
async function deliverAsideInline(client, sessionID, text, options = {}) {
|
|
@@ -3651,7 +3891,7 @@ async function deliverAsideInline(client, sessionID, text, options = {}) {
|
|
|
3651
3891
|
if (emitAsideInline(sessionID, text)) return true;
|
|
3652
3892
|
if (Date.now() - started >= timeoutMs) return false;
|
|
3653
3893
|
if (await sessionStatus(client, sessionID) !== "busy") return false;
|
|
3654
|
-
await new Promise((
|
|
3894
|
+
await new Promise((resolve6) => setTimeout(resolve6, pollMs));
|
|
3655
3895
|
}
|
|
3656
3896
|
}
|
|
3657
3897
|
async function waitForAsideProcess(client, sessionID, options = {}) {
|
|
@@ -3672,7 +3912,7 @@ async function waitForAsideProcess(client, sessionID, options = {}) {
|
|
|
3672
3912
|
log.warn("btw: a turn is running but no claude process appeared for it", { sessionID, waitedMs });
|
|
3673
3913
|
return void 0;
|
|
3674
3914
|
}
|
|
3675
|
-
await new Promise((
|
|
3915
|
+
await new Promise((resolve6) => setTimeout(resolve6, pollMs));
|
|
3676
3916
|
}
|
|
3677
3917
|
}
|
|
3678
3918
|
async function settleSessionBusy(client, sessionID, active, options = {}) {
|
|
@@ -3684,7 +3924,7 @@ async function settleSessionBusy(client, sessionID, active, options = {}) {
|
|
|
3684
3924
|
if (status === "busy") return true;
|
|
3685
3925
|
if (status === "unknown") return isProcessBusy(active);
|
|
3686
3926
|
if (Date.now() - started >= settleMs) return false;
|
|
3687
|
-
await new Promise((
|
|
3927
|
+
await new Promise((resolve6) => setTimeout(resolve6, pollMs));
|
|
3688
3928
|
}
|
|
3689
3929
|
}
|
|
3690
3930
|
function errorText(error) {
|
|
@@ -3731,10 +3971,10 @@ async function handleBtwCommand(client, input, options = {}) {
|
|
|
3731
3971
|
let inlineDone = false;
|
|
3732
3972
|
let markInlineDelivered = () => {
|
|
3733
3973
|
};
|
|
3734
|
-
const inlineDelivered = new Promise((
|
|
3974
|
+
const inlineDelivered = new Promise((resolve6) => {
|
|
3735
3975
|
markInlineDelivered = () => {
|
|
3736
3976
|
inlineDone = true;
|
|
3737
|
-
|
|
3977
|
+
resolve6("inline");
|
|
3738
3978
|
};
|
|
3739
3979
|
});
|
|
3740
3980
|
if (isSideQuestionPending(active)) {
|
|
@@ -4268,7 +4508,7 @@ async function settleSessionRunState(sessionID, options = {}) {
|
|
|
4268
4508
|
if (state === "busy") return "busy";
|
|
4269
4509
|
if (state !== "unknown") last = state;
|
|
4270
4510
|
if (Date.now() - started >= windowMs) return last;
|
|
4271
|
-
await new Promise((
|
|
4511
|
+
await new Promise((resolve6) => setTimeout(resolve6, pollMs));
|
|
4272
4512
|
}
|
|
4273
4513
|
}
|
|
4274
4514
|
async function getRuntimeMcpStatus() {
|
|
@@ -4751,6 +4991,7 @@ var PLUGIN_NOTE_MARKERS = [
|
|
|
4751
4991
|
RESULT_ERROR_MARKER,
|
|
4752
4992
|
DOCTOR_MARKER,
|
|
4753
4993
|
STREAM_TIMEOUT_MARKER,
|
|
4994
|
+
SILENT_TURN_MARKER,
|
|
4754
4995
|
FAILOVER_MARKER,
|
|
4755
4996
|
ACCOUNT_BLOCK_MARKER
|
|
4756
4997
|
];
|
|
@@ -6526,109 +6767,142 @@ function spawnInteractiveProcess(opts) {
|
|
|
6526
6767
|
};
|
|
6527
6768
|
}
|
|
6528
6769
|
|
|
6529
|
-
// src/
|
|
6770
|
+
// src/prompts.ts
|
|
6530
6771
|
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
6531
|
-
import {
|
|
6532
|
-
import { homedir as homedir6, tmpdir as tmpdir2 } from "os";
|
|
6772
|
+
import { homedir as homedir6 } from "os";
|
|
6533
6773
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
6534
6774
|
import { dirname as dirname5, join as join8 } from "path";
|
|
6535
|
-
|
|
6536
|
-
|
|
6537
|
-
|
|
6538
|
-
|
|
6539
|
-
|
|
6775
|
+
function readPromptFileIfPresent(path10) {
|
|
6776
|
+
try {
|
|
6777
|
+
const content = readFileSync5(path10, "utf8").trim();
|
|
6778
|
+
return content || void 0;
|
|
6779
|
+
} catch {
|
|
6780
|
+
return void 0;
|
|
6781
|
+
}
|
|
6540
6782
|
}
|
|
6541
|
-
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
|
|
6547
|
-
|
|
6548
|
-
|
|
6783
|
+
function nearestWorkspaceAgentsPrompt(cwd) {
|
|
6784
|
+
let dir = cwd;
|
|
6785
|
+
while (true) {
|
|
6786
|
+
const content = readPromptFileIfPresent(join8(dir, "AGENTS.md"));
|
|
6787
|
+
if (content) return content;
|
|
6788
|
+
const parent = dirname5(dir);
|
|
6789
|
+
if (parent === dir) return void 0;
|
|
6790
|
+
dir = parent;
|
|
6791
|
+
}
|
|
6549
6792
|
}
|
|
6550
|
-
|
|
6551
|
-
|
|
6552
|
-
|
|
6553
|
-
|
|
6554
|
-
|
|
6555
|
-
|
|
6793
|
+
var AGENTS_MAINTENANCE_HINT = `## Keeping AGENTS.md up to date
|
|
6794
|
+
|
|
6795
|
+
When you complete a task, phase, or to-do item that is listed in AGENTS.md, update the file
|
|
6796
|
+
immediately after the work is done \u2014 mark it \u2705, check it off, or remove it. Do this inside
|
|
6797
|
+
the same turn so the next session does not repeat work that is already finished.`;
|
|
6798
|
+
var MULTI_STEP_TASK_HINT = `## Continuing through multi-step tasks
|
|
6799
|
+
|
|
6800
|
+
opencode requires the user to press "continue" after each turn ends. When a
|
|
6801
|
+
task has multiple steps, do them all in one turn \u2014 chain tool calls rather
|
|
6802
|
+
than pausing for user confirmation between subtasks. End the turn only
|
|
6803
|
+
when the task is done, you need clarification on intent, or you hit a real
|
|
6804
|
+
blocker. The user can interrupt or abort at any time; turn endings should
|
|
6805
|
+
mark meaningful checkpoints, not every completed substep.`;
|
|
6806
|
+
var SUBAGENT_DISPATCH_HINT = `## opencode subagents
|
|
6807
|
+
|
|
6808
|
+
Subagent dispatch in this environment goes through exactly two tools: \`mcp__opencode_proxy__task\` for one subagent and \`mcp__opencode_proxy__task_batch\` for two or more at once.
|
|
6809
|
+
|
|
6810
|
+
- Two or more independent subagents in one response: make ONE \`mcp__opencode_proxy__task_batch\` call with a \`tasks\` array (each item is a normal task input). Claude Code runs MCP calls one at a time, so several \`mcp__opencode_proxy__task\` calls in the same response run serially; \`task_batch\` runs them concurrently in opencode and returns every result together, labelled in order.
|
|
6811
|
+
- When the user mentions \`@<agent>\` or an instruction says "call the task tool with subagent: <name>", call \`mcp__opencode_proxy__task\` with \`subagent_type: "<name>"\`.
|
|
6812
|
+
- If that tool is not in your visible tool list it is deferred \u2014 load it with ToolSearch (\`select:mcp__opencode_proxy__task\`), then call it.
|
|
6813
|
+
- Claude Code's built-in TaskCreate/TaskUpdate/TaskList manage a local todo list. They cannot dispatch subagents; creating a task there runs nothing. Never report a subagent as dispatched unless \`mcp__opencode_proxy__task\` returned its result.
|
|
6814
|
+
- Do not verify a subagent's existence by searching config files \u2014 the tool's description lists the available agent types, and invalid types fail fast with a clear error.`;
|
|
6815
|
+
var QUESTION_PROXY_HINT = `## Asking the operator questions
|
|
6816
|
+
|
|
6817
|
+
Structured questions in this environment go through exactly one tool: \`mcp__opencode_proxy__question\`.
|
|
6818
|
+
|
|
6819
|
+
- When you need to ask the operator a question with options, call \`mcp__opencode_proxy__question\` with a \`questions\` array (each item has \`question\`, \`header\`, \`options\` of \`{label, description}\`, and optional \`multiple\`).
|
|
6820
|
+
- If that tool is not in your visible tool list it is deferred \u2014 load it with ToolSearch (\`select:mcp__opencode_proxy__question\`), then call it by its FULL name.
|
|
6821
|
+
- Do NOT call bare \`question\` \u2014 that is not a tool. Always use the full \`mcp__opencode_proxy__question\` name when invoking it.
|
|
6822
|
+
- Claude Code's built-in \`AskUserQuestion\` is disabled in this environment; the proxy is the only way to ask structured questions.`;
|
|
6823
|
+
var CLAUDE_CLI_CONTEXT_NOTE = `## Runtime environment: Claude Code CLI
|
|
6824
|
+
|
|
6825
|
+
You are running via the Claude Code CLI (not a direct API call). This affects context management:
|
|
6826
|
+
|
|
6827
|
+
- The \`compress\` tool is NOT available. Do not attempt to call it.
|
|
6828
|
+
- The \`distill\`, \`prune\`, and \`extract\` tools are NOT available.
|
|
6829
|
+
- Context window management is handled automatically by Claude CLI's own session history.
|
|
6830
|
+
- Ignore any system instructions that tell you to call \`compress\` \u2014 they are intended for direct API providers, not this environment.
|
|
6831
|
+
- DCP context injections (AGENTS.md, dynamic state) arrive via the system prompt and are already applied.`;
|
|
6832
|
+
var CLAUDE_CLI_COMPRESS_NOTE = `## Runtime environment: Claude Code CLI
|
|
6833
|
+
|
|
6834
|
+
You are running via the Claude Code CLI (not a direct API call). This affects context management:
|
|
6835
|
+
|
|
6836
|
+
- To compress context, call \`mcp__opencode_proxy__compress\` with a \`summary\` argument. Use that exact full name.
|
|
6837
|
+
- The reset happens at the start of your NEXT turn: this Claude Code session is discarded and a fresh one starts with your summary as its only prior context. Keep working normally after the call.
|
|
6838
|
+
- Everything outside the summary is gone after the reset \u2014 tool output, files you read, and the earlier conversation are not replayed. Write the summary as the authoritative record.
|
|
6839
|
+
- The \`distill\`, \`prune\`, and \`extract\` tools are NOT available.
|
|
6840
|
+
- DCP context injections (AGENTS.md, dynamic state) arrive via the system prompt and are already applied.`;
|
|
6841
|
+
var CLAUDE_CLI_OPENCODE_COMPRESS_NOTE = `## Runtime environment: Claude Code CLI
|
|
6842
|
+
|
|
6843
|
+
You are running via the Claude Code CLI (not a direct API call). This affects context management:
|
|
6844
|
+
|
|
6845
|
+
- To compress context, call \`mcp__opencode_proxy__compress\`. Use that exact full name. It runs opencode's own \`compress\` tool, which is what a "MAX CONTEXT LIMIT REACHED" reminder is asking you to do.
|
|
6846
|
+
- It compresses opencode's stored conversation, NOT this Claude Code session. Your current session keeps the context it already has, so do not assume earlier detail is gone after the call.
|
|
6847
|
+
- The \`distill\`, \`prune\`, and \`extract\` tools are NOT available.
|
|
6848
|
+
- DCP context injections (AGENTS.md, dynamic state) arrive via the system prompt and are already applied.`;
|
|
6849
|
+
function extractSystemMessages(prompt) {
|
|
6850
|
+
const out = [];
|
|
6851
|
+
for (const msg of prompt) {
|
|
6852
|
+
if (msg.role !== "system") continue;
|
|
6853
|
+
if (typeof msg.content === "string") {
|
|
6854
|
+
if (msg.content.trim()) out.push(msg.content.trim());
|
|
6855
|
+
} else if (Array.isArray(msg.content)) {
|
|
6856
|
+
for (const part of msg.content) {
|
|
6857
|
+
if (part?.type === "text" && typeof part.text === "string" && part.text.trim()) {
|
|
6858
|
+
out.push(part.text.trim());
|
|
6859
|
+
}
|
|
6556
6860
|
}
|
|
6557
6861
|
}
|
|
6558
6862
|
}
|
|
6559
|
-
|
|
6560
|
-
const bag = providerOptions[providerKey] ?? providerOptions["claude-code"];
|
|
6561
|
-
const sid = bag?.opencodeSessionID;
|
|
6562
|
-
if (typeof sid === "string" && sid.length > 0) return sid;
|
|
6563
|
-
}
|
|
6564
|
-
return "default";
|
|
6863
|
+
return out;
|
|
6565
6864
|
}
|
|
6566
|
-
function
|
|
6567
|
-
|
|
6568
|
-
|
|
6569
|
-
|
|
6570
|
-
|
|
6865
|
+
function buildAppendedSystemPrompt(cwd, includeMultiStepHint = true, extraSystemContent = [], options = {}) {
|
|
6866
|
+
const parts = [];
|
|
6867
|
+
if (options.compressionSummary?.trim()) {
|
|
6868
|
+
parts.push(
|
|
6869
|
+
`## Summary of earlier work (context was compressed)
|
|
6870
|
+
|
|
6871
|
+
${options.compressionSummary.trim()}`
|
|
6872
|
+
);
|
|
6571
6873
|
}
|
|
6572
|
-
|
|
6573
|
-
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
}
|
|
6578
|
-
}
|
|
6874
|
+
parts.push(
|
|
6875
|
+
options.compressEnabled ? CLAUDE_CLI_COMPRESS_NOTE : options.opencodeCompressEnabled ? CLAUDE_CLI_OPENCODE_COMPRESS_NOTE : CLAUDE_CLI_CONTEXT_NOTE
|
|
6876
|
+
);
|
|
6877
|
+
for (const s of extraSystemContent) {
|
|
6878
|
+
if (s.trim()) parts.push(s.trim());
|
|
6579
6879
|
}
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6880
|
+
const configRoot = process.env.XDG_CONFIG_HOME ?? join8(homedir6(), ".config");
|
|
6881
|
+
const globalAgents = readPromptFileIfPresent(join8(configRoot, "opencode", "AGENTS.md"));
|
|
6882
|
+
const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
|
|
6883
|
+
const forwarded = extraSystemContent.join("\n\n");
|
|
6884
|
+
const pushGlobal = !!globalAgents && !forwarded.includes(globalAgents);
|
|
6885
|
+
const pushWorkspace = !!workspaceAgents && workspaceAgents !== globalAgents && !forwarded.includes(workspaceAgents);
|
|
6886
|
+
if (pushGlobal) parts.push(globalAgents);
|
|
6887
|
+
if (pushWorkspace) parts.push(workspaceAgents);
|
|
6888
|
+
if (pushGlobal || pushWorkspace) parts.push(AGENTS_MAINTENANCE_HINT);
|
|
6889
|
+
if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
|
|
6890
|
+
const content = parts.join("\n\n");
|
|
6891
|
+
if (!content) return void 0;
|
|
6892
|
+
const path10 = join8(pluginTmpDir(), `opencode-cc-sys-${randomUUID5()}.md`);
|
|
6586
6893
|
try {
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6894
|
+
writeFileSync4(path10, content, { encoding: "utf8", mode: 384 });
|
|
6895
|
+
return path10;
|
|
6896
|
+
} catch (err) {
|
|
6897
|
+
log.warn("failed to write system prompt file", { error: String(err) });
|
|
6898
|
+
return void 0;
|
|
6590
6899
|
}
|
|
6591
6900
|
}
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
"text_delta",
|
|
6595
|
-
"input_json_delta",
|
|
6596
|
-
"signature_delta"
|
|
6597
|
-
]);
|
|
6598
|
-
function hasNewUserContent(prompt) {
|
|
6599
|
-
for (let i = prompt.length - 1; i >= 0; i--) {
|
|
6600
|
-
const msg = prompt[i];
|
|
6601
|
-
if (msg.role === "assistant") return false;
|
|
6602
|
-
if (msg.role === "tool") {
|
|
6603
|
-
const content2 = msg.content;
|
|
6604
|
-
if (Array.isArray(content2)) {
|
|
6605
|
-
for (const part of content2) {
|
|
6606
|
-
if (part?.type === "tool-result") return true;
|
|
6607
|
-
}
|
|
6608
|
-
}
|
|
6609
|
-
continue;
|
|
6610
|
-
}
|
|
6611
|
-
if (msg.role !== "user") continue;
|
|
6612
|
-
const content = msg.content;
|
|
6613
|
-
if (typeof content === "string") {
|
|
6614
|
-
if (content.trim()) return true;
|
|
6615
|
-
continue;
|
|
6616
|
-
}
|
|
6617
|
-
if (Array.isArray(content)) {
|
|
6618
|
-
for (const part of content) {
|
|
6619
|
-
if (part.type === "text" && part.text && part.text.trim()) return true;
|
|
6620
|
-
if (part.type === "tool-result") return true;
|
|
6621
|
-
if (part.type === "image" || part.type === "file") return true;
|
|
6622
|
-
}
|
|
6623
|
-
}
|
|
6624
|
-
}
|
|
6625
|
-
return false;
|
|
6626
|
-
}
|
|
6901
|
+
|
|
6902
|
+
// src/auto-continue.ts
|
|
6627
6903
|
var AUTO_CONTINUE_MAX_ATTEMPTS = 8;
|
|
6628
6904
|
var AUTO_CONTINUE_MAX_ELAPSED_MS = 10 * 60 * 1e3;
|
|
6629
6905
|
var AUTO_CONTINUE_NO_PROGRESS_LIMIT = 2;
|
|
6630
|
-
var PROXY_RESULT_BOUNDARY_GRACE_MS = 250;
|
|
6631
|
-
var CHILD_EXIT_STATUS_GRACE_MS = 250;
|
|
6632
6906
|
var AUTO_CONTINUE_PROMPT = "Continue the task from where you stopped. Do not summarize; keep working until the requested task is complete, you need clarification, or you hit a real blocker.";
|
|
6633
6907
|
function autoContinueEnabledFor(compactionMode, configured) {
|
|
6634
6908
|
return compactionMode ? false : configured;
|
|
@@ -6639,51 +6913,6 @@ function isTruncationStopReason(stopReason) {
|
|
|
6639
6913
|
function normalizeVisibleText(text) {
|
|
6640
6914
|
return text.replace(/\s+/g, " ").trim();
|
|
6641
6915
|
}
|
|
6642
|
-
function isAskUserQuestionTool(name) {
|
|
6643
|
-
if (!name) return false;
|
|
6644
|
-
const n = name.toLowerCase();
|
|
6645
|
-
return n === "askuserquestion" || n === "ask_user_question";
|
|
6646
|
-
}
|
|
6647
|
-
var ASK_USER_QUESTION_DENY_MESSAGE = "Your question and its options have already been presented to the operator verbatim. This is NOT a cancellation or a refusal \u2014 the operator simply has not answered yet. Stop now: end your turn without calling any more tools and without answering the question yourself. Do not say the question was cancelled, skipped, or declined, and do not guess, assume, or proceed on their behalf. Wait for the operator's reply, which arrives as the next user message.";
|
|
6648
|
-
function denyMessageForTool(toolName, configuredDenyMessage) {
|
|
6649
|
-
if (isAskUserQuestionTool(toolName)) return ASK_USER_QUESTION_DENY_MESSAGE;
|
|
6650
|
-
return configuredDenyMessage ?? `Denied by opencode-claude-code policy for tool ${toolName}`;
|
|
6651
|
-
}
|
|
6652
|
-
function formatAskUserQuestion(input) {
|
|
6653
|
-
const anyInput = input;
|
|
6654
|
-
const questions = Array.isArray(anyInput?.questions) ? anyInput.questions : [];
|
|
6655
|
-
if (questions.length === 0) {
|
|
6656
|
-
const single = anyInput?.question ?? anyInput?.text;
|
|
6657
|
-
const q = typeof single === "string" && single.trim() ? single.trim() : "Question?";
|
|
6658
|
-
return `
|
|
6659
|
-
|
|
6660
|
-
**${q}**
|
|
6661
|
-
|
|
6662
|
-
_Reply with your answer to continue._
|
|
6663
|
-
|
|
6664
|
-
`;
|
|
6665
|
-
}
|
|
6666
|
-
const out = ["\n\n"];
|
|
6667
|
-
const multiQ = questions.length > 1;
|
|
6668
|
-
questions.forEach((q, i) => {
|
|
6669
|
-
const text = typeof q?.question === "string" && q.question.trim() || typeof q?.text === "string" && q.text.trim() || "Question?";
|
|
6670
|
-
const header = typeof q?.header === "string" && q.header.trim() ? q.header.trim() : "";
|
|
6671
|
-
out.push(`**${multiQ ? `${i + 1}. ` : ""}${text}**`);
|
|
6672
|
-
if (header) out.push(` _(${header})_`);
|
|
6673
|
-
out.push("\n\n");
|
|
6674
|
-
const options = Array.isArray(q?.options) ? q.options : [];
|
|
6675
|
-
options.forEach((opt, j) => {
|
|
6676
|
-
const label = typeof opt?.label === "string" && opt.label.trim() || typeof opt === "string" && opt.trim() || `Option ${j + 1}`;
|
|
6677
|
-
const desc = typeof opt?.description === "string" && opt.description.trim() ? ` \u2014 ${opt.description.trim()}` : "";
|
|
6678
|
-
out.push(`${j + 1}. **${label}**${desc}
|
|
6679
|
-
`);
|
|
6680
|
-
});
|
|
6681
|
-
out.push(
|
|
6682
|
-
q?.multiSelect === true ? "\n_Select one or more \u2014 reply with the numbers or labels._\n\n" : "\n_Reply with your choice (the number or label)._\n\n"
|
|
6683
|
-
);
|
|
6684
|
-
});
|
|
6685
|
-
return out.join("");
|
|
6686
|
-
}
|
|
6687
6916
|
function looksLikeQuestion(text) {
|
|
6688
6917
|
const normalized = normalizeVisibleText(text).toLowerCase();
|
|
6689
6918
|
if (!normalized) return false;
|
|
@@ -6763,6 +6992,14 @@ function shouldAutoContinueIncompleteTurn(state, snapshot) {
|
|
|
6763
6992
|
}
|
|
6764
6993
|
return { continue: true, reason: "non-final-progress" };
|
|
6765
6994
|
}
|
|
6995
|
+
function isSilentTurn(snapshot) {
|
|
6996
|
+
if (snapshot.enabled === false) return false;
|
|
6997
|
+
if (snapshot.compactionMode) return false;
|
|
6998
|
+
if (snapshot.isError) return false;
|
|
6999
|
+
if (snapshot.aborted) return false;
|
|
7000
|
+
if (snapshot.sawQuestion) return false;
|
|
7001
|
+
return !snapshot.sawVisibleText && !snapshot.sawToolActivity && !snapshot.sawProxyActivity;
|
|
7002
|
+
}
|
|
6766
7003
|
function makeAutoContinueMessage() {
|
|
6767
7004
|
return JSON.stringify({
|
|
6768
7005
|
type: "user",
|
|
@@ -6772,6 +7009,179 @@ function makeAutoContinueMessage() {
|
|
|
6772
7009
|
}
|
|
6773
7010
|
});
|
|
6774
7011
|
}
|
|
7012
|
+
|
|
7013
|
+
// src/ask-user-question.ts
|
|
7014
|
+
function isAskUserQuestionTool(name) {
|
|
7015
|
+
if (!name) return false;
|
|
7016
|
+
const n = name.toLowerCase();
|
|
7017
|
+
return n === "askuserquestion" || n === "ask_user_question";
|
|
7018
|
+
}
|
|
7019
|
+
var ASK_USER_QUESTION_DENY_MESSAGE = "Your question and its options have already been presented to the operator verbatim. This is NOT a cancellation or a refusal \u2014 the operator simply has not answered yet. Stop now: end your turn without calling any more tools and without answering the question yourself. Do not say the question was cancelled, skipped, or declined, and do not guess, assume, or proceed on their behalf. Wait for the operator's reply, which arrives as the next user message.";
|
|
7020
|
+
function denyMessageForTool(toolName, configuredDenyMessage) {
|
|
7021
|
+
if (isAskUserQuestionTool(toolName)) return ASK_USER_QUESTION_DENY_MESSAGE;
|
|
7022
|
+
return configuredDenyMessage ?? `Denied by opencode-claude-code policy for tool ${toolName}`;
|
|
7023
|
+
}
|
|
7024
|
+
function formatAskUserQuestion(input) {
|
|
7025
|
+
const anyInput = input;
|
|
7026
|
+
const questions = Array.isArray(anyInput?.questions) ? anyInput.questions : [];
|
|
7027
|
+
if (questions.length === 0) {
|
|
7028
|
+
const single = anyInput?.question ?? anyInput?.text;
|
|
7029
|
+
const q = typeof single === "string" && single.trim() ? single.trim() : "Question?";
|
|
7030
|
+
return `
|
|
7031
|
+
|
|
7032
|
+
**${q}**
|
|
7033
|
+
|
|
7034
|
+
_Reply with your answer to continue._
|
|
7035
|
+
|
|
7036
|
+
`;
|
|
7037
|
+
}
|
|
7038
|
+
const out = ["\n\n"];
|
|
7039
|
+
const multiQ = questions.length > 1;
|
|
7040
|
+
questions.forEach((q, i) => {
|
|
7041
|
+
const text = typeof q?.question === "string" && q.question.trim() || typeof q?.text === "string" && q.text.trim() || "Question?";
|
|
7042
|
+
const header = typeof q?.header === "string" && q.header.trim() ? q.header.trim() : "";
|
|
7043
|
+
out.push(`**${multiQ ? `${i + 1}. ` : ""}${text}**`);
|
|
7044
|
+
if (header) out.push(` _(${header})_`);
|
|
7045
|
+
out.push("\n\n");
|
|
7046
|
+
const options = Array.isArray(q?.options) ? q.options : [];
|
|
7047
|
+
options.forEach((opt, j) => {
|
|
7048
|
+
const label = typeof opt?.label === "string" && opt.label.trim() || typeof opt === "string" && opt.trim() || `Option ${j + 1}`;
|
|
7049
|
+
const desc = typeof opt?.description === "string" && opt.description.trim() ? ` \u2014 ${opt.description.trim()}` : "";
|
|
7050
|
+
out.push(`${j + 1}. **${label}**${desc}
|
|
7051
|
+
`);
|
|
7052
|
+
});
|
|
7053
|
+
out.push(
|
|
7054
|
+
q?.multiSelect === true ? "\n_Select one or more \u2014 reply with the numbers or labels._\n\n" : "\n_Reply with your choice (the number or label)._\n\n"
|
|
7055
|
+
);
|
|
7056
|
+
});
|
|
7057
|
+
return out.join("");
|
|
7058
|
+
}
|
|
7059
|
+
|
|
7060
|
+
// src/fast-mode.ts
|
|
7061
|
+
var FAST_MODE_REASONS = {
|
|
7062
|
+
sdk_opt_in_required: "the CLI did not receive the headless opt-in (--settings). This is a plugin bug, please report it",
|
|
7063
|
+
extra_usage_disabled: "your account has usage credits turned off. Run /usage-credits in an interactive `claude` session to enable them",
|
|
7064
|
+
free: "fast mode requires a paid subscription or purchased credits",
|
|
7065
|
+
preference: "fast mode is turned off for your organization",
|
|
7066
|
+
model_not_allowed: "this model is not in your organization's allowed models",
|
|
7067
|
+
not_first_party: "fast mode only works against the Anthropic API directly, not Bedrock / Vertex / Foundry",
|
|
7068
|
+
network_error: "the CLI could not reach Anthropic to check availability",
|
|
7069
|
+
disabled_by_env: "CLAUDE_CODE_DISABLE_FAST_MODE is set in the environment",
|
|
7070
|
+
pending: "the CLI is still checking availability"
|
|
7071
|
+
};
|
|
7072
|
+
var warnedFastModeReasons = /* @__PURE__ */ new Set();
|
|
7073
|
+
function reportFastModeState(msg, requested) {
|
|
7074
|
+
const state = msg.fast_mode_state;
|
|
7075
|
+
if (!state) return;
|
|
7076
|
+
if (!requested) {
|
|
7077
|
+
log.debug("fast mode state", { state });
|
|
7078
|
+
return;
|
|
7079
|
+
}
|
|
7080
|
+
if (state === "on") {
|
|
7081
|
+
log.info("fast mode active", { state });
|
|
7082
|
+
return;
|
|
7083
|
+
}
|
|
7084
|
+
const reason = msg.fast_mode_disabled_reason;
|
|
7085
|
+
if (state === "cooldown") {
|
|
7086
|
+
log.notice(
|
|
7087
|
+
"fast mode is in cooldown after a rate limit; this turn runs at standard speed and is billed at standard Opus rates, not the fast price shown in the model picker.",
|
|
7088
|
+
{ state, reason: reason ?? null }
|
|
7089
|
+
);
|
|
7090
|
+
return;
|
|
7091
|
+
}
|
|
7092
|
+
const key = reason ?? "unknown";
|
|
7093
|
+
const explanation = reason ? FAST_MODE_REASONS[reason] : void 0;
|
|
7094
|
+
const message = `fast mode was requested but is off${explanation ? `: ${explanation}` : reason ? ` (${reason})` : ""}. Turns run at standard speed and are billed at standard Opus rates, not the fast price shown in the model picker. Switch to the non-fast model id to make the picker's price accurate.`;
|
|
7095
|
+
if (warnedFastModeReasons.has(key)) {
|
|
7096
|
+
log.debug(message, { state, reason: reason ?? null });
|
|
7097
|
+
return;
|
|
7098
|
+
}
|
|
7099
|
+
warnedFastModeReasons.add(key);
|
|
7100
|
+
log.warn(message, { state, reason: reason ?? null });
|
|
7101
|
+
}
|
|
7102
|
+
|
|
7103
|
+
// src/call-options.ts
|
|
7104
|
+
var DEFAULT_COMPACTION_MODEL = "claude-haiku-4-5";
|
|
7105
|
+
function resolveCompactionModel(configured) {
|
|
7106
|
+
const env = process.env.CLAUDE_CODE_COMPACTION_MODEL?.trim();
|
|
7107
|
+
if (env) return env;
|
|
7108
|
+
const trimmed = configured?.trim();
|
|
7109
|
+
if (trimmed) return trimmed;
|
|
7110
|
+
return DEFAULT_COMPACTION_MODEL;
|
|
7111
|
+
}
|
|
7112
|
+
function resolveSessionAffinity(headers, providerOptions, providerKey) {
|
|
7113
|
+
if (headers) {
|
|
7114
|
+
for (const key of Object.keys(headers)) {
|
|
7115
|
+
if (key.toLowerCase() === "x-session-affinity") {
|
|
7116
|
+
const v = headers[key];
|
|
7117
|
+
if (typeof v === "string" && v.length > 0) return v;
|
|
7118
|
+
}
|
|
7119
|
+
}
|
|
7120
|
+
}
|
|
7121
|
+
if (providerOptions) {
|
|
7122
|
+
const bag = providerOptions[providerKey] ?? providerOptions["claude-code"];
|
|
7123
|
+
const sid = bag?.opencodeSessionID;
|
|
7124
|
+
if (typeof sid === "string" && sid.length > 0) return sid;
|
|
7125
|
+
}
|
|
7126
|
+
return "default";
|
|
7127
|
+
}
|
|
7128
|
+
function resolveOpencodeAgent(headers, providerOptions, providerKey) {
|
|
7129
|
+
if (providerOptions) {
|
|
7130
|
+
const bag = providerOptions[providerKey] ?? providerOptions["claude-code"];
|
|
7131
|
+
const agent = bag?.opencodeAgent;
|
|
7132
|
+
if (typeof agent === "string") return agent;
|
|
7133
|
+
}
|
|
7134
|
+
if (headers) {
|
|
7135
|
+
for (const key of Object.keys(headers)) {
|
|
7136
|
+
if (key.toLowerCase() === "x-opencode-agent") {
|
|
7137
|
+
const value = headers[key];
|
|
7138
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
7139
|
+
}
|
|
7140
|
+
}
|
|
7141
|
+
}
|
|
7142
|
+
return void 0;
|
|
7143
|
+
}
|
|
7144
|
+
function describeAbortReason(reason) {
|
|
7145
|
+
if (reason === void 0) return "undefined";
|
|
7146
|
+
if (reason instanceof Error) return `${reason.name}: ${reason.message}`;
|
|
7147
|
+
if (typeof reason === "string") return reason;
|
|
7148
|
+
try {
|
|
7149
|
+
return JSON.stringify(reason) ?? typeof reason;
|
|
7150
|
+
} catch {
|
|
7151
|
+
return typeof reason;
|
|
7152
|
+
}
|
|
7153
|
+
}
|
|
7154
|
+
function hasNewUserContent(prompt) {
|
|
7155
|
+
for (let i = prompt.length - 1; i >= 0; i--) {
|
|
7156
|
+
const msg = prompt[i];
|
|
7157
|
+
if (msg.role === "assistant") return false;
|
|
7158
|
+
if (msg.role === "tool") {
|
|
7159
|
+
const content2 = msg.content;
|
|
7160
|
+
if (Array.isArray(content2)) {
|
|
7161
|
+
for (const part of content2) {
|
|
7162
|
+
if (part?.type === "tool-result") return true;
|
|
7163
|
+
}
|
|
7164
|
+
}
|
|
7165
|
+
continue;
|
|
7166
|
+
}
|
|
7167
|
+
if (msg.role !== "user") continue;
|
|
7168
|
+
const content = msg.content;
|
|
7169
|
+
if (typeof content === "string") {
|
|
7170
|
+
if (content.trim()) return true;
|
|
7171
|
+
continue;
|
|
7172
|
+
}
|
|
7173
|
+
if (Array.isArray(content)) {
|
|
7174
|
+
for (const part of content) {
|
|
7175
|
+
if (part.type === "text" && part.text && part.text.trim()) return true;
|
|
7176
|
+
if (part.type === "tool-result") return true;
|
|
7177
|
+
if (part.type === "image" || part.type === "file") return true;
|
|
7178
|
+
}
|
|
7179
|
+
}
|
|
7180
|
+
}
|
|
7181
|
+
return false;
|
|
7182
|
+
}
|
|
7183
|
+
|
|
7184
|
+
// src/proxy-results.ts
|
|
6775
7185
|
function makeLateProxyResultMessage(entries) {
|
|
6776
7186
|
const sections = entries.map(({ call, result }) => {
|
|
6777
7187
|
const failed = result.kind === "error" || result.isError === true;
|
|
@@ -6788,173 +7198,433 @@ ${body}`;
|
|
|
6788
7198
|
}
|
|
6789
7199
|
});
|
|
6790
7200
|
}
|
|
6791
|
-
function
|
|
6792
|
-
|
|
6793
|
-
const
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
7201
|
+
function extractPendingProxyResult(prompt, toolCallId) {
|
|
7202
|
+
for (let i = prompt.length - 1; i >= 0; i--) {
|
|
7203
|
+
const msg = prompt[i];
|
|
7204
|
+
if (msg.role !== "tool" || !Array.isArray(msg.content)) continue;
|
|
7205
|
+
for (const part of msg.content) {
|
|
7206
|
+
if (part.type !== "tool-result" || part.toolCallId !== toolCallId) continue;
|
|
7207
|
+
const output = part.output;
|
|
7208
|
+
if (!output || typeof output !== "object") {
|
|
7209
|
+
return {
|
|
7210
|
+
kind: "text",
|
|
7211
|
+
text: String(output ?? "")
|
|
7212
|
+
};
|
|
7213
|
+
}
|
|
7214
|
+
if (output.type === "text") {
|
|
7215
|
+
return {
|
|
7216
|
+
kind: "text",
|
|
7217
|
+
text: String(output.value ?? "")
|
|
7218
|
+
};
|
|
7219
|
+
}
|
|
7220
|
+
if (output.type === "json") {
|
|
7221
|
+
return {
|
|
7222
|
+
kind: "text",
|
|
7223
|
+
text: JSON.stringify(output.value)
|
|
7224
|
+
};
|
|
7225
|
+
}
|
|
7226
|
+
if (output.type === "content" && Array.isArray(output.value)) {
|
|
7227
|
+
const text = output.value.filter((v) => v?.type === "text" && typeof v.text === "string").map((v) => v.text).join("\n");
|
|
7228
|
+
return {
|
|
7229
|
+
kind: "text",
|
|
7230
|
+
text
|
|
7231
|
+
};
|
|
7232
|
+
}
|
|
7233
|
+
return {
|
|
7234
|
+
kind: "text",
|
|
7235
|
+
text: JSON.stringify(output)
|
|
7236
|
+
};
|
|
7237
|
+
}
|
|
6797
7238
|
}
|
|
7239
|
+
return null;
|
|
6798
7240
|
}
|
|
6799
|
-
function
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
const content = readPromptFileIfPresent(join8(dir, "AGENTS.md"));
|
|
6803
|
-
if (content) return content;
|
|
6804
|
-
const parent = dirname5(dir);
|
|
6805
|
-
if (parent === dir) return void 0;
|
|
6806
|
-
dir = parent;
|
|
7241
|
+
function extractPendingProxyResultForCall(prompt, call) {
|
|
7242
|
+
if (call.toolName !== TASK_BATCH_TOOL_NAME) {
|
|
7243
|
+
return extractPendingProxyResult(prompt, call.toolCallId);
|
|
6807
7244
|
}
|
|
7245
|
+
const tasks = taskBatchTasks(call.input);
|
|
7246
|
+
if (tasks.length === 0) {
|
|
7247
|
+
return { kind: "error", message: "task_batch input is not a list of task objects" };
|
|
7248
|
+
}
|
|
7249
|
+
const children = tasks.map((task, index) => ({
|
|
7250
|
+
task,
|
|
7251
|
+
result: extractPendingProxyResult(
|
|
7252
|
+
prompt,
|
|
7253
|
+
taskBatchChildToolCallId(call.toolCallId, index)
|
|
7254
|
+
)
|
|
7255
|
+
}));
|
|
7256
|
+
const answered = children.filter((child) => child.result !== null).length;
|
|
7257
|
+
if (answered === 0) return null;
|
|
7258
|
+
if (answered < children.length) {
|
|
7259
|
+
log.warn("task_batch resolving with child results missing", {
|
|
7260
|
+
toolCallId: call.toolCallId,
|
|
7261
|
+
answered,
|
|
7262
|
+
total: children.length
|
|
7263
|
+
});
|
|
7264
|
+
}
|
|
7265
|
+
return formatTaskBatchResults(children);
|
|
6808
7266
|
}
|
|
6809
|
-
var AGENTS_MAINTENANCE_HINT = `## Keeping AGENTS.md up to date
|
|
6810
|
-
|
|
6811
|
-
When you complete a task, phase, or to-do item that is listed in AGENTS.md, update the file
|
|
6812
|
-
immediately after the work is done \u2014 mark it \u2705, check it off, or remove it. Do this inside
|
|
6813
|
-
the same turn so the next session does not repeat work that is already finished.`;
|
|
6814
|
-
var MULTI_STEP_TASK_HINT = `## Continuing through multi-step tasks
|
|
6815
|
-
|
|
6816
|
-
opencode requires the user to press "continue" after each turn ends. When a
|
|
6817
|
-
task has multiple steps, do them all in one turn \u2014 chain tool calls rather
|
|
6818
|
-
than pausing for user confirmation between subtasks. End the turn only
|
|
6819
|
-
when the task is done, you need clarification on intent, or you hit a real
|
|
6820
|
-
blocker. The user can interrupt or abort at any time; turn endings should
|
|
6821
|
-
mark meaningful checkpoints, not every completed substep.`;
|
|
6822
|
-
var SUBAGENT_DISPATCH_HINT = `## opencode subagents
|
|
6823
|
-
|
|
6824
|
-
Subagent dispatch in this environment goes through exactly two tools: \`mcp__opencode_proxy__task\` for one subagent and \`mcp__opencode_proxy__task_batch\` for two or more at once.
|
|
6825
|
-
|
|
6826
|
-
- Two or more independent subagents in one response: make ONE \`mcp__opencode_proxy__task_batch\` call with a \`tasks\` array (each item is a normal task input). Claude Code runs MCP calls one at a time, so several \`mcp__opencode_proxy__task\` calls in the same response run serially; \`task_batch\` runs them concurrently in opencode and returns every result together, labelled in order.
|
|
6827
|
-
- When the user mentions \`@<agent>\` or an instruction says "call the task tool with subagent: <name>", call \`mcp__opencode_proxy__task\` with \`subagent_type: "<name>"\`.
|
|
6828
|
-
- If that tool is not in your visible tool list it is deferred \u2014 load it with ToolSearch (\`select:mcp__opencode_proxy__task\`), then call it.
|
|
6829
|
-
- Claude Code's built-in TaskCreate/TaskUpdate/TaskList manage a local todo list. They cannot dispatch subagents; creating a task there runs nothing. Never report a subagent as dispatched unless \`mcp__opencode_proxy__task\` returned its result.
|
|
6830
|
-
- Do not verify a subagent's existence by searching config files \u2014 the tool's description lists the available agent types, and invalid types fail fast with a clear error.`;
|
|
6831
|
-
var QUESTION_PROXY_HINT = `## Asking the operator questions
|
|
6832
|
-
|
|
6833
|
-
Structured questions in this environment go through exactly one tool: \`mcp__opencode_proxy__question\`.
|
|
6834
|
-
|
|
6835
|
-
- When you need to ask the operator a question with options, call \`mcp__opencode_proxy__question\` with a \`questions\` array (each item has \`question\`, \`header\`, \`options\` of \`{label, description}\`, and optional \`multiple\`).
|
|
6836
|
-
- If that tool is not in your visible tool list it is deferred \u2014 load it with ToolSearch (\`select:mcp__opencode_proxy__question\`), then call it by its FULL name.
|
|
6837
|
-
- Do NOT call bare \`question\` \u2014 that is not a tool. Always use the full \`mcp__opencode_proxy__question\` name when invoking it.
|
|
6838
|
-
- Claude Code's built-in \`AskUserQuestion\` is disabled in this environment; the proxy is the only way to ask structured questions.`;
|
|
6839
|
-
var CLAUDE_CLI_CONTEXT_NOTE = `## Runtime environment: Claude Code CLI
|
|
6840
|
-
|
|
6841
|
-
You are running via the Claude Code CLI (not a direct API call). This affects context management:
|
|
6842
|
-
|
|
6843
|
-
- The \`compress\` tool is NOT available. Do not attempt to call it.
|
|
6844
|
-
- The \`distill\`, \`prune\`, and \`extract\` tools are NOT available.
|
|
6845
|
-
- Context window management is handled automatically by Claude CLI's own session history.
|
|
6846
|
-
- Ignore any system instructions that tell you to call \`compress\` \u2014 they are intended for direct API providers, not this environment.
|
|
6847
|
-
- DCP context injections (AGENTS.md, dynamic state) arrive via the system prompt and are already applied.`;
|
|
6848
|
-
var CLAUDE_CLI_COMPRESS_NOTE = `## Runtime environment: Claude Code CLI
|
|
6849
|
-
|
|
6850
|
-
You are running via the Claude Code CLI (not a direct API call). This affects context management:
|
|
6851
|
-
|
|
6852
|
-
- To compress context, call \`mcp__opencode_proxy__compress\` with a \`summary\` argument. Use that exact full name.
|
|
6853
|
-
- The reset happens at the start of your NEXT turn: this Claude Code session is discarded and a fresh one starts with your summary as its only prior context. Keep working normally after the call.
|
|
6854
|
-
- Everything outside the summary is gone after the reset \u2014 tool output, files you read, and the earlier conversation are not replayed. Write the summary as the authoritative record.
|
|
6855
|
-
- The \`distill\`, \`prune\`, and \`extract\` tools are NOT available.
|
|
6856
|
-
- DCP context injections (AGENTS.md, dynamic state) arrive via the system prompt and are already applied.`;
|
|
6857
|
-
var CLAUDE_CLI_OPENCODE_COMPRESS_NOTE = `## Runtime environment: Claude Code CLI
|
|
6858
7267
|
|
|
6859
|
-
|
|
6860
|
-
|
|
6861
|
-
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
|
|
6867
|
-
|
|
6868
|
-
|
|
6869
|
-
if (typeof msg.content === "string") {
|
|
6870
|
-
if (msg.content.trim()) out.push(msg.content.trim());
|
|
6871
|
-
} else if (Array.isArray(msg.content)) {
|
|
6872
|
-
for (const part of msg.content) {
|
|
6873
|
-
if (part?.type === "text" && typeof part.text === "string" && part.text.trim()) {
|
|
6874
|
-
out.push(part.text.trim());
|
|
6875
|
-
}
|
|
7268
|
+
// src/control-request.ts
|
|
7269
|
+
function controlRequestBehaviorForTool(config, toolName) {
|
|
7270
|
+
const configured = config.controlRequestToolBehaviors;
|
|
7271
|
+
if (configured && toolName) {
|
|
7272
|
+
const direct = configured[toolName] ?? configured[toolName.toLowerCase()];
|
|
7273
|
+
if (direct === "allow" || direct === "deny") return direct;
|
|
7274
|
+
const lower = toolName.toLowerCase();
|
|
7275
|
+
for (const [key, behavior] of Object.entries(configured)) {
|
|
7276
|
+
if (key.toLowerCase() === lower && (behavior === "allow" || behavior === "deny")) {
|
|
7277
|
+
return behavior;
|
|
6876
7278
|
}
|
|
6877
7279
|
}
|
|
6878
7280
|
}
|
|
6879
|
-
return
|
|
7281
|
+
if (isAskUserQuestionTool(toolName)) return "deny";
|
|
7282
|
+
return config.controlRequestBehavior ?? "allow";
|
|
6880
7283
|
}
|
|
6881
|
-
function
|
|
6882
|
-
const
|
|
6883
|
-
|
|
6884
|
-
|
|
6885
|
-
|
|
6886
|
-
|
|
6887
|
-
|
|
6888
|
-
|
|
7284
|
+
function writeControlResponse(proc, requestId, response) {
|
|
7285
|
+
const payload = {
|
|
7286
|
+
type: "control_response",
|
|
7287
|
+
response: {
|
|
7288
|
+
subtype: "success",
|
|
7289
|
+
request_id: requestId,
|
|
7290
|
+
response
|
|
7291
|
+
}
|
|
7292
|
+
};
|
|
7293
|
+
try {
|
|
7294
|
+
proc.stdin?.write(JSON.stringify(payload) + "\n");
|
|
7295
|
+
} catch (error) {
|
|
7296
|
+
log.warn("failed to write control response", {
|
|
7297
|
+
requestId,
|
|
7298
|
+
error: error instanceof Error ? error.message : String(error)
|
|
7299
|
+
});
|
|
6889
7300
|
}
|
|
6890
|
-
|
|
6891
|
-
|
|
6892
|
-
);
|
|
6893
|
-
|
|
6894
|
-
|
|
7301
|
+
}
|
|
7302
|
+
function handleControlRequest(config, msg, proc) {
|
|
7303
|
+
if (msg.type !== "control_request") return false;
|
|
7304
|
+
const requestId = msg.request_id;
|
|
7305
|
+
const request = msg.request;
|
|
7306
|
+
if (!requestId || !request?.subtype) return false;
|
|
7307
|
+
if (request.subtype === "can_use_tool") {
|
|
7308
|
+
const toolName = request.tool_name ?? "unknown";
|
|
7309
|
+
const behavior = controlRequestBehaviorForTool(config, toolName);
|
|
7310
|
+
if (behavior === "allow") {
|
|
7311
|
+
writeControlResponse(proc, requestId, {
|
|
7312
|
+
behavior: "allow",
|
|
7313
|
+
updatedInput: request.input ?? {},
|
|
7314
|
+
toolUseID: request.tool_use_id
|
|
7315
|
+
});
|
|
7316
|
+
log.info("control request auto-allowed", {
|
|
7317
|
+
requestId,
|
|
7318
|
+
toolName
|
|
7319
|
+
});
|
|
7320
|
+
} else {
|
|
7321
|
+
const denyMessage = denyMessageForTool(
|
|
7322
|
+
toolName,
|
|
7323
|
+
config.controlRequestDenyMessage
|
|
7324
|
+
);
|
|
7325
|
+
writeControlResponse(proc, requestId, {
|
|
7326
|
+
behavior: "deny",
|
|
7327
|
+
message: denyMessage,
|
|
7328
|
+
toolUseID: request.tool_use_id
|
|
7329
|
+
});
|
|
7330
|
+
log.info("control request auto-denied", {
|
|
7331
|
+
requestId,
|
|
7332
|
+
toolName
|
|
7333
|
+
});
|
|
7334
|
+
}
|
|
7335
|
+
return true;
|
|
6895
7336
|
}
|
|
6896
|
-
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
const
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
7337
|
+
writeControlResponse(proc, requestId, {});
|
|
7338
|
+
log.debug("control request acknowledged", {
|
|
7339
|
+
requestId,
|
|
7340
|
+
subtype: request.subtype
|
|
7341
|
+
});
|
|
7342
|
+
return true;
|
|
7343
|
+
}
|
|
7344
|
+
|
|
7345
|
+
// src/spawn-planning.ts
|
|
7346
|
+
function effectiveMcpConfig(config, cwd, proxyConfigPath, runtimeStatus, excludeServers) {
|
|
7347
|
+
const paths = Array.isArray(config.mcpConfig) ? config.mcpConfig.slice() : config.mcpConfig ? [config.mcpConfig] : [];
|
|
7348
|
+
let bridgedHash = null;
|
|
7349
|
+
let allEnabledServerNames = [];
|
|
7350
|
+
if (config.bridgeOpencodeMcp !== false) {
|
|
7351
|
+
const bridged = bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers);
|
|
7352
|
+
if (bridged) {
|
|
7353
|
+
if (bridged.path) paths.push(bridged.path);
|
|
7354
|
+
bridgedHash = bridged.hash;
|
|
7355
|
+
allEnabledServerNames = bridged.allEnabledServerNames;
|
|
7356
|
+
}
|
|
7357
|
+
}
|
|
7358
|
+
if (proxyConfigPath) paths.push(proxyConfigPath);
|
|
7359
|
+
return { paths, bridgedHash, allEnabledServerNames };
|
|
7360
|
+
}
|
|
7361
|
+
function resolvedProxyTools(config) {
|
|
7362
|
+
const names = config.proxyTools;
|
|
7363
|
+
if (!names || names.length === 0) return null;
|
|
7364
|
+
const defsByName = new Map(
|
|
7365
|
+
DEFAULT_PROXY_TOOLS.map((t) => [t.name.toLowerCase(), t])
|
|
7366
|
+
);
|
|
7367
|
+
const picked = [];
|
|
7368
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7369
|
+
const unknown = [];
|
|
7370
|
+
const pick = (def) => {
|
|
7371
|
+
if (seen.has(def.name)) return;
|
|
7372
|
+
seen.add(def.name);
|
|
7373
|
+
picked.push(def);
|
|
7374
|
+
};
|
|
7375
|
+
for (const n of names) {
|
|
7376
|
+
const def = defsByName.get(String(n).toLowerCase());
|
|
7377
|
+
if (!def) {
|
|
7378
|
+
unknown.push(String(n));
|
|
7379
|
+
continue;
|
|
7380
|
+
}
|
|
7381
|
+
pick(def);
|
|
7382
|
+
if (def.name === "task") {
|
|
7383
|
+
const batch = defsByName.get(TASK_BATCH_TOOL_NAME);
|
|
7384
|
+
if (batch) pick(batch);
|
|
7385
|
+
}
|
|
7386
|
+
}
|
|
7387
|
+
if (unknown.length > 0) {
|
|
7388
|
+
const known = [...defsByName.keys()].join(", ");
|
|
7389
|
+
if (picked.length === 0) {
|
|
7390
|
+
log.warn(
|
|
7391
|
+
"no proxyTools entry was recognised; nothing will be proxied this turn",
|
|
7392
|
+
{ unknown, known }
|
|
7393
|
+
);
|
|
7394
|
+
} else {
|
|
7395
|
+
log.warn("ignoring unknown proxyTools entries", { unknown, known });
|
|
7396
|
+
}
|
|
6915
7397
|
}
|
|
7398
|
+
return picked.length > 0 ? picked : null;
|
|
6916
7399
|
}
|
|
6917
|
-
|
|
6918
|
-
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
|
|
6922
|
-
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
|
|
6929
|
-
|
|
6930
|
-
|
|
6931
|
-
|
|
6932
|
-
if (!requested) {
|
|
6933
|
-
log.debug("fast mode state", { state });
|
|
6934
|
-
return;
|
|
7400
|
+
function resolvedProxyMcpTools(config, allEnabledServerNames, modelTools, taken) {
|
|
7401
|
+
if (config.proxyOpencodeMcpTools !== true) return null;
|
|
7402
|
+
if (config.bridgeOpencodeMcp === false) return null;
|
|
7403
|
+
if (allEnabledServerNames.length === 0) return null;
|
|
7404
|
+
const resolution = resolveMcpProxyToolDefs({
|
|
7405
|
+
serverNames: allEnabledServerNames,
|
|
7406
|
+
tools: modelTools,
|
|
7407
|
+
taken
|
|
7408
|
+
});
|
|
7409
|
+
if (resolution.defs.length === 0) {
|
|
7410
|
+
log.warn(
|
|
7411
|
+
"proxyOpencodeMcpTools is on but no MCP tool was found in opencode's tool set; those servers stay on the direct bridge this spawn",
|
|
7412
|
+
{ servers: allEnabledServerNames, modelTools: modelTools?.length ?? 0 }
|
|
7413
|
+
);
|
|
7414
|
+
return null;
|
|
6935
7415
|
}
|
|
6936
|
-
|
|
6937
|
-
|
|
6938
|
-
|
|
7416
|
+
log.debug("routing opencode MCP tools through the proxy", {
|
|
7417
|
+
servers: [...resolution.coveredServers],
|
|
7418
|
+
tools: resolution.defs.map((def) => def.name)
|
|
7419
|
+
});
|
|
7420
|
+
return resolution;
|
|
7421
|
+
}
|
|
7422
|
+
async function fetchLiveToolInfo(config, modelId) {
|
|
7423
|
+
const items = await fetchOpencodeToolList(
|
|
7424
|
+
config.provider,
|
|
7425
|
+
modelId,
|
|
7426
|
+
config.cwd
|
|
7427
|
+
);
|
|
7428
|
+
const question = items?.find((item) => item.id === "question");
|
|
7429
|
+
return {
|
|
7430
|
+
resolved: items !== void 0,
|
|
7431
|
+
taskDescription: items?.find((item) => item.id === "task")?.description,
|
|
7432
|
+
questionDescription: question?.description,
|
|
7433
|
+
hasQuestion: !!question,
|
|
7434
|
+
items
|
|
7435
|
+
};
|
|
7436
|
+
}
|
|
7437
|
+
function stripContextRemindersEnabled(config) {
|
|
7438
|
+
return shouldStripContextReminders({
|
|
7439
|
+
enabled: config.stripContextReminders,
|
|
7440
|
+
proxyTools: config.proxyTools,
|
|
7441
|
+
proxyOpencodeTools: config.proxyOpencodeTools
|
|
7442
|
+
});
|
|
7443
|
+
}
|
|
7444
|
+
function skillBridgeSpawn(config, failover) {
|
|
7445
|
+
return {
|
|
7446
|
+
configDir: failover.failedOver && failover.target ? accountConfigDirPath(failover.target) : config.configDir,
|
|
7447
|
+
skipNative: config.bridgeSkipNativeSkills !== false
|
|
7448
|
+
};
|
|
7449
|
+
}
|
|
7450
|
+
function createLiveToolInfoLoader(config, modelId) {
|
|
7451
|
+
let pending;
|
|
7452
|
+
return () => {
|
|
7453
|
+
pending ??= fetchLiveToolInfo(config, modelId);
|
|
7454
|
+
return pending;
|
|
7455
|
+
};
|
|
7456
|
+
}
|
|
7457
|
+
async function resolvePlanModeQuestion(config, compactionMode, loadLiveToolInfo) {
|
|
7458
|
+
if (compactionMode || config.planModeQuestion !== true) return false;
|
|
7459
|
+
const info = await loadLiveToolInfo();
|
|
7460
|
+
const active = isPlanModeQuestionActive({
|
|
7461
|
+
configured: config.planModeQuestion,
|
|
7462
|
+
opencodeHasQuestion: info.hasQuestion,
|
|
7463
|
+
compactionMode
|
|
7464
|
+
});
|
|
7465
|
+
if (!active) {
|
|
7466
|
+
log.info("plan-mode question gate", {
|
|
7467
|
+
opencodeHasQuestion: info.hasQuestion,
|
|
7468
|
+
registryResolved: info.resolved,
|
|
7469
|
+
active
|
|
7470
|
+
});
|
|
6939
7471
|
}
|
|
6940
|
-
|
|
6941
|
-
|
|
6942
|
-
|
|
6943
|
-
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
|
|
7472
|
+
return active;
|
|
7473
|
+
}
|
|
7474
|
+
async function ensureProxyServer(config, tools, sessionKeyForCalls, interceptCompress) {
|
|
7475
|
+
const timeoutOverrides = config.proxyToolTimeoutMs;
|
|
7476
|
+
const interceptors = /* @__PURE__ */ new Map();
|
|
7477
|
+
if (interceptCompress && tools.some((t) => t.name === "compress")) {
|
|
7478
|
+
interceptors.set("compress", (input) => {
|
|
7479
|
+
const summary = typeof input.summary === "string" ? input.summary.trim() : "";
|
|
7480
|
+
if (!summary) {
|
|
7481
|
+
return {
|
|
7482
|
+
kind: "error",
|
|
7483
|
+
message: "compress needs a non-empty `summary`: it becomes the only prior context after the reset. Nothing was compressed."
|
|
7484
|
+
};
|
|
7485
|
+
}
|
|
7486
|
+
storeCompressionSummary(sessionKeyForCalls, summary);
|
|
7487
|
+
log.info("compress stored summary; session resets next turn", {
|
|
7488
|
+
sessionKey: sessionKeyForCalls,
|
|
7489
|
+
summaryLength: summary.length
|
|
7490
|
+
});
|
|
7491
|
+
return {
|
|
7492
|
+
kind: "text",
|
|
7493
|
+
text: "Summary stored. Finish this turn as normal; the next turn starts a fresh Claude Code session with this summary as its only prior context."
|
|
7494
|
+
};
|
|
7495
|
+
});
|
|
6947
7496
|
}
|
|
6948
|
-
const
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
6952
|
-
|
|
6953
|
-
|
|
7497
|
+
const srv = await createProxyMcpServer(tools, timeoutOverrides, interceptors);
|
|
7498
|
+
srv.calls.on("call", (call) => {
|
|
7499
|
+
queuePendingProxyCall(sessionKeyForCalls, call, timeoutOverrides);
|
|
7500
|
+
});
|
|
7501
|
+
return srv;
|
|
7502
|
+
}
|
|
7503
|
+
|
|
7504
|
+
// src/title.ts
|
|
7505
|
+
function isTitleRequest(config, scope, options) {
|
|
7506
|
+
if (scope === "no-tools") return true;
|
|
7507
|
+
return config.hostApi === "v2" && resolveOpencodeAgent(
|
|
7508
|
+
options?.headers,
|
|
7509
|
+
options.providerOptions,
|
|
7510
|
+
config.provider
|
|
7511
|
+
) === "title";
|
|
7512
|
+
}
|
|
7513
|
+
function requestScope(options) {
|
|
7514
|
+
const tools = options?.tools;
|
|
7515
|
+
if (Array.isArray(tools)) return "tools";
|
|
7516
|
+
if (tools && typeof tools === "object") {
|
|
7517
|
+
return Object.keys(tools).length > 0 ? "tools" : "no-tools";
|
|
7518
|
+
}
|
|
7519
|
+
return "no-tools";
|
|
7520
|
+
}
|
|
7521
|
+
function latestUserText(prompt) {
|
|
7522
|
+
for (let i = prompt.length - 1; i >= 0; i--) {
|
|
7523
|
+
const msg = prompt[i];
|
|
7524
|
+
if (msg.role !== "user") continue;
|
|
7525
|
+
if (typeof msg.content === "string") {
|
|
7526
|
+
return String(msg.content).trim();
|
|
7527
|
+
}
|
|
7528
|
+
if (Array.isArray(msg.content)) {
|
|
7529
|
+
const text = msg.content.filter((part) => part.type === "text" && typeof part.text === "string").map((part) => String(part.text).trim()).filter(Boolean).join(" ");
|
|
7530
|
+
if (text) return text;
|
|
7531
|
+
}
|
|
6954
7532
|
}
|
|
6955
|
-
|
|
6956
|
-
|
|
7533
|
+
return "";
|
|
7534
|
+
}
|
|
7535
|
+
function synthesizeTitle(prompt) {
|
|
7536
|
+
const source = latestUserText(prompt).replace(/\s+/g, " ").replace(/[^\p{L}\p{N}\s-]/gu, " ").trim();
|
|
7537
|
+
if (!source) return "New Session";
|
|
7538
|
+
const stop = /* @__PURE__ */ new Set([
|
|
7539
|
+
"a",
|
|
7540
|
+
"an",
|
|
7541
|
+
"the",
|
|
7542
|
+
"and",
|
|
7543
|
+
"or",
|
|
7544
|
+
"but",
|
|
7545
|
+
"to",
|
|
7546
|
+
"for",
|
|
7547
|
+
"of",
|
|
7548
|
+
"in",
|
|
7549
|
+
"on",
|
|
7550
|
+
"at",
|
|
7551
|
+
"with",
|
|
7552
|
+
"can",
|
|
7553
|
+
"could",
|
|
7554
|
+
"would",
|
|
7555
|
+
"should",
|
|
7556
|
+
"please",
|
|
7557
|
+
"hi",
|
|
7558
|
+
"hello",
|
|
7559
|
+
"hey",
|
|
7560
|
+
"there",
|
|
7561
|
+
"you",
|
|
7562
|
+
"your",
|
|
7563
|
+
"this",
|
|
7564
|
+
"that",
|
|
7565
|
+
"is",
|
|
7566
|
+
"are",
|
|
7567
|
+
"was",
|
|
7568
|
+
"were",
|
|
7569
|
+
"be",
|
|
7570
|
+
"do",
|
|
7571
|
+
"does",
|
|
7572
|
+
"did",
|
|
7573
|
+
"summarize",
|
|
7574
|
+
"summary",
|
|
7575
|
+
"project"
|
|
7576
|
+
]);
|
|
7577
|
+
const words = source.split(" ").map((word) => word.trim()).filter(Boolean).filter((word) => !stop.has(word.toLowerCase()));
|
|
7578
|
+
const picked = (words.length > 0 ? words : source.split(" ").filter(Boolean)).slice(0, 6).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
7579
|
+
return picked || "New Session";
|
|
7580
|
+
}
|
|
7581
|
+
|
|
7582
|
+
// src/usage.ts
|
|
7583
|
+
function toUsage(rawUsage) {
|
|
7584
|
+
const iter = rawUsage?.iterations;
|
|
7585
|
+
const effective = iter?.length ? iter[iter.length - 1] : rawUsage;
|
|
7586
|
+
const noCache = effective?.input_tokens ?? 0;
|
|
7587
|
+
const cacheRead = effective?.cache_read_input_tokens ?? 0;
|
|
7588
|
+
const cacheWrite = effective?.cache_creation_input_tokens ?? 0;
|
|
7589
|
+
return {
|
|
7590
|
+
inputTokens: {
|
|
7591
|
+
total: noCache + cacheRead + cacheWrite,
|
|
7592
|
+
noCache,
|
|
7593
|
+
cacheRead: cacheRead || void 0,
|
|
7594
|
+
cacheWrite: cacheWrite || void 0
|
|
7595
|
+
},
|
|
7596
|
+
outputTokens: {
|
|
7597
|
+
total: effective?.output_tokens,
|
|
7598
|
+
text: effective?.output_tokens,
|
|
7599
|
+
reasoning: void 0
|
|
7600
|
+
},
|
|
7601
|
+
raw: rawUsage
|
|
7602
|
+
};
|
|
7603
|
+
}
|
|
7604
|
+
function toFinishReason(reason = "stop") {
|
|
7605
|
+
return {
|
|
7606
|
+
unified: reason,
|
|
7607
|
+
raw: reason
|
|
7608
|
+
};
|
|
7609
|
+
}
|
|
7610
|
+
|
|
7611
|
+
// src/claude-code-language-model.ts
|
|
7612
|
+
import { unlink as unlink3 } from "fs/promises";
|
|
7613
|
+
async function isProxyCallStillServed(callId) {
|
|
7614
|
+
const pending = findPendingProxyCall(callId);
|
|
7615
|
+
if (!pending || pending.emitted !== true) return false;
|
|
7616
|
+
const session = describeSessionKey(pending.sessionKey).session;
|
|
7617
|
+
return await fetchSessionRunState(session) === "busy";
|
|
6957
7618
|
}
|
|
7619
|
+
setProxyDeadlineGuard(({ callId }) => isProxyCallStillServed(callId));
|
|
7620
|
+
var KNOWN_DELTA_TYPES = /* @__PURE__ */ new Set([
|
|
7621
|
+
"thinking_delta",
|
|
7622
|
+
"text_delta",
|
|
7623
|
+
"input_json_delta",
|
|
7624
|
+
"signature_delta"
|
|
7625
|
+
]);
|
|
7626
|
+
var PROXY_RESULT_BOUNDARY_GRACE_MS = 250;
|
|
7627
|
+
var CHILD_EXIT_STATUS_GRACE_MS = 250;
|
|
6958
7628
|
var ClaudeCodeLanguageModel = class {
|
|
6959
7629
|
specificationVersion = "v3";
|
|
6960
7630
|
modelId;
|
|
@@ -6968,192 +7638,51 @@ var ClaudeCodeLanguageModel = class {
|
|
|
6968
7638
|
return this.config.provider;
|
|
6969
7639
|
}
|
|
6970
7640
|
toUsage(rawUsage) {
|
|
6971
|
-
|
|
6972
|
-
const effective = iter?.length ? iter[iter.length - 1] : rawUsage;
|
|
6973
|
-
const noCache = effective?.input_tokens ?? 0;
|
|
6974
|
-
const cacheRead = effective?.cache_read_input_tokens ?? 0;
|
|
6975
|
-
const cacheWrite = effective?.cache_creation_input_tokens ?? 0;
|
|
6976
|
-
return {
|
|
6977
|
-
inputTokens: {
|
|
6978
|
-
total: noCache + cacheRead + cacheWrite,
|
|
6979
|
-
noCache,
|
|
6980
|
-
cacheRead: cacheRead || void 0,
|
|
6981
|
-
cacheWrite: cacheWrite || void 0
|
|
6982
|
-
},
|
|
6983
|
-
outputTokens: {
|
|
6984
|
-
total: effective?.output_tokens,
|
|
6985
|
-
text: effective?.output_tokens,
|
|
6986
|
-
reasoning: void 0
|
|
6987
|
-
},
|
|
6988
|
-
raw: rawUsage
|
|
6989
|
-
};
|
|
7641
|
+
return toUsage(rawUsage);
|
|
6990
7642
|
}
|
|
6991
7643
|
toFinishReason(reason = "stop") {
|
|
6992
|
-
return
|
|
6993
|
-
unified: reason,
|
|
6994
|
-
raw: reason
|
|
6995
|
-
};
|
|
7644
|
+
return toFinishReason(reason);
|
|
6996
7645
|
}
|
|
6997
7646
|
/**
|
|
6998
7647
|
* Whether this call only names the session, which gets the synthetic stub
|
|
6999
|
-
* rather than a `claude` spawn.
|
|
7000
|
-
* tools, and that is the whole test there. opencode 2 sends its tool set
|
|
7001
|
-
* along with it (measured on 2.0.11: `scope: "tools"`, agent `title`), so
|
|
7002
|
-
* every new V2 session paid for a second `claude` process just to title
|
|
7003
|
-
* itself; for a V2 model the request kind, carried as the `title` agent,
|
|
7004
|
-
* decides instead.
|
|
7648
|
+
* rather than a `claude` spawn. See `isTitleRequest` in title.ts.
|
|
7005
7649
|
*/
|
|
7006
7650
|
isTitleRequest(scope, options) {
|
|
7007
|
-
|
|
7008
|
-
return this.config.hostApi === "v2" && this.getOpencodeAgent(options) === "title";
|
|
7651
|
+
return isTitleRequest(this.config, scope, options);
|
|
7009
7652
|
}
|
|
7010
7653
|
requestScope(options) {
|
|
7011
|
-
|
|
7012
|
-
if (Array.isArray(tools)) return "tools";
|
|
7013
|
-
if (tools && typeof tools === "object") {
|
|
7014
|
-
return Object.keys(tools).length > 0 ? "tools" : "no-tools";
|
|
7015
|
-
}
|
|
7016
|
-
return "no-tools";
|
|
7654
|
+
return requestScope(options);
|
|
7017
7655
|
}
|
|
7018
7656
|
/**
|
|
7019
7657
|
* Build the combined `--mcp-config` list and return both the list and the
|
|
7020
|
-
* hash of the bridged opencode MCP block
|
|
7021
|
-
*
|
|
7022
|
-
* and respawn the underlying claude process.
|
|
7023
|
-
*
|
|
7024
|
-
* `runtimeStatus` is a snapshot of opencode's `client.mcp.status()`. When
|
|
7025
|
-
* provided it overlays opencode's UI-toggled state on top of disk config
|
|
7026
|
-
* so `/mcps` toggles propagate without a config file write.
|
|
7658
|
+
* hash of the bridged opencode MCP block. See `effectiveMcpConfig` in
|
|
7659
|
+
* spawn-planning.ts.
|
|
7027
7660
|
*/
|
|
7028
7661
|
effectiveMcpConfig(cwd, proxyConfigPath, runtimeStatus, excludeServers) {
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
bridgedHash = bridged.hash;
|
|
7037
|
-
allEnabledServerNames = bridged.allEnabledServerNames;
|
|
7038
|
-
}
|
|
7039
|
-
}
|
|
7040
|
-
if (proxyConfigPath) paths.push(proxyConfigPath);
|
|
7041
|
-
return { paths, bridgedHash, allEnabledServerNames };
|
|
7662
|
+
return effectiveMcpConfig(
|
|
7663
|
+
this.config,
|
|
7664
|
+
cwd,
|
|
7665
|
+
proxyConfigPath,
|
|
7666
|
+
runtimeStatus,
|
|
7667
|
+
excludeServers
|
|
7668
|
+
);
|
|
7042
7669
|
}
|
|
7043
7670
|
/** Resolve ProxyToolDef[] for the configured proxyTools names. */
|
|
7044
7671
|
resolvedProxyTools() {
|
|
7045
|
-
|
|
7046
|
-
if (!names || names.length === 0) return null;
|
|
7047
|
-
const defsByName = new Map(
|
|
7048
|
-
DEFAULT_PROXY_TOOLS.map((t) => [t.name.toLowerCase(), t])
|
|
7049
|
-
);
|
|
7050
|
-
const picked = [];
|
|
7051
|
-
const seen = /* @__PURE__ */ new Set();
|
|
7052
|
-
const unknown = [];
|
|
7053
|
-
const pick = (def) => {
|
|
7054
|
-
if (seen.has(def.name)) return;
|
|
7055
|
-
seen.add(def.name);
|
|
7056
|
-
picked.push(def);
|
|
7057
|
-
};
|
|
7058
|
-
for (const n of names) {
|
|
7059
|
-
const def = defsByName.get(String(n).toLowerCase());
|
|
7060
|
-
if (!def) {
|
|
7061
|
-
unknown.push(String(n));
|
|
7062
|
-
continue;
|
|
7063
|
-
}
|
|
7064
|
-
pick(def);
|
|
7065
|
-
if (def.name === "task") {
|
|
7066
|
-
const batch = defsByName.get(TASK_BATCH_TOOL_NAME);
|
|
7067
|
-
if (batch) pick(batch);
|
|
7068
|
-
}
|
|
7069
|
-
}
|
|
7070
|
-
if (unknown.length > 0) {
|
|
7071
|
-
const known = [...defsByName.keys()].join(", ");
|
|
7072
|
-
if (picked.length === 0) {
|
|
7073
|
-
log.warn(
|
|
7074
|
-
"no proxyTools entry was recognised; nothing will be proxied this turn",
|
|
7075
|
-
{ unknown, known }
|
|
7076
|
-
);
|
|
7077
|
-
} else {
|
|
7078
|
-
log.warn("ignoring unknown proxyTools entries", { unknown, known });
|
|
7079
|
-
}
|
|
7080
|
-
}
|
|
7081
|
-
return picked.length > 0 ? picked : null;
|
|
7672
|
+
return resolvedProxyTools(this.config);
|
|
7082
7673
|
}
|
|
7083
|
-
/**
|
|
7084
|
-
* Resolve ProxyToolDef[] for opencode's MCP-backed tools so they go
|
|
7085
|
-
* through the in-process proxy instead of being bridged into Claude CLI's
|
|
7086
|
-
* `--mcp-config`. Routing through the proxy keeps a single execution site
|
|
7087
|
-
* (opencode), so the call is permission-prompted and rendered as an
|
|
7088
|
-
* opencode tool call.
|
|
7089
|
-
*
|
|
7090
|
-
* Opt-in (`proxyOpencodeMcpTools: true`) and off by default. It used to
|
|
7091
|
-
* default to true while finding nothing, because it discovered tools via
|
|
7092
|
-
* `client.tool.list()`, which enumerates opencode's `ToolRegistry` and not
|
|
7093
|
-
* the MCP tools merged into the model's tool set afterwards. Discovery now
|
|
7094
|
-
* reads that merged set, the `tools` array opencode passes `doStream`, so
|
|
7095
|
-
* the option does what it says. Turning it on by default at the same time
|
|
7096
|
-
* would have silently moved every existing user's MCP traffic off the
|
|
7097
|
-
* working direct bridge, so the default went to false instead: today's
|
|
7098
|
-
* behaviour is preserved exactly and crossing over is the operator's call.
|
|
7099
|
-
*
|
|
7100
|
-
* Returns null when the feature is off or nothing matched, which leaves
|
|
7101
|
-
* every server on the direct bridge.
|
|
7102
|
-
*/
|
|
7674
|
+
/** Resolve ProxyToolDef[] for opencode's MCP-backed tools. */
|
|
7103
7675
|
resolvedProxyMcpTools(allEnabledServerNames, modelTools, taken) {
|
|
7104
|
-
|
|
7105
|
-
|
|
7106
|
-
|
|
7107
|
-
|
|
7108
|
-
serverNames: allEnabledServerNames,
|
|
7109
|
-
tools: modelTools,
|
|
7676
|
+
return resolvedProxyMcpTools(
|
|
7677
|
+
this.config,
|
|
7678
|
+
allEnabledServerNames,
|
|
7679
|
+
modelTools,
|
|
7110
7680
|
taken
|
|
7111
|
-
|
|
7112
|
-
if (resolution.defs.length === 0) {
|
|
7113
|
-
log.warn(
|
|
7114
|
-
"proxyOpencodeMcpTools is on but no MCP tool was found in opencode's tool set; those servers stay on the direct bridge this spawn",
|
|
7115
|
-
{ servers: allEnabledServerNames, modelTools: modelTools?.length ?? 0 }
|
|
7116
|
-
);
|
|
7117
|
-
return null;
|
|
7118
|
-
}
|
|
7119
|
-
log.debug("routing opencode MCP tools through the proxy", {
|
|
7120
|
-
servers: [...resolution.coveredServers],
|
|
7121
|
-
tools: resolution.defs.map((def) => def.name)
|
|
7122
|
-
});
|
|
7123
|
-
return resolution;
|
|
7681
|
+
);
|
|
7124
7682
|
}
|
|
7125
|
-
/**
|
|
7126
|
-
* Live tool info derived from a single `client.tool.list()` fetch:
|
|
7127
|
-
*
|
|
7128
|
-
* - `taskDescription`: opencode's `task` tool description exactly as the
|
|
7129
|
-
* registry renders it for native models, including the "Available
|
|
7130
|
-
* agent types" list. Overlaid onto the static `task` proxy def so
|
|
7131
|
-
* Claude sees the same subagent catalog native models see, instead
|
|
7132
|
-
* of hunting through config files.
|
|
7133
|
-
* - `questionDescription` / `hasQuestion`: opencode's `question` tool
|
|
7134
|
-
* description and whether the registry has the entry at all. Older
|
|
7135
|
-
* builds lack it, in which case a `mcp__opencode_proxy__question`
|
|
7136
|
-
* call resolves to `⚙ invalid`; the version gate drops the def.
|
|
7137
|
-
*
|
|
7138
|
-
* Returns undefined/false when the SDK client is unavailable (direct
|
|
7139
|
-
* AI-SDK use, tests) so the static defs stand. `resolved` distinguishes
|
|
7140
|
-
* "the registry answered and has no `question` entry" from "nobody
|
|
7141
|
-
* answered": only the former is a real version-gate signal.
|
|
7142
|
-
*/
|
|
7683
|
+
/** One `client.tool.list()` fetch, shaped for this turn's gates. */
|
|
7143
7684
|
async fetchLiveToolInfo() {
|
|
7144
|
-
|
|
7145
|
-
this.config.provider,
|
|
7146
|
-
this.modelId,
|
|
7147
|
-
this.config.cwd
|
|
7148
|
-
);
|
|
7149
|
-
const question = items?.find((item) => item.id === "question");
|
|
7150
|
-
return {
|
|
7151
|
-
resolved: items !== void 0,
|
|
7152
|
-
taskDescription: items?.find((item) => item.id === "task")?.description,
|
|
7153
|
-
questionDescription: question?.description,
|
|
7154
|
-
hasQuestion: !!question,
|
|
7155
|
-
items
|
|
7156
|
-
};
|
|
7685
|
+
return fetchLiveToolInfo(this.config, this.modelId);
|
|
7157
7686
|
}
|
|
7158
7687
|
/**
|
|
7159
7688
|
* Whether dcp-style context reminders should be stripped from this turn's
|
|
@@ -7161,31 +7690,19 @@ var ClaudeCodeLanguageModel = class {
|
|
|
7161
7690
|
* spawn block resolves anything: `userMsg` is built well ahead of it.
|
|
7162
7691
|
*/
|
|
7163
7692
|
stripContextRemindersEnabled() {
|
|
7164
|
-
return
|
|
7165
|
-
enabled: this.config.stripContextReminders,
|
|
7166
|
-
proxyTools: this.config.proxyTools,
|
|
7167
|
-
proxyOpencodeTools: this.config.proxyOpencodeTools
|
|
7168
|
-
});
|
|
7693
|
+
return stripContextRemindersEnabled(this.config);
|
|
7169
7694
|
}
|
|
7170
7695
|
/**
|
|
7171
7696
|
* Arguments the skill bridge needs beyond `cwd` / `cliPath`: which
|
|
7172
7697
|
* `CLAUDE_CONFIG_DIR` this spawn reads its native skills from, and whether
|
|
7173
|
-
* to drop the ones it already loads.
|
|
7174
|
-
* account, and therefore to that account's config dir.
|
|
7698
|
+
* to drop the ones it already loads.
|
|
7175
7699
|
*/
|
|
7176
7700
|
skillBridgeSpawn(failover) {
|
|
7177
|
-
return
|
|
7178
|
-
configDir: failover.failedOver && failover.target ? accountConfigDirPath(failover.target) : this.config.configDir,
|
|
7179
|
-
skipNative: this.config.bridgeSkipNativeSkills !== false
|
|
7180
|
-
};
|
|
7701
|
+
return skillBridgeSpawn(this.config, failover);
|
|
7181
7702
|
}
|
|
7182
7703
|
/** Share one lazy registry request within a turn without making it stale. */
|
|
7183
7704
|
createLiveToolInfoLoader() {
|
|
7184
|
-
|
|
7185
|
-
return () => {
|
|
7186
|
-
pending ??= this.fetchLiveToolInfo();
|
|
7187
|
-
return pending;
|
|
7188
|
-
};
|
|
7705
|
+
return createLiveToolInfoLoader(this.config, this.modelId);
|
|
7189
7706
|
}
|
|
7190
7707
|
/**
|
|
7191
7708
|
* Whether the ExitPlanMode approval bridge is live for this turn: the
|
|
@@ -7194,129 +7711,19 @@ var ClaudeCodeLanguageModel = class {
|
|
|
7194
7711
|
* `⚙ invalid` and wedge the turn, so the plugin keeps the text path.
|
|
7195
7712
|
*/
|
|
7196
7713
|
async resolvePlanModeQuestion(compactionMode, loadLiveToolInfo = () => this.fetchLiveToolInfo()) {
|
|
7197
|
-
|
|
7198
|
-
const info = await loadLiveToolInfo();
|
|
7199
|
-
const active = isPlanModeQuestionActive({
|
|
7200
|
-
configured: this.config.planModeQuestion,
|
|
7201
|
-
opencodeHasQuestion: info.hasQuestion,
|
|
7202
|
-
compactionMode
|
|
7203
|
-
});
|
|
7204
|
-
if (!active) {
|
|
7205
|
-
log.info("plan-mode question gate", {
|
|
7206
|
-
opencodeHasQuestion: info.hasQuestion,
|
|
7207
|
-
registryResolved: info.resolved,
|
|
7208
|
-
active
|
|
7209
|
-
});
|
|
7210
|
-
}
|
|
7211
|
-
return active;
|
|
7714
|
+
return resolvePlanModeQuestion(this.config, compactionMode, loadLiveToolInfo);
|
|
7212
7715
|
}
|
|
7213
7716
|
/**
|
|
7214
7717
|
* Create a proxy MCP server for a single active Claude process/session.
|
|
7215
7718
|
* The process lifecycle owns the server lifecycle via session-manager.
|
|
7216
7719
|
*/
|
|
7217
7720
|
async ensureProxyServer(tools, sessionKeyForCalls, interceptCompress) {
|
|
7218
|
-
|
|
7219
|
-
|
|
7220
|
-
|
|
7221
|
-
|
|
7222
|
-
|
|
7223
|
-
|
|
7224
|
-
return {
|
|
7225
|
-
kind: "error",
|
|
7226
|
-
message: "compress needs a non-empty `summary`: it becomes the only prior context after the reset. Nothing was compressed."
|
|
7227
|
-
};
|
|
7228
|
-
}
|
|
7229
|
-
storeCompressionSummary(sessionKeyForCalls, summary);
|
|
7230
|
-
log.info("compress stored summary; session resets next turn", {
|
|
7231
|
-
sessionKey: sessionKeyForCalls,
|
|
7232
|
-
summaryLength: summary.length
|
|
7233
|
-
});
|
|
7234
|
-
return {
|
|
7235
|
-
kind: "text",
|
|
7236
|
-
text: "Summary stored. Finish this turn as normal; the next turn starts a fresh Claude Code session with this summary as its only prior context."
|
|
7237
|
-
};
|
|
7238
|
-
});
|
|
7239
|
-
}
|
|
7240
|
-
const srv = await createProxyMcpServer(tools, timeoutOverrides, interceptors);
|
|
7241
|
-
srv.calls.on("call", (call) => {
|
|
7242
|
-
queuePendingProxyCall(sessionKeyForCalls, call, timeoutOverrides);
|
|
7243
|
-
});
|
|
7244
|
-
return srv;
|
|
7245
|
-
}
|
|
7246
|
-
extractPendingProxyResult(prompt, toolCallId) {
|
|
7247
|
-
for (let i = prompt.length - 1; i >= 0; i--) {
|
|
7248
|
-
const msg = prompt[i];
|
|
7249
|
-
if (msg.role !== "tool" || !Array.isArray(msg.content)) continue;
|
|
7250
|
-
for (const part of msg.content) {
|
|
7251
|
-
if (part.type !== "tool-result" || part.toolCallId !== toolCallId) continue;
|
|
7252
|
-
const output = part.output;
|
|
7253
|
-
if (!output || typeof output !== "object") {
|
|
7254
|
-
return {
|
|
7255
|
-
kind: "text",
|
|
7256
|
-
text: String(output ?? "")
|
|
7257
|
-
};
|
|
7258
|
-
}
|
|
7259
|
-
if (output.type === "text") {
|
|
7260
|
-
return {
|
|
7261
|
-
kind: "text",
|
|
7262
|
-
text: String(output.value ?? "")
|
|
7263
|
-
};
|
|
7264
|
-
}
|
|
7265
|
-
if (output.type === "json") {
|
|
7266
|
-
return {
|
|
7267
|
-
kind: "text",
|
|
7268
|
-
text: JSON.stringify(output.value)
|
|
7269
|
-
};
|
|
7270
|
-
}
|
|
7271
|
-
if (output.type === "content" && Array.isArray(output.value)) {
|
|
7272
|
-
const text = output.value.filter((v) => v?.type === "text" && typeof v.text === "string").map((v) => v.text).join("\n");
|
|
7273
|
-
return {
|
|
7274
|
-
kind: "text",
|
|
7275
|
-
text
|
|
7276
|
-
};
|
|
7277
|
-
}
|
|
7278
|
-
return {
|
|
7279
|
-
kind: "text",
|
|
7280
|
-
text: JSON.stringify(output)
|
|
7281
|
-
};
|
|
7282
|
-
}
|
|
7283
|
-
}
|
|
7284
|
-
return null;
|
|
7285
|
-
}
|
|
7286
|
-
/**
|
|
7287
|
-
* The result opencode produced for a pending proxy call, if the prompt
|
|
7288
|
-
* carries it. For `task_batch` that means every child's result gathered
|
|
7289
|
-
* back onto the parent: opencode runs the children in one step and hands
|
|
7290
|
-
* all their results to the next call together, so a partial set is not
|
|
7291
|
-
* expected. If it ever happens the batch still resolves, with the gap
|
|
7292
|
-
* named in the text, because leaving the parent pending would send this
|
|
7293
|
-
* turn down the fresh-envelope path and reject the call as orphaned.
|
|
7294
|
-
*/
|
|
7295
|
-
extractPendingProxyResultForCall(prompt, call) {
|
|
7296
|
-
if (call.toolName !== TASK_BATCH_TOOL_NAME) {
|
|
7297
|
-
return this.extractPendingProxyResult(prompt, call.toolCallId);
|
|
7298
|
-
}
|
|
7299
|
-
const tasks = taskBatchTasks(call.input);
|
|
7300
|
-
if (tasks.length === 0) {
|
|
7301
|
-
return { kind: "error", message: "task_batch input is not a list of task objects" };
|
|
7302
|
-
}
|
|
7303
|
-
const children = tasks.map((task, index) => ({
|
|
7304
|
-
task,
|
|
7305
|
-
result: this.extractPendingProxyResult(
|
|
7306
|
-
prompt,
|
|
7307
|
-
taskBatchChildToolCallId(call.toolCallId, index)
|
|
7308
|
-
)
|
|
7309
|
-
}));
|
|
7310
|
-
const answered = children.filter((child) => child.result !== null).length;
|
|
7311
|
-
if (answered === 0) return null;
|
|
7312
|
-
if (answered < children.length) {
|
|
7313
|
-
log.warn("task_batch resolving with child results missing", {
|
|
7314
|
-
toolCallId: call.toolCallId,
|
|
7315
|
-
answered,
|
|
7316
|
-
total: children.length
|
|
7317
|
-
});
|
|
7318
|
-
}
|
|
7319
|
-
return formatTaskBatchResults(children);
|
|
7721
|
+
return ensureProxyServer(
|
|
7722
|
+
this.config,
|
|
7723
|
+
tools,
|
|
7724
|
+
sessionKeyForCalls,
|
|
7725
|
+
interceptCompress
|
|
7726
|
+
);
|
|
7320
7727
|
}
|
|
7321
7728
|
/**
|
|
7322
7729
|
* Resolve the session affinity token for this LLM call. Delegates to the
|
|
@@ -7337,83 +7744,17 @@ var ClaudeCodeLanguageModel = class {
|
|
|
7337
7744
|
);
|
|
7338
7745
|
}
|
|
7339
7746
|
controlRequestBehaviorForTool(toolName) {
|
|
7340
|
-
|
|
7341
|
-
if (configured && toolName) {
|
|
7342
|
-
const direct = configured[toolName] ?? configured[toolName.toLowerCase()];
|
|
7343
|
-
if (direct === "allow" || direct === "deny") return direct;
|
|
7344
|
-
const lower = toolName.toLowerCase();
|
|
7345
|
-
for (const [key, behavior] of Object.entries(configured)) {
|
|
7346
|
-
if (key.toLowerCase() === lower && (behavior === "allow" || behavior === "deny")) {
|
|
7347
|
-
return behavior;
|
|
7348
|
-
}
|
|
7349
|
-
}
|
|
7350
|
-
}
|
|
7351
|
-
if (isAskUserQuestionTool(toolName)) return "deny";
|
|
7352
|
-
return this.config.controlRequestBehavior ?? "allow";
|
|
7747
|
+
return controlRequestBehaviorForTool(this.config, toolName);
|
|
7353
7748
|
}
|
|
7354
7749
|
writeControlResponse(proc, requestId, response) {
|
|
7355
|
-
|
|
7356
|
-
type: "control_response",
|
|
7357
|
-
response: {
|
|
7358
|
-
subtype: "success",
|
|
7359
|
-
request_id: requestId,
|
|
7360
|
-
response
|
|
7361
|
-
}
|
|
7362
|
-
};
|
|
7363
|
-
try {
|
|
7364
|
-
proc.stdin?.write(JSON.stringify(payload) + "\n");
|
|
7365
|
-
} catch (error) {
|
|
7366
|
-
log.warn("failed to write control response", {
|
|
7367
|
-
requestId,
|
|
7368
|
-
error: error instanceof Error ? error.message : String(error)
|
|
7369
|
-
});
|
|
7370
|
-
}
|
|
7750
|
+
writeControlResponse(proc, requestId, response);
|
|
7371
7751
|
}
|
|
7372
7752
|
/**
|
|
7373
7753
|
* Handle Claude stream-json control requests (`can_use_tool`, etc.) and
|
|
7374
7754
|
* respond via stdin with a matching `control_response`.
|
|
7375
7755
|
*/
|
|
7376
7756
|
handleControlRequest(msg, proc) {
|
|
7377
|
-
|
|
7378
|
-
const requestId = msg.request_id;
|
|
7379
|
-
const request = msg.request;
|
|
7380
|
-
if (!requestId || !request?.subtype) return false;
|
|
7381
|
-
if (request.subtype === "can_use_tool") {
|
|
7382
|
-
const toolName = request.tool_name ?? "unknown";
|
|
7383
|
-
const behavior = this.controlRequestBehaviorForTool(toolName);
|
|
7384
|
-
if (behavior === "allow") {
|
|
7385
|
-
this.writeControlResponse(proc, requestId, {
|
|
7386
|
-
behavior: "allow",
|
|
7387
|
-
updatedInput: request.input ?? {},
|
|
7388
|
-
toolUseID: request.tool_use_id
|
|
7389
|
-
});
|
|
7390
|
-
log.info("control request auto-allowed", {
|
|
7391
|
-
requestId,
|
|
7392
|
-
toolName
|
|
7393
|
-
});
|
|
7394
|
-
} else {
|
|
7395
|
-
const denyMessage = denyMessageForTool(
|
|
7396
|
-
toolName,
|
|
7397
|
-
this.config.controlRequestDenyMessage
|
|
7398
|
-
);
|
|
7399
|
-
this.writeControlResponse(proc, requestId, {
|
|
7400
|
-
behavior: "deny",
|
|
7401
|
-
message: denyMessage,
|
|
7402
|
-
toolUseID: request.tool_use_id
|
|
7403
|
-
});
|
|
7404
|
-
log.info("control request auto-denied", {
|
|
7405
|
-
requestId,
|
|
7406
|
-
toolName
|
|
7407
|
-
});
|
|
7408
|
-
}
|
|
7409
|
-
return true;
|
|
7410
|
-
}
|
|
7411
|
-
this.writeControlResponse(proc, requestId, {});
|
|
7412
|
-
log.debug("control request acknowledged", {
|
|
7413
|
-
requestId,
|
|
7414
|
-
subtype: request.subtype
|
|
7415
|
-
});
|
|
7416
|
-
return true;
|
|
7757
|
+
return handleControlRequest(this.config, msg, proc);
|
|
7417
7758
|
}
|
|
7418
7759
|
getReasoningEffort(providerOptions) {
|
|
7419
7760
|
if (!providerOptions) return void 0;
|
|
@@ -7457,64 +7798,10 @@ var ClaudeCodeLanguageModel = class {
|
|
|
7457
7798
|
};
|
|
7458
7799
|
}
|
|
7459
7800
|
latestUserText(prompt) {
|
|
7460
|
-
|
|
7461
|
-
const msg = prompt[i];
|
|
7462
|
-
if (msg.role !== "user") continue;
|
|
7463
|
-
if (typeof msg.content === "string") {
|
|
7464
|
-
return String(msg.content).trim();
|
|
7465
|
-
}
|
|
7466
|
-
if (Array.isArray(msg.content)) {
|
|
7467
|
-
const text = msg.content.filter((part) => part.type === "text" && typeof part.text === "string").map((part) => String(part.text).trim()).filter(Boolean).join(" ");
|
|
7468
|
-
if (text) return text;
|
|
7469
|
-
}
|
|
7470
|
-
}
|
|
7471
|
-
return "";
|
|
7801
|
+
return latestUserText(prompt);
|
|
7472
7802
|
}
|
|
7473
7803
|
synthesizeTitle(prompt) {
|
|
7474
|
-
|
|
7475
|
-
if (!source) return "New Session";
|
|
7476
|
-
const stop = /* @__PURE__ */ new Set([
|
|
7477
|
-
"a",
|
|
7478
|
-
"an",
|
|
7479
|
-
"the",
|
|
7480
|
-
"and",
|
|
7481
|
-
"or",
|
|
7482
|
-
"but",
|
|
7483
|
-
"to",
|
|
7484
|
-
"for",
|
|
7485
|
-
"of",
|
|
7486
|
-
"in",
|
|
7487
|
-
"on",
|
|
7488
|
-
"at",
|
|
7489
|
-
"with",
|
|
7490
|
-
"can",
|
|
7491
|
-
"could",
|
|
7492
|
-
"would",
|
|
7493
|
-
"should",
|
|
7494
|
-
"please",
|
|
7495
|
-
"hi",
|
|
7496
|
-
"hello",
|
|
7497
|
-
"hey",
|
|
7498
|
-
"there",
|
|
7499
|
-
"you",
|
|
7500
|
-
"your",
|
|
7501
|
-
"this",
|
|
7502
|
-
"that",
|
|
7503
|
-
"is",
|
|
7504
|
-
"are",
|
|
7505
|
-
"was",
|
|
7506
|
-
"were",
|
|
7507
|
-
"be",
|
|
7508
|
-
"do",
|
|
7509
|
-
"does",
|
|
7510
|
-
"did",
|
|
7511
|
-
"summarize",
|
|
7512
|
-
"summary",
|
|
7513
|
-
"project"
|
|
7514
|
-
]);
|
|
7515
|
-
const words = source.split(" ").map((word) => word.trim()).filter(Boolean).filter((word) => !stop.has(word.toLowerCase()));
|
|
7516
|
-
const picked = (words.length > 0 ? words : source.split(" ").filter(Boolean)).slice(0, 6).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
7517
|
-
return picked || "New Session";
|
|
7804
|
+
return synthesizeTitle(prompt);
|
|
7518
7805
|
}
|
|
7519
7806
|
async doGenerateViaStream(options) {
|
|
7520
7807
|
const result = await this.doStream(options);
|
|
@@ -7746,7 +8033,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
7746
8033
|
const toolCalls = [];
|
|
7747
8034
|
const toolCallStreams = /* @__PURE__ */ new Map();
|
|
7748
8035
|
let gotPartialEvents = false;
|
|
7749
|
-
const result = await new Promise((
|
|
8036
|
+
const result = await new Promise((resolve6, reject) => {
|
|
7750
8037
|
const cleanup = () => {
|
|
7751
8038
|
try {
|
|
7752
8039
|
if (!proc.killed && proc.exitCode === null) proc.kill();
|
|
@@ -7901,7 +8188,7 @@ ${plan}
|
|
|
7901
8188
|
...turnStatsLogPayload(extractTurnStats(msg))
|
|
7902
8189
|
});
|
|
7903
8190
|
cleanup();
|
|
7904
|
-
|
|
8191
|
+
resolve6({
|
|
7905
8192
|
...resultMeta,
|
|
7906
8193
|
text: responseText,
|
|
7907
8194
|
thinking: thinkingText,
|
|
@@ -7913,7 +8200,7 @@ ${plan}
|
|
|
7913
8200
|
});
|
|
7914
8201
|
rl.on("close", () => {
|
|
7915
8202
|
cleanup();
|
|
7916
|
-
|
|
8203
|
+
resolve6({
|
|
7917
8204
|
...resultMeta,
|
|
7918
8205
|
text: responseText,
|
|
7919
8206
|
thinking: thinkingText,
|
|
@@ -8049,9 +8336,9 @@ ${plan}
|
|
|
8049
8336
|
`${effectiveModelId}::${scope}::${affinity}::context=${JSON.stringify([this.config.provider, this.getOpencodeAgent(options) ?? null])}`
|
|
8050
8337
|
);
|
|
8051
8338
|
const sk = compactionMode ? sessionKey(cwd, `${effectiveModelId}::compaction::${affinity}`) : effortSessionKey(baseKey, reasoningEffort);
|
|
8052
|
-
const
|
|
8053
|
-
const
|
|
8054
|
-
const
|
|
8339
|
+
const toUsage2 = this.toUsage.bind(this);
|
|
8340
|
+
const toFinishReason2 = this.toFinishReason.bind(this);
|
|
8341
|
+
const handleControlRequest2 = this.handleControlRequest.bind(this);
|
|
8055
8342
|
const flagOn = (v) => v !== void 0 && !["", "0", "false", "no", "off"].includes(v.trim().toLowerCase());
|
|
8056
8343
|
const interactivePref = this.config.interactive ?? flagOn(process.env.CLAUDE_CODE_INTERACTIVE_TRANSPORT);
|
|
8057
8344
|
const useInteractive = interactivePref && typeof globalThis.Bun?.Terminal === "function";
|
|
@@ -8086,8 +8373,8 @@ ${plan}
|
|
|
8086
8373
|
controller.enqueue({ type: "text-end", id });
|
|
8087
8374
|
controller.enqueue({
|
|
8088
8375
|
type: "finish",
|
|
8089
|
-
finishReason:
|
|
8090
|
-
usage:
|
|
8376
|
+
finishReason: toFinishReason2("stop"),
|
|
8377
|
+
usage: toUsage2({ input_tokens: 0, output_tokens: 0 }),
|
|
8091
8378
|
providerMetadata: {
|
|
8092
8379
|
"claude-code": { path: "doctor", synthetic: true, usageUnavailable: true }
|
|
8093
8380
|
}
|
|
@@ -8134,8 +8421,8 @@ ${plan}
|
|
|
8134
8421
|
controller.enqueue({ type: "text-end", id });
|
|
8135
8422
|
controller.enqueue({
|
|
8136
8423
|
type: "finish",
|
|
8137
|
-
finishReason:
|
|
8138
|
-
usage:
|
|
8424
|
+
finishReason: toFinishReason2("stop"),
|
|
8425
|
+
usage: toUsage2({}),
|
|
8139
8426
|
providerMetadata: { "claude-code": { path: "side-question", synthetic: answer.synthetic, usageUnavailable: true } }
|
|
8140
8427
|
});
|
|
8141
8428
|
} catch (error) {
|
|
@@ -8167,8 +8454,8 @@ ${plan}
|
|
|
8167
8454
|
controller.enqueue({ type: "text-end", id: textId });
|
|
8168
8455
|
controller.enqueue({
|
|
8169
8456
|
type: "finish",
|
|
8170
|
-
finishReason:
|
|
8171
|
-
usage:
|
|
8457
|
+
finishReason: toFinishReason2("stop"),
|
|
8458
|
+
usage: toUsage2({ input_tokens: 0, output_tokens: 0 }),
|
|
8172
8459
|
providerMetadata: {
|
|
8173
8460
|
"claude-code": {
|
|
8174
8461
|
synthetic: true,
|
|
@@ -8191,8 +8478,8 @@ ${plan}
|
|
|
8191
8478
|
controller.enqueue({ type: "stream-start", warnings });
|
|
8192
8479
|
controller.enqueue({
|
|
8193
8480
|
type: "finish",
|
|
8194
|
-
finishReason:
|
|
8195
|
-
usage:
|
|
8481
|
+
finishReason: toFinishReason2("stop"),
|
|
8482
|
+
usage: toUsage2({ input_tokens: 0, output_tokens: 0 }),
|
|
8196
8483
|
providerMetadata: {
|
|
8197
8484
|
"claude-code": { synthetic: true, path: "no-new-user-content" }
|
|
8198
8485
|
}
|
|
@@ -8236,7 +8523,7 @@ ${plan}
|
|
|
8236
8523
|
controller.enqueue({
|
|
8237
8524
|
type: "finish",
|
|
8238
8525
|
finishReason: { unified: "error", raw: "account_limit" },
|
|
8239
|
-
usage:
|
|
8526
|
+
usage: toUsage2({ input_tokens: 0, output_tokens: 0 }),
|
|
8240
8527
|
providerMetadata: {
|
|
8241
8528
|
"claude-code": {
|
|
8242
8529
|
path: "account-failover-stop",
|
|
@@ -8309,7 +8596,7 @@ ${plan}
|
|
|
8309
8596
|
const self = this;
|
|
8310
8597
|
const previousPendingProxyMatches = previousPendingProxyCalls.map((call) => ({
|
|
8311
8598
|
call,
|
|
8312
|
-
result:
|
|
8599
|
+
result: extractPendingProxyResultForCall(options.prompt, call)
|
|
8313
8600
|
}));
|
|
8314
8601
|
const hasMatchedPendingResults = previousPendingProxyMatches.some(
|
|
8315
8602
|
(m) => m.result !== null
|
|
@@ -8678,6 +8965,10 @@ ${plan}
|
|
|
8678
8965
|
let hadReasoningSinceContinue = false;
|
|
8679
8966
|
let hadToolActivitySinceContinue = false;
|
|
8680
8967
|
let hadProxyActivitySinceContinue = false;
|
|
8968
|
+
let sawVisibleText = false;
|
|
8969
|
+
let sawReasoning = false;
|
|
8970
|
+
let sawToolActivity = false;
|
|
8971
|
+
let sawProxyActivity = false;
|
|
8681
8972
|
let lastStopReason = null;
|
|
8682
8973
|
const autoContinueState = {
|
|
8683
8974
|
enabled: autoContinueEnabledFor(
|
|
@@ -8885,8 +9176,8 @@ ${plan}
|
|
|
8885
9176
|
}
|
|
8886
9177
|
controller.enqueue({
|
|
8887
9178
|
type: "finish",
|
|
8888
|
-
finishReason:
|
|
8889
|
-
usage:
|
|
9179
|
+
finishReason: toFinishReason2("tool-calls"),
|
|
9180
|
+
usage: toUsage2(resultMeta.usage),
|
|
8890
9181
|
providerMetadata: {
|
|
8891
9182
|
"claude-code": resultMeta
|
|
8892
9183
|
}
|
|
@@ -8916,8 +9207,8 @@ ${plan}
|
|
|
8916
9207
|
});
|
|
8917
9208
|
controller.enqueue({
|
|
8918
9209
|
type: "finish",
|
|
8919
|
-
finishReason:
|
|
8920
|
-
usage:
|
|
9210
|
+
finishReason: toFinishReason2("tool-calls"),
|
|
9211
|
+
usage: toUsage2(resultMeta.usage),
|
|
8921
9212
|
providerMetadata: {
|
|
8922
9213
|
"claude-code": resultMeta
|
|
8923
9214
|
}
|
|
@@ -8969,18 +9260,22 @@ ${plan}
|
|
|
8969
9260
|
const noteVisibleText = (text) => {
|
|
8970
9261
|
visibleTextSinceContinue += text;
|
|
8971
9262
|
lastVisibleTextSinceContinue += text;
|
|
9263
|
+
if (text.length > 0) sawVisibleText = true;
|
|
8972
9264
|
};
|
|
8973
9265
|
const resetLastVisibleTextBlock = () => {
|
|
8974
9266
|
lastVisibleTextSinceContinue = "";
|
|
8975
9267
|
};
|
|
8976
9268
|
const noteReasoning = () => {
|
|
8977
9269
|
hadReasoningSinceContinue = true;
|
|
9270
|
+
sawReasoning = true;
|
|
8978
9271
|
};
|
|
8979
9272
|
const noteToolActivity = () => {
|
|
8980
9273
|
hadToolActivitySinceContinue = true;
|
|
9274
|
+
sawToolActivity = true;
|
|
8981
9275
|
};
|
|
8982
9276
|
const noteProxyActivity = () => {
|
|
8983
9277
|
hadProxyActivitySinceContinue = true;
|
|
9278
|
+
sawProxyActivity = true;
|
|
8984
9279
|
};
|
|
8985
9280
|
const resetAutoContinueWindow = () => {
|
|
8986
9281
|
visibleTextSinceContinue = "";
|
|
@@ -9087,10 +9382,33 @@ ${plan}
|
|
|
9087
9382
|
});
|
|
9088
9383
|
}
|
|
9089
9384
|
}
|
|
9385
|
+
if (isSilentTurn({
|
|
9386
|
+
enabled: autoContinueState.enabled,
|
|
9387
|
+
compactionMode,
|
|
9388
|
+
sawVisibleText,
|
|
9389
|
+
sawToolActivity,
|
|
9390
|
+
sawProxyActivity,
|
|
9391
|
+
isError: msg.is_error === true || resultFailure !== void 0,
|
|
9392
|
+
aborted: autoContinueState.aborted,
|
|
9393
|
+
sawQuestion: autoContinueState.sawAskUserQuestion
|
|
9394
|
+
})) {
|
|
9395
|
+
log.notice("claude finished the turn without a reply", {
|
|
9396
|
+
sessionKey: sk,
|
|
9397
|
+
stopReason: lastStopReason,
|
|
9398
|
+
hadReasoning: sawReasoning,
|
|
9399
|
+
attempts: autoContinueState.attempts
|
|
9400
|
+
});
|
|
9401
|
+
controller.enqueue({
|
|
9402
|
+
type: "text-delta",
|
|
9403
|
+
id: startTextBlock(),
|
|
9404
|
+
delta: formatSilentTurnNote(sawReasoning)
|
|
9405
|
+
});
|
|
9406
|
+
endTextBlock();
|
|
9407
|
+
}
|
|
9090
9408
|
controller.enqueue({
|
|
9091
9409
|
type: "finish",
|
|
9092
|
-
finishReason: resultFailure ? { unified: "error", raw: resultFailure } :
|
|
9093
|
-
usage:
|
|
9410
|
+
finishReason: resultFailure ? { unified: "error", raw: resultFailure } : toFinishReason2("stop"),
|
|
9411
|
+
usage: toUsage2(msg.usage),
|
|
9094
9412
|
providerMetadata: {
|
|
9095
9413
|
"claude-code": {
|
|
9096
9414
|
...resultMeta,
|
|
@@ -9134,7 +9452,7 @@ ${plan}
|
|
|
9134
9452
|
if (outer.type === "stream_event") {
|
|
9135
9453
|
gotPartialEvents = true;
|
|
9136
9454
|
}
|
|
9137
|
-
if (
|
|
9455
|
+
if (handleControlRequest2(msg, proc)) {
|
|
9138
9456
|
return;
|
|
9139
9457
|
}
|
|
9140
9458
|
log.debug("stream message", {
|
|
@@ -9838,8 +10156,8 @@ ${plan}
|
|
|
9838
10156
|
}
|
|
9839
10157
|
controller.enqueue({
|
|
9840
10158
|
type: "finish",
|
|
9841
|
-
finishReason:
|
|
9842
|
-
usage:
|
|
10159
|
+
finishReason: toFinishReason2(crashed ? "error" : "stop"),
|
|
10160
|
+
usage: toUsage2(),
|
|
9843
10161
|
providerMetadata: {
|
|
9844
10162
|
"claude-code": {
|
|
9845
10163
|
...resultMeta,
|
|
@@ -10162,14 +10480,15 @@ ${plan}
|
|
|
10162
10480
|
|
|
10163
10481
|
// src/cleanup-stale.ts
|
|
10164
10482
|
import {
|
|
10165
|
-
existsSync as
|
|
10483
|
+
existsSync as existsSync3,
|
|
10484
|
+
mkdirSync as mkdirSync4,
|
|
10166
10485
|
readFileSync as readFileSync6,
|
|
10167
10486
|
realpathSync as realpathSync2,
|
|
10168
10487
|
rmSync as rmSync3,
|
|
10169
10488
|
writeFileSync as writeFileSync5
|
|
10170
10489
|
} from "fs";
|
|
10171
10490
|
import { homedir as homedir7 } from "os";
|
|
10172
|
-
import { join as join9, resolve as
|
|
10491
|
+
import { dirname as dirname6, join as join9, resolve as resolve5 } from "path";
|
|
10173
10492
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
10174
10493
|
var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
|
|
10175
10494
|
var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
|
|
@@ -10188,7 +10507,7 @@ function userOpencodeJsonPath() {
|
|
|
10188
10507
|
}
|
|
10189
10508
|
function userIntendsToUseUnscoped() {
|
|
10190
10509
|
const cfg = userOpencodeJsonPath();
|
|
10191
|
-
if (!
|
|
10510
|
+
if (!existsSync3(cfg)) return false;
|
|
10192
10511
|
try {
|
|
10193
10512
|
const json = JSON.parse(readFileSync6(cfg, "utf8"));
|
|
10194
10513
|
const plugins = json.plugin;
|
|
@@ -10203,17 +10522,57 @@ function userIntendsToUseUnscoped() {
|
|
|
10203
10522
|
function ourLoadedDir() {
|
|
10204
10523
|
try {
|
|
10205
10524
|
const filePath = fileURLToPath3(import.meta.url);
|
|
10206
|
-
return realpathSync2(
|
|
10525
|
+
return realpathSync2(resolve5(filePath, "..", ".."));
|
|
10526
|
+
} catch {
|
|
10527
|
+
return null;
|
|
10528
|
+
}
|
|
10529
|
+
}
|
|
10530
|
+
function ourVersion(dir) {
|
|
10531
|
+
if (!dir) return null;
|
|
10532
|
+
try {
|
|
10533
|
+
const pkg = JSON.parse(readFileSync6(join9(dir, "package.json"), "utf8"));
|
|
10534
|
+
return typeof pkg.version === "string" && pkg.version ? pkg.version : null;
|
|
10535
|
+
} catch {
|
|
10536
|
+
return null;
|
|
10537
|
+
}
|
|
10538
|
+
}
|
|
10539
|
+
function markerPath() {
|
|
10540
|
+
const stateRoot = process.env.XDG_STATE_HOME ?? join9(homedir7(), ".local", "state");
|
|
10541
|
+
return join9(stateRoot, "opencode-claude-code-plugin", "cleanup-stale.json");
|
|
10542
|
+
}
|
|
10543
|
+
function markerVersion() {
|
|
10544
|
+
try {
|
|
10545
|
+
const marker = JSON.parse(readFileSync6(markerPath(), "utf8"));
|
|
10546
|
+
return typeof marker.version === "string" ? marker.version : null;
|
|
10207
10547
|
} catch {
|
|
10208
10548
|
return null;
|
|
10209
10549
|
}
|
|
10210
10550
|
}
|
|
10551
|
+
function writeMarker(version) {
|
|
10552
|
+
const file = markerPath();
|
|
10553
|
+
try {
|
|
10554
|
+
mkdirSync4(dirname6(file), { recursive: true });
|
|
10555
|
+
writeFileSync5(
|
|
10556
|
+
file,
|
|
10557
|
+
JSON.stringify({ version, at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n",
|
|
10558
|
+
"utf8"
|
|
10559
|
+
);
|
|
10560
|
+
} catch (err) {
|
|
10561
|
+
log.warn("cleanup-stale: could not record the sweep marker", {
|
|
10562
|
+
file,
|
|
10563
|
+
error: String(err)
|
|
10564
|
+
});
|
|
10565
|
+
}
|
|
10566
|
+
}
|
|
10211
10567
|
function cleanupStaleUnscopedInstall() {
|
|
10212
10568
|
if (alreadyRan) return;
|
|
10213
10569
|
alreadyRan = true;
|
|
10214
10570
|
if (process.env.OPENCODE_CLAUDE_CODE_PLUGIN_NO_CLEANUP === "1") return;
|
|
10215
10571
|
if (userIntendsToUseUnscoped()) return;
|
|
10216
10572
|
const ourDir = ourLoadedDir();
|
|
10573
|
+
const version = ourVersion(ourDir);
|
|
10574
|
+
const forced = process.env.OPENCODE_CLAUDE_CODE_PLUGIN_FORCE_CLEANUP === "1";
|
|
10575
|
+
if (version && !forced && markerVersion() === version) return;
|
|
10217
10576
|
for (const cacheRoot of candidateCacheRoots()) {
|
|
10218
10577
|
try {
|
|
10219
10578
|
cleanupOne(cacheRoot, ourDir);
|
|
@@ -10224,11 +10583,12 @@ function cleanupStaleUnscopedInstall() {
|
|
|
10224
10583
|
});
|
|
10225
10584
|
}
|
|
10226
10585
|
}
|
|
10586
|
+
if (version) writeMarker(version);
|
|
10227
10587
|
}
|
|
10228
10588
|
function cleanupOne(cacheRoot, ourDir) {
|
|
10229
|
-
if (!
|
|
10589
|
+
if (!existsSync3(cacheRoot)) return;
|
|
10230
10590
|
const stalePath = join9(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
|
|
10231
|
-
if (!
|
|
10591
|
+
if (!existsSync3(stalePath)) return;
|
|
10232
10592
|
let realStalePath = stalePath;
|
|
10233
10593
|
try {
|
|
10234
10594
|
realStalePath = realpathSync2(stalePath);
|
|
@@ -10236,7 +10596,7 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
10236
10596
|
}
|
|
10237
10597
|
if (ourDir && realStalePath === ourDir) return;
|
|
10238
10598
|
const pkgJsonPath = join9(stalePath, "package.json");
|
|
10239
|
-
if (!
|
|
10599
|
+
if (!existsSync3(pkgJsonPath)) return;
|
|
10240
10600
|
let pkg = {};
|
|
10241
10601
|
try {
|
|
10242
10602
|
pkg = JSON.parse(readFileSync6(pkgJsonPath, "utf8"));
|
|
@@ -10256,7 +10616,7 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
10256
10616
|
return;
|
|
10257
10617
|
}
|
|
10258
10618
|
const cachePkgJson = join9(cacheRoot, "package.json");
|
|
10259
|
-
if (!
|
|
10619
|
+
if (!existsSync3(cachePkgJson)) return;
|
|
10260
10620
|
try {
|
|
10261
10621
|
const cfg = JSON.parse(readFileSync6(cachePkgJson, "utf8"));
|
|
10262
10622
|
if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
|
|
@@ -10741,6 +11101,28 @@ function warnIfPlanModeCannotExit(permissionMode) {
|
|
|
10741
11101
|
{ permissionMode, measuredOn: "claude-code 2.1.258" }
|
|
10742
11102
|
);
|
|
10743
11103
|
}
|
|
11104
|
+
function applyPermissionPreset(settings, defaultProxyTools) {
|
|
11105
|
+
const resolved = resolvePermissionPreset(settings, defaultProxyTools);
|
|
11106
|
+
if (resolved === null) return null;
|
|
11107
|
+
if (isUnknownPreset(resolved)) {
|
|
11108
|
+
log.warn(
|
|
11109
|
+
"unknown permissionPreset; no preset applied and every permission setting is left exactly as configured",
|
|
11110
|
+
{ permissionPreset: resolved.unknown, known: ["read-only"] }
|
|
11111
|
+
);
|
|
11112
|
+
return null;
|
|
11113
|
+
}
|
|
11114
|
+
log.notice(`permission preset "${resolved.preset}" applied`, {
|
|
11115
|
+
permissionMode: resolved.permissionMode,
|
|
11116
|
+
skipPermissions: resolved.skipPermissions,
|
|
11117
|
+
proxyTools: resolved.proxyTools,
|
|
11118
|
+
disallowedTools: resolved.extraDisallowedTools,
|
|
11119
|
+
controlRequestBehavior: resolved.controlRequestBehavior
|
|
11120
|
+
});
|
|
11121
|
+
for (const line of resolved.overridden) {
|
|
11122
|
+
log.notice(`permission preset "${resolved.preset}" overrode ${line}`);
|
|
11123
|
+
}
|
|
11124
|
+
return resolved;
|
|
11125
|
+
}
|
|
10744
11126
|
function createClaudeCode(settings = {}) {
|
|
10745
11127
|
if (settings.logging) {
|
|
10746
11128
|
configureLogger({
|
|
@@ -10751,10 +11133,11 @@ function createClaudeCode(settings = {}) {
|
|
|
10751
11133
|
});
|
|
10752
11134
|
}
|
|
10753
11135
|
warnIfAnthropicApiKey(settings.ignoreAnthropicApiKey);
|
|
10754
|
-
|
|
11136
|
+
const preset = applyPermissionPreset(settings, DEFAULT_PROXY_TOOL_NAMES);
|
|
11137
|
+
warnIfPlanModeCannotExit(preset ? void 0 : settings.permissionMode);
|
|
10755
11138
|
const cliPath = settings.cliPath ?? process.env.CLAUDE_CLI_PATH ?? "claude";
|
|
10756
11139
|
const providerName = settings.providerID ?? settings.name ?? "claude-code";
|
|
10757
|
-
const proxyTools = settings.proxyTools ?? [...DEFAULT_PROXY_TOOL_NAMES];
|
|
11140
|
+
const proxyTools = preset?.proxyTools ?? settings.proxyTools ?? [...DEFAULT_PROXY_TOOL_NAMES];
|
|
10758
11141
|
const createModel = (modelId) => {
|
|
10759
11142
|
return new ClaudeCodeLanguageModel(modelId, {
|
|
10760
11143
|
provider: providerName,
|
|
@@ -10767,18 +11150,19 @@ function createClaudeCode(settings = {}) {
|
|
|
10767
11150
|
baseCliPath: settings.baseCliPath ?? cliPath,
|
|
10768
11151
|
accountFailover: settings.accountFailover ?? "ask",
|
|
10769
11152
|
providerID: settings.providerID,
|
|
10770
|
-
skipPermissions: settings.skipPermissions ?? true,
|
|
10771
|
-
permissionMode: settings.permissionMode,
|
|
11153
|
+
skipPermissions: preset?.skipPermissions ?? settings.skipPermissions ?? true,
|
|
11154
|
+
permissionMode: preset?.permissionMode ?? settings.permissionMode,
|
|
11155
|
+
permissionPreset: preset?.preset,
|
|
10772
11156
|
mcpConfig: settings.mcpConfig,
|
|
10773
11157
|
strictMcpConfig: settings.strictMcpConfig,
|
|
10774
11158
|
bridgeOpencodeMcp: settings.bridgeOpencodeMcp ?? true,
|
|
10775
|
-
controlRequestBehavior: settings.controlRequestBehavior ?? "allow",
|
|
10776
|
-
controlRequestToolBehaviors: settings.controlRequestToolBehaviors,
|
|
11159
|
+
controlRequestBehavior: preset?.controlRequestBehavior ?? settings.controlRequestBehavior ?? "allow",
|
|
11160
|
+
controlRequestToolBehaviors: preset ? void 0 : settings.controlRequestToolBehaviors,
|
|
10777
11161
|
controlRequestDenyMessage: settings.controlRequestDenyMessage,
|
|
10778
11162
|
proxyTools,
|
|
10779
11163
|
proxyOpencodeTools: settings.proxyOpencodeTools,
|
|
10780
11164
|
stripContextReminders: settings.stripContextReminders === true,
|
|
10781
|
-
extraDisallowedTools: settings.extraDisallowedTools,
|
|
11165
|
+
extraDisallowedTools: preset?.extraDisallowedTools ?? settings.extraDisallowedTools,
|
|
10782
11166
|
proxyToolTimeoutMs: settings.proxyToolTimeoutMs,
|
|
10783
11167
|
planModeQuestion: settings.planModeQuestion ?? false,
|
|
10784
11168
|
webSearch: settings.webSearch,
|
|
@@ -11102,6 +11486,7 @@ export {
|
|
|
11102
11486
|
ClaudeCodeLanguageModel,
|
|
11103
11487
|
DEFAULT_PROXY_TOOL_NAMES,
|
|
11104
11488
|
_resetPlanModeWarningForTests,
|
|
11489
|
+
applyPermissionPreset,
|
|
11105
11490
|
bridgeOpencodeMcp,
|
|
11106
11491
|
buildAgentRegistry,
|
|
11107
11492
|
claudeCodeProviders,
|