@khalilgharbaoui/opencode-claude-code-plugin 0.18.3 → 0.20.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/dist/index.js CHANGED
@@ -324,8 +324,8 @@ function mapTool(name, input, opts) {
324
324
  }
325
325
  if (name === "TaskUpdate") {
326
326
  if (opts?.sessionId) {
327
- const list = applyTaskUpdate(opts.sessionId, input);
328
- if (list !== null) return emitTodoWrite(list);
327
+ const list2 = applyTaskUpdate(opts.sessionId, input);
328
+ if (list2 !== null) return emitTodoWrite(list2);
329
329
  }
330
330
  return { name, input, executed: true, skip: true };
331
331
  }
@@ -475,9 +475,6 @@ import { randomUUID as randomUUID3 } from "crypto";
475
475
  import { EventEmitter as EventEmitter3 } from "events";
476
476
  import { unlink } from "fs/promises";
477
477
 
478
- // src/proxy-broker.ts
479
- import { EventEmitter as EventEmitter2 } from "events";
480
-
481
478
  // src/proxy-mcp.ts
482
479
  import { createServer } from "http";
483
480
  import * as fs2 from "fs";
@@ -511,7 +508,7 @@ function pluginTmpDir() {
511
508
  }
512
509
 
513
510
  // src/proxy-mcp.ts
514
- var SSE_KEEPALIVE_MS = 15e3;
511
+ var PROXY_KEEPALIVE_MS = 15e3;
515
512
  function acceptsEventStream(acceptHeader) {
516
513
  return typeof acceptHeader === "string" && acceptHeader.toLowerCase().includes("text/event-stream");
517
514
  }
@@ -523,11 +520,11 @@ var PROTOCOL_VERSION = "2024-11-05";
523
520
  var SERVER_NAME = "opencode_proxy";
524
521
  var PROXY_TOOL_PREFIX = `mcp__${SERVER_NAME}__`;
525
522
  var PROXY_DEFAULT_TIMEOUT_MS = 10 * 60 * 1e3;
523
+ var PROXY_NO_DEADLINE_MS = 0;
526
524
  var PROXY_PER_TOOL_DEFAULT_TIMEOUT_MS = {
527
- task: 60 * 60 * 1e3,
528
- // 60 min
529
- task_batch: 60 * 60 * 1e3,
530
- // 60 min, same reasoning: it IS task calls
525
+ task: PROXY_NO_DEADLINE_MS,
526
+ task_batch: PROXY_NO_DEADLINE_MS,
527
+ // same reasoning: it IS task calls
531
528
  question: 30 * 60 * 1e3
532
529
  // 30 min
533
530
  };
@@ -537,7 +534,7 @@ function resolveProxyCallTimeoutMs(toolName, input, overrides) {
537
534
  let ms = PROXY_PER_TOOL_DEFAULT_TIMEOUT_MS[key] ?? PROXY_DEFAULT_TIMEOUT_MS;
538
535
  if (overrides) {
539
536
  const ov = lookupCaseInsensitive(overrides, key);
540
- if (typeof ov === "number" && ov > 0) ms = ov;
537
+ if (isDeadlineOverride(ov)) ms = ov;
541
538
  }
542
539
  if (key === "bash") {
543
540
  const requested = input?.timeout;
@@ -545,6 +542,9 @@ function resolveProxyCallTimeoutMs(toolName, input, overrides) {
545
542
  }
546
543
  return Math.min(ms, MAX_PROXY_TIMEOUT_MS);
547
544
  }
545
+ function isDeadlineOverride(value) {
546
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
547
+ }
548
548
  function lookupCaseInsensitive(map, key) {
549
549
  if (Object.prototype.hasOwnProperty.call(map, key)) return map[key];
550
550
  for (const k of Object.keys(map)) {
@@ -554,14 +554,20 @@ function lookupCaseInsensitive(map, key) {
554
554
  }
555
555
  function resolveProxyClientCeilingMs(overrides) {
556
556
  let ms = PROXY_DEFAULT_TIMEOUT_MS;
557
- for (const v of Object.values(PROXY_PER_TOOL_DEFAULT_TIMEOUT_MS)) {
558
- if (v > ms) ms = v;
559
- }
560
- if (overrides) {
561
- for (const v of Object.values(overrides)) {
562
- if (typeof v === "number" && v > ms) ms = v;
557
+ const consider = (deadlineMs) => {
558
+ if (deadlineMs === PROXY_NO_DEADLINE_MS) return true;
559
+ if (deadlineMs > ms) ms = deadlineMs;
560
+ return false;
561
+ };
562
+ for (const [toolName, defaultMs] of Object.entries(PROXY_PER_TOOL_DEFAULT_TIMEOUT_MS)) {
563
+ const override = overrides ? lookupCaseInsensitive(overrides, toolName) : void 0;
564
+ if (consider(isDeadlineOverride(override) ? override : defaultMs)) {
565
+ return MAX_PROXY_TIMEOUT_MS;
563
566
  }
564
567
  }
568
+ for (const value of Object.values(overrides ?? {})) {
569
+ if (isDeadlineOverride(value) && consider(value)) return MAX_PROXY_TIMEOUT_MS;
570
+ }
565
571
  return Math.min(ms, MAX_PROXY_TIMEOUT_MS);
566
572
  }
567
573
  function buildProxyTimeoutError(toolName, ms) {
@@ -574,9 +580,9 @@ function buildProxyTimeoutError(toolName, ms) {
574
580
  }
575
581
  return new Error(base);
576
582
  }
577
- var TASK_PROXY_NOTE = "This and task_batch are the ONLY tools that dispatch opencode subagents (including user @-mentions). Claude Code's built-in TaskCreate/TaskUpdate manage a local todo list and cannot dispatch subagents. Do not search config files to verify a subagent type exists: invalid types fail fast with a clear error. Foreground calls block until the subagent finishes; set `background` to request opencode's background execution mode. For two or more independent subagents in one response use task_batch, not several task calls: those run one after another. Task calls get a 60-minute proxy deadline by default (configurable via proxyToolTimeoutMs).";
583
+ var TASK_PROXY_NOTE = "This and task_batch are the ONLY tools that dispatch opencode subagents (including user @-mentions). Claude Code's built-in TaskCreate/TaskUpdate manage a local todo list and cannot dispatch subagents. Do not search config files to verify a subagent type exists: invalid types fail fast with a clear error. Foreground calls block until the subagent finishes; set `background` to request opencode's background execution mode. For two or more independent subagents in one response use task_batch, not several task calls: those run one after another. Task calls have no proxy deadline by default: the call waits for the subagent to finish (a positive proxyToolTimeoutMs override adds a deadline).";
578
584
  var TASK_BATCH_TOOL_NAME = "task_batch";
579
- var TASK_BATCH_PROXY_NOTE = "Use this instead of several task calls in one response: Claude Code runs MCP tool calls one at a time, so separate task calls run serially even when emitted together, while one task_batch call fans them out as parallel opencode task calls. Each task takes the same fields as the task tool. Results come back in task order, each labelled. Same 60-minute proxy deadline as task (configurable via proxyToolTimeoutMs).";
585
+ var TASK_BATCH_PROXY_NOTE = "Use this instead of several task calls in one response: Claude Code runs MCP tool calls one at a time, so separate task calls run serially even when emitted together, while one task_batch call fans them out as parallel opencode task calls. Each task takes the same fields as the task tool. Results come back in task order, each labelled. Like task it has no proxy deadline by default (a positive proxyToolTimeoutMs override adds one).";
580
586
  var TASK_INPUT_REQUIRED = ["description", "prompt", "subagent_type"];
581
587
  function taskBatchInputError(input) {
582
588
  const tasks = input?.tasks;
@@ -874,9 +880,10 @@ var DEFAULT_PROXY_TOOLS = [
874
880
  }
875
881
  }
876
882
  ];
877
- async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverrides, interceptors) {
883
+ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverrides, interceptors, options = {}) {
878
884
  const calls = new EventEmitter();
879
885
  const pending = /* @__PURE__ */ new Map();
886
+ const keepaliveMs = options.keepaliveMs ?? PROXY_KEEPALIVE_MS;
880
887
  const authToken = crypto.randomBytes(32).toString("hex");
881
888
  const expectedAuth = Buffer.from(`Bearer ${authToken}`);
882
889
  let boundAuthority = "";
@@ -925,7 +932,7 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
925
932
  }
926
933
  let requestId = null;
927
934
  let requestMethod = null;
928
- let sse = null;
935
+ let reply = null;
929
936
  try {
930
937
  const body = await readBody(req);
931
938
  const request = JSON.parse(body);
@@ -1020,11 +1027,9 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
1020
1027
  sse: acceptsEventStream(req.headers.accept)
1021
1028
  });
1022
1029
  const channel = { closed: false };
1023
- if (acceptsEventStream(req.headers.accept)) {
1024
- sse = openEventStream(res);
1025
- }
1030
+ reply = acceptsEventStream(req.headers.accept) ? openEventStream(res, keepaliveMs) : openJsonStream(res, keepaliveMs);
1026
1031
  res.once("close", () => {
1027
- sse?.stop();
1032
+ reply?.stop();
1028
1033
  if (res.writableFinished) return;
1029
1034
  channel.closed = true;
1030
1035
  log.notice("proxy-mcp client closed a tool call before its result", {
@@ -1049,16 +1054,18 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
1049
1054
  input,
1050
1055
  timeoutOverrides
1051
1056
  );
1052
- timer = setTimeout(() => {
1053
- if (!pending.has(callId)) return;
1054
- pending.delete(callId);
1055
- log.notice("proxy-mcp tool call timed out", {
1056
- callId,
1057
- toolName,
1058
- deadlineMs
1059
- });
1060
- reject2(buildProxyTimeoutError(toolName, deadlineMs));
1061
- }, deadlineMs);
1057
+ if (deadlineMs > PROXY_NO_DEADLINE_MS) {
1058
+ timer = setTimeout(() => {
1059
+ if (!pending.has(callId)) return;
1060
+ pending.delete(callId);
1061
+ log.notice("proxy-mcp tool call timed out", {
1062
+ callId,
1063
+ toolName,
1064
+ deadlineMs
1065
+ });
1066
+ reject2(buildProxyTimeoutError(toolName, deadlineMs));
1067
+ }, deadlineMs);
1068
+ }
1062
1069
  calls.emit("call", entry);
1063
1070
  }
1064
1071
  ).finally(() => {
@@ -1072,7 +1079,7 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
1072
1079
  });
1073
1080
  return;
1074
1081
  }
1075
- writeToolCallResult(res, requestId, result, sse);
1082
+ writeToolCallResult(res, requestId, result, reply);
1076
1083
  return;
1077
1084
  }
1078
1085
  writeJson(res, {
@@ -1092,7 +1099,7 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
1092
1099
  res,
1093
1100
  requestId,
1094
1101
  { kind: "error", message: errorMessage },
1095
- sse
1102
+ reply
1096
1103
  );
1097
1104
  } catch {
1098
1105
  try {
@@ -1174,6 +1181,9 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
1174
1181
  configFilePath = outPath;
1175
1182
  return outPath;
1176
1183
  },
1184
+ pendingCallIds() {
1185
+ return [...pending.keys()];
1186
+ },
1177
1187
  async close() {
1178
1188
  for (const entry of pending.values()) {
1179
1189
  entry.reject(new Error(SERVER_CLOSED_MESSAGE));
@@ -1246,7 +1256,7 @@ function readBody(req) {
1246
1256
  req.on("error", reject);
1247
1257
  });
1248
1258
  }
1249
- function writeToolCallResult(res, requestId, result, sse = null) {
1259
+ function writeToolCallResult(res, requestId, result, reply = null) {
1250
1260
  const text = result.kind === "error" ? result.message : result.text;
1251
1261
  const isError = result.kind === "error" || result.isError === true;
1252
1262
  const envelope = {
@@ -1257,26 +1267,20 @@ function writeToolCallResult(res, requestId, result, sse = null) {
1257
1267
  isError
1258
1268
  }
1259
1269
  };
1260
- if (sse) {
1261
- sse.finish(envelope);
1270
+ if (reply) {
1271
+ reply.finish(envelope);
1262
1272
  return;
1263
1273
  }
1264
1274
  writeJson(res, envelope);
1265
1275
  }
1266
- function openEventStream(res) {
1267
- res.statusCode = 200;
1268
- res.setHeader("Content-Type", "text/event-stream");
1269
- res.setHeader("Cache-Control", "no-cache, no-transform");
1270
- res.setHeader("Connection", "keep-alive");
1271
- res.flushHeaders();
1272
- res.write(": open\n\n");
1276
+ function startKeepalive(res, keepaliveMs, ping) {
1273
1277
  let timer = setInterval(() => {
1274
1278
  if (res.writableEnded || res.destroyed) {
1275
1279
  stop();
1276
1280
  return;
1277
1281
  }
1278
- res.write(": keepalive\n\n");
1279
- }, SSE_KEEPALIVE_MS);
1282
+ res.write(ping);
1283
+ }, keepaliveMs);
1280
1284
  timer.unref?.();
1281
1285
  const stop = () => {
1282
1286
  if (timer) {
@@ -1284,6 +1288,16 @@ function openEventStream(res) {
1284
1288
  timer = null;
1285
1289
  }
1286
1290
  };
1291
+ return stop;
1292
+ }
1293
+ function openEventStream(res, keepaliveMs) {
1294
+ res.statusCode = 200;
1295
+ res.setHeader("Content-Type", "text/event-stream");
1296
+ res.setHeader("Cache-Control", "no-cache, no-transform");
1297
+ res.setHeader("Connection", "keep-alive");
1298
+ res.flushHeaders();
1299
+ res.write(": open\n\n");
1300
+ const stop = startKeepalive(res, keepaliveMs, ": keepalive\n\n");
1287
1301
  return {
1288
1302
  stop,
1289
1303
  finish(envelope) {
@@ -1296,6 +1310,21 @@ data: ${JSON.stringify(envelope)}
1296
1310
  }
1297
1311
  };
1298
1312
  }
1313
+ function openJsonStream(res, keepaliveMs) {
1314
+ res.statusCode = 200;
1315
+ res.setHeader("Content-Type", "application/json");
1316
+ res.setHeader("Cache-Control", "no-cache, no-transform");
1317
+ res.flushHeaders();
1318
+ const stop = startKeepalive(res, keepaliveMs, " ");
1319
+ return {
1320
+ stop,
1321
+ finish(envelope) {
1322
+ stop();
1323
+ if (res.writableEnded || res.destroyed) return;
1324
+ res.end(JSON.stringify(envelope));
1325
+ }
1326
+ };
1327
+ }
1299
1328
  function writeJson(res, body) {
1300
1329
  if (res.destroyed || res.writableEnded) return;
1301
1330
  const payload = JSON.stringify(body);
@@ -1306,6 +1335,7 @@ function writeJson(res, body) {
1306
1335
  }
1307
1336
 
1308
1337
  // src/proxy-broker.ts
1338
+ import { EventEmitter as EventEmitter2 } from "events";
1309
1339
  var pendingByCallId = /* @__PURE__ */ new Map();
1310
1340
  var callIdsBySession = /* @__PURE__ */ new Map();
1311
1341
  var emitter = new EventEmitter2();
@@ -1334,7 +1364,7 @@ function onPendingProxyCall(sessionKey2, handler) {
1334
1364
  function queuePendingProxyCall(sessionKey2, call, timeoutOverrides) {
1335
1365
  const previous = pendingByCallId.get(call.id);
1336
1366
  if (previous) {
1337
- clearTimeout(previous.timer);
1367
+ if (previous.timer) clearTimeout(previous.timer);
1338
1368
  previous.reject(
1339
1369
  new Error(`Replaced pending proxy call ${call.id} with a fresh one`)
1340
1370
  );
@@ -1346,7 +1376,7 @@ function queuePendingProxyCall(sessionKey2, call, timeoutOverrides) {
1346
1376
  call.input,
1347
1377
  timeoutOverrides
1348
1378
  );
1349
- const timer = setTimeout(() => {
1379
+ const timer = deadlineMs > PROXY_NO_DEADLINE_MS ? setTimeout(() => {
1350
1380
  const current = pendingByCallId.get(call.id);
1351
1381
  if (!current) return;
1352
1382
  pendingByCallId.delete(call.id);
@@ -1358,7 +1388,7 @@ function queuePendingProxyCall(sessionKey2, call, timeoutOverrides) {
1358
1388
  toolName: call.toolName,
1359
1389
  deadlineMs
1360
1390
  });
1361
- }, deadlineMs);
1391
+ }, deadlineMs) : null;
1362
1392
  const pending = {
1363
1393
  sessionKey: sessionKey2,
1364
1394
  toolCallId: call.id,
@@ -1366,6 +1396,7 @@ function queuePendingProxyCall(sessionKey2, call, timeoutOverrides) {
1366
1396
  input: call.input,
1367
1397
  channel: call.channel,
1368
1398
  createdAt: Date.now(),
1399
+ deadlineMs,
1369
1400
  timer,
1370
1401
  resolve: call.resolve,
1371
1402
  reject: call.reject
@@ -1397,12 +1428,27 @@ function getPendingProxyCalls(sessionKey2) {
1397
1428
  }
1398
1429
  return out;
1399
1430
  }
1431
+ function snapshotPendingProxyCalls(now = Date.now()) {
1432
+ const out = [];
1433
+ for (const pending of pendingByCallId.values()) {
1434
+ out.push({
1435
+ sessionKey: pending.sessionKey,
1436
+ toolCallId: pending.toolCallId,
1437
+ toolName: pending.toolName,
1438
+ ageMs: Math.max(0, now - pending.createdAt),
1439
+ deadlineMs: pending.deadlineMs,
1440
+ emitted: pending.emitted === true,
1441
+ channelClosed: pending.channel?.closed === true
1442
+ });
1443
+ }
1444
+ return out;
1445
+ }
1400
1446
  function resolvePendingProxyCallById(toolCallId, result) {
1401
1447
  const pending = pendingByCallId.get(toolCallId);
1402
1448
  if (!pending) return false;
1403
1449
  pendingByCallId.delete(toolCallId);
1404
1450
  indexRemove(pending.sessionKey, toolCallId);
1405
- clearTimeout(pending.timer);
1451
+ if (pending.timer) clearTimeout(pending.timer);
1406
1452
  pending.resolve(result);
1407
1453
  log.info("resolved pending proxy call", {
1408
1454
  sessionKey: pending.sessionKey,
@@ -1416,7 +1462,7 @@ function rejectPendingProxyCallById(toolCallId, error) {
1416
1462
  if (!pending) return false;
1417
1463
  pendingByCallId.delete(toolCallId);
1418
1464
  indexRemove(pending.sessionKey, toolCallId);
1419
- clearTimeout(pending.timer);
1465
+ if (pending.timer) clearTimeout(pending.timer);
1420
1466
  pending.reject(error);
1421
1467
  log.notice("rejected pending proxy call", {
1422
1468
  sessionKey: pending.sessionKey,
@@ -1832,13 +1878,30 @@ function takeUnattendedLines(ap) {
1832
1878
  ap.unattendedDropped = 0;
1833
1879
  return { lines, dropped };
1834
1880
  }
1881
+ var STDERR_RETAIN_BYTES = 2 * 1024;
1882
+ function retainStderr(ap, chunk) {
1883
+ ap.lastStderr = ((ap.lastStderr ?? "") + chunk).slice(-STDERR_RETAIN_BYTES);
1884
+ }
1885
+ function describeChildCrash(exitCode, signal, lastStderr) {
1886
+ const how = signal ? `was killed by ${signal}` : typeof exitCode === "number" ? `exited with code ${exitCode}` : "closed its output";
1887
+ const tail = lastStderr?.trim();
1888
+ return `The Claude Code CLI ${how} before finishing this turn (no result was emitted), so the answer above may be incomplete.` + (tail ? `
1889
+
1890
+ Last stderr from the CLI:
1891
+ ${tail}` : "");
1892
+ }
1835
1893
  var activeProcesses = /* @__PURE__ */ new Map();
1836
1894
  var claudeSessions = /* @__PURE__ */ new Map();
1837
1895
  var idleEvictionTimers = /* @__PURE__ */ new Map();
1838
1896
  var MAX_IDLE_TIMEOUT_MS = 2147483647;
1897
+ var DEFAULT_IDLE_PROCESS_TIMEOUT_MS = 0;
1898
+ function resolveIdleProcessTimeoutMs(configured) {
1899
+ return configured === void 0 ? DEFAULT_IDLE_PROCESS_TIMEOUT_MS : configured;
1900
+ }
1839
1901
  var MAX_ACTIVE_PROCESSES = 16;
1840
1902
  var PROCESS_EXIT_TIMEOUT_MS = 1500;
1841
1903
  var PROCESS_FORCE_EXIT_TIMEOUT_MS = 500;
1904
+ var CHILD_EXITED_MESSAGE = "Claude CLI subprocess closed before pending tool calls were resolved";
1842
1905
  function envFlagEnabled(value) {
1843
1906
  if (value === void 0) return false;
1844
1907
  const normalized = value.trim().toLowerCase();
@@ -1877,10 +1940,22 @@ function touch(key) {
1877
1940
  }
1878
1941
  function evictIfNeeded() {
1879
1942
  while (activeProcesses.size >= MAX_ACTIVE_PROCESSES) {
1880
- const oldestKey = activeProcesses.keys().next().value;
1881
- if (!oldestKey) break;
1882
- log.info("evicting LRU claude process", { sessionKey: oldestKey });
1883
- deleteActiveProcess(oldestKey);
1943
+ let victimKey;
1944
+ for (const [key, ap] of activeProcesses) {
1945
+ if (!isTurnInFlight(ap)) {
1946
+ victimKey = key;
1947
+ break;
1948
+ }
1949
+ }
1950
+ if (!victimKey) {
1951
+ log.warn("every claude process is mid-turn; skipping LRU eviction", {
1952
+ active: activeProcesses.size,
1953
+ cap: MAX_ACTIVE_PROCESSES
1954
+ });
1955
+ return;
1956
+ }
1957
+ log.info("evicting LRU claude process", { sessionKey: victimKey });
1958
+ deleteActiveProcess(victimKey);
1884
1959
  }
1885
1960
  }
1886
1961
  var TURN_INTERRUPT_TIMEOUT_MS = 5e3;
@@ -1976,6 +2051,11 @@ function scheduleIdleProcessEviction(key, timeoutMs) {
1976
2051
  const timer = setTimeout(() => {
1977
2052
  idleEvictionTimers.delete(key);
1978
2053
  if (activeProcesses.get(key) !== scheduledProcess) return;
2054
+ if (isTurnInFlight(scheduledProcess)) {
2055
+ log.info("idle timer found a turn in flight; re-arming", { sessionKey: key, timeoutMs });
2056
+ scheduleIdleProcessEviction(key, timeoutMs);
2057
+ return;
2058
+ }
1979
2059
  log.info("evicting idle claude process", { sessionKey: key, timeoutMs });
1980
2060
  deleteActiveProcess(key);
1981
2061
  }, timeoutMs);
@@ -1987,9 +2067,44 @@ function detachActiveProcess(key) {
1987
2067
  const ap = activeProcesses.get(key);
1988
2068
  if (!ap) return void 0;
1989
2069
  activeProcesses.delete(key);
1990
- void ap.proxyServer?.close();
2070
+ if (ap.proxyServer) {
2071
+ void ap.proxyServer.close();
2072
+ rejectAllPendingProxyCallsForSession(key, new Error(SERVER_CLOSED_MESSAGE));
2073
+ }
1991
2074
  return ap;
1992
2075
  }
2076
+ function deleteActiveProcessesForSession(sessionID) {
2077
+ if (!sessionID || sessionID === "default") return [];
2078
+ const released = [];
2079
+ for (const [key, ap] of [...activeProcesses]) {
2080
+ const owned = ap.opencodeSessionID === sessionID || describeSessionKey(key).session === sessionID;
2081
+ if (!owned) continue;
2082
+ log.info("releasing claude process for deleted session", { sessionKey: key, sessionID });
2083
+ void deleteActiveProcessAndWait(key);
2084
+ released.push(key);
2085
+ }
2086
+ for (const key of [...claudeSessions.keys()]) {
2087
+ if (describeSessionKey(key).session !== sessionID) continue;
2088
+ deleteClaudeSessionId(key);
2089
+ clearCompression(key);
2090
+ if (!released.includes(key)) released.push(key);
2091
+ }
2092
+ return released;
2093
+ }
2094
+ function killAllActiveProcesses() {
2095
+ const keys = [...activeProcesses.keys()];
2096
+ for (const key of keys) deleteActiveProcess(key);
2097
+ return keys;
2098
+ }
2099
+ var processExitCleanupWired = false;
2100
+ function ensureProcessExitCleanup() {
2101
+ if (processExitCleanupWired) return false;
2102
+ processExitCleanupWired = true;
2103
+ process.once("exit", () => {
2104
+ killAllActiveProcesses();
2105
+ });
2106
+ return true;
2107
+ }
1993
2108
  function deleteActiveProcess(key) {
1994
2109
  const ap = detachActiveProcess(key);
1995
2110
  ap?.proc.kill();
@@ -2095,6 +2210,7 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
2095
2210
  mcpHash,
2096
2211
  systemPromptFile,
2097
2212
  effort,
2213
+ startedAt: Date.now(),
2098
2214
  cliArgs: [...cliArgs],
2099
2215
  unattendedLines: [],
2100
2216
  unattendedDropped: 0
@@ -2118,6 +2234,14 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
2118
2234
  proc.on("error", (err) => {
2119
2235
  log.error("claude process error", { sessionKey: sessionKey2, error: err.message });
2120
2236
  });
2237
+ proc.stdin?.on("error", (err) => {
2238
+ log.warn("claude process stdin error", {
2239
+ sessionKey: sessionKey2,
2240
+ code: err.code,
2241
+ error: err.message
2242
+ });
2243
+ settleTurn(ap);
2244
+ });
2121
2245
  proc.on("exit", (code, signal) => {
2122
2246
  log.info("claude process exited", { code, signal, sessionKey: sessionKey2 });
2123
2247
  void proxyServer?.close();
@@ -2129,6 +2253,12 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
2129
2253
  if (ownsSessionKey) {
2130
2254
  cancelIdleProcessEviction(sessionKey2);
2131
2255
  activeProcesses.delete(sessionKey2);
2256
+ if (getPendingProxyCalls(sessionKey2).length > 0) {
2257
+ rejectAllPendingProxyCallsForSession(
2258
+ sessionKey2,
2259
+ new Error(CHILD_EXITED_MESSAGE)
2260
+ );
2261
+ }
2132
2262
  }
2133
2263
  if (ownsSessionKey && code !== 0 && code !== null) {
2134
2264
  log.info("process exited with error, clearing session", {
@@ -2141,6 +2271,7 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
2141
2271
  proc.stderr?.on("data", (data) => {
2142
2272
  const stderr = data.toString();
2143
2273
  log.debug("stderr", { data: stderr.slice(0, 200) });
2274
+ retainStderr(ap, stderr);
2144
2275
  if (stderr.includes("No conversation found") || stderr.includes("Session ID") && (stderr.includes("already in use") || stderr.includes("not found") || stderr.includes("invalid"))) {
2145
2276
  if (activeProcesses.get(sessionKey2) === ap) {
2146
2277
  log.warn("claude session ID error, clearing session", {
@@ -2168,6 +2299,7 @@ function appendResumeIfNeeded(sessionKey2, cliArgs) {
2168
2299
  function respawnActiveProcess(sessionKey2, cliPath, cliArgs, cwd, ignoreAnthropicApiKey) {
2169
2300
  const old = activeProcesses.get(sessionKey2);
2170
2301
  if (!old) return void 0;
2302
+ const turnWasInFlight = isTurnInFlight(old);
2171
2303
  activeProcesses.delete(sessionKey2);
2172
2304
  old.proc.removeAllListeners("exit");
2173
2305
  try {
@@ -2187,6 +2319,7 @@ function respawnActiveProcess(sessionKey2, cliPath, cliArgs, cwd, ignoreAnthropi
2187
2319
  );
2188
2320
  replacement.pendingProxyCompletions = old.pendingProxyCompletions;
2189
2321
  delete old.pendingProxyCompletions;
2322
+ if (turnWasInFlight) noteTurnStarted(replacement);
2190
2323
  return replacement;
2191
2324
  }
2192
2325
  function buildCliArgs(opts) {
@@ -2263,6 +2396,36 @@ function buildCliArgs(opts) {
2263
2396
  function sessionKey(cwd, modelId) {
2264
2397
  return `${cwd}::${modelId}`;
2265
2398
  }
2399
+ function describeSessionKey(key) {
2400
+ const parts = key.split("::");
2401
+ return {
2402
+ cwd: parts[0] ?? "unknown",
2403
+ model: parts[1] ?? "unknown",
2404
+ session: parts[3] ?? "unknown",
2405
+ compaction: parts[2] === "compaction"
2406
+ };
2407
+ }
2408
+ function snapshotActiveProcesses(now = Date.now()) {
2409
+ const out = [];
2410
+ for (const [key, ap] of activeProcesses) {
2411
+ const described = describeSessionKey(key);
2412
+ const lastStderr = ap.lastStderr;
2413
+ out.push({
2414
+ sessionKey: key,
2415
+ session: ap.opencodeSessionID ?? described.session,
2416
+ model: described.model,
2417
+ compaction: described.compaction,
2418
+ pid: ap.proc.pid,
2419
+ inFlight: ap.turnInFlight === true,
2420
+ ageMs: ap.startedAt === void 0 ? void 0 : Math.max(0, now - ap.startedAt),
2421
+ effort: ap.effort,
2422
+ attached: ap.lineEmitter.listenerCount("line") > 0,
2423
+ proxyUrl: ap.proxyServer?.url,
2424
+ lastStderr: typeof lastStderr === "string" ? lastStderr : void 0
2425
+ });
2426
+ }
2427
+ return out;
2428
+ }
2266
2429
 
2267
2430
  // src/btw-command.ts
2268
2431
  var BtwHandledError = class extends Error {
@@ -2548,1359 +2711,2023 @@ async function handleBtwCommand(client, input, options = {}) {
2548
2711
  }
2549
2712
  }
2550
2713
 
2551
- // src/message-builder.ts
2552
- var ASIDE_MARKERS = [INLINE_ASIDE_MARKER, ...LEGACY_INLINE_ASIDE_MARKERS];
2553
- function isInlineAside(part) {
2554
- if (!part || part.type !== "text" || typeof part.text !== "string") return false;
2555
- const text = part.text.trimStart();
2556
- return ASIDE_MARKERS.some((marker) => text.startsWith(marker));
2714
+ // src/cli-events.ts
2715
+ function isRecord2(value) {
2716
+ return value !== null && typeof value === "object" && !Array.isArray(value);
2557
2717
  }
2558
- function stripInlineAsides(content) {
2559
- if (!Array.isArray(content)) return content;
2560
- const kept = content.filter((part) => !isInlineAside(part));
2561
- return kept.length === content.length ? content : kept;
2718
+ function str(value) {
2719
+ return typeof value === "string" && value.length > 0 ? value : void 0;
2720
+ }
2721
+ function num(value) {
2722
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2723
+ }
2724
+ var RATE_LIMIT_MARKER = "\u258C **rate limit:**";
2725
+ var RATE_LIMIT_ACTION = "Enable extra usage or add credits on the account, wait for the window to reset, switch account, org or plan, or authenticate with an API key.";
2726
+ var OVERAGE_DISABLED_REASONS = {
2727
+ overage_not_provisioned: "extra usage is not set up on this account",
2728
+ org_level_disabled: "extra usage is disabled for your organization",
2729
+ org_level_disabled_until: "extra usage is disabled for your organization for now",
2730
+ out_of_credits: "the account's usage credits are spent",
2731
+ seat_tier_level_disabled: "your seat tier does not allow extra usage",
2732
+ member_level_disabled: "extra usage is disabled for your member account",
2733
+ seat_tier_zero_credit_limit: "your seat tier has a zero credit limit",
2734
+ group_zero_credit_limit: "your group has a zero credit limit",
2735
+ member_zero_credit_limit: "your member account has a zero credit limit",
2736
+ org_service_level_disabled: "your organization's service level does not include extra usage",
2737
+ no_limits_configured: "no usage limits are configured for this account",
2738
+ fetch_error: "the CLI could not read the account's usage limits"
2739
+ };
2740
+ var RATE_LIMIT_WINDOWS = {
2741
+ five_hour: "the 5-hour window",
2742
+ seven_day: "the 7-day window",
2743
+ seven_day_opus: "the 7-day Opus window",
2744
+ seven_day_sonnet: "the 7-day Sonnet window",
2745
+ seven_day_overage_included: "the 7-day extra-usage window",
2746
+ overage: "extra usage"
2747
+ };
2748
+ function parseRateLimitEvent(msg) {
2749
+ if (msg.type !== "rate_limit_event") return null;
2750
+ const info = msg.rate_limit_info;
2751
+ if (!isRecord2(info)) return null;
2752
+ return {
2753
+ status: str(info.status),
2754
+ rateLimitType: str(info.rateLimitType),
2755
+ resetsAt: num(info.resetsAt),
2756
+ utilization: num(info.utilization),
2757
+ isUsingOverage: info.isUsingOverage === true,
2758
+ overageStatus: str(info.overageStatus),
2759
+ overageResetsAt: num(info.overageResetsAt),
2760
+ overageDisabledReason: str(info.overageDisabledReason)
2761
+ };
2562
2762
  }
2563
- function filterSideQuestionHistory(prompt) {
2564
- let aside = false;
2565
- const kept = prompt.filter((message) => {
2566
- if (message.role === "user") {
2567
- aside = parseSideQuestionContent(message.content) !== null;
2568
- return !aside;
2569
- }
2570
- return message.role !== "assistant" || !aside;
2571
- });
2572
- return kept.map(
2573
- (message) => message.role === "assistant" ? { ...message, content: stripInlineAsides(message.content) } : message
2574
- );
2763
+ function formatResetsAt(resetsAt) {
2764
+ if (resetsAt === void 0) return void 0;
2765
+ const ms = resetsAt < 1e12 ? resetsAt * 1e3 : resetsAt;
2766
+ const date = new Date(ms);
2767
+ return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
2575
2768
  }
2576
- var SUPPORTED_IMAGE_TYPES = /* @__PURE__ */ new Set([
2577
- "image/jpeg",
2578
- "image/png",
2579
- "image/gif",
2580
- "image/webp"
2581
- ]);
2582
- function toImageBlock(part) {
2583
- const raw = part.image ?? part.data ?? part.url ?? part.source?.data;
2584
- if (!raw) {
2585
- log.warn("file part without data, skipping");
2586
- return null;
2769
+ function rateLimitKey(info) {
2770
+ return [
2771
+ info.status ?? "?",
2772
+ info.rateLimitType ?? "?",
2773
+ info.overageStatus ?? "?",
2774
+ info.overageDisabledReason ?? "?"
2775
+ ].join("|");
2776
+ }
2777
+ function isRateLimitRejected(info) {
2778
+ return info.status === "rejected" || info.overageStatus === "rejected";
2779
+ }
2780
+ function describeRateLimit(info) {
2781
+ if (!info.status && !info.overageStatus) return null;
2782
+ const window = info.rateLimitType ? RATE_LIMIT_WINDOWS[info.rateLimitType] ?? info.rateLimitType : void 0;
2783
+ const resets = formatResetsAt(info.resetsAt);
2784
+ const overageResets = formatResetsAt(info.overageResetsAt);
2785
+ const key = rateLimitKey(info);
2786
+ if (isRateLimitRejected(info)) {
2787
+ const parts = [];
2788
+ parts.push(
2789
+ info.status === "rejected" ? `Claude Code rejected this request: you are out of usage${window ? ` in ${window}` : ""}.` : "Claude Code rejected this request: paid extra usage is not available on this account."
2790
+ );
2791
+ const reason = info.overageDisabledReason ? OVERAGE_DISABLED_REASONS[info.overageDisabledReason] ?? info.overageDisabledReason : void 0;
2792
+ if (reason) parts.push(`Extra usage is unavailable because ${reason}.`);
2793
+ const resetAt = resets ?? overageResets;
2794
+ if (resetAt) parts.push(`Resets at ${resetAt}.`);
2795
+ parts.push(RATE_LIMIT_ACTION);
2796
+ const message = parts.join(" ");
2797
+ return { key, level: "warn", message, transcript: `
2798
+ ${RATE_LIMIT_MARKER} ${message}
2799
+ ` };
2800
+ }
2801
+ if (info.status === "allowed_warning" || info.overageStatus === "allowed_warning") {
2802
+ const used = info.utilization === void 0 ? "" : ` (${Math.round(info.utilization * 100)}% used)`;
2803
+ return {
2804
+ key,
2805
+ level: "notice",
2806
+ message: `Approaching the usage limit${window ? ` for ${window}` : ""}${used}${resets ? `, resets at ${resets}` : ""}.`,
2807
+ transcript: null
2808
+ };
2587
2809
  }
2588
- let resolvedMediaType = part.mediaType || part.mimeType || part.mime || "";
2589
- let base64 = null;
2590
- if (typeof raw === "string") {
2591
- if (raw.startsWith("data:")) {
2592
- const match = /^data:([^;,]+)(?:;[^,]*)*(?:;base64)?,(.*)$/s.exec(raw);
2593
- if (!match) {
2594
- log.warn("malformed data URI, skipping file part");
2595
- return null;
2596
- }
2597
- resolvedMediaType = resolvedMediaType || match[1];
2598
- base64 = match[2];
2599
- } else if (/^https?:\/\//i.test(raw)) {
2600
- log.warn("remote URL images are not supported by Claude CLI, skipping");
2601
- return null;
2602
- } else {
2603
- base64 = raw;
2604
- }
2605
- } else if (raw instanceof URL) {
2606
- log.warn("remote URL images are not supported by Claude CLI, skipping");
2607
- return null;
2608
- } else if (raw instanceof Uint8Array || Buffer.isBuffer(raw)) {
2609
- base64 = Buffer.from(raw).toString("base64");
2610
- } else {
2611
- log.warn("unsupported file part data type", { dataType: typeof raw });
2810
+ return {
2811
+ key,
2812
+ level: "info",
2813
+ message: `usage limits updated${window ? ` for ${window}` : ""}`,
2814
+ transcript: null
2815
+ };
2816
+ }
2817
+ var reportedRateLimits = /* @__PURE__ */ new Set();
2818
+ function reportRateLimitEvent(msg) {
2819
+ const info = parseRateLimitEvent(msg);
2820
+ if (!info) return null;
2821
+ const report = describeRateLimit(info);
2822
+ if (!report) return null;
2823
+ const data = { ...info };
2824
+ if (reportedRateLimits.has(report.key)) {
2825
+ log.debug(report.message, data);
2612
2826
  return null;
2613
2827
  }
2614
- if (!resolvedMediaType || !SUPPORTED_IMAGE_TYPES.has(resolvedMediaType)) {
2615
- log.warn("unsupported media type for Claude image block, skipping", {
2616
- mediaType: resolvedMediaType
2617
- });
2618
- return null;
2828
+ reportedRateLimits.add(report.key);
2829
+ log[report.level](report.message, data);
2830
+ return report.transcript;
2831
+ }
2832
+ var API_KEY_SOURCES = /* @__PURE__ */ new Set([
2833
+ "ANTHROPIC_API_KEY",
2834
+ "apiKeyHelper",
2835
+ "/login managed key",
2836
+ "user",
2837
+ "project",
2838
+ "org",
2839
+ "temporary"
2840
+ ]);
2841
+ function parseSystemInit(msg) {
2842
+ if (msg.type !== "system" || msg.subtype !== "init") return null;
2843
+ const raw = msg;
2844
+ const servers = [];
2845
+ if (Array.isArray(raw.mcp_servers)) {
2846
+ for (const entry of raw.mcp_servers) {
2847
+ if (!isRecord2(entry)) continue;
2848
+ servers.push({ name: str(entry.name) ?? "unknown", status: str(entry.status) ?? "unknown" });
2849
+ }
2619
2850
  }
2620
2851
  return {
2621
- type: "image",
2622
- source: { type: "base64", media_type: resolvedMediaType, data: base64 }
2852
+ apiKeySource: str(raw.apiKeySource),
2853
+ permissionMode: str(raw.permissionMode),
2854
+ model: str(raw.model),
2855
+ cliVersion: str(raw.claude_code_version),
2856
+ toolCount: Array.isArray(raw.tools) ? raw.tools.length : 0,
2857
+ mcpServers: servers
2623
2858
  };
2624
2859
  }
2625
- function getToolResultText(part) {
2626
- const value = part.output ?? part.result;
2627
- if (typeof value === "string") {
2628
- return value;
2629
- }
2630
- if (!value || typeof value !== "object") {
2631
- return JSON.stringify(value);
2860
+ function apiKeySourceWarning(apiKeySource, ignoreAnthropicApiKey) {
2861
+ if (!apiKeySource || !API_KEY_SOURCES.has(apiKeySource)) return null;
2862
+ const base = `Claude Code authenticated with an API key (apiKeySource: ${apiKeySource}), so these turns bill as pay-as-you-go API usage instead of your subscription's Agent SDK credit.`;
2863
+ return ignoreAnthropicApiKey ? `${base} \`ignoreAnthropicApiKey\` is already on, so the key is not coming from the environment: check the CLI's own settings (\`claude config\`) or an \`apiKeyHelper\`.` : `${base} Set the provider option \`ignoreAnthropicApiKey: true\` to strip the key from spawns and fall back to the stored subscription auth.`;
2864
+ }
2865
+ var warnedApiKeySources = /* @__PURE__ */ new Set();
2866
+ var warnedMcpFailures = /* @__PURE__ */ new Set();
2867
+ function reportSystemInit(msg, options = {}) {
2868
+ const info = parseSystemInit(msg);
2869
+ if (!info) return;
2870
+ log.info("claude session init", {
2871
+ apiKeySource: info.apiKeySource ?? null,
2872
+ permissionMode: info.permissionMode ?? null,
2873
+ model: info.model ?? null,
2874
+ cliVersion: info.cliVersion ?? null,
2875
+ tools: info.toolCount,
2876
+ mcpServers: info.mcpServers
2877
+ });
2878
+ for (const server2 of info.mcpServers) {
2879
+ if (server2.status === "connected") continue;
2880
+ const key2 = `${server2.name}:${server2.status}`;
2881
+ const message = `MCP server "${server2.name}" is ${server2.status} in Claude Code; its tools are not available to the model this session.`;
2882
+ if (warnedMcpFailures.has(key2)) {
2883
+ log.debug(message, { server: server2.name, status: server2.status });
2884
+ continue;
2885
+ }
2886
+ warnedMcpFailures.add(key2);
2887
+ log.warn(message, { server: server2.name, status: server2.status });
2632
2888
  }
2633
- switch (value.type) {
2634
- case "text":
2635
- case "error-text":
2636
- return String(value.value);
2637
- case "json":
2638
- case "error-json":
2639
- return JSON.stringify(value.value);
2640
- case "execution-denied":
2641
- return value.reason ? `Execution denied: ${value.reason}` : "Execution denied";
2642
- case "content":
2643
- return Array.isArray(value.value) ? value.value.map((item) => {
2644
- if (item?.type === "text") return item.text;
2645
- return JSON.stringify(item);
2646
- }).join("\n") : JSON.stringify(value.value);
2647
- default:
2648
- return JSON.stringify(value);
2889
+ const apiKeyMessage = apiKeySourceWarning(info.apiKeySource, options.ignoreAnthropicApiKey);
2890
+ if (!apiKeyMessage) return;
2891
+ const key = `${info.apiKeySource}:${options.ignoreAnthropicApiKey ? "ignored" : "passed"}`;
2892
+ if (warnedApiKeySources.has(key)) {
2893
+ log.debug(apiKeyMessage, { apiKeySource: info.apiKeySource });
2894
+ return;
2649
2895
  }
2896
+ warnedApiKeySources.add(key);
2897
+ log.warn(apiKeyMessage, { apiKeySource: info.apiKeySource });
2650
2898
  }
2651
- var MAX_HISTORY_CHARS = 18e4;
2652
- var MAX_TOOL_RESULT_CHARS = 1e4;
2653
- var MAX_TOOL_INPUT_CHARS = 2e3;
2654
- function clipWithMarker(text, max) {
2655
- if (text.length <= max) return text;
2656
- return `${text.slice(0, max)}
2657
- \u2026[truncated ${text.length - max} chars]`;
2899
+ var COMPACT_BOUNDARY_MARKER = "\u258C **context compacted:**";
2900
+ function parseCompactBoundary(msg) {
2901
+ if (msg.type !== "system" || msg.subtype !== "compact_boundary") return null;
2902
+ const raw = msg;
2903
+ const meta = isRecord2(raw.compact_metadata) ? raw.compact_metadata : isRecord2(raw.compactMetadata) ? raw.compactMetadata : void 0;
2904
+ return {
2905
+ trigger: str(meta?.trigger) ?? "unknown",
2906
+ preTokens: num(meta?.pre_tokens) ?? num(meta?.preTokens),
2907
+ postTokens: num(meta?.post_tokens) ?? num(meta?.postTokens)
2908
+ };
2658
2909
  }
2659
- function renderToolInput(input) {
2660
- let raw;
2910
+ function formatCompactBoundaryNote(boundary) {
2911
+ const how = boundary.trigger === "auto" ? "on its own" : `on a ${boundary.trigger} request`;
2912
+ const sizes = boundary.preTokens !== void 0 && boundary.postTokens !== void 0 ? ` (${boundary.preTokens.toLocaleString("en-US")} tokens to ${boundary.postTokens.toLocaleString("en-US")})` : "";
2913
+ return `
2914
+ ${COMPACT_BOUNDARY_MARKER} Claude Code compacted its own context ${how}${sizes}. Earlier detail in this conversation is now a summary.
2915
+ `;
2916
+ }
2917
+ function reportCompactBoundary(msg) {
2918
+ const boundary = parseCompactBoundary(msg);
2919
+ if (!boundary) return null;
2920
+ log.notice("claude code compacted its own context", {
2921
+ trigger: boundary.trigger,
2922
+ preTokens: boundary.preTokens ?? null,
2923
+ postTokens: boundary.postTokens ?? null
2924
+ });
2925
+ return formatCompactBoundaryNote(boundary);
2926
+ }
2927
+ var RESULT_ERROR_MARKER = "\u258C **claude code error:**";
2928
+ var RESULT_SUBTYPES = {
2929
+ error_max_turns: "it hit its internal turn limit before finishing",
2930
+ error_during_execution: "it failed while running the turn",
2931
+ error_max_budget_usd: "it hit the configured spend limit for the turn",
2932
+ error_max_structured_output_retries: "it could not produce valid structured output"
2933
+ };
2934
+ function describeResultFailure(msg) {
2935
+ if (msg.type !== "result") return null;
2936
+ const subtype = msg.subtype;
2937
+ if (!subtype || subtype === "success") return null;
2938
+ const explanation = RESULT_SUBTYPES[subtype];
2939
+ return explanation ? `Claude Code ended the turn with \`${subtype}\`: ${explanation}.` : `Claude Code ended the turn with \`${subtype}\`.`;
2940
+ }
2941
+ function formatResultFailureNote(message) {
2942
+ return `
2943
+ ${RESULT_ERROR_MARKER} ${message}
2944
+ `;
2945
+ }
2946
+
2947
+ // src/startup-diagnostics.ts
2948
+ import { execFile as execFile2 } from "child_process";
2949
+ import * as fs4 from "fs";
2950
+ import * as path4 from "path";
2951
+ import { promisify as promisify2 } from "util";
2952
+ import { fileURLToPath } from "url";
2953
+
2954
+ // src/mcp-bridge.ts
2955
+ import * as fs3 from "fs";
2956
+ import * as path3 from "path";
2957
+ import * as os2 from "os";
2958
+ import * as crypto2 from "crypto";
2959
+ import {
2960
+ parse as parseJsonc,
2961
+ printParseErrorCode
2962
+ } from "jsonc-parser";
2963
+ var FILE_NAMES = ["opencode.jsonc", "opencode.json", "config.json"];
2964
+ var PROJECT_FILE_NAMES = ["opencode.json", "opencode.jsonc"];
2965
+ function fileExists(p) {
2661
2966
  try {
2662
- raw = typeof input === "string" ? input : JSON.stringify(input);
2967
+ return fs3.statSync(p).isFile();
2663
2968
  } catch {
2664
- raw = String(input);
2969
+ return false;
2665
2970
  }
2666
- return clipWithMarker(raw, MAX_TOOL_INPUT_CHARS);
2667
2971
  }
2668
- function renderMessageContentForCompaction(msg) {
2669
- const lines = [];
2670
- let toolResultCount = 0;
2671
- if (typeof msg.content === "string") {
2672
- return { text: msg.content, toolResultCount: 0 };
2972
+ function dirExists(p) {
2973
+ try {
2974
+ return fs3.statSync(p).isDirectory();
2975
+ } catch {
2976
+ return false;
2673
2977
  }
2674
- if (!Array.isArray(msg.content)) {
2675
- return { text: "", toolResultCount: 0 };
2978
+ }
2979
+ function readAndParse(file) {
2980
+ try {
2981
+ const raw = fs3.readFileSync(file, "utf8");
2982
+ const errors = [];
2983
+ const parsed = parseJsonc(raw, errors, { allowTrailingComma: true });
2984
+ if (errors.length > 0) {
2985
+ const first = errors[0];
2986
+ throw new Error(
2987
+ `${printParseErrorCode(first.error)} at offset ${first.offset}`
2988
+ );
2989
+ }
2990
+ return parsed;
2991
+ } catch (e) {
2992
+ log.warn("failed to parse opencode config", {
2993
+ file,
2994
+ error: e instanceof Error ? e.message : String(e)
2995
+ });
2996
+ return null;
2676
2997
  }
2677
- for (const part of msg.content) {
2678
- if (!part) continue;
2679
- switch (part.type) {
2680
- case "text":
2681
- if (part.text) lines.push(part.text);
2682
- break;
2683
- case "tool-call":
2684
- lines.push(
2685
- `[tool_use:${part.toolName ?? "unknown"}(${renderToolInput(part.input)})]`
2686
- );
2687
- break;
2688
- case "tool-result":
2689
- toolResultCount++;
2690
- lines.push(
2691
- `[tool_result:${part.toolName ?? part.toolCallId ?? "unknown"}]
2692
- ${clipWithMarker(
2693
- getToolResultText(part),
2694
- MAX_TOOL_RESULT_CHARS
2695
- )}`
2696
- );
2697
- break;
2698
- case "image":
2699
- lines.push(
2700
- `[image: ${part.mediaType ?? part.mimeType ?? "unknown"}]`
2701
- );
2702
- break;
2703
- case "file":
2704
- lines.push(
2705
- `[file: ${part.mediaType ?? part.mimeType ?? "unknown"}]`
2706
- );
2707
- break;
2708
- case "reasoning":
2709
- break;
2998
+ }
2999
+ function isPlainObject(x) {
3000
+ return typeof x === "object" && x !== null && !Array.isArray(x);
3001
+ }
3002
+ function deepMerge(target, source) {
3003
+ const out = { ...target };
3004
+ for (const [k, v] of Object.entries(source)) {
3005
+ if (v === void 0) continue;
3006
+ const existing = out[k];
3007
+ if (isPlainObject(existing) && isPlainObject(v)) {
3008
+ out[k] = deepMerge(existing, v);
3009
+ } else {
3010
+ out[k] = v;
2710
3011
  }
2711
3012
  }
2712
- return { text: lines.join("\n"), toolResultCount };
3013
+ return out;
2713
3014
  }
2714
- function compactConversationHistory(prompt, opts = {}) {
2715
- const mode = opts.mode ?? "fresh-session";
2716
- prompt = filterSideQuestionHistory(prompt);
2717
- if (mode === "compaction") {
2718
- return buildCompactionHistory(prompt);
2719
- }
2720
- const conversationMessages = prompt.filter(
2721
- (m) => m.role === "user" || m.role === "assistant" || m.role === "tool"
2722
- );
2723
- if (conversationMessages.length <= 1) {
2724
- return null;
3015
+ function walkUp(opts) {
3016
+ const out = [];
3017
+ let current = path3.resolve(opts.start);
3018
+ while (true) {
3019
+ for (const target of opts.targets) {
3020
+ const candidate = path3.join(current, target);
3021
+ if (opts.predicate(candidate)) out.push(candidate);
3022
+ }
3023
+ if (opts.stop && current === path3.resolve(opts.stop)) break;
3024
+ const parent = path3.dirname(current);
3025
+ if (parent === current) break;
3026
+ current = parent;
2725
3027
  }
2726
- const historyParts = [];
2727
- for (let i = 0; i < conversationMessages.length - 1; i++) {
2728
- const msg = conversationMessages[i];
2729
- const role = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : "Tool";
2730
- const { text } = renderMessageContentForCompaction(msg);
2731
- if (text.trim()) {
2732
- const truncated = text.length > 2e3 ? text.slice(0, 2e3) + "..." : text;
2733
- historyParts.push(`${role}: ${truncated}`);
3028
+ return out;
3029
+ }
3030
+ function detectWorktree(cwd) {
3031
+ const override = process.env.OPENCODE_WORKTREE;
3032
+ if (override) return path3.resolve(override);
3033
+ let current = path3.resolve(cwd);
3034
+ while (true) {
3035
+ const gitPath = path3.join(current, ".git");
3036
+ try {
3037
+ if (fs3.existsSync(gitPath)) return current;
3038
+ } catch {
2734
3039
  }
3040
+ const parent = path3.dirname(current);
3041
+ if (parent === current) return void 0;
3042
+ current = parent;
2735
3043
  }
2736
- if (historyParts.length === 0) {
2737
- return null;
3044
+ }
3045
+ function globalConfigDir() {
3046
+ const xdg = process.env.XDG_CONFIG_HOME ?? path3.join(os2.homedir(), ".config");
3047
+ return path3.join(xdg, "opencode");
3048
+ }
3049
+ function loadGlobalConfig() {
3050
+ const dir = globalConfigDir();
3051
+ let merged = {};
3052
+ for (const name of FILE_NAMES.slice().reverse()) {
3053
+ const file = path3.join(dir, name);
3054
+ if (!fileExists(file)) continue;
3055
+ const parsed = readAndParse(file);
3056
+ if (parsed) merged = deepMerge(merged, parsed);
2738
3057
  }
2739
- return historyParts.join("\n\n");
3058
+ return merged;
2740
3059
  }
2741
- function buildCompactionHistory(prompt) {
2742
- const entries = [];
2743
- let total = 0;
2744
- let totalToolResults = 0;
2745
- let droppedOldest = 0;
2746
- const end = prompt.length > 0 && prompt[prompt.length - 1].role === "user" ? prompt.length - 1 : prompt.length;
2747
- for (let i = end - 1; i >= 0; i--) {
2748
- const msg = prompt[i];
2749
- const roleLabel = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : msg.role === "tool" ? "Tool" : msg.role;
2750
- const { text, toolResultCount } = renderMessageContentForCompaction(msg);
2751
- if (!text.trim()) continue;
2752
- const entry = `${roleLabel}: ${text}`;
2753
- if (total + entry.length > MAX_HISTORY_CHARS) {
2754
- droppedOldest = i + 1;
2755
- break;
2756
- }
2757
- entries.push(entry);
2758
- total += entry.length + 2;
2759
- totalToolResults += toolResultCount;
3060
+ function loadProjectFilesInDir(dir) {
3061
+ let merged = {};
3062
+ for (const name of PROJECT_FILE_NAMES) {
3063
+ const file = path3.join(dir, name);
3064
+ if (!fileExists(file)) continue;
3065
+ const parsed = readAndParse(file);
3066
+ if (parsed) merged = deepMerge(merged, parsed);
2760
3067
  }
2761
- if (entries.length === 0) return null;
2762
- entries.reverse();
2763
- log.info("built compaction history", {
2764
- entries: entries.length,
2765
- chars: total,
2766
- toolResults: totalToolResults,
2767
- droppedOldestBefore: droppedOldest
2768
- });
2769
- return entries.join("\n\n");
3068
+ return merged;
2770
3069
  }
2771
- function getClaudeUserMessage(prompt, includeHistoryContext = false, opts = {}) {
2772
- const compactionMode = opts.compactionMode === true;
2773
- const cliToolCallIds = opts.cliToolCallIds;
2774
- const content = [];
2775
- const pushToolResult = (part) => {
2776
- const id = part.toolCallId;
2777
- const text = getToolResultText(part);
2778
- if (!cliToolCallIds || cliToolCallIds.has(id)) {
2779
- content.push({ type: "tool_result", tool_use_id: id, content: text });
2780
- return;
3070
+ function dotOpencodeDirs(cwd, worktree) {
3071
+ const dirs = [];
3072
+ const seen = /* @__PURE__ */ new Set();
3073
+ const push = (p) => {
3074
+ const abs = path3.resolve(p);
3075
+ if (!seen.has(abs) && dirExists(abs)) {
3076
+ seen.add(abs);
3077
+ dirs.push(abs);
2781
3078
  }
2782
- log.info("rendering opencode-side tool result as text", {
2783
- toolCallId: id,
2784
- toolName: part.toolName,
2785
- chars: text.length
2786
- });
2787
- content.push({
2788
- type: "text",
2789
- text: `<opencode_tool_result tool="${part.toolName ?? "unknown"}">
2790
- ${text}
2791
- </opencode_tool_result>`
2792
- });
2793
3079
  };
2794
- if (compactionMode) {
2795
- const transcript = compactConversationHistory(prompt, {
2796
- mode: "compaction"
3080
+ for (const dir of walkUp({
3081
+ start: cwd,
3082
+ stop: worktree,
3083
+ targets: [".opencode"],
3084
+ predicate: dirExists
3085
+ })) {
3086
+ push(dir);
3087
+ }
3088
+ const home = os2.homedir();
3089
+ if (home) {
3090
+ const homeDot = path3.join(home, ".opencode");
3091
+ if (dirExists(homeDot)) push(homeDot);
3092
+ }
3093
+ const envDir = process.env.OPENCODE_CONFIG_DIR;
3094
+ if (envDir && dirExists(envDir)) push(envDir);
3095
+ return dirs;
3096
+ }
3097
+ function substituteEnvPlaceholders(source) {
3098
+ const out = {};
3099
+ for (const [k, v] of Object.entries(source)) {
3100
+ if (typeof v !== "string") continue;
3101
+ out[k] = v.replace(/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, name) => {
3102
+ const resolved = process.env[name];
3103
+ return typeof resolved === "string" ? resolved : "";
2797
3104
  });
2798
- if (transcript) {
2799
- log.info("including compaction transcript", {
2800
- historyLength: transcript.length
2801
- });
2802
- content.push({
2803
- type: "text",
2804
- text: `<conversation_transcript>
2805
- ${transcript}
2806
- </conversation_transcript>
2807
-
2808
- The complete prior conversation appears above. The synthesis instructions follow below.
2809
-
2810
- `
2811
- });
3105
+ }
3106
+ return out;
3107
+ }
3108
+ function translateServer(name, spec) {
3109
+ if (spec.enabled === false) return null;
3110
+ const type = spec.type;
3111
+ if (type === "local") {
3112
+ const cmd = spec.command;
3113
+ if (!Array.isArray(cmd) || cmd.length === 0) {
3114
+ log.warn("skipping local MCP server with no command", { name });
3115
+ return null;
2812
3116
  }
2813
- } else if (includeHistoryContext) {
2814
- const historyContext = compactConversationHistory(prompt);
2815
- if (historyContext) {
2816
- log.info("including conversation history context", {
2817
- historyLength: historyContext.length
2818
- });
2819
- content.push({
2820
- type: "text",
2821
- text: `<conversation_history>
2822
- The following is a summary of our conversation so far (from a previous session that couldn't be resumed):
2823
-
2824
- ${historyContext}
2825
-
2826
- </conversation_history>
2827
-
2828
- Now continuing with the current message:
2829
-
2830
- `
2831
- });
3117
+ const out = {
3118
+ type: "stdio",
3119
+ command: String(cmd[0])
3120
+ };
3121
+ if (cmd.length > 1) out.args = cmd.slice(1).map((s) => String(s));
3122
+ if (spec.environment && typeof spec.environment === "object") {
3123
+ out.env = substituteEnvPlaceholders(
3124
+ spec.environment
3125
+ );
2832
3126
  }
3127
+ return out;
2833
3128
  }
2834
- const messages = [];
2835
- for (let i = prompt.length - 1; i >= 0; i--) {
2836
- if (prompt[i].role === "assistant") break;
2837
- messages.unshift(prompt[i]);
3129
+ if (type === "remote") {
3130
+ if (typeof spec.url !== "string" || !spec.url) {
3131
+ log.warn("skipping remote MCP server with no url", { name });
3132
+ return null;
3133
+ }
3134
+ const out = {
3135
+ type: "http",
3136
+ url: spec.url
3137
+ };
3138
+ if (spec.headers && typeof spec.headers === "object") {
3139
+ out.headers = substituteEnvPlaceholders(
3140
+ spec.headers
3141
+ );
3142
+ }
3143
+ return out;
2838
3144
  }
2839
- for (const msg of messages) {
2840
- if (msg.role === "user") {
2841
- if (parseSideQuestionContent(msg.content) !== null) continue;
2842
- if (typeof msg.content === "string") {
2843
- const str = msg.content;
2844
- if (str.trim()) {
2845
- content.push({ type: "text", text: str });
2846
- }
2847
- } else if (Array.isArray(msg.content)) {
2848
- for (const part of msg.content) {
2849
- if (part.type === "text") {
2850
- if (part.text && part.text.trim()) {
2851
- content.push({ type: "text", text: part.text });
2852
- }
2853
- } else if (part.type === "file" || part.type === "image") {
2854
- const block = toImageBlock(part);
2855
- if (block) {
2856
- content.push(block);
2857
- } else {
2858
- log.debug("skipped non-image file part", {
2859
- mediaType: part.mediaType
2860
- });
2861
- }
2862
- } else if (part.type === "tool-result") {
2863
- pushToolResult(part);
2864
- }
2865
- }
2866
- }
2867
- } else if (msg.role === "tool") {
2868
- if (Array.isArray(msg.content)) {
2869
- for (const part of msg.content) {
2870
- if (part?.type === "tool-result") {
2871
- pushToolResult(part);
2872
- }
2873
- }
2874
- }
3145
+ log.warn("skipping MCP server with unknown type", {
3146
+ name,
3147
+ type: type ?? null
3148
+ });
3149
+ return null;
3150
+ }
3151
+ function extractMcpBlock(config) {
3152
+ const mcp = config.mcp;
3153
+ if (!mcp || typeof mcp !== "object" || Array.isArray(mcp)) return {};
3154
+ return mcp;
3155
+ }
3156
+ function mergeMcp(target, source) {
3157
+ const out = { ...target };
3158
+ for (const [name, spec] of Object.entries(source)) {
3159
+ if (!spec || typeof spec !== "object") continue;
3160
+ const existing = out[name];
3161
+ if (existing && typeof existing === "object") {
3162
+ out[name] = deepMerge(
3163
+ existing,
3164
+ spec
3165
+ );
3166
+ } else {
3167
+ out[name] = spec;
2875
3168
  }
2876
3169
  }
2877
- if (content.length === 0) {
2878
- log.warn("empty user content; sending sentinel to satisfy CLI");
2879
- return JSON.stringify({
2880
- type: "user",
2881
- message: {
2882
- role: "user",
2883
- content: [{ type: "text", text: "(empty)" }]
2884
- }
2885
- });
2886
- }
2887
- return JSON.stringify({
2888
- type: "user",
2889
- message: {
2890
- role: "user",
2891
- content
3170
+ return out;
3171
+ }
3172
+ function bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers) {
3173
+ const {
3174
+ servers: merged,
3175
+ enabledServerNames: allEnabledServerNames,
3176
+ hash
3177
+ } = mergeOpencodeMcp(cwd, runtimeStatus);
3178
+ const servers = {};
3179
+ const bridgedServerNames = [];
3180
+ for (const [name, spec] of Object.entries(merged)) {
3181
+ if (!spec || typeof spec !== "object") continue;
3182
+ if (excludeServers?.has(name)) continue;
3183
+ const translated = translateServer(name, spec);
3184
+ if (translated) {
3185
+ servers[name] = translated;
3186
+ bridgedServerNames.push(name);
2892
3187
  }
3188
+ }
3189
+ return finishBridge({
3190
+ servers,
3191
+ bridgedServerNames,
3192
+ allEnabledServerNames,
3193
+ hash,
3194
+ excludeServers
2893
3195
  });
2894
3196
  }
2895
-
2896
- // src/agent-models.ts
2897
- import { readFile, readdir } from "fs/promises";
2898
- import path3 from "path";
2899
-
2900
- // src/models.ts
2901
- var PROVIDER_ID = "claude-code";
2902
- var NPM = "@khalilgharbaoui/opencode-claude-code-plugin";
2903
- var reasoningVariants = {
2904
- low: { reasoningEffort: "low" },
2905
- medium: { reasoningEffort: "medium" },
2906
- high: { reasoningEffort: "high" },
2907
- xhigh: { reasoningEffort: "xhigh" },
2908
- max: { reasoningEffort: "max" }
2909
- };
2910
- var baseCapabilities = {
2911
- temperature: false,
2912
- attachment: true,
2913
- toolcall: true,
2914
- input: { text: true, audio: false, image: true, video: false, pdf: false },
2915
- output: { text: true, audio: false, image: false, video: false, pdf: false },
2916
- interleaved: false
2917
- };
2918
- function defineModel(opts) {
2919
- return {
2920
- id: opts.id,
2921
- providerID: PROVIDER_ID,
2922
- api: { id: opts.id, url: "", npm: NPM },
2923
- name: `${opts.name} (${opts.multiplier}\xD7)`,
2924
- family: opts.family,
2925
- capabilities: { ...baseCapabilities, reasoning: opts.reasoning },
2926
- cost: {
2927
- input: opts.cost.input,
2928
- output: opts.cost.output,
2929
- cache: { read: opts.cost.cacheRead, write: opts.cost.cacheWrite }
2930
- },
2931
- limit: { context: opts.context, output: opts.output },
2932
- status: opts.status ?? "active",
2933
- options: {},
2934
- headers: {},
2935
- release_date: opts.releaseDate,
2936
- variants: opts.reasoning ? reasoningVariants : void 0
2937
- };
3197
+ function mergeOpencodeMcp(cwd, runtimeStatus) {
3198
+ const worktree = detectWorktree(cwd);
3199
+ let merged = {};
3200
+ merged = mergeMcp(merged, extractMcpBlock(loadGlobalConfig()));
3201
+ const explicitConfig = process.env.OPENCODE_CONFIG;
3202
+ if (explicitConfig && fileExists(explicitConfig)) {
3203
+ const parsed = readAndParse(explicitConfig);
3204
+ if (parsed) merged = mergeMcp(merged, extractMcpBlock(parsed));
3205
+ }
3206
+ const projectFiles = walkUp({
3207
+ start: cwd,
3208
+ stop: worktree,
3209
+ targets: PROJECT_FILE_NAMES,
3210
+ predicate: fileExists
3211
+ });
3212
+ const projectDirs = [];
3213
+ const seenProjectDirs = /* @__PURE__ */ new Set();
3214
+ for (const f of projectFiles) {
3215
+ const d = path3.dirname(f);
3216
+ if (!seenProjectDirs.has(d)) {
3217
+ seenProjectDirs.add(d);
3218
+ projectDirs.push(d);
3219
+ }
3220
+ }
3221
+ for (const dir of projectDirs.slice().reverse()) {
3222
+ merged = mergeMcp(merged, extractMcpBlock(loadProjectFilesInDir(dir)));
3223
+ }
3224
+ for (const dir of dotOpencodeDirs(cwd, worktree)) {
3225
+ merged = mergeMcp(merged, extractMcpBlock(loadProjectFilesInDir(dir)));
3226
+ }
3227
+ if (runtimeStatus) {
3228
+ for (const name of Object.keys(merged)) {
3229
+ const status = runtimeStatus[name];
3230
+ if (status === void 0) continue;
3231
+ const existing = merged[name];
3232
+ const base = existing && typeof existing === "object" ? existing : {};
3233
+ merged[name] = { ...base, enabled: status === "connected" };
3234
+ }
3235
+ }
3236
+ const enabledServerNames = [];
3237
+ for (const [name, spec] of Object.entries(merged)) {
3238
+ if (!spec || typeof spec !== "object") continue;
3239
+ const enabled = spec.enabled;
3240
+ if (enabled === false) continue;
3241
+ enabledServerNames.push(name);
3242
+ }
3243
+ const mergedBody = JSON.stringify({ mcpServers: merged }, null, 2);
3244
+ const hash = crypto2.createHash("sha256").update(mergedBody).digest("hex").slice(0, 12);
3245
+ return { servers: merged, enabledServerNames, hash };
2938
3246
  }
2939
- var haikuCost = { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 };
2940
- var sonnetCost = { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 };
2941
- var opusCost = { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 };
2942
- var fableCost = { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 };
2943
- var fable51Cost = { input: 10, output: 50, cacheRead: 0.25, cacheWrite: 12.5 };
2944
- var opusFastCost = { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 };
2945
- function toConfigModel(model) {
2946
- const inputMods = [];
2947
- const outputMods = [];
2948
- for (const [k, v] of Object.entries(model.capabilities.input)) {
2949
- if (v) inputMods.push(k);
3247
+ function finishBridge(input) {
3248
+ const { servers, bridgedServerNames, allEnabledServerNames, hash, excludeServers } = input;
3249
+ if (Object.keys(servers).length === 0) {
3250
+ const allEnabledServersExcluded = excludeServers && allEnabledServerNames.length > 0 && allEnabledServerNames.every((name) => excludeServers.has(name));
3251
+ if (!allEnabledServersExcluded) return null;
3252
+ return {
3253
+ path: "",
3254
+ hash,
3255
+ serverNames: [],
3256
+ allEnabledServerNames
3257
+ };
2950
3258
  }
2951
- for (const [k, v] of Object.entries(model.capabilities.output)) {
2952
- if (v) outputMods.push(k);
3259
+ const body = JSON.stringify({ mcpServers: servers }, null, 2);
3260
+ const outPath = path3.join(
3261
+ pluginTmpDir(),
3262
+ `mcp-${hash}.json`
3263
+ );
3264
+ try {
3265
+ if (!fileExists(outPath)) {
3266
+ fs3.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
3267
+ }
3268
+ } catch (e) {
3269
+ log.warn("failed to write bridged MCP config", {
3270
+ error: e instanceof Error ? e.message : String(e)
3271
+ });
3272
+ return null;
2953
3273
  }
3274
+ log.info("bridged opencode MCP config", {
3275
+ target: outPath,
3276
+ hash,
3277
+ servers: bridgedServerNames,
3278
+ excluded: excludeServers ? Array.from(excludeServers) : []
3279
+ });
2954
3280
  return {
2955
- id: model.api.id,
2956
- name: model.name,
2957
- status: model.status,
2958
- family: model.family ?? "",
2959
- release_date: model.release_date,
2960
- temperature: model.capabilities.temperature,
2961
- reasoning: model.capabilities.reasoning,
2962
- attachment: model.capabilities.attachment,
2963
- tool_call: model.capabilities.toolcall,
2964
- modalities: { input: inputMods, output: outputMods },
2965
- cost: {
2966
- input: model.cost.input,
2967
- output: model.cost.output,
2968
- cache_read: model.cost.cache.read,
2969
- cache_write: model.cost.cache.write
2970
- },
2971
- limit: model.limit,
2972
- options: model.options,
2973
- headers: model.headers,
2974
- variants: model.variants
3281
+ path: outPath,
3282
+ hash,
3283
+ serverNames: bridgedServerNames,
3284
+ allEnabledServerNames
2975
3285
  };
2976
3286
  }
2977
- var defaultModels = {
2978
- "claude-haiku-4-5": defineModel({
2979
- id: "claude-haiku-4-5",
2980
- name: "Claude Haiku 4.5",
2981
- family: "haiku",
2982
- reasoning: false,
2983
- context: 2e5,
2984
- output: 64e3,
2985
- cost: haikuCost,
2986
- multiplier: 1,
2987
- releaseDate: "2025-10-01"
2988
- }),
2989
- "claude-sonnet-4-5": defineModel({
2990
- id: "claude-sonnet-4-5",
2991
- name: "Claude Sonnet 4.5",
2992
- family: "sonnet",
2993
- reasoning: true,
2994
- context: 2e5,
2995
- output: 64e3,
2996
- cost: sonnetCost,
2997
- multiplier: 3,
2998
- releaseDate: "2025-09-29"
2999
- }),
3000
- "claude-sonnet-4-6": defineModel({
3001
- id: "claude-sonnet-4-6",
3002
- name: "Claude Sonnet 4.6",
3003
- family: "sonnet",
3004
- reasoning: true,
3005
- context: 1e6,
3006
- output: 128e3,
3007
- cost: sonnetCost,
3008
- multiplier: 3,
3009
- releaseDate: "2025-06-19"
3010
- }),
3011
- "claude-sonnet-5": defineModel({
3012
- id: "claude-sonnet-5",
3013
- name: "Claude Sonnet 5",
3014
- family: "sonnet",
3015
- reasoning: true,
3016
- context: 1e6,
3017
- output: 128e3,
3018
- cost: sonnetCost,
3019
- multiplier: 3,
3020
- releaseDate: "2026-06-30"
3021
- }),
3022
- "claude-opus-4-5": defineModel({
3023
- id: "claude-opus-4-5",
3024
- name: "Claude Opus 4.5",
3025
- family: "opus",
3026
- reasoning: true,
3027
- context: 2e5,
3028
- output: 64e3,
3029
- cost: opusCost,
3030
- multiplier: 5,
3031
- releaseDate: "2025-11-01"
3032
- }),
3033
- "claude-opus-4-6": defineModel({
3034
- id: "claude-opus-4-6",
3035
- name: "Claude Opus 4.6",
3036
- family: "opus",
3037
- reasoning: true,
3038
- context: 1e6,
3039
- output: 128e3,
3040
- cost: opusCost,
3041
- multiplier: 5,
3042
- releaseDate: "2025-06-19"
3043
- }),
3044
- "claude-opus-4-7": defineModel({
3045
- id: "claude-opus-4-7",
3046
- name: "Claude Opus 4.7",
3047
- family: "opus",
3048
- reasoning: true,
3049
- context: 1e6,
3050
- output: 128e3,
3051
- cost: opusCost,
3052
- multiplier: 5,
3053
- releaseDate: "2025-07-16"
3054
- }),
3055
- "claude-opus-4-8": defineModel({
3056
- id: "claude-opus-4-8",
3057
- name: "Claude Opus 4.8",
3058
- family: "opus",
3059
- reasoning: true,
3060
- context: 1e6,
3061
- output: 128e3,
3062
- cost: opusCost,
3063
- multiplier: 5,
3064
- releaseDate: "2026-05-28"
3065
- }),
3066
- // Fast mode. The `-fast` suffix is OUR marker, not a model name Anthropic
3067
- // serves: `parseModelId` strips it before `--model` and turns it into
3068
- // `--settings {"fastMode":true}` on the spawn. Retired `-fast` model strings
3069
- // (`claude-opus-4-6-fast`) are a different thing and are not registered here.
3070
- //
3071
- // Only Opus 4.8 and Opus 5 qualify: the CLI gates fast mode on the resolved
3072
- // model name containing `opus-4-8` or `opus-5`, so registering a fast entry
3073
- // for any other model would produce a picker option that silently runs at
3074
- // standard speed while displaying the 10x price.
3075
- "claude-opus-4-8-fast": defineModel({
3076
- id: "claude-opus-4-8-fast",
3077
- name: "Claude Opus 4.8 Fast",
3078
- family: "opus",
3079
- reasoning: true,
3080
- context: 1e6,
3081
- output: 128e3,
3082
- cost: opusFastCost,
3083
- multiplier: 10,
3084
- releaseDate: "2026-05-28"
3085
- }),
3086
- "claude-opus-5": defineModel({
3087
- id: "claude-opus-5",
3088
- name: "Claude Opus 5",
3089
- family: "opus",
3090
- reasoning: true,
3091
- context: 1e6,
3092
- output: 128e3,
3093
- cost: opusCost,
3094
- multiplier: 5,
3095
- releaseDate: "2026-07-24"
3096
- }),
3097
- "claude-opus-5-fast": defineModel({
3098
- id: "claude-opus-5-fast",
3099
- name: "Claude Opus 5 Fast",
3100
- family: "opus",
3101
- reasoning: true,
3102
- context: 1e6,
3103
- output: 128e3,
3104
- cost: opusFastCost,
3105
- multiplier: 10,
3106
- releaseDate: "2026-07-24"
3107
- }),
3108
- "claude-fable-5": defineModel({
3109
- id: "claude-fable-5",
3110
- name: "Claude Fable 5",
3111
- family: "fable",
3112
- reasoning: true,
3113
- context: 1e6,
3114
- output: 128e3,
3115
- cost: fableCost,
3116
- multiplier: 10,
3117
- releaseDate: "2026-06-09"
3118
- }),
3119
- "claude-fable-5-1": defineModel({
3120
- id: "claude-fable-5-1",
3121
- name: "Claude Fable 5.1",
3122
- family: "fable",
3123
- reasoning: true,
3124
- context: 1e6,
3125
- output: 128e3,
3126
- cost: fable51Cost,
3127
- multiplier: 10,
3128
- releaseDate: "2026-09-01"
3129
- }),
3130
- // Mythos 5 and 5.1 share the corresponding Fable models' capabilities and
3131
- // pricing without the safety classifiers; limited availability via Project
3132
- // Glasswing. `claude --model` simply errors for accounts without access, so
3133
- // they are safe to register unconditionally.
3134
- "claude-mythos-5": defineModel({
3135
- id: "claude-mythos-5",
3136
- name: "Claude Mythos 5",
3137
- family: "mythos",
3138
- reasoning: true,
3139
- context: 1e6,
3140
- output: 128e3,
3141
- cost: fableCost,
3142
- multiplier: 10,
3143
- releaseDate: "2026-06-09"
3144
- }),
3145
- "claude-mythos-5-1": defineModel({
3146
- id: "claude-mythos-5-1",
3147
- name: "Claude Mythos 5.1",
3148
- family: "mythos",
3149
- reasoning: true,
3150
- context: 1e6,
3151
- output: 128e3,
3152
- cost: fable51Cost,
3153
- multiplier: 10,
3154
- releaseDate: "2026-09-01"
3155
- })
3156
- };
3157
- var FAST_SUFFIX = "-fast";
3158
- function parseModelId(modelId) {
3159
- const at = modelId.indexOf("@");
3160
- const base = at === -1 ? modelId : modelId.slice(0, at);
3161
- const account = at === -1 ? "" : modelId.slice(at);
3162
- if (!base.endsWith(FAST_SUFFIX)) return { model: modelId, fast: false };
3163
- if (!Object.hasOwn(defaultModels, base)) return { model: modelId, fast: false };
3164
- return { model: base.slice(0, -FAST_SUFFIX.length) + account, fast: true };
3165
- }
3166
3287
 
3167
- // src/agent-models.ts
3168
- var AGENT_DIR_NAMES = ["agents", "agent"];
3169
- var REASONING_EFFORTS = [
3170
- "minimal",
3171
- "low",
3172
- "medium",
3173
- "high",
3174
- "xhigh",
3175
- "max"
3176
- ];
3177
- var registry = {};
3178
- var defaultSubagentModel;
3179
- function setAgentRegistry(records) {
3180
- registry = records;
3288
+ // src/runtime-status.ts
3289
+ var opencodeClient = null;
3290
+ function setOpencodeClient(client) {
3291
+ if (client && typeof client === "object") {
3292
+ opencodeClient = client;
3293
+ }
3181
3294
  }
3182
- function getAgentRegistry() {
3183
- return registry;
3295
+ function getOpencodeClient() {
3296
+ return opencodeClient;
3184
3297
  }
3185
- function setDefaultSubagentModel(model) {
3186
- defaultSubagentModel = model?.trim() || void 0;
3298
+ var opencodeProjectDirectory;
3299
+ function setOpencodeProjectDirectory(dir) {
3300
+ opencodeProjectDirectory = dir;
3187
3301
  }
3188
- function getDefaultSubagentModel() {
3189
- return defaultSubagentModel;
3302
+ function getOpencodeProjectDirectory() {
3303
+ return opencodeProjectDirectory;
3190
3304
  }
3191
- function accountMarker(modelId) {
3192
- const at = modelId.indexOf("@");
3193
- return at === -1 ? "" : modelId.slice(at);
3305
+ function isUsableDirectory(d) {
3306
+ return typeof d === "string" && d.length > 1 && d !== "/";
3194
3307
  }
3195
- function withoutAccountMarker(modelId) {
3196
- const at = modelId.indexOf("@");
3197
- return at === -1 ? modelId : modelId.slice(0, at);
3308
+ function resolveSpawnCwdFrom(configured, live, captured, sessionDir) {
3309
+ if (configured) return configured;
3310
+ if (isUsableDirectory(sessionDir)) return sessionDir;
3311
+ if (isUsableDirectory(live)) return live;
3312
+ return captured ?? live;
3198
3313
  }
3199
- function resolveAgentModel(agent, modelId, overrides) {
3200
- if (!agent) return modelId;
3201
- const record = (overrides?.records ?? registry)[agent];
3202
- if (!record) return modelId;
3203
- if (record.model?.includes("/")) return modelId;
3204
- const fallback = overrides ? overrides.defaultSubagentModel : defaultSubagentModel;
3205
- const declared = record.forceModel?.trim();
3206
- const wanted = declared || (record.mode === "subagent" ? fallback : void 0);
3207
- if (!wanted) return modelId;
3208
- const base = withoutAccountMarker(wanted);
3209
- if (!Object.hasOwn(defaultModels, base)) {
3210
- log.warn("agent model override refused: unknown model", {
3211
- agent,
3212
- wanted: base,
3213
- keeping: modelId
3214
- });
3215
- return modelId;
3216
- }
3217
- const resolved = `${base}${accountMarker(modelId)}`;
3218
- if (resolved !== modelId) {
3219
- log.debug("agent model override", { agent, from: modelId, to: resolved });
3220
- }
3221
- return resolved;
3314
+ async function resolveSpawnCwdForSession(configured, sessionID) {
3315
+ if (configured) return configured;
3316
+ const sessionDir = sessionID ? await fetchSessionDirectory(sessionID) : void 0;
3317
+ return resolveSpawnCwdFrom(
3318
+ configured,
3319
+ process.cwd(),
3320
+ opencodeProjectDirectory,
3321
+ sessionDir
3322
+ );
3222
3323
  }
3223
- function resolveAgentEffort(agent, inherited, overrides) {
3224
- if (!agent) return inherited;
3225
- const record = (overrides?.records ?? registry)[agent];
3226
- const declared = record?.reasoningEffort?.trim();
3227
- if (!declared) return inherited;
3228
- if (!REASONING_EFFORTS.includes(declared)) {
3229
- log.warn("agent effort override refused: unknown level", {
3230
- agent,
3231
- wanted: declared,
3232
- keeping: inherited
3233
- });
3234
- return inherited;
3235
- }
3236
- if (declared !== inherited) {
3237
- log.debug("agent effort override", {
3238
- agent,
3239
- from: inherited,
3240
- to: declared
3324
+ async function fetchSessionDirectory(sessionID) {
3325
+ if (!sessionID || sessionID === "default") return void 0;
3326
+ const client = opencodeClient;
3327
+ if (!client?.session?.get) return void 0;
3328
+ try {
3329
+ const res = await client.session.get({ path: { id: sessionID } });
3330
+ const data = res.data;
3331
+ if (!data || typeof data !== "object") return void 0;
3332
+ const dir = data.directory;
3333
+ return isUsableDirectory(dir) ? dir : void 0;
3334
+ } catch (err) {
3335
+ log.warn("failed to fetch opencode session directory", {
3336
+ sessionID,
3337
+ error: err instanceof Error ? err.message : String(err)
3241
3338
  });
3339
+ return void 0;
3242
3340
  }
3243
- return declared;
3244
- }
3245
- function parseAgentFrontmatter(text) {
3246
- const record = {};
3247
- if (!text.startsWith("---")) return record;
3248
- const lines = text.split(/\r?\n/);
3249
- for (let i = 1; i < lines.length; i++) {
3250
- const line = lines[i];
3251
- if (line.trim() === "---") break;
3252
- const match = /^([A-Za-z_][A-Za-z0-9_-]*):[ \t]*(.*)$/.exec(line);
3253
- if (!match) continue;
3254
- const key = match[1];
3255
- if (key !== "mode" && key !== "model" && key !== "forceModel" && key !== "reasoningEffort")
3256
- continue;
3257
- const value = match[2].trim().replace(/^["']|["']$/g, "");
3258
- if (value) record[key] = value;
3259
- }
3260
- return record;
3261
3341
  }
3262
- async function readAgentMarkdownRecords(directories) {
3263
- const records = {};
3264
- for (const directory of directories) {
3265
- let entries;
3266
- try {
3267
- entries = await readdir(directory);
3268
- } catch {
3269
- continue;
3270
- }
3271
- for (const entry of entries) {
3272
- if (!entry.endsWith(".md")) continue;
3273
- const name = entry.slice(0, -3);
3274
- if (records[name]) continue;
3275
- try {
3276
- const text = await readFile(path3.join(directory, entry), "utf8");
3277
- records[name] = parseAgentFrontmatter(text);
3278
- } catch (err) {
3279
- log.debug("failed to read agent markdown", {
3280
- file: path3.join(directory, entry),
3281
- error: String(err)
3282
- });
3342
+ async function getRuntimeMcpStatus() {
3343
+ const client = opencodeClient;
3344
+ if (!client?.mcp?.status) return void 0;
3345
+ try {
3346
+ const res = await client.mcp.status();
3347
+ const data = res.data;
3348
+ if (!data || typeof data !== "object") return void 0;
3349
+ const out = {};
3350
+ for (const [name, entry] of Object.entries(data)) {
3351
+ if (entry && typeof entry === "object") {
3352
+ const status = entry.status;
3353
+ if (typeof status === "string") out[name] = status;
3283
3354
  }
3284
3355
  }
3356
+ return out;
3357
+ } catch (err) {
3358
+ log.warn("failed to fetch opencode MCP runtime status", {
3359
+ error: err instanceof Error ? err.message : String(err)
3360
+ });
3361
+ return void 0;
3285
3362
  }
3286
- return records;
3287
3363
  }
3288
- function agentDirectories(home, projectDirectory) {
3289
- const directories = [];
3290
- if (projectDirectory) {
3291
- for (const name of AGENT_DIR_NAMES) {
3292
- directories.push(path3.join(projectDirectory, ".opencode", name));
3293
- }
3294
- }
3295
- if (home) {
3296
- for (const name of AGENT_DIR_NAMES) {
3297
- directories.push(path3.join(home, ".config", "opencode", name));
3364
+ async function fetchOpencodeToolList(provider, model, directory) {
3365
+ const client = opencodeClient;
3366
+ if (!client?.tool?.list) return void 0;
3367
+ try {
3368
+ const res = await client.tool.list({
3369
+ query: { provider, model, ...directory ? { directory } : {} }
3370
+ });
3371
+ const data = res.data;
3372
+ if (!Array.isArray(data)) return void 0;
3373
+ const out = [];
3374
+ for (const entry of data) {
3375
+ if (!entry || typeof entry !== "object") continue;
3376
+ const e = entry;
3377
+ const id = typeof e.id === "string" ? e.id : null;
3378
+ const description = typeof e.description === "string" ? e.description : "";
3379
+ const parameters = e.parameters && typeof e.parameters === "object" ? e.parameters : {};
3380
+ if (!id) continue;
3381
+ out.push({ id, description, parameters });
3298
3382
  }
3383
+ return out;
3384
+ } catch (err) {
3385
+ log.warn("failed to fetch opencode tool list", {
3386
+ provider,
3387
+ model,
3388
+ error: err instanceof Error ? err.message : String(err)
3389
+ });
3390
+ return void 0;
3299
3391
  }
3300
- return directories;
3301
3392
  }
3302
3393
 
3303
- // src/skill-bridge.ts
3304
- import * as crypto2 from "crypto";
3305
- import * as fs3 from "fs";
3306
- import * as os2 from "os";
3307
- import * as path4 from "path";
3308
- import { fileURLToPath } from "url";
3309
- var SKILL_PLUGIN_NAME = "opencode-skills";
3310
- function bundledSkillsDir() {
3394
+ // src/startup-diagnostics.ts
3395
+ var cachedPluginVersion;
3396
+ function pluginVersion() {
3397
+ if (cachedPluginVersion) return cachedPluginVersion;
3311
3398
  try {
3312
- const here = fileURLToPath(import.meta.url);
3313
- const dir = path4.resolve(path4.dirname(here), "..", "skills");
3314
- return dirExists(dir) ? dir : null;
3399
+ const here = path4.dirname(fileURLToPath(import.meta.url));
3400
+ const raw = fs4.readFileSync(path4.join(here, "..", "package.json"), "utf8");
3401
+ const version = JSON.parse(raw).version;
3402
+ cachedPluginVersion = typeof version === "string" ? version : "unknown";
3315
3403
  } catch {
3316
- return null;
3404
+ cachedPluginVersion = "unknown";
3317
3405
  }
3406
+ return cachedPluginVersion;
3318
3407
  }
3319
- function dirExists(p) {
3320
- try {
3321
- return fs3.statSync(p).isDirectory();
3322
- } catch {
3323
- return false;
3408
+ function pickOpencodeVersion(input) {
3409
+ if (!input || typeof input !== "object") return void 0;
3410
+ const app = input.app;
3411
+ if (app && typeof app === "object") {
3412
+ const version = app.version;
3413
+ if (typeof version === "string" && version.length > 0) return version;
3324
3414
  }
3415
+ const direct = input.version;
3416
+ if (typeof direct === "string" && direct.length > 0) return direct;
3417
+ return void 0;
3325
3418
  }
3326
- function fileExists(p) {
3327
- try {
3328
- return fs3.statSync(p).isFile();
3329
- } catch {
3330
- return false;
3419
+ var execFileAsync2 = promisify2(execFile2);
3420
+ var opencodeVersionProbe;
3421
+ function detectOpencodeVersion(execPath = process.execPath) {
3422
+ if (opencodeVersionProbe) return opencodeVersionProbe;
3423
+ opencodeVersionProbe = (async () => {
3424
+ if (!path4.basename(execPath).toLowerCase().includes("opencode")) {
3425
+ log.debug("skipping opencode version probe: execPath is not opencode", { execPath });
3426
+ return void 0;
3427
+ }
3428
+ try {
3429
+ const { stdout } = await execFileAsync2(execPath, ["--version"], { timeout: 5e3 });
3430
+ const match = /\d+\.\d+\.\d+\S*/.exec(stdout.trim());
3431
+ return match ? match[0] : void 0;
3432
+ } catch (err) {
3433
+ log.debug("opencode version probe failed", {
3434
+ execPath,
3435
+ error: err instanceof Error ? err.message : String(err)
3436
+ });
3437
+ return void 0;
3438
+ }
3439
+ })();
3440
+ return opencodeVersionProbe;
3441
+ }
3442
+ function describeSpawnCwd(configured, live = process.cwd(), captured = getOpencodeProjectDirectory()) {
3443
+ if (typeof configured === "string" && configured.length > 0) {
3444
+ return { resolved: configured, source: "configured" };
3331
3445
  }
3446
+ if (isUsableDirectory(live)) return { resolved: live, source: "process" };
3447
+ if (isUsableDirectory(captured)) return { resolved: captured, source: "captured" };
3448
+ return { resolved: live, source: "unresolved" };
3332
3449
  }
3333
- function skillRoots(cwd) {
3334
- const roots = [];
3335
- const seen = /* @__PURE__ */ new Set();
3336
- const push = (p) => {
3337
- const abs = path4.resolve(p);
3338
- if (seen.has(abs)) return;
3339
- seen.add(abs);
3340
- if (dirExists(abs)) roots.push(abs);
3341
- };
3342
- let current = path4.resolve(cwd);
3343
- while (true) {
3344
- push(path4.join(current, ".opencode", "skills"));
3345
- const parent = path4.dirname(current);
3346
- if (parent === current) break;
3347
- current = parent;
3450
+ function stringList(value) {
3451
+ if (!Array.isArray(value)) return [];
3452
+ return value.filter((entry) => typeof entry === "string");
3453
+ }
3454
+ function firstOption(providers, key) {
3455
+ for (const entry of Object.values(providers)) {
3456
+ const value = entry?.options?.[key];
3457
+ if (value !== void 0) return value;
3348
3458
  }
3349
- const home = os2.homedir();
3350
- if (home) push(path4.join(home, ".opencode", "skills"));
3351
- const envDir = process.env.OPENCODE_CONFIG_DIR;
3352
- if (envDir) push(path4.join(envDir, "skills"));
3353
- const xdg = process.env.XDG_CONFIG_HOME ?? (home ? path4.join(home, ".config") : null);
3354
- if (xdg) push(path4.join(xdg, "opencode", "skills"));
3355
- return roots;
3459
+ return void 0;
3356
3460
  }
3357
- function collectSkills(root, claimed, found) {
3358
- let entries;
3359
- try {
3360
- entries = fs3.readdirSync(root, { withFileTypes: true });
3361
- } catch {
3362
- return;
3461
+ function collectStartupDiagnostics(providers, opencodeVersion) {
3462
+ const accounts = [];
3463
+ for (const entry of Object.values(providers)) {
3464
+ const account = entry?.options?.account;
3465
+ if (typeof account === "string" && account.length > 0) accounts.push(account);
3363
3466
  }
3364
- for (const entry of entries) {
3365
- if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
3366
- const name = entry.name;
3367
- if (name.startsWith(".")) continue;
3368
- if (claimed.has(name)) continue;
3369
- const dir = path4.join(root, name);
3370
- if (!fileExists(path4.join(dir, "SKILL.md"))) continue;
3371
- claimed.add(name);
3372
- found.push({ name, dir });
3467
+ const cwd = describeSpawnCwd(firstOption(providers, "cwd"));
3468
+ let mcpServers = [];
3469
+ try {
3470
+ mcpServers = mergeOpencodeMcp(cwd.resolved).enabledServerNames;
3471
+ } catch (err) {
3472
+ log.debug("startup diagnostics could not read MCP config", {
3473
+ error: err instanceof Error ? err.message : String(err)
3474
+ });
3373
3475
  }
3476
+ return {
3477
+ plugin: pluginVersion(),
3478
+ opencode: opencodeVersion ?? process.env.OPENCODE_VERSION ?? "unknown",
3479
+ claudeCliPath: String(firstOption(providers, "cliPath") ?? "claude"),
3480
+ cwd,
3481
+ providers: Object.keys(providers),
3482
+ accounts,
3483
+ proxyTools: stringList(firstOption(providers, "proxyTools")),
3484
+ mcpServers,
3485
+ interactiveTransport: firstOption(providers, "interactive") === true || process.env.CLAUDE_CODE_INTERACTIVE_TRANSPORT === "1",
3486
+ planModeQuestion: firstOption(providers, "planModeQuestion") === true,
3487
+ anthropicApiKeyInEnv: Boolean(
3488
+ process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN
3489
+ )
3490
+ };
3374
3491
  }
3375
- var byName = (a, b) => a.name.localeCompare(b.name);
3376
- function discoverOpencodeSkills(cwd) {
3377
- const found = [];
3378
- const claimed = /* @__PURE__ */ new Set();
3379
- for (const root of skillRoots(cwd)) collectSkills(root, claimed, found);
3380
- return found.sort(byName);
3492
+ var logged = false;
3493
+ var lastProviders = {};
3494
+ var lastOpencodeVersion;
3495
+ function lastDiagnosticsProviders() {
3496
+ return lastProviders;
3381
3497
  }
3382
- function discoverBundledSkills() {
3383
- const root = bundledSkillsDir();
3384
- if (!root) return [];
3385
- const found = [];
3386
- collectSkills(root, /* @__PURE__ */ new Set(), found);
3387
- return found.sort(byName);
3498
+ function lastKnownOpencodeVersion() {
3499
+ return lastOpencodeVersion;
3388
3500
  }
3389
- function registerBundledSkillPath(config) {
3390
- const dir = bundledSkillsDir();
3391
- if (!dir) return false;
3392
- config.skills ??= {};
3393
- const paths = config.skills.paths ??= [];
3394
- if (paths.some((p) => path4.resolve(p) === dir)) return false;
3395
- paths.push(dir);
3396
- return true;
3501
+ function logStartupDiagnostics(providers, opencodeVersion) {
3502
+ lastProviders = providers;
3503
+ if (opencodeVersion) lastOpencodeVersion = opencodeVersion;
3504
+ if (logged) return;
3505
+ logged = true;
3506
+ void (async () => {
3507
+ try {
3508
+ const version = opencodeVersion ?? process.env.OPENCODE_VERSION ?? await detectOpencodeVersion();
3509
+ const { claudeCliPath, ...rest } = collectStartupDiagnostics(providers, version);
3510
+ const cli = await detectCliVersion(claudeCliPath);
3511
+ const diagnostics = {
3512
+ ...rest,
3513
+ claudeCli: { path: claudeCliPath, version: cli?.raw ?? "not detected" }
3514
+ };
3515
+ log.notice("claude-code plugin ready", { ...diagnostics });
3516
+ } catch (err) {
3517
+ log.debug("startup diagnostics failed", {
3518
+ error: err instanceof Error ? err.message : String(err)
3519
+ });
3520
+ }
3521
+ })();
3397
3522
  }
3398
- function linkSkill(source, target) {
3399
- try {
3400
- fs3.symlinkSync(source, target, process.platform === "win32" ? "junction" : "dir");
3401
- return;
3402
- } catch {
3403
- fs3.cpSync(source, target, { recursive: true, dereference: true });
3404
- }
3523
+
3524
+ // src/doctor.ts
3525
+ var DOCTOR_COMMAND = "claude-code-doctor";
3526
+ var DOCTOR_MARKER = "\u258C **claude-code doctor**";
3527
+ function isRecord3(value) {
3528
+ return value !== null && typeof value === "object" && !Array.isArray(value);
3405
3529
  }
3406
- function buildSkillPluginDir(skills) {
3407
- if (skills.length === 0) return null;
3408
- const fingerprint = skills.map((s) => `${s.name}\0${s.dir}`).join("\n");
3409
- const hash = crypto2.createHash("sha256").update(fingerprint).digest("hex").slice(0, 12);
3410
- const root = path4.join(pluginTmpDir(), `skills-${hash}`);
3411
- const manifest = path4.join(root, ".claude-plugin", "plugin.json");
3412
- if (fileExists(manifest)) return root;
3413
- try {
3414
- fs3.rmSync(root, { recursive: true, force: true });
3415
- fs3.mkdirSync(path4.join(root, ".claude-plugin"), { recursive: true });
3416
- fs3.mkdirSync(path4.join(root, "skills"), { recursive: true });
3417
- fs3.writeFileSync(
3418
- manifest,
3419
- JSON.stringify(
3420
- {
3421
- name: SKILL_PLUGIN_NAME,
3422
- description: "Skills discovered from this opencode installation, bridged into Claude Code."
3423
- },
3424
- null,
3425
- 2
3426
- ),
3427
- { encoding: "utf8", mode: 384 }
3428
- );
3429
- for (const skill of skills) {
3430
- linkSkill(skill.dir, path4.join(root, "skills", skill.name));
3530
+ var SYSTEM_REMINDER_BLOCK2 = /<system-reminder>[\s\S]*?<\/system-reminder>/g;
3531
+ function parseDoctorCommandContent(content) {
3532
+ let text;
3533
+ if (typeof content === "string") {
3534
+ text = content;
3535
+ } else if (Array.isArray(content)) {
3536
+ const parts = [];
3537
+ for (const part of content) {
3538
+ if (!isRecord3(part) || part.type !== "text" || typeof part.text !== "string") return null;
3539
+ parts.push(part.text);
3431
3540
  }
3432
- } catch (err) {
3433
- log.warn("failed to stage opencode skill plugin dir", {
3434
- root,
3435
- error: err instanceof Error ? err.message : String(err)
3436
- });
3541
+ text = parts.join("\n");
3542
+ } else {
3437
3543
  return null;
3438
3544
  }
3439
- return root;
3545
+ const match = new RegExp(`^/${DOCTOR_COMMAND}(?:\\s+([\\s\\S]*))?$`).exec(
3546
+ text.replace(SYSTEM_REMINDER_BLOCK2, "").trim()
3547
+ );
3548
+ return match ? { rest: (match[1] ?? "").trim() } : null;
3440
3549
  }
3441
- async function resolveSkillPluginDirs(opts) {
3442
- const user = opts.enabled ? discoverOpencodeSkills(opts.cwd) : [];
3443
- const claimed = new Set(user.map((s) => s.name));
3444
- const bundled = discoverBundledSkills().filter((s) => !claimed.has(s.name));
3445
- const skills = [...user, ...bundled].sort(byName);
3446
- if (skills.length === 0) return [];
3447
- const supported = await detectCliSupportsFlag(opts.cliPath, "--plugin-dir");
3448
- if (!supported) {
3449
- log.notice(
3450
- "claude cli does not support --plugin-dir; opencode skills will not be bridged. Run `npm i -g @anthropic-ai/claude-code` to upgrade.",
3451
- { skills: skills.length }
3452
- );
3453
- return [];
3550
+ function parseDoctorCommand(prompt) {
3551
+ const latest = prompt.at(-1);
3552
+ return latest?.role === "user" ? parseDoctorCommandContent(latest.content) : null;
3553
+ }
3554
+ function formatAge(ms) {
3555
+ if (ms === void 0) return "unknown";
3556
+ if (ms < 1e3) return `${Math.round(ms)}ms`;
3557
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
3558
+ const minutes = Math.floor(ms / 6e4);
3559
+ if (minutes < 60) return `${minutes}m`;
3560
+ return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
3561
+ }
3562
+ function list(values) {
3563
+ return values.length ? values.join(", ") : "none";
3564
+ }
3565
+ function describeAuth(auth) {
3566
+ switch (auth.status) {
3567
+ case "ok":
3568
+ return `${auth.code}, good`;
3569
+ case "unsafe":
3570
+ return `${auth.code}, UNSAFE: an unauthenticated caller was accepted. Restart every opencode window; a window opened before 0.13.2 keeps serving an open port.`;
3571
+ case "unreachable":
3572
+ return `could not be checked (${auth.error})`;
3573
+ case "skipped":
3574
+ return "not checked";
3575
+ }
3576
+ }
3577
+ function formatDoctorReport(report) {
3578
+ const lines = [];
3579
+ lines.push(DOCTOR_MARKER);
3580
+ lines.push("");
3581
+ lines.push("| Field | Value |");
3582
+ lines.push("|---|---|");
3583
+ lines.push(`| plugin | ${report.plugin} |`);
3584
+ lines.push(`| opencode | ${report.opencode} |`);
3585
+ lines.push(`| claude CLI | \`${report.claudeCli.path}\` (${report.claudeCli.version}) |`);
3586
+ lines.push(`| cwd | \`${report.cwd.resolved}\` (${report.cwd.source}) |`);
3587
+ lines.push(`| providers | ${list(report.providers)} |`);
3588
+ lines.push(`| accounts | ${list(report.accounts)} |`);
3589
+ lines.push(`| proxyTools | ${list(report.proxyTools)} |`);
3590
+ lines.push(`| MCP servers (on disk) | ${list(report.mcpServers)} |`);
3591
+ lines.push(`| transport | ${report.transport} |`);
3592
+ lines.push(`| planModeQuestion | ${report.planModeQuestion} |`);
3593
+ lines.push(`| turnStats | ${report.turnStats} |`);
3594
+ lines.push(`| ANTHROPIC_API_KEY in env | ${report.anthropicApiKeyInEnv ? "yes" : "no"} |`);
3595
+ lines.push("");
3596
+ lines.push("**Live `claude` processes**");
3597
+ lines.push("");
3598
+ if (report.processes.length === 0) {
3599
+ lines.push("None. The next message in a Claude Code session spawns one.");
3600
+ } else {
3601
+ lines.push("| session | model | pid | in flight | age | effort |");
3602
+ lines.push("|---|---|---|---|---|---|");
3603
+ for (const proc of report.processes) {
3604
+ lines.push(
3605
+ `| ${proc.session}${proc.compaction ? " (compaction)" : ""} | ${proc.model} | ${proc.pid ?? "unknown"} | ${proc.inFlight ? "yes" : "no"} | ${formatAge(proc.ageMs)} | ${proc.effort ?? "inherited"} |`
3606
+ );
3607
+ }
3454
3608
  }
3455
- const dir = buildSkillPluginDir(skills);
3456
- if (!dir) return [];
3457
- log.info("bridged opencode skills into claude", {
3458
- count: skills.length,
3459
- names: skills.map((s) => s.name),
3460
- bundled: bundled.map((s) => s.name),
3461
- pluginDir: dir
3462
- });
3463
- return [dir];
3609
+ lines.push("");
3610
+ lines.push("**Pending proxy calls**");
3611
+ lines.push("");
3612
+ if (report.pendingCalls.length === 0) {
3613
+ lines.push("None.");
3614
+ } else {
3615
+ lines.push("| tool | call id | age | deadline |");
3616
+ lines.push("|---|---|---|---|");
3617
+ for (const call of report.pendingCalls) {
3618
+ const deadline = call.deadlineMs > 0 ? formatAge(call.deadlineMs) : "none";
3619
+ lines.push(
3620
+ `| ${call.toolName} | \`${call.toolCallId}\` | ${formatAge(call.ageMs)} | ${deadline} |`
3621
+ );
3622
+ }
3623
+ }
3624
+ lines.push("");
3625
+ lines.push("**Proxy servers**");
3626
+ lines.push("");
3627
+ if (report.proxyServers.length === 0) {
3628
+ lines.push("None running.");
3629
+ } else {
3630
+ lines.push("| url | unauthenticated `initialize` |");
3631
+ lines.push("|---|---|");
3632
+ for (const server2 of report.proxyServers) {
3633
+ lines.push(`| ${server2.url} | ${describeAuth(server2.auth)} |`);
3634
+ }
3635
+ }
3636
+ const stderr = report.processes.filter((proc) => proc.lastStderr);
3637
+ if (stderr.length > 0) {
3638
+ lines.push("");
3639
+ lines.push("**Last stderr**");
3640
+ lines.push("");
3641
+ for (const proc of stderr) {
3642
+ lines.push(`\`${proc.session}\`:`);
3643
+ lines.push("");
3644
+ lines.push("```text");
3645
+ lines.push(proc.lastStderr.trimEnd());
3646
+ lines.push("```");
3647
+ }
3648
+ }
3649
+ return lines.join("\n");
3464
3650
  }
3465
-
3466
- // src/mcp-bridge.ts
3467
- import * as fs4 from "fs";
3468
- import * as path5 from "path";
3469
- import * as os3 from "os";
3470
- import * as crypto3 from "crypto";
3471
- import {
3472
- parse as parseJsonc,
3473
- printParseErrorCode
3474
- } from "jsonc-parser";
3475
- var FILE_NAMES = ["opencode.jsonc", "opencode.json", "config.json"];
3476
- var PROJECT_FILE_NAMES = ["opencode.json", "opencode.jsonc"];
3477
- function fileExists2(p) {
3651
+ async function checkProxyAuth(url, fetchImpl = fetch, timeoutMs = 3e3) {
3652
+ let authority;
3478
3653
  try {
3479
- return fs4.statSync(p).isFile();
3654
+ authority = new URL(url).host;
3480
3655
  } catch {
3481
- return false;
3656
+ return { status: "skipped" };
3482
3657
  }
3483
- }
3484
- function dirExists2(p) {
3658
+ const controller = new AbortController();
3659
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
3485
3660
  try {
3486
- return fs4.statSync(p).isDirectory();
3487
- } catch {
3488
- return false;
3661
+ const response = await fetchImpl(url, {
3662
+ method: "POST",
3663
+ headers: { "content-type": "application/json", host: authority },
3664
+ body: JSON.stringify({ jsonrpc: "2.0", id: 0, method: "initialize", params: {} }),
3665
+ signal: controller.signal
3666
+ });
3667
+ await response.text().catch(() => "");
3668
+ return response.status === 401 ? { status: "ok", code: response.status } : { status: "unsafe", code: response.status };
3669
+ } catch (error) {
3670
+ return {
3671
+ status: "unreachable",
3672
+ error: error instanceof Error ? error.message : String(error)
3673
+ };
3674
+ } finally {
3675
+ clearTimeout(timer);
3489
3676
  }
3490
3677
  }
3491
- function readAndParse(file) {
3678
+ async function gatherDoctorReport(options) {
3679
+ const providers = lastDiagnosticsProviders();
3680
+ const opencodeVersion = lastKnownOpencodeVersion() ?? process.env.OPENCODE_VERSION ?? await detectOpencodeVersion().catch(() => void 0);
3681
+ const { claudeCliPath, ...base } = collectStartupDiagnostics(providers, opencodeVersion);
3682
+ const cliPath = options.cliPath || claudeCliPath;
3683
+ const cli = await detectCliVersion(cliPath).catch(() => null);
3684
+ const processes = snapshotActiveProcesses();
3685
+ const seen = /* @__PURE__ */ new Set();
3686
+ const proxyServers = [];
3687
+ for (const proc of processes) {
3688
+ if (!proc.proxyUrl || seen.has(proc.proxyUrl)) continue;
3689
+ seen.add(proc.proxyUrl);
3690
+ proxyServers.push({
3691
+ url: proc.proxyUrl,
3692
+ auth: await checkProxyAuth(proc.proxyUrl, options.fetchImpl ?? fetch)
3693
+ });
3694
+ }
3695
+ return {
3696
+ plugin: base.plugin,
3697
+ opencode: base.opencode,
3698
+ claudeCli: { path: cliPath, version: cli?.raw ?? "not detected" },
3699
+ cwd: base.cwd,
3700
+ providers: base.providers,
3701
+ accounts: base.accounts,
3702
+ proxyTools: base.proxyTools,
3703
+ mcpServers: base.mcpServers,
3704
+ transport: options.interactive || base.interactiveTransport ? "interactive" : "headless",
3705
+ planModeQuestion: base.planModeQuestion,
3706
+ turnStats: options.turnStats,
3707
+ anthropicApiKeyInEnv: base.anthropicApiKeyInEnv,
3708
+ processes,
3709
+ pendingCalls: snapshotPendingProxyCalls(),
3710
+ proxyServers
3711
+ };
3712
+ }
3713
+ async function buildDoctorReport(options) {
3492
3714
  try {
3493
- const raw = fs4.readFileSync(file, "utf8");
3494
- const errors = [];
3495
- const parsed = parseJsonc(raw, errors, { allowTrailingComma: true });
3496
- if (errors.length > 0) {
3497
- const first = errors[0];
3498
- throw new Error(
3499
- `${printParseErrorCode(first.error)} at offset ${first.offset}`
3500
- );
3501
- }
3502
- return parsed;
3503
- } catch (e) {
3504
- log.warn("failed to parse opencode config", {
3505
- file,
3506
- error: e instanceof Error ? e.message : String(e)
3715
+ const report = await gatherDoctorReport(options);
3716
+ log.info("claude-code doctor report", {
3717
+ plugin: report.plugin,
3718
+ opencode: report.opencode,
3719
+ cwd: report.cwd,
3720
+ processes: report.processes.length,
3721
+ pendingCalls: report.pendingCalls.length,
3722
+ proxyServers: report.proxyServers.map((server2) => server2.auth.status)
3507
3723
  });
3508
- return null;
3724
+ return formatDoctorReport(report);
3725
+ } catch (error) {
3726
+ const message = error instanceof Error ? error.message : String(error);
3727
+ log.warn("claude-code doctor failed to build its report", { error: message });
3728
+ return `${DOCTOR_MARKER}
3729
+
3730
+ Could not build the report: ${message}`;
3509
3731
  }
3510
3732
  }
3511
- function isPlainObject(x) {
3512
- return typeof x === "object" && x !== null && !Array.isArray(x);
3733
+
3734
+ // src/turn-stats.ts
3735
+ var TURN_STATS_MARKER = "\u258C **stats:**";
3736
+ function extractTurnStats(msg) {
3737
+ const usage = msg.usage;
3738
+ const stats = {};
3739
+ if (typeof msg.total_cost_usd === "number") stats.costUsd = msg.total_cost_usd;
3740
+ if (typeof msg.duration_ms === "number") stats.durationMs = msg.duration_ms;
3741
+ if (typeof msg.duration_api_ms === "number") stats.durationApiMs = msg.duration_api_ms;
3742
+ if (typeof msg.num_turns === "number") stats.numTurns = msg.num_turns;
3743
+ if (typeof usage?.input_tokens === "number") stats.inputTokens = usage.input_tokens;
3744
+ if (typeof usage?.output_tokens === "number") stats.outputTokens = usage.output_tokens;
3745
+ if (typeof usage?.cache_read_input_tokens === "number") {
3746
+ stats.cacheReadTokens = usage.cache_read_input_tokens;
3747
+ }
3748
+ if (typeof usage?.cache_creation_input_tokens === "number") {
3749
+ stats.cacheWriteTokens = usage.cache_creation_input_tokens;
3750
+ }
3751
+ if (msg.modelUsage && typeof msg.modelUsage === "object") stats.modelUsage = msg.modelUsage;
3752
+ if (Array.isArray(msg.permission_denials)) stats.permissionDenials = msg.permission_denials;
3753
+ return stats;
3754
+ }
3755
+ function formatCost(costUsd) {
3756
+ if (!Number.isFinite(costUsd) || costUsd < 0) return "$0.00";
3757
+ return costUsd >= 1 ? `$${costUsd.toFixed(2)}` : `$${costUsd.toFixed(4)}`;
3758
+ }
3759
+ function formatDuration(durationMs) {
3760
+ if (!Number.isFinite(durationMs) || durationMs < 0) return "0.0 s";
3761
+ if (durationMs < 6e4) return `${(durationMs / 1e3).toFixed(1)} s`;
3762
+ const totalSeconds = Math.round(durationMs / 1e3);
3763
+ return `${Math.floor(totalSeconds / 60)}m ${totalSeconds % 60}s`;
3764
+ }
3765
+ function formatTokens(tokens) {
3766
+ if (!Number.isFinite(tokens) || tokens < 0) return "0";
3767
+ if (tokens < 1e3) return String(Math.round(tokens));
3768
+ if (tokens < 1e6) return `${(tokens / 1e3).toFixed(1)}k`;
3769
+ return `${(tokens / 1e6).toFixed(1)}M`;
3770
+ }
3771
+ function formatTurnStatsLine(stats) {
3772
+ const parts = [];
3773
+ if (stats.costUsd !== void 0) parts.push(formatCost(stats.costUsd));
3774
+ if (stats.durationMs !== void 0) parts.push(formatDuration(stats.durationMs));
3775
+ if (stats.numTurns !== void 0) {
3776
+ parts.push(`${stats.numTurns} CLI ${stats.numTurns === 1 ? "turn" : "turns"}`);
3777
+ }
3778
+ if (stats.inputTokens !== void 0) parts.push(`in ${formatTokens(stats.inputTokens)}`);
3779
+ if (stats.outputTokens !== void 0) parts.push(`out ${formatTokens(stats.outputTokens)}`);
3780
+ if (stats.cacheReadTokens) parts.push(`cache read ${formatTokens(stats.cacheReadTokens)}`);
3781
+ if (stats.cacheWriteTokens) parts.push(`cache write ${formatTokens(stats.cacheWriteTokens)}`);
3782
+ if (stats.permissionDenials?.length) {
3783
+ const count = stats.permissionDenials.length;
3784
+ parts.push(`${count} permission ${count === 1 ? "denial" : "denials"}`);
3785
+ }
3786
+ if (parts.length === 0) return null;
3787
+ return `${TURN_STATS_MARKER} ${parts.join(" \xB7 ")}`;
3788
+ }
3789
+ function formatTurnStatsBlock(stats) {
3790
+ const line = formatTurnStatsLine(stats);
3791
+ return line === null ? null : `
3792
+ ${line}
3793
+ `;
3513
3794
  }
3514
- function deepMerge(target, source) {
3515
- const out = { ...target };
3516
- for (const [k, v] of Object.entries(source)) {
3517
- if (v === void 0) continue;
3518
- const existing = out[k];
3519
- if (isPlainObject(existing) && isPlainObject(v)) {
3520
- out[k] = deepMerge(existing, v);
3795
+ function turnStatsLogPayload(stats) {
3796
+ return {
3797
+ costUsd: stats.costUsd ?? null,
3798
+ durationMs: stats.durationMs ?? null,
3799
+ durationApiMs: stats.durationApiMs ?? null,
3800
+ numTurns: stats.numTurns ?? null,
3801
+ inputTokens: stats.inputTokens ?? null,
3802
+ outputTokens: stats.outputTokens ?? null,
3803
+ cacheReadTokens: stats.cacheReadTokens ?? null,
3804
+ cacheWriteTokens: stats.cacheWriteTokens ?? null,
3805
+ modelUsage: stats.modelUsage ?? null,
3806
+ permissionDenials: stats.permissionDenials?.length ?? 0
3807
+ };
3808
+ }
3809
+
3810
+ // src/message-builder.ts
3811
+ var PLUGIN_NOTE_MARKERS = [
3812
+ INLINE_ASIDE_MARKER,
3813
+ ...LEGACY_INLINE_ASIDE_MARKERS,
3814
+ TURN_STATS_MARKER,
3815
+ COMPACT_BOUNDARY_MARKER,
3816
+ RATE_LIMIT_MARKER,
3817
+ RESULT_ERROR_MARKER,
3818
+ DOCTOR_MARKER
3819
+ ];
3820
+ function isPluginNote(part) {
3821
+ if (!part || part.type !== "text" || typeof part.text !== "string") return false;
3822
+ const text = part.text.trimStart();
3823
+ return PLUGIN_NOTE_MARKERS.some((marker) => text.startsWith(marker));
3824
+ }
3825
+ function stripPluginNotes(content) {
3826
+ if (!Array.isArray(content)) return content;
3827
+ const kept = content.filter((part) => !isPluginNote(part));
3828
+ return kept.length === content.length ? content : kept;
3829
+ }
3830
+ function filterSideQuestionHistory(prompt) {
3831
+ let pluginCommand = false;
3832
+ const kept = prompt.filter((message) => {
3833
+ if (message.role === "user") {
3834
+ pluginCommand = parseSideQuestionContent(message.content) !== null || parseDoctorCommandContent(message.content) !== null;
3835
+ return !pluginCommand;
3836
+ }
3837
+ return message.role !== "assistant" || !pluginCommand;
3838
+ });
3839
+ return kept.map(
3840
+ (message) => message.role === "assistant" ? { ...message, content: stripPluginNotes(message.content) } : message
3841
+ );
3842
+ }
3843
+ var SUPPORTED_IMAGE_TYPES = /* @__PURE__ */ new Set([
3844
+ "image/jpeg",
3845
+ "image/png",
3846
+ "image/gif",
3847
+ "image/webp"
3848
+ ]);
3849
+ function toImageBlock(part) {
3850
+ const raw = part.image ?? part.data ?? part.url ?? part.source?.data;
3851
+ if (!raw) {
3852
+ log.warn("file part without data, skipping");
3853
+ return null;
3854
+ }
3855
+ let resolvedMediaType = part.mediaType || part.mimeType || part.mime || "";
3856
+ let base64 = null;
3857
+ if (typeof raw === "string") {
3858
+ if (raw.startsWith("data:")) {
3859
+ const match = /^data:([^;,]+)(?:;[^,]*)*(?:;base64)?,(.*)$/s.exec(raw);
3860
+ if (!match) {
3861
+ log.warn("malformed data URI, skipping file part");
3862
+ return null;
3863
+ }
3864
+ resolvedMediaType = resolvedMediaType || match[1];
3865
+ base64 = match[2];
3866
+ } else if (/^https?:\/\//i.test(raw)) {
3867
+ log.warn("remote URL images are not supported by Claude CLI, skipping");
3868
+ return null;
3521
3869
  } else {
3522
- out[k] = v;
3870
+ base64 = raw;
3523
3871
  }
3872
+ } else if (raw instanceof URL) {
3873
+ log.warn("remote URL images are not supported by Claude CLI, skipping");
3874
+ return null;
3875
+ } else if (raw instanceof Uint8Array || Buffer.isBuffer(raw)) {
3876
+ base64 = Buffer.from(raw).toString("base64");
3877
+ } else {
3878
+ log.warn("unsupported file part data type", { dataType: typeof raw });
3879
+ return null;
3524
3880
  }
3525
- return out;
3881
+ if (!resolvedMediaType || !SUPPORTED_IMAGE_TYPES.has(resolvedMediaType)) {
3882
+ log.warn("unsupported media type for Claude image block, skipping", {
3883
+ mediaType: resolvedMediaType
3884
+ });
3885
+ return null;
3886
+ }
3887
+ return {
3888
+ type: "image",
3889
+ source: { type: "base64", media_type: resolvedMediaType, data: base64 }
3890
+ };
3891
+ }
3892
+ function getToolResultText(part) {
3893
+ const value = part.output ?? part.result;
3894
+ if (typeof value === "string") {
3895
+ return value;
3896
+ }
3897
+ if (!value || typeof value !== "object") {
3898
+ return JSON.stringify(value);
3899
+ }
3900
+ switch (value.type) {
3901
+ case "text":
3902
+ case "error-text":
3903
+ return String(value.value);
3904
+ case "json":
3905
+ case "error-json":
3906
+ return JSON.stringify(value.value);
3907
+ case "execution-denied":
3908
+ return value.reason ? `Execution denied: ${value.reason}` : "Execution denied";
3909
+ case "content":
3910
+ return Array.isArray(value.value) ? value.value.map((item) => {
3911
+ if (item?.type === "text") return item.text;
3912
+ return JSON.stringify(item);
3913
+ }).join("\n") : JSON.stringify(value.value);
3914
+ default:
3915
+ return JSON.stringify(value);
3916
+ }
3917
+ }
3918
+ var MAX_HISTORY_CHARS = 18e4;
3919
+ var MAX_TOOL_RESULT_CHARS = 1e4;
3920
+ var MAX_TOOL_INPUT_CHARS = 2e3;
3921
+ function clipWithMarker(text, max) {
3922
+ if (text.length <= max) return text;
3923
+ return `${text.slice(0, max)}
3924
+ \u2026[truncated ${text.length - max} chars]`;
3925
+ }
3926
+ function renderToolInput(input) {
3927
+ let raw;
3928
+ try {
3929
+ raw = typeof input === "string" ? input : JSON.stringify(input);
3930
+ } catch {
3931
+ raw = String(input);
3932
+ }
3933
+ return clipWithMarker(raw, MAX_TOOL_INPUT_CHARS);
3934
+ }
3935
+ function renderMessageContentForCompaction(msg) {
3936
+ const lines = [];
3937
+ let toolResultCount = 0;
3938
+ if (typeof msg.content === "string") {
3939
+ return { text: msg.content, toolResultCount: 0 };
3940
+ }
3941
+ if (!Array.isArray(msg.content)) {
3942
+ return { text: "", toolResultCount: 0 };
3943
+ }
3944
+ for (const part of msg.content) {
3945
+ if (!part) continue;
3946
+ switch (part.type) {
3947
+ case "text":
3948
+ if (part.text) lines.push(part.text);
3949
+ break;
3950
+ case "tool-call":
3951
+ lines.push(
3952
+ `[tool_use:${part.toolName ?? "unknown"}(${renderToolInput(part.input)})]`
3953
+ );
3954
+ break;
3955
+ case "tool-result":
3956
+ toolResultCount++;
3957
+ lines.push(
3958
+ `[tool_result:${part.toolName ?? part.toolCallId ?? "unknown"}]
3959
+ ${clipWithMarker(
3960
+ getToolResultText(part),
3961
+ MAX_TOOL_RESULT_CHARS
3962
+ )}`
3963
+ );
3964
+ break;
3965
+ case "image":
3966
+ lines.push(
3967
+ `[image: ${part.mediaType ?? part.mimeType ?? "unknown"}]`
3968
+ );
3969
+ break;
3970
+ case "file":
3971
+ lines.push(
3972
+ `[file: ${part.mediaType ?? part.mimeType ?? "unknown"}]`
3973
+ );
3974
+ break;
3975
+ case "reasoning":
3976
+ break;
3977
+ }
3978
+ }
3979
+ return { text: lines.join("\n"), toolResultCount };
3980
+ }
3981
+ function compactConversationHistory(prompt, opts = {}) {
3982
+ const mode = opts.mode ?? "fresh-session";
3983
+ prompt = filterSideQuestionHistory(prompt);
3984
+ if (mode === "compaction") {
3985
+ return buildCompactionHistory(prompt);
3986
+ }
3987
+ const conversationMessages = prompt.filter(
3988
+ (m) => m.role === "user" || m.role === "assistant" || m.role === "tool"
3989
+ );
3990
+ if (conversationMessages.length <= 1) {
3991
+ return null;
3992
+ }
3993
+ const historyParts = [];
3994
+ for (let i = 0; i < conversationMessages.length - 1; i++) {
3995
+ const msg = conversationMessages[i];
3996
+ const role = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : "Tool";
3997
+ const { text } = renderMessageContentForCompaction(msg);
3998
+ if (text.trim()) {
3999
+ const truncated = text.length > 2e3 ? text.slice(0, 2e3) + "..." : text;
4000
+ historyParts.push(`${role}: ${truncated}`);
4001
+ }
4002
+ }
4003
+ if (historyParts.length === 0) {
4004
+ return null;
4005
+ }
4006
+ return historyParts.join("\n\n");
4007
+ }
4008
+ function buildCompactionHistory(prompt) {
4009
+ const entries = [];
4010
+ let total = 0;
4011
+ let totalToolResults = 0;
4012
+ let droppedOldest = 0;
4013
+ const end = prompt.length > 0 && prompt[prompt.length - 1].role === "user" ? prompt.length - 1 : prompt.length;
4014
+ for (let i = end - 1; i >= 0; i--) {
4015
+ const msg = prompt[i];
4016
+ const roleLabel = msg.role === "user" ? "User" : msg.role === "assistant" ? "Assistant" : msg.role === "tool" ? "Tool" : msg.role;
4017
+ const { text, toolResultCount } = renderMessageContentForCompaction(msg);
4018
+ if (!text.trim()) continue;
4019
+ const entry = `${roleLabel}: ${text}`;
4020
+ if (total + entry.length > MAX_HISTORY_CHARS) {
4021
+ droppedOldest = i + 1;
4022
+ break;
4023
+ }
4024
+ entries.push(entry);
4025
+ total += entry.length + 2;
4026
+ totalToolResults += toolResultCount;
4027
+ }
4028
+ if (entries.length === 0) return null;
4029
+ entries.reverse();
4030
+ log.info("built compaction history", {
4031
+ entries: entries.length,
4032
+ chars: total,
4033
+ toolResults: totalToolResults,
4034
+ droppedOldestBefore: droppedOldest
4035
+ });
4036
+ return entries.join("\n\n");
4037
+ }
4038
+ function getClaudeUserMessage(prompt, includeHistoryContext = false, opts = {}) {
4039
+ const compactionMode = opts.compactionMode === true;
4040
+ const cliToolCallIds = opts.cliToolCallIds;
4041
+ const content = [];
4042
+ const pushToolResult = (part) => {
4043
+ const id = part.toolCallId;
4044
+ const text = getToolResultText(part);
4045
+ if (!cliToolCallIds || cliToolCallIds.has(id)) {
4046
+ content.push({ type: "tool_result", tool_use_id: id, content: text });
4047
+ return;
4048
+ }
4049
+ log.info("rendering opencode-side tool result as text", {
4050
+ toolCallId: id,
4051
+ toolName: part.toolName,
4052
+ chars: text.length
4053
+ });
4054
+ content.push({
4055
+ type: "text",
4056
+ text: `<opencode_tool_result tool="${part.toolName ?? "unknown"}">
4057
+ ${text}
4058
+ </opencode_tool_result>`
4059
+ });
4060
+ };
4061
+ if (compactionMode) {
4062
+ const transcript = compactConversationHistory(prompt, {
4063
+ mode: "compaction"
4064
+ });
4065
+ if (transcript) {
4066
+ log.info("including compaction transcript", {
4067
+ historyLength: transcript.length
4068
+ });
4069
+ content.push({
4070
+ type: "text",
4071
+ text: `<conversation_transcript>
4072
+ ${transcript}
4073
+ </conversation_transcript>
4074
+
4075
+ The complete prior conversation appears above. The synthesis instructions follow below.
4076
+
4077
+ `
4078
+ });
4079
+ }
4080
+ } else if (includeHistoryContext) {
4081
+ const historyContext = compactConversationHistory(prompt);
4082
+ if (historyContext) {
4083
+ log.info("including conversation history context", {
4084
+ historyLength: historyContext.length
4085
+ });
4086
+ content.push({
4087
+ type: "text",
4088
+ text: `<conversation_history>
4089
+ The following is a summary of our conversation so far (from a previous session that couldn't be resumed):
4090
+
4091
+ ${historyContext}
4092
+
4093
+ </conversation_history>
4094
+
4095
+ Now continuing with the current message:
4096
+
4097
+ `
4098
+ });
4099
+ }
4100
+ }
4101
+ const messages = [];
4102
+ for (let i = prompt.length - 1; i >= 0; i--) {
4103
+ if (prompt[i].role === "assistant") break;
4104
+ messages.unshift(prompt[i]);
4105
+ }
4106
+ for (const msg of messages) {
4107
+ if (msg.role === "user") {
4108
+ if (parseSideQuestionContent(msg.content) !== null) continue;
4109
+ if (typeof msg.content === "string") {
4110
+ const str2 = msg.content;
4111
+ if (str2.trim()) {
4112
+ content.push({ type: "text", text: str2 });
4113
+ }
4114
+ } else if (Array.isArray(msg.content)) {
4115
+ for (const part of msg.content) {
4116
+ if (part.type === "text") {
4117
+ if (part.text && part.text.trim()) {
4118
+ content.push({ type: "text", text: part.text });
4119
+ }
4120
+ } else if (part.type === "file" || part.type === "image") {
4121
+ const block = toImageBlock(part);
4122
+ if (block) {
4123
+ content.push(block);
4124
+ } else {
4125
+ log.debug("skipped non-image file part", {
4126
+ mediaType: part.mediaType
4127
+ });
4128
+ }
4129
+ } else if (part.type === "tool-result") {
4130
+ pushToolResult(part);
4131
+ }
4132
+ }
4133
+ }
4134
+ } else if (msg.role === "tool") {
4135
+ if (Array.isArray(msg.content)) {
4136
+ for (const part of msg.content) {
4137
+ if (part?.type === "tool-result") {
4138
+ pushToolResult(part);
4139
+ }
4140
+ }
4141
+ }
4142
+ }
4143
+ }
4144
+ if (content.length === 0) {
4145
+ log.warn("empty user content; sending sentinel to satisfy CLI");
4146
+ return JSON.stringify({
4147
+ type: "user",
4148
+ message: {
4149
+ role: "user",
4150
+ content: [{ type: "text", text: "(empty)" }]
4151
+ }
4152
+ });
4153
+ }
4154
+ return JSON.stringify({
4155
+ type: "user",
4156
+ message: {
4157
+ role: "user",
4158
+ content
4159
+ }
4160
+ });
4161
+ }
4162
+
4163
+ // src/agent-models.ts
4164
+ import { readFile, readdir } from "fs/promises";
4165
+ import path5 from "path";
4166
+
4167
+ // src/models.ts
4168
+ var PROVIDER_ID = "claude-code";
4169
+ var NPM = "@khalilgharbaoui/opencode-claude-code-plugin";
4170
+ var reasoningVariants = {
4171
+ low: { reasoningEffort: "low" },
4172
+ medium: { reasoningEffort: "medium" },
4173
+ high: { reasoningEffort: "high" },
4174
+ xhigh: { reasoningEffort: "xhigh" },
4175
+ max: { reasoningEffort: "max" }
4176
+ };
4177
+ var baseCapabilities = {
4178
+ temperature: false,
4179
+ attachment: true,
4180
+ toolcall: true,
4181
+ input: { text: true, audio: false, image: true, video: false, pdf: false },
4182
+ output: { text: true, audio: false, image: false, video: false, pdf: false },
4183
+ interleaved: false
4184
+ };
4185
+ function defineModel(opts) {
4186
+ return {
4187
+ id: opts.id,
4188
+ providerID: PROVIDER_ID,
4189
+ api: { id: opts.id, url: "", npm: NPM },
4190
+ name: `${opts.name} (${opts.multiplier}\xD7)`,
4191
+ family: opts.family,
4192
+ capabilities: { ...baseCapabilities, reasoning: opts.reasoning },
4193
+ cost: {
4194
+ input: opts.cost.input,
4195
+ output: opts.cost.output,
4196
+ cache: { read: opts.cost.cacheRead, write: opts.cost.cacheWrite }
4197
+ },
4198
+ limit: { context: opts.context, output: opts.output },
4199
+ status: opts.status ?? "active",
4200
+ options: {},
4201
+ headers: {},
4202
+ release_date: opts.releaseDate,
4203
+ variants: opts.reasoning ? reasoningVariants : void 0
4204
+ };
4205
+ }
4206
+ var haikuCost = { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 };
4207
+ var sonnetCost = { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 };
4208
+ var opusCost = { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 };
4209
+ var fableCost = { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 };
4210
+ var fable51Cost = { input: 10, output: 50, cacheRead: 0.25, cacheWrite: 12.5 };
4211
+ var opusFastCost = { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 };
4212
+ function toConfigModel(model) {
4213
+ const inputMods = [];
4214
+ const outputMods = [];
4215
+ for (const [k, v] of Object.entries(model.capabilities.input)) {
4216
+ if (v) inputMods.push(k);
4217
+ }
4218
+ for (const [k, v] of Object.entries(model.capabilities.output)) {
4219
+ if (v) outputMods.push(k);
4220
+ }
4221
+ return {
4222
+ id: model.api.id,
4223
+ name: model.name,
4224
+ status: model.status,
4225
+ family: model.family ?? "",
4226
+ release_date: model.release_date,
4227
+ temperature: model.capabilities.temperature,
4228
+ reasoning: model.capabilities.reasoning,
4229
+ attachment: model.capabilities.attachment,
4230
+ tool_call: model.capabilities.toolcall,
4231
+ modalities: { input: inputMods, output: outputMods },
4232
+ cost: {
4233
+ input: model.cost.input,
4234
+ output: model.cost.output,
4235
+ cache_read: model.cost.cache.read,
4236
+ cache_write: model.cost.cache.write
4237
+ },
4238
+ limit: model.limit,
4239
+ options: model.options,
4240
+ headers: model.headers,
4241
+ variants: model.variants
4242
+ };
4243
+ }
4244
+ var defaultModels = {
4245
+ "claude-haiku-4-5": defineModel({
4246
+ id: "claude-haiku-4-5",
4247
+ name: "Claude Haiku 4.5",
4248
+ family: "haiku",
4249
+ reasoning: false,
4250
+ context: 2e5,
4251
+ output: 64e3,
4252
+ cost: haikuCost,
4253
+ multiplier: 1,
4254
+ releaseDate: "2025-10-01"
4255
+ }),
4256
+ "claude-sonnet-4-5": defineModel({
4257
+ id: "claude-sonnet-4-5",
4258
+ name: "Claude Sonnet 4.5",
4259
+ family: "sonnet",
4260
+ reasoning: true,
4261
+ context: 2e5,
4262
+ output: 64e3,
4263
+ cost: sonnetCost,
4264
+ multiplier: 3,
4265
+ releaseDate: "2025-09-29"
4266
+ }),
4267
+ "claude-sonnet-4-6": defineModel({
4268
+ id: "claude-sonnet-4-6",
4269
+ name: "Claude Sonnet 4.6",
4270
+ family: "sonnet",
4271
+ reasoning: true,
4272
+ context: 1e6,
4273
+ output: 128e3,
4274
+ cost: sonnetCost,
4275
+ multiplier: 3,
4276
+ releaseDate: "2025-06-19"
4277
+ }),
4278
+ "claude-sonnet-5": defineModel({
4279
+ id: "claude-sonnet-5",
4280
+ name: "Claude Sonnet 5",
4281
+ family: "sonnet",
4282
+ reasoning: true,
4283
+ context: 1e6,
4284
+ output: 128e3,
4285
+ cost: sonnetCost,
4286
+ multiplier: 3,
4287
+ releaseDate: "2026-06-30"
4288
+ }),
4289
+ "claude-opus-4-5": defineModel({
4290
+ id: "claude-opus-4-5",
4291
+ name: "Claude Opus 4.5",
4292
+ family: "opus",
4293
+ reasoning: true,
4294
+ context: 2e5,
4295
+ output: 64e3,
4296
+ cost: opusCost,
4297
+ multiplier: 5,
4298
+ releaseDate: "2025-11-01"
4299
+ }),
4300
+ "claude-opus-4-6": defineModel({
4301
+ id: "claude-opus-4-6",
4302
+ name: "Claude Opus 4.6",
4303
+ family: "opus",
4304
+ reasoning: true,
4305
+ context: 1e6,
4306
+ output: 128e3,
4307
+ cost: opusCost,
4308
+ multiplier: 5,
4309
+ releaseDate: "2025-06-19"
4310
+ }),
4311
+ "claude-opus-4-7": defineModel({
4312
+ id: "claude-opus-4-7",
4313
+ name: "Claude Opus 4.7",
4314
+ family: "opus",
4315
+ reasoning: true,
4316
+ context: 1e6,
4317
+ output: 128e3,
4318
+ cost: opusCost,
4319
+ multiplier: 5,
4320
+ releaseDate: "2025-07-16"
4321
+ }),
4322
+ "claude-opus-4-8": defineModel({
4323
+ id: "claude-opus-4-8",
4324
+ name: "Claude Opus 4.8",
4325
+ family: "opus",
4326
+ reasoning: true,
4327
+ context: 1e6,
4328
+ output: 128e3,
4329
+ cost: opusCost,
4330
+ multiplier: 5,
4331
+ releaseDate: "2026-05-28"
4332
+ }),
4333
+ // Fast mode. The `-fast` suffix is OUR marker, not a model name Anthropic
4334
+ // serves: `parseModelId` strips it before `--model` and turns it into
4335
+ // `--settings {"fastMode":true}` on the spawn. Retired `-fast` model strings
4336
+ // (`claude-opus-4-6-fast`) are a different thing and are not registered here.
4337
+ //
4338
+ // Only Opus 4.8 and Opus 5 qualify: the CLI gates fast mode on the resolved
4339
+ // model name containing `opus-4-8` or `opus-5`, so registering a fast entry
4340
+ // for any other model would produce a picker option that silently runs at
4341
+ // standard speed while displaying the 10x price.
4342
+ "claude-opus-4-8-fast": defineModel({
4343
+ id: "claude-opus-4-8-fast",
4344
+ name: "Claude Opus 4.8 Fast",
4345
+ family: "opus",
4346
+ reasoning: true,
4347
+ context: 1e6,
4348
+ output: 128e3,
4349
+ cost: opusFastCost,
4350
+ multiplier: 10,
4351
+ releaseDate: "2026-05-28"
4352
+ }),
4353
+ "claude-opus-5": defineModel({
4354
+ id: "claude-opus-5",
4355
+ name: "Claude Opus 5",
4356
+ family: "opus",
4357
+ reasoning: true,
4358
+ context: 1e6,
4359
+ output: 128e3,
4360
+ cost: opusCost,
4361
+ multiplier: 5,
4362
+ releaseDate: "2026-07-24"
4363
+ }),
4364
+ "claude-opus-5-fast": defineModel({
4365
+ id: "claude-opus-5-fast",
4366
+ name: "Claude Opus 5 Fast",
4367
+ family: "opus",
4368
+ reasoning: true,
4369
+ context: 1e6,
4370
+ output: 128e3,
4371
+ cost: opusFastCost,
4372
+ multiplier: 10,
4373
+ releaseDate: "2026-07-24"
4374
+ }),
4375
+ "claude-fable-5": defineModel({
4376
+ id: "claude-fable-5",
4377
+ name: "Claude Fable 5",
4378
+ family: "fable",
4379
+ reasoning: true,
4380
+ context: 1e6,
4381
+ output: 128e3,
4382
+ cost: fableCost,
4383
+ multiplier: 10,
4384
+ releaseDate: "2026-06-09"
4385
+ }),
4386
+ "claude-fable-5-1": defineModel({
4387
+ id: "claude-fable-5-1",
4388
+ name: "Claude Fable 5.1",
4389
+ family: "fable",
4390
+ reasoning: true,
4391
+ context: 1e6,
4392
+ output: 128e3,
4393
+ cost: fable51Cost,
4394
+ multiplier: 10,
4395
+ releaseDate: "2026-09-01"
4396
+ }),
4397
+ // Mythos 5 and 5.1 share the corresponding Fable models' capabilities and
4398
+ // pricing without the safety classifiers; limited availability via Project
4399
+ // Glasswing. `claude --model` simply errors for accounts without access, so
4400
+ // they are safe to register unconditionally.
4401
+ "claude-mythos-5": defineModel({
4402
+ id: "claude-mythos-5",
4403
+ name: "Claude Mythos 5",
4404
+ family: "mythos",
4405
+ reasoning: true,
4406
+ context: 1e6,
4407
+ output: 128e3,
4408
+ cost: fableCost,
4409
+ multiplier: 10,
4410
+ releaseDate: "2026-06-09"
4411
+ }),
4412
+ "claude-mythos-5-1": defineModel({
4413
+ id: "claude-mythos-5-1",
4414
+ name: "Claude Mythos 5.1",
4415
+ family: "mythos",
4416
+ reasoning: true,
4417
+ context: 1e6,
4418
+ output: 128e3,
4419
+ cost: fable51Cost,
4420
+ multiplier: 10,
4421
+ releaseDate: "2026-09-01"
4422
+ })
4423
+ };
4424
+ var FAST_SUFFIX = "-fast";
4425
+ function parseModelId(modelId) {
4426
+ const at = modelId.indexOf("@");
4427
+ const base = at === -1 ? modelId : modelId.slice(0, at);
4428
+ const account = at === -1 ? "" : modelId.slice(at);
4429
+ if (!base.endsWith(FAST_SUFFIX)) return { model: modelId, fast: false };
4430
+ if (!Object.hasOwn(defaultModels, base)) return { model: modelId, fast: false };
4431
+ return { model: base.slice(0, -FAST_SUFFIX.length) + account, fast: true };
3526
4432
  }
3527
- function walkUp(opts) {
3528
- const out = [];
3529
- let current = path5.resolve(opts.start);
3530
- while (true) {
3531
- for (const target of opts.targets) {
3532
- const candidate = path5.join(current, target);
3533
- if (opts.predicate(candidate)) out.push(candidate);
3534
- }
3535
- if (opts.stop && current === path5.resolve(opts.stop)) break;
3536
- const parent = path5.dirname(current);
3537
- if (parent === current) break;
3538
- current = parent;
3539
- }
3540
- return out;
4433
+
4434
+ // src/agent-models.ts
4435
+ var AGENT_DIR_NAMES = ["agents", "agent"];
4436
+ var REASONING_EFFORTS = [
4437
+ "minimal",
4438
+ "low",
4439
+ "medium",
4440
+ "high",
4441
+ "xhigh",
4442
+ "max"
4443
+ ];
4444
+ var registry = {};
4445
+ var defaultSubagentModel;
4446
+ function setAgentRegistry(records) {
4447
+ registry = records;
3541
4448
  }
3542
- function detectWorktree(cwd) {
3543
- const override = process.env.OPENCODE_WORKTREE;
3544
- if (override) return path5.resolve(override);
3545
- let current = path5.resolve(cwd);
3546
- while (true) {
3547
- const gitPath = path5.join(current, ".git");
3548
- try {
3549
- if (fs4.existsSync(gitPath)) return current;
3550
- } catch {
3551
- }
3552
- const parent = path5.dirname(current);
3553
- if (parent === current) return void 0;
3554
- current = parent;
3555
- }
4449
+ function getAgentRegistry() {
4450
+ return registry;
3556
4451
  }
3557
- function globalConfigDir() {
3558
- const xdg = process.env.XDG_CONFIG_HOME ?? path5.join(os3.homedir(), ".config");
3559
- return path5.join(xdg, "opencode");
4452
+ function setDefaultSubagentModel(model) {
4453
+ defaultSubagentModel = model?.trim() || void 0;
3560
4454
  }
3561
- function loadGlobalConfig() {
3562
- const dir = globalConfigDir();
3563
- let merged = {};
3564
- for (const name of FILE_NAMES.slice().reverse()) {
3565
- const file = path5.join(dir, name);
3566
- if (!fileExists2(file)) continue;
3567
- const parsed = readAndParse(file);
3568
- if (parsed) merged = deepMerge(merged, parsed);
3569
- }
3570
- return merged;
4455
+ function getDefaultSubagentModel() {
4456
+ return defaultSubagentModel;
3571
4457
  }
3572
- function loadProjectFilesInDir(dir) {
3573
- let merged = {};
3574
- for (const name of PROJECT_FILE_NAMES) {
3575
- const file = path5.join(dir, name);
3576
- if (!fileExists2(file)) continue;
3577
- const parsed = readAndParse(file);
3578
- if (parsed) merged = deepMerge(merged, parsed);
3579
- }
3580
- return merged;
4458
+ function accountMarker(modelId) {
4459
+ const at = modelId.indexOf("@");
4460
+ return at === -1 ? "" : modelId.slice(at);
3581
4461
  }
3582
- function dotOpencodeDirs(cwd, worktree) {
3583
- const dirs = [];
3584
- const seen = /* @__PURE__ */ new Set();
3585
- const push = (p) => {
3586
- const abs = path5.resolve(p);
3587
- if (!seen.has(abs) && dirExists2(abs)) {
3588
- seen.add(abs);
3589
- dirs.push(abs);
3590
- }
3591
- };
3592
- for (const dir of walkUp({
3593
- start: cwd,
3594
- stop: worktree,
3595
- targets: [".opencode"],
3596
- predicate: dirExists2
3597
- })) {
3598
- push(dir);
3599
- }
3600
- const home = os3.homedir();
3601
- if (home) {
3602
- const homeDot = path5.join(home, ".opencode");
3603
- if (dirExists2(homeDot)) push(homeDot);
3604
- }
3605
- const envDir = process.env.OPENCODE_CONFIG_DIR;
3606
- if (envDir && dirExists2(envDir)) push(envDir);
3607
- return dirs;
4462
+ function withoutAccountMarker(modelId) {
4463
+ const at = modelId.indexOf("@");
4464
+ return at === -1 ? modelId : modelId.slice(0, at);
3608
4465
  }
3609
- function substituteEnvPlaceholders(source) {
3610
- const out = {};
3611
- for (const [k, v] of Object.entries(source)) {
3612
- if (typeof v !== "string") continue;
3613
- out[k] = v.replace(/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, name) => {
3614
- const resolved = process.env[name];
3615
- return typeof resolved === "string" ? resolved : "";
4466
+ function resolveAgentModel(agent, modelId, overrides) {
4467
+ if (!agent) return modelId;
4468
+ const record = (overrides?.records ?? registry)[agent];
4469
+ if (!record) return modelId;
4470
+ if (record.model?.includes("/")) return modelId;
4471
+ const fallback = overrides ? overrides.defaultSubagentModel : defaultSubagentModel;
4472
+ const declared = record.forceModel?.trim();
4473
+ const wanted = declared || (record.mode === "subagent" ? fallback : void 0);
4474
+ if (!wanted) return modelId;
4475
+ const base = withoutAccountMarker(wanted);
4476
+ if (!Object.hasOwn(defaultModels, base)) {
4477
+ log.warn("agent model override refused: unknown model", {
4478
+ agent,
4479
+ wanted: base,
4480
+ keeping: modelId
3616
4481
  });
4482
+ return modelId;
3617
4483
  }
3618
- return out;
3619
- }
3620
- function translateServer(name, spec) {
3621
- if (spec.enabled === false) return null;
3622
- const type = spec.type;
3623
- if (type === "local") {
3624
- const cmd = spec.command;
3625
- if (!Array.isArray(cmd) || cmd.length === 0) {
3626
- log.warn("skipping local MCP server with no command", { name });
3627
- return null;
3628
- }
3629
- const out = {
3630
- type: "stdio",
3631
- command: String(cmd[0])
3632
- };
3633
- if (cmd.length > 1) out.args = cmd.slice(1).map((s) => String(s));
3634
- if (spec.environment && typeof spec.environment === "object") {
3635
- out.env = substituteEnvPlaceholders(
3636
- spec.environment
3637
- );
3638
- }
3639
- return out;
3640
- }
3641
- if (type === "remote") {
3642
- if (typeof spec.url !== "string" || !spec.url) {
3643
- log.warn("skipping remote MCP server with no url", { name });
3644
- return null;
3645
- }
3646
- const out = {
3647
- type: "http",
3648
- url: spec.url
3649
- };
3650
- if (spec.headers && typeof spec.headers === "object") {
3651
- out.headers = substituteEnvPlaceholders(
3652
- spec.headers
3653
- );
3654
- }
3655
- return out;
4484
+ const resolved = `${base}${accountMarker(modelId)}`;
4485
+ if (resolved !== modelId) {
4486
+ log.debug("agent model override", { agent, from: modelId, to: resolved });
3656
4487
  }
3657
- log.warn("skipping MCP server with unknown type", {
3658
- name,
3659
- type: type ?? null
3660
- });
3661
- return null;
3662
- }
3663
- function extractMcpBlock(config) {
3664
- const mcp = config.mcp;
3665
- if (!mcp || typeof mcp !== "object" || Array.isArray(mcp)) return {};
3666
- return mcp;
4488
+ return resolved;
3667
4489
  }
3668
- function mergeMcp(target, source) {
3669
- const out = { ...target };
3670
- for (const [name, spec] of Object.entries(source)) {
3671
- if (!spec || typeof spec !== "object") continue;
3672
- const existing = out[name];
3673
- if (existing && typeof existing === "object") {
3674
- out[name] = deepMerge(
3675
- existing,
3676
- spec
3677
- );
3678
- } else {
3679
- out[name] = spec;
3680
- }
4490
+ function resolveAgentEffort(agent, inherited, overrides) {
4491
+ if (!agent) return inherited;
4492
+ const record = (overrides?.records ?? registry)[agent];
4493
+ const declared = record?.reasoningEffort?.trim();
4494
+ if (!declared) return inherited;
4495
+ if (!REASONING_EFFORTS.includes(declared)) {
4496
+ log.warn("agent effort override refused: unknown level", {
4497
+ agent,
4498
+ wanted: declared,
4499
+ keeping: inherited
4500
+ });
4501
+ return inherited;
3681
4502
  }
3682
- return out;
3683
- }
3684
- function bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers) {
3685
- const {
3686
- servers: merged,
3687
- enabledServerNames: allEnabledServerNames,
3688
- hash
3689
- } = mergeOpencodeMcp(cwd, runtimeStatus);
3690
- const servers = {};
3691
- const bridgedServerNames = [];
3692
- for (const [name, spec] of Object.entries(merged)) {
3693
- if (!spec || typeof spec !== "object") continue;
3694
- if (excludeServers?.has(name)) continue;
3695
- const translated = translateServer(name, spec);
3696
- if (translated) {
3697
- servers[name] = translated;
3698
- bridgedServerNames.push(name);
3699
- }
4503
+ if (declared !== inherited) {
4504
+ log.debug("agent effort override", {
4505
+ agent,
4506
+ from: inherited,
4507
+ to: declared
4508
+ });
3700
4509
  }
3701
- return finishBridge({
3702
- servers,
3703
- bridgedServerNames,
3704
- allEnabledServerNames,
3705
- hash,
3706
- excludeServers
3707
- });
4510
+ return declared;
3708
4511
  }
3709
- function mergeOpencodeMcp(cwd, runtimeStatus) {
3710
- const worktree = detectWorktree(cwd);
3711
- let merged = {};
3712
- merged = mergeMcp(merged, extractMcpBlock(loadGlobalConfig()));
3713
- const explicitConfig = process.env.OPENCODE_CONFIG;
3714
- if (explicitConfig && fileExists2(explicitConfig)) {
3715
- const parsed = readAndParse(explicitConfig);
3716
- if (parsed) merged = mergeMcp(merged, extractMcpBlock(parsed));
4512
+ function parseAgentFrontmatter(text) {
4513
+ const record = {};
4514
+ if (!text.startsWith("---")) return record;
4515
+ const lines = text.split(/\r?\n/);
4516
+ for (let i = 1; i < lines.length; i++) {
4517
+ const line = lines[i];
4518
+ if (line.trim() === "---") break;
4519
+ const match = /^([A-Za-z_][A-Za-z0-9_-]*):[ \t]*(.*)$/.exec(line);
4520
+ if (!match) continue;
4521
+ const key = match[1];
4522
+ if (key !== "mode" && key !== "model" && key !== "forceModel" && key !== "reasoningEffort")
4523
+ continue;
4524
+ const value = match[2].trim().replace(/^["']|["']$/g, "");
4525
+ if (value) record[key] = value;
3717
4526
  }
3718
- const projectFiles = walkUp({
3719
- start: cwd,
3720
- stop: worktree,
3721
- targets: PROJECT_FILE_NAMES,
3722
- predicate: fileExists2
3723
- });
3724
- const projectDirs = [];
3725
- const seenProjectDirs = /* @__PURE__ */ new Set();
3726
- for (const f of projectFiles) {
3727
- const d = path5.dirname(f);
3728
- if (!seenProjectDirs.has(d)) {
3729
- seenProjectDirs.add(d);
3730
- projectDirs.push(d);
4527
+ return record;
4528
+ }
4529
+ async function readAgentMarkdownRecords(directories) {
4530
+ const records = {};
4531
+ for (const directory of directories) {
4532
+ let entries;
4533
+ try {
4534
+ entries = await readdir(directory);
4535
+ } catch {
4536
+ continue;
4537
+ }
4538
+ for (const entry of entries) {
4539
+ if (!entry.endsWith(".md")) continue;
4540
+ const name = entry.slice(0, -3);
4541
+ if (records[name]) continue;
4542
+ try {
4543
+ const text = await readFile(path5.join(directory, entry), "utf8");
4544
+ records[name] = parseAgentFrontmatter(text);
4545
+ } catch (err) {
4546
+ log.debug("failed to read agent markdown", {
4547
+ file: path5.join(directory, entry),
4548
+ error: String(err)
4549
+ });
4550
+ }
3731
4551
  }
3732
4552
  }
3733
- for (const dir of projectDirs.slice().reverse()) {
3734
- merged = mergeMcp(merged, extractMcpBlock(loadProjectFilesInDir(dir)));
3735
- }
3736
- for (const dir of dotOpencodeDirs(cwd, worktree)) {
3737
- merged = mergeMcp(merged, extractMcpBlock(loadProjectFilesInDir(dir)));
3738
- }
3739
- if (runtimeStatus) {
3740
- for (const name of Object.keys(merged)) {
3741
- const status = runtimeStatus[name];
3742
- if (status === void 0) continue;
3743
- const existing = merged[name];
3744
- const base = existing && typeof existing === "object" ? existing : {};
3745
- merged[name] = { ...base, enabled: status === "connected" };
4553
+ return records;
4554
+ }
4555
+ function agentDirectories(home, projectDirectory) {
4556
+ const directories = [];
4557
+ if (projectDirectory) {
4558
+ for (const name of AGENT_DIR_NAMES) {
4559
+ directories.push(path5.join(projectDirectory, ".opencode", name));
3746
4560
  }
3747
4561
  }
3748
- const enabledServerNames = [];
3749
- for (const [name, spec] of Object.entries(merged)) {
3750
- if (!spec || typeof spec !== "object") continue;
3751
- const enabled = spec.enabled;
3752
- if (enabled === false) continue;
3753
- enabledServerNames.push(name);
4562
+ if (home) {
4563
+ for (const name of AGENT_DIR_NAMES) {
4564
+ directories.push(path5.join(home, ".config", "opencode", name));
4565
+ }
3754
4566
  }
3755
- const mergedBody = JSON.stringify({ mcpServers: merged }, null, 2);
3756
- const hash = crypto3.createHash("sha256").update(mergedBody).digest("hex").slice(0, 12);
3757
- return { servers: merged, enabledServerNames, hash };
4567
+ return directories;
3758
4568
  }
3759
- function finishBridge(input) {
3760
- const { servers, bridgedServerNames, allEnabledServerNames, hash, excludeServers } = input;
3761
- if (Object.keys(servers).length === 0) {
3762
- const allEnabledServersExcluded = excludeServers && allEnabledServerNames.length > 0 && allEnabledServerNames.every((name) => excludeServers.has(name));
3763
- if (!allEnabledServersExcluded) return null;
3764
- return {
3765
- path: "",
3766
- hash,
3767
- serverNames: [],
3768
- allEnabledServerNames
3769
- };
3770
- }
3771
- const body = JSON.stringify({ mcpServers: servers }, null, 2);
3772
- const outPath = path5.join(
3773
- pluginTmpDir(),
3774
- `mcp-${hash}.json`
3775
- );
4569
+
4570
+ // src/skill-bridge.ts
4571
+ import * as crypto3 from "crypto";
4572
+ import * as fs5 from "fs";
4573
+ import * as os3 from "os";
4574
+ import * as path6 from "path";
4575
+ import { fileURLToPath as fileURLToPath2 } from "url";
4576
+ var SKILL_PLUGIN_NAME = "opencode-skills";
4577
+ function bundledSkillsDir() {
3776
4578
  try {
3777
- if (!fileExists2(outPath)) {
3778
- fs4.writeFileSync(outPath, body, { encoding: "utf8", mode: 384 });
3779
- }
3780
- } catch (e) {
3781
- log.warn("failed to write bridged MCP config", {
3782
- error: e instanceof Error ? e.message : String(e)
3783
- });
4579
+ const here = fileURLToPath2(import.meta.url);
4580
+ const dir = path6.resolve(path6.dirname(here), "..", "skills");
4581
+ return dirExists2(dir) ? dir : null;
4582
+ } catch {
3784
4583
  return null;
3785
4584
  }
3786
- log.info("bridged opencode MCP config", {
3787
- target: outPath,
3788
- hash,
3789
- servers: bridgedServerNames,
3790
- excluded: excludeServers ? Array.from(excludeServers) : []
3791
- });
3792
- return {
3793
- path: outPath,
3794
- hash,
3795
- serverNames: bridgedServerNames,
3796
- allEnabledServerNames
3797
- };
3798
4585
  }
3799
-
3800
- // src/runtime-status.ts
3801
- var opencodeClient = null;
3802
- function setOpencodeClient(client) {
3803
- if (client && typeof client === "object") {
3804
- opencodeClient = client;
4586
+ function dirExists2(p) {
4587
+ try {
4588
+ return fs5.statSync(p).isDirectory();
4589
+ } catch {
4590
+ return false;
3805
4591
  }
3806
4592
  }
3807
- function getOpencodeClient() {
3808
- return opencodeClient;
4593
+ function fileExists2(p) {
4594
+ try {
4595
+ return fs5.statSync(p).isFile();
4596
+ } catch {
4597
+ return false;
4598
+ }
3809
4599
  }
3810
- var opencodeProjectDirectory;
3811
- function setOpencodeProjectDirectory(dir) {
3812
- opencodeProjectDirectory = dir;
4600
+ function skillRoots(cwd) {
4601
+ const roots = [];
4602
+ const seen = /* @__PURE__ */ new Set();
4603
+ const push = (p) => {
4604
+ const abs = path6.resolve(p);
4605
+ if (seen.has(abs)) return;
4606
+ seen.add(abs);
4607
+ if (dirExists2(abs)) roots.push(abs);
4608
+ };
4609
+ let current = path6.resolve(cwd);
4610
+ while (true) {
4611
+ push(path6.join(current, ".opencode", "skills"));
4612
+ const parent = path6.dirname(current);
4613
+ if (parent === current) break;
4614
+ current = parent;
4615
+ }
4616
+ const home = os3.homedir();
4617
+ if (home) push(path6.join(home, ".opencode", "skills"));
4618
+ const envDir = process.env.OPENCODE_CONFIG_DIR;
4619
+ if (envDir) push(path6.join(envDir, "skills"));
4620
+ const xdg = process.env.XDG_CONFIG_HOME ?? (home ? path6.join(home, ".config") : null);
4621
+ if (xdg) push(path6.join(xdg, "opencode", "skills"));
4622
+ return roots;
3813
4623
  }
3814
- function getOpencodeProjectDirectory() {
3815
- return opencodeProjectDirectory;
4624
+ function collectSkills(root, claimed, found) {
4625
+ let entries;
4626
+ try {
4627
+ entries = fs5.readdirSync(root, { withFileTypes: true });
4628
+ } catch {
4629
+ return;
4630
+ }
4631
+ for (const entry of entries) {
4632
+ if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
4633
+ const name = entry.name;
4634
+ if (name.startsWith(".")) continue;
4635
+ if (claimed.has(name)) continue;
4636
+ const dir = path6.join(root, name);
4637
+ if (!fileExists2(path6.join(dir, "SKILL.md"))) continue;
4638
+ claimed.add(name);
4639
+ found.push({ name, dir });
4640
+ }
3816
4641
  }
3817
- function isUsableDirectory(d) {
3818
- return typeof d === "string" && d.length > 1 && d !== "/";
4642
+ var byName = (a, b) => a.name.localeCompare(b.name);
4643
+ function discoverOpencodeSkills(cwd) {
4644
+ const found = [];
4645
+ const claimed = /* @__PURE__ */ new Set();
4646
+ for (const root of skillRoots(cwd)) collectSkills(root, claimed, found);
4647
+ return found.sort(byName);
3819
4648
  }
3820
- function resolveSpawnCwdFrom(configured, live, captured, sessionDir) {
3821
- if (configured) return configured;
3822
- if (isUsableDirectory(sessionDir)) return sessionDir;
3823
- if (isUsableDirectory(live)) return live;
3824
- return captured ?? live;
4649
+ function discoverBundledSkills() {
4650
+ const root = bundledSkillsDir();
4651
+ if (!root) return [];
4652
+ const found = [];
4653
+ collectSkills(root, /* @__PURE__ */ new Set(), found);
4654
+ return found.sort(byName);
3825
4655
  }
3826
- async function resolveSpawnCwdForSession(configured, sessionID) {
3827
- if (configured) return configured;
3828
- const sessionDir = sessionID ? await fetchSessionDirectory(sessionID) : void 0;
3829
- return resolveSpawnCwdFrom(
3830
- configured,
3831
- process.cwd(),
3832
- opencodeProjectDirectory,
3833
- sessionDir
3834
- );
4656
+ function registerBundledSkillPath(config) {
4657
+ const dir = bundledSkillsDir();
4658
+ if (!dir) return false;
4659
+ config.skills ??= {};
4660
+ const paths = config.skills.paths ??= [];
4661
+ if (paths.some((p) => path6.resolve(p) === dir)) return false;
4662
+ paths.push(dir);
4663
+ return true;
3835
4664
  }
3836
- async function fetchSessionDirectory(sessionID) {
3837
- if (!sessionID || sessionID === "default") return void 0;
3838
- const client = opencodeClient;
3839
- if (!client?.session?.get) return void 0;
4665
+ function linkSkill(source, target) {
3840
4666
  try {
3841
- const res = await client.session.get({ path: { id: sessionID } });
3842
- const data = res.data;
3843
- if (!data || typeof data !== "object") return void 0;
3844
- const dir = data.directory;
3845
- return isUsableDirectory(dir) ? dir : void 0;
3846
- } catch (err) {
3847
- log.warn("failed to fetch opencode session directory", {
3848
- sessionID,
3849
- error: err instanceof Error ? err.message : String(err)
3850
- });
3851
- return void 0;
4667
+ fs5.symlinkSync(source, target, process.platform === "win32" ? "junction" : "dir");
4668
+ return;
4669
+ } catch {
4670
+ fs5.cpSync(source, target, { recursive: true, dereference: true });
3852
4671
  }
3853
4672
  }
3854
- async function getRuntimeMcpStatus() {
3855
- const client = opencodeClient;
3856
- if (!client?.mcp?.status) return void 0;
4673
+ function buildSkillPluginDir(skills) {
4674
+ if (skills.length === 0) return null;
4675
+ const fingerprint = skills.map((s) => `${s.name}\0${s.dir}`).join("\n");
4676
+ const hash = crypto3.createHash("sha256").update(fingerprint).digest("hex").slice(0, 12);
4677
+ const root = path6.join(pluginTmpDir(), `skills-${hash}`);
4678
+ const manifest = path6.join(root, ".claude-plugin", "plugin.json");
4679
+ if (fileExists2(manifest)) return root;
3857
4680
  try {
3858
- const res = await client.mcp.status();
3859
- const data = res.data;
3860
- if (!data || typeof data !== "object") return void 0;
3861
- const out = {};
3862
- for (const [name, entry] of Object.entries(data)) {
3863
- if (entry && typeof entry === "object") {
3864
- const status = entry.status;
3865
- if (typeof status === "string") out[name] = status;
3866
- }
4681
+ fs5.rmSync(root, { recursive: true, force: true });
4682
+ fs5.mkdirSync(path6.join(root, ".claude-plugin"), { recursive: true });
4683
+ fs5.mkdirSync(path6.join(root, "skills"), { recursive: true });
4684
+ fs5.writeFileSync(
4685
+ manifest,
4686
+ JSON.stringify(
4687
+ {
4688
+ name: SKILL_PLUGIN_NAME,
4689
+ description: "Skills discovered from this opencode installation, bridged into Claude Code."
4690
+ },
4691
+ null,
4692
+ 2
4693
+ ),
4694
+ { encoding: "utf8", mode: 384 }
4695
+ );
4696
+ for (const skill of skills) {
4697
+ linkSkill(skill.dir, path6.join(root, "skills", skill.name));
3867
4698
  }
3868
- return out;
3869
4699
  } catch (err) {
3870
- log.warn("failed to fetch opencode MCP runtime status", {
4700
+ log.warn("failed to stage opencode skill plugin dir", {
4701
+ root,
3871
4702
  error: err instanceof Error ? err.message : String(err)
3872
4703
  });
3873
- return void 0;
4704
+ return null;
3874
4705
  }
4706
+ return root;
3875
4707
  }
3876
- async function fetchOpencodeToolList(provider, model, directory) {
3877
- const client = opencodeClient;
3878
- if (!client?.tool?.list) return void 0;
3879
- try {
3880
- const res = await client.tool.list({
3881
- query: { provider, model, ...directory ? { directory } : {} }
3882
- });
3883
- const data = res.data;
3884
- if (!Array.isArray(data)) return void 0;
3885
- const out = [];
3886
- for (const entry of data) {
3887
- if (!entry || typeof entry !== "object") continue;
3888
- const e = entry;
3889
- const id = typeof e.id === "string" ? e.id : null;
3890
- const description = typeof e.description === "string" ? e.description : "";
3891
- const parameters = e.parameters && typeof e.parameters === "object" ? e.parameters : {};
3892
- if (!id) continue;
3893
- out.push({ id, description, parameters });
3894
- }
3895
- return out;
3896
- } catch (err) {
3897
- log.warn("failed to fetch opencode tool list", {
3898
- provider,
3899
- model,
3900
- error: err instanceof Error ? err.message : String(err)
3901
- });
3902
- return void 0;
4708
+ async function resolveSkillPluginDirs(opts) {
4709
+ const user = opts.enabled ? discoverOpencodeSkills(opts.cwd) : [];
4710
+ const claimed = new Set(user.map((s) => s.name));
4711
+ const bundled = discoverBundledSkills().filter((s) => !claimed.has(s.name));
4712
+ const skills = [...user, ...bundled].sort(byName);
4713
+ if (skills.length === 0) return [];
4714
+ const supported = await detectCliSupportsFlag(opts.cliPath, "--plugin-dir");
4715
+ if (!supported) {
4716
+ log.notice(
4717
+ "claude cli does not support --plugin-dir; opencode skills will not be bridged. Run `npm i -g @anthropic-ai/claude-code` to upgrade.",
4718
+ { skills: skills.length }
4719
+ );
4720
+ return [];
3903
4721
  }
4722
+ const dir = buildSkillPluginDir(skills);
4723
+ if (!dir) return [];
4724
+ log.info("bridged opencode skills into claude", {
4725
+ count: skills.length,
4726
+ names: skills.map((s) => s.name),
4727
+ bundled: bundled.map((s) => s.name),
4728
+ pluginDir: dir
4729
+ });
4730
+ return [dir];
3904
4731
  }
3905
4732
 
3906
4733
  // src/claude-session-wrapper.ts
@@ -3909,12 +4736,12 @@ import { unlink as unlink2 } from "fs/promises";
3909
4736
 
3910
4737
  // src/claude-session-bun.ts
3911
4738
  import * as os4 from "os";
3912
- import * as fs5 from "fs";
3913
- import * as path6 from "path";
4739
+ import * as fs6 from "fs";
4740
+ import * as path7 from "path";
3914
4741
  import { execFileSync } from "child_process";
3915
4742
  import { randomUUID as randomUUID4 } from "crypto";
3916
4743
  function resolveClaude(cmd = "claude") {
3917
- if (path6.isAbsolute(cmd) && fs5.existsSync(cmd)) return cmd;
4744
+ if (path7.isAbsolute(cmd) && fs6.existsSync(cmd)) return cmd;
3918
4745
  const viaBun = Bun.which(cmd);
3919
4746
  if (viaBun) return viaBun;
3920
4747
  const isWin = os4.platform() === "win32";
@@ -3923,25 +4750,25 @@ function resolveClaude(cmd = "claude") {
3923
4750
  encoding: "utf8",
3924
4751
  stdio: ["ignore", "pipe", "ignore"]
3925
4752
  });
3926
- const first = out.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).find((p) => fs5.existsSync(p));
4753
+ const first = out.split(/\r?\n/).map((l) => l.trim()).filter(Boolean).find((p) => fs6.existsSync(p));
3927
4754
  if (first) return first;
3928
4755
  } catch {
3929
4756
  }
3930
4757
  throw new Error(`Could not resolve command on PATH: ${cmd}`);
3931
4758
  }
3932
4759
  function encodeCwd(cwd) {
3933
- return path6.resolve(cwd).replace(/[^a-zA-Z0-9]/g, "-");
4760
+ return path7.resolve(cwd).replace(/[^a-zA-Z0-9]/g, "-");
3934
4761
  }
3935
4762
  var TERMINAL_STOP = /* @__PURE__ */ new Set(["end_turn", "stop_sequence", "max_tokens"]);
3936
4763
  var delay = (ms) => new Promise((r) => setTimeout(r, ms));
3937
4764
  function resolveConfigDir(configDir) {
3938
4765
  const value = configDir ?? process.env.CLAUDE_CONFIG_DIR;
3939
- if (!value) return path6.join(os4.homedir(), ".claude");
4766
+ if (!value) return path7.join(os4.homedir(), ".claude");
3940
4767
  if (value === "~") return os4.homedir();
3941
4768
  if (value.startsWith("~/") || value.startsWith("~\\")) {
3942
- return path6.join(os4.homedir(), value.slice(2));
4769
+ return path7.join(os4.homedir(), value.slice(2));
3943
4770
  }
3944
- return path6.resolve(value);
4771
+ return path7.resolve(value);
3945
4772
  }
3946
4773
  var ClaudeSession = class {
3947
4774
  sessionId;
@@ -3959,11 +4786,11 @@ var ClaudeSession = class {
3959
4786
  signal;
3960
4787
  o;
3961
4788
  constructor(opts = {}) {
3962
- this.cwd = path6.resolve(opts.cwd ?? process.cwd());
4789
+ this.cwd = path7.resolve(opts.cwd ?? process.cwd());
3963
4790
  this.configDir = resolveConfigDir(opts.configDir);
3964
4791
  this.signal = opts.signal;
3965
4792
  this.sessionId = randomUUID4();
3966
- this.jsonlPath = path6.join(
4793
+ this.jsonlPath = path7.join(
3967
4794
  this.configDir,
3968
4795
  "projects",
3969
4796
  encodeCwd(this.cwd),
@@ -4086,7 +4913,7 @@ var ClaudeSession = class {
4086
4913
  }
4087
4914
  readRawLines() {
4088
4915
  try {
4089
- return fs5.readFileSync(this.jsonlPath, "utf8").split("\n");
4916
+ return fs6.readFileSync(this.jsonlPath, "utf8").split("\n");
4090
4917
  } catch {
4091
4918
  return [];
4092
4919
  }
@@ -4314,7 +5141,7 @@ ${text}`
4314
5141
  }
4315
5142
  return parts.join("\n\n");
4316
5143
  }
4317
- function spawnInteractiveProcess(opts) {
5144
+ function interactiveExtraArgs(opts) {
4318
5145
  const extraArgs = [];
4319
5146
  if (opts.mcpConfigPaths && opts.mcpConfigPaths.length > 0) {
4320
5147
  extraArgs.push(
@@ -4323,6 +5150,9 @@ function spawnInteractiveProcess(opts) {
4323
5150
  "--strict-mcp-config"
4324
5151
  );
4325
5152
  }
5153
+ for (const dir of opts.pluginDirs ?? []) {
5154
+ extraArgs.push("--plugin-dir", dir);
5155
+ }
4326
5156
  const flagSettings = {};
4327
5157
  if (opts.permissionsAllow && opts.permissionsAllow.length > 0) {
4328
5158
  flagSettings.permissions = { allow: opts.permissionsAllow };
@@ -4343,6 +5173,10 @@ function spawnInteractiveProcess(opts) {
4343
5173
  if (opts.systemPromptFile) {
4344
5174
  extraArgs.push("--append-system-prompt-file", opts.systemPromptFile);
4345
5175
  }
5176
+ return extraArgs;
5177
+ }
5178
+ function spawnInteractiveProcess(opts) {
5179
+ const extraArgs = interactiveExtraArgs(opts);
4346
5180
  const session = new ClaudeSession({
4347
5181
  cwd: opts.cwd,
4348
5182
  cliPath: opts.cliPath,
@@ -4464,11 +5298,11 @@ function spawnInteractiveProcess(opts) {
4464
5298
  }
4465
5299
 
4466
5300
  // src/claude-code-language-model.ts
4467
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
5301
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
4468
5302
  import { unlink as unlink3 } from "fs/promises";
4469
5303
  import { homedir as homedir5, tmpdir as tmpdir2 } from "os";
4470
5304
  import { randomUUID as randomUUID5 } from "crypto";
4471
- import { dirname as dirname4, join as join7 } from "path";
5305
+ import { dirname as dirname5, join as join8 } from "path";
4472
5306
  var DEFAULT_COMPACTION_MODEL = "claude-haiku-4-5";
4473
5307
  function resolveCompactionModel(configured) {
4474
5308
  const env = process.env.CLAUDE_CODE_COMPACTION_MODEL?.trim();
@@ -4532,6 +5366,7 @@ var AUTO_CONTINUE_MAX_ATTEMPTS = 8;
4532
5366
  var AUTO_CONTINUE_MAX_ELAPSED_MS = 10 * 60 * 1e3;
4533
5367
  var AUTO_CONTINUE_NO_PROGRESS_LIMIT = 2;
4534
5368
  var PROXY_RESULT_BOUNDARY_GRACE_MS = 250;
5369
+ var CHILD_EXIT_STATUS_GRACE_MS = 250;
4535
5370
  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.";
4536
5371
  function autoContinueEnabledFor(compactionMode, configured) {
4537
5372
  return compactionMode ? false : configured;
@@ -4693,7 +5528,7 @@ ${body}`;
4693
5528
  }
4694
5529
  function readPromptFileIfPresent(path9) {
4695
5530
  try {
4696
- const content = readFileSync3(path9, "utf8").trim();
5531
+ const content = readFileSync4(path9, "utf8").trim();
4697
5532
  return content || void 0;
4698
5533
  } catch {
4699
5534
  return void 0;
@@ -4702,9 +5537,9 @@ function readPromptFileIfPresent(path9) {
4702
5537
  function nearestWorkspaceAgentsPrompt(cwd) {
4703
5538
  let dir = cwd;
4704
5539
  while (true) {
4705
- const content = readPromptFileIfPresent(join7(dir, "AGENTS.md"));
5540
+ const content = readPromptFileIfPresent(join8(dir, "AGENTS.md"));
4706
5541
  if (content) return content;
4707
- const parent = dirname4(dir);
5542
+ const parent = dirname5(dir);
4708
5543
  if (parent === dir) return void 0;
4709
5544
  dir = parent;
4710
5545
  }
@@ -4788,8 +5623,8 @@ ${options.compressionSummary.trim()}`
4788
5623
  for (const s of extraSystemContent) {
4789
5624
  if (s.trim()) parts.push(s.trim());
4790
5625
  }
4791
- const configRoot = process.env.XDG_CONFIG_HOME ?? join7(homedir5(), ".config");
4792
- const globalAgents = readPromptFileIfPresent(join7(configRoot, "opencode", "AGENTS.md"));
5626
+ const configRoot = process.env.XDG_CONFIG_HOME ?? join8(homedir5(), ".config");
5627
+ const globalAgents = readPromptFileIfPresent(join8(configRoot, "opencode", "AGENTS.md"));
4793
5628
  const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
4794
5629
  const forwarded = extraSystemContent.join("\n\n");
4795
5630
  const pushGlobal = !!globalAgents && !forwarded.includes(globalAgents);
@@ -4800,7 +5635,7 @@ ${options.compressionSummary.trim()}`
4800
5635
  if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
4801
5636
  const content = parts.join("\n\n");
4802
5637
  if (!content) return void 0;
4803
- const path9 = join7(tmpdir2(), `opencode-cc-sys-${randomUUID5()}.md`);
5638
+ const path9 = join8(tmpdir2(), `opencode-cc-sys-${randomUUID5()}.md`);
4804
5639
  try {
4805
5640
  writeFileSync4(path9, content, "utf8");
4806
5641
  return path9;
@@ -5528,6 +6363,11 @@ var ClaudeCodeLanguageModel = class {
5528
6363
  { compressEnabled: false, compressionSummary: getCompressionSummary(sk) }
5529
6364
  );
5530
6365
  const { model: spawnModelId, fast: fastMode } = parseModelId(effectiveModelId);
6366
+ const skillPluginDirs = await resolveSkillPluginDirs({
6367
+ cwd,
6368
+ cliPath: this.config.cliPath,
6369
+ enabled: this.config.bridgeOpencodeSkills === true
6370
+ });
5531
6371
  const cliArgs = buildCliArgs({
5532
6372
  sessionKey: sk,
5533
6373
  skipPermissions: this.config.skipPermissions !== false,
@@ -5538,6 +6378,7 @@ var ClaudeCodeLanguageModel = class {
5538
6378
  strictMcpConfig: this.config.strictMcpConfig,
5539
6379
  disallowedTools: this.config.webSearch === "disabled" ? ["WebSearch"] : void 0,
5540
6380
  appendSystemPromptFile: systemPromptFile,
6381
+ pluginDirs: skillPluginDirs,
5541
6382
  ...this.thinkingCliOptions(),
5542
6383
  fastMode,
5543
6384
  cliVersion
@@ -5596,6 +6437,13 @@ var ClaudeCodeLanguageModel = class {
5596
6437
  setClaudeSessionId(sk, msg.session_id);
5597
6438
  }
5598
6439
  reportFastModeState(msg, fastMode);
6440
+ reportSystemInit(msg, {
6441
+ ignoreAnthropicApiKey: this.config.ignoreAnthropicApiKey
6442
+ });
6443
+ }
6444
+ if (msg.type === "rate_limit_event") {
6445
+ reportRateLimitEvent(msg);
6446
+ return;
5599
6447
  }
5600
6448
  if (msg.type === "assistant" && msg.message?.content && !gotPartialEvents) {
5601
6449
  for (const block of msg.message.content) {
@@ -5705,8 +6553,21 @@ ${plan}
5705
6553
  sessionId: msg.session_id,
5706
6554
  costUsd: msg.total_cost_usd,
5707
6555
  durationMs: msg.duration_ms,
5708
- usage: msg.usage
6556
+ durationApiMs: msg.duration_api_ms,
6557
+ numTurns: msg.num_turns,
6558
+ usage: msg.usage,
6559
+ modelUsage: msg.modelUsage,
6560
+ permissionDenials: msg.permission_denials?.map((denial) => ({
6561
+ tool_name: denial.tool_name,
6562
+ tool_use_id: denial.tool_use_id
6563
+ }))
5709
6564
  };
6565
+ log.info("conversation result", {
6566
+ sessionId: msg.session_id,
6567
+ isError: msg.is_error,
6568
+ subtype: msg.subtype,
6569
+ ...turnStatsLogPayload(extractTurnStats(msg))
6570
+ });
5710
6571
  cleanup();
5711
6572
  resolve5({
5712
6573
  ...resultMeta,
@@ -5854,6 +6715,39 @@ ${plan}
5854
6715
  const useInteractive = interactivePref && typeof globalThis.Bun?.Terminal === "function";
5855
6716
  const interactiveBypassRequested = this.config.interactiveBypass ?? flagOn(process.env.CLAUDE_CODE_INTERACTIVE_BYPASS);
5856
6717
  const asideTransportRef = { cliPath, interactive: !!useInteractive };
6718
+ const doctor = !compactionMode && scope !== "no-tools" ? parseDoctorCommand(options.prompt) : null;
6719
+ if (doctor) {
6720
+ const doctorOptions = {
6721
+ cliPath,
6722
+ interactive: !!useInteractive,
6723
+ turnStats: this.config.turnStats === true
6724
+ };
6725
+ const stream2 = new ReadableStream({
6726
+ async start(controller) {
6727
+ controller.enqueue({ type: "stream-start", warnings });
6728
+ try {
6729
+ const text = await buildDoctorReport(doctorOptions);
6730
+ const id = generateId();
6731
+ controller.enqueue({ type: "text-start", id });
6732
+ controller.enqueue({ type: "text-delta", id, delta: text });
6733
+ controller.enqueue({ type: "text-end", id });
6734
+ controller.enqueue({
6735
+ type: "finish",
6736
+ finishReason: toFinishReason("stop"),
6737
+ usage: toUsage({ input_tokens: 0, output_tokens: 0 }),
6738
+ providerMetadata: {
6739
+ "claude-code": { path: "doctor", synthetic: true, usageUnavailable: true }
6740
+ }
6741
+ });
6742
+ } catch (error) {
6743
+ controller.enqueue({ type: "error", error });
6744
+ } finally {
6745
+ controller.close();
6746
+ }
6747
+ }
6748
+ });
6749
+ return { stream: stream2, request: { body: { text: `/${DOCTOR_COMMAND}` } } };
6750
+ }
5857
6751
  const aside = !compactionMode && scope !== "no-tools" ? parseSideQuestion(options.prompt) : null;
5858
6752
  if (aside) {
5859
6753
  const active = getActiveProcess(sk);
@@ -6084,6 +6978,11 @@ ${plan}
6084
6978
  "interactiveBypass ignored: Claude Code prompts for bypassPermissions confirmation in the interactive TUI"
6085
6979
  );
6086
6980
  }
6981
+ const skillPluginDirs = await resolveSkillPluginDirs({
6982
+ cwd,
6983
+ cliPath,
6984
+ enabled: self.config.bridgeOpencodeSkills === true
6985
+ });
6087
6986
  const ap = spawnInteractiveProcess({
6088
6987
  cwd,
6089
6988
  cliPath,
@@ -6091,6 +6990,7 @@ ${plan}
6091
6990
  model: spawnModelId,
6092
6991
  fastMode,
6093
6992
  mcpConfigPaths: mcp.paths,
6993
+ pluginDirs: skillPluginDirs,
6094
6994
  permissionsAllow: allow,
6095
6995
  systemPromptFile,
6096
6996
  ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey,
@@ -6422,6 +7322,7 @@ ${plan}
6422
7322
  if (entries.length === 0) return false;
6423
7323
  endTextBlock();
6424
7324
  watchdogMessage = makeLateProxyResultMessage(entries);
7325
+ if (activeProcess) noteTurnStarted(activeProcess);
6425
7326
  proc.stdin.write(watchdogMessage + "\n");
6426
7327
  for (const { call } of entries) pending.delete(call.toolCallId);
6427
7328
  log.warn("delivering proxy results after interrupted continuation", {
@@ -6442,6 +7343,7 @@ ${plan}
6442
7343
  const skipResultForIds = /* @__PURE__ */ new Set();
6443
7344
  const toolCallsById = /* @__PURE__ */ new Map();
6444
7345
  let resultMeta = {};
7346
+ let resultFailure;
6445
7347
  const drainBuffer = [];
6446
7348
  let drainTimer = null;
6447
7349
  const DRAIN_QUIET_MS = 100;
@@ -6664,11 +7566,12 @@ ${plan}
6664
7566
  }
6665
7567
  controller.enqueue({
6666
7568
  type: "finish",
6667
- finishReason: toFinishReason("stop"),
7569
+ finishReason: resultFailure ? { unified: "error", raw: resultFailure } : toFinishReason("stop"),
6668
7570
  usage: toUsage(msg.usage),
6669
7571
  providerMetadata: {
6670
7572
  "claude-code": {
6671
7573
  ...resultMeta,
7574
+ ...resultFailure ? { resultSubtype: resultFailure } : {},
6672
7575
  ...compactionMode ? { compactionModel: effectiveModelId } : {}
6673
7576
  },
6674
7577
  ...typeof msg.usage?.cache_creation_input_tokens === "number" ? {
@@ -6681,7 +7584,10 @@ ${plan}
6681
7584
  controllerClosed = true;
6682
7585
  cleanupTurn();
6683
7586
  if (!useInteractive && !compactionMode) {
6684
- scheduleIdleProcessEviction(sk, self.config.idleProcessTimeoutMs);
7587
+ scheduleIdleProcessEviction(
7588
+ sk,
7589
+ resolveIdleProcessTimeoutMs(self.config.idleProcessTimeoutMs)
7590
+ );
6685
7591
  }
6686
7592
  try {
6687
7593
  controller.close();
@@ -6720,6 +7626,24 @@ ${plan}
6720
7626
  });
6721
7627
  }
6722
7628
  reportFastModeState(msg, fastMode);
7629
+ reportSystemInit(msg, {
7630
+ ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey
7631
+ });
7632
+ }
7633
+ if (msg.type === "system" && msg.subtype === "compact_boundary") {
7634
+ const note = reportCompactBoundary(msg);
7635
+ if (note) {
7636
+ controller.enqueue({ type: "text-delta", id: startTextBlock(), delta: note });
7637
+ endTextBlock();
7638
+ }
7639
+ }
7640
+ if (msg.type === "rate_limit_event") {
7641
+ const note = reportRateLimitEvent(msg);
7642
+ if (note) {
7643
+ controller.enqueue({ type: "text-delta", id: startTextBlock(), delta: note });
7644
+ endTextBlock();
7645
+ }
7646
+ return;
6723
7647
  }
6724
7648
  if (msg.type === "content_block_start" && msg.content_block && msg.index !== void 0) {
6725
7649
  const block = msg.content_block;
@@ -7158,12 +8082,12 @@ ${plan}
7158
8082
  }
7159
8083
  const claudeSessionId = getClaudeSessionId(sk);
7160
8084
  if (claudeSessionId) {
7161
- const list = applyTaskCreateToolResult(
8085
+ const list2 = applyTaskCreateToolResult(
7162
8086
  claudeSessionId,
7163
8087
  block.tool_use_id,
7164
8088
  resultText
7165
8089
  );
7166
- if (list) {
8090
+ if (list2) {
7167
8091
  const synthId = `todowrite_${block.tool_use_id}`;
7168
8092
  controller.enqueue({
7169
8093
  type: "tool-input-start",
@@ -7176,7 +8100,7 @@ ${plan}
7176
8100
  toolCallId: synthId,
7177
8101
  toolName: "todowrite",
7178
8102
  input: JSON.stringify({
7179
- todos: list.map((t) => ({
8103
+ todos: list2.map((t) => ({
7180
8104
  id: t.id,
7181
8105
  content: t.content,
7182
8106
  status: t.status,
@@ -7190,6 +8114,7 @@ ${plan}
7190
8114
  }
7191
8115
  const toolCall = toolCallsById.get(block.tool_use_id);
7192
8116
  if (toolCall) {
8117
+ const isError = block.is_error === true;
7193
8118
  controller.enqueue({
7194
8119
  type: "tool-result",
7195
8120
  toolCallId: block.tool_use_id,
@@ -7197,14 +8122,16 @@ ${plan}
7197
8122
  result: {
7198
8123
  output: resultText,
7199
8124
  title: toolCall.name,
7200
- metadata: {}
8125
+ metadata: isError ? { error: true } : {}
7201
8126
  },
8127
+ ...isError ? { isError: true } : {},
7202
8128
  providerExecuted: true
7203
8129
  });
7204
8130
  noteToolActivity();
7205
8131
  log.info("tool result emitted", {
7206
8132
  toolUseId: block.tool_use_id,
7207
- name: toolCall.name
8133
+ name: toolCall.name,
8134
+ isError
7208
8135
  });
7209
8136
  toolCallsById.delete(block.tool_use_id);
7210
8137
  }
@@ -7227,18 +8154,49 @@ ${plan}
7227
8154
  delta: msg.result
7228
8155
  });
7229
8156
  }
8157
+ const failure = describeResultFailure(msg);
8158
+ if (failure) {
8159
+ resultFailure = msg.subtype;
8160
+ controller.enqueue({
8161
+ type: "text-delta",
8162
+ id: startTextBlock(),
8163
+ delta: formatResultFailureNote(failure)
8164
+ });
8165
+ log.warn(failure, { sessionKey: sk, subtype: msg.subtype });
8166
+ }
8167
+ const turnStats = extractTurnStats(msg);
7230
8168
  resultMeta = {
7231
8169
  sessionId: msg.session_id,
7232
8170
  costUsd: msg.total_cost_usd,
7233
8171
  durationMs: msg.duration_ms,
7234
- usage: msg.usage
8172
+ durationApiMs: msg.duration_api_ms,
8173
+ numTurns: msg.num_turns,
8174
+ usage: msg.usage,
8175
+ modelUsage: msg.modelUsage,
8176
+ // Names and ids only: a denial's `tool_input` can be a whole
8177
+ // file write payload and has no business in metadata.
8178
+ permissionDenials: msg.permission_denials?.map((denial) => ({
8179
+ tool_name: denial.tool_name,
8180
+ tool_use_id: denial.tool_use_id
8181
+ }))
7235
8182
  };
7236
8183
  log.info("conversation result", {
7237
8184
  sessionId: msg.session_id,
7238
- durationMs: msg.duration_ms,
7239
8185
  numTurns: msg.num_turns,
7240
- isError: msg.is_error
8186
+ isError: msg.is_error,
8187
+ subtype: msg.subtype,
8188
+ ...turnStatsLogPayload(turnStats)
7241
8189
  });
8190
+ if (self.config.turnStats && !compactionMode && !msg.is_error && !failure) {
8191
+ const footer = formatTurnStatsBlock(turnStats);
8192
+ if (footer) {
8193
+ controller.enqueue({
8194
+ type: "text-delta",
8195
+ id: startTextBlock(),
8196
+ delta: footer
8197
+ });
8198
+ }
8199
+ }
7242
8200
  turnCompleted = true;
7243
8201
  endTextBlock();
7244
8202
  const shouldDeferResult = !msg.is_error && !autoContinueState.aborted && !autoContinueState.sawAskUserQuestion;
@@ -7290,24 +8248,59 @@ ${plan}
7290
8248
  );
7291
8249
  drainBuffer.length = 0;
7292
8250
  }
8251
+ const crashed = !turnCompleted && !autoContinueState.aborted;
7293
8252
  controllerClosed = true;
7294
8253
  cleanupTurn();
7295
8254
  endTextBlock();
7296
- controller.enqueue({
7297
- type: "finish",
7298
- finishReason: toFinishReason("stop"),
7299
- usage: toUsage(),
7300
- providerMetadata: {
7301
- "claude-code": {
7302
- ...resultMeta,
7303
- ...compactionMode ? { compactionModel: effectiveModelId } : {}
8255
+ const finishClose = (exitCode, signal) => {
8256
+ if (crashed) {
8257
+ log.warn("claude process closed without a result", {
8258
+ sessionKey: sk,
8259
+ exitCode,
8260
+ signal,
8261
+ stderrBytes: activeProcess?.lastStderr?.length ?? 0
8262
+ });
8263
+ controller.enqueue({
8264
+ type: "error",
8265
+ error: new Error(
8266
+ describeChildCrash(exitCode, signal, activeProcess?.lastStderr)
8267
+ )
8268
+ });
8269
+ }
8270
+ controller.enqueue({
8271
+ type: "finish",
8272
+ finishReason: toFinishReason(crashed ? "error" : "stop"),
8273
+ usage: toUsage(),
8274
+ providerMetadata: {
8275
+ "claude-code": {
8276
+ ...resultMeta,
8277
+ ...compactionMode ? { compactionModel: effectiveModelId } : {}
8278
+ }
7304
8279
  }
8280
+ });
8281
+ try {
8282
+ controller.close();
8283
+ } catch {
7305
8284
  }
7306
- });
7307
- try {
7308
- controller.close();
7309
- } catch {
8285
+ };
8286
+ if (crashed && proc.exitCode === null && proc.signalCode === null) {
8287
+ let reported = false;
8288
+ const report = (exitCode, signal) => {
8289
+ if (reported) return;
8290
+ reported = true;
8291
+ clearTimeout(exitGrace);
8292
+ proc.off("exit", onExit);
8293
+ finishClose(exitCode, signal);
8294
+ };
8295
+ const onExit = (code, signal) => report(code, signal);
8296
+ const exitGrace = setTimeout(
8297
+ () => report(proc.exitCode, proc.signalCode),
8298
+ CHILD_EXIT_STATUS_GRACE_MS
8299
+ );
8300
+ proc.once("exit", onExit);
8301
+ return;
7310
8302
  }
8303
+ finishClose(proc.exitCode, proc.signalCode);
7311
8304
  };
7312
8305
  let cleanedUp = false;
7313
8306
  const cleanupTurn = () => {
@@ -7389,7 +8382,7 @@ ${plan}
7389
8382
  if (outer.session_id) setClaudeSessionId(sk, outer.session_id);
7390
8383
  if (msg.is_error && msg.result) text = msg.result;
7391
8384
  }
7392
- if (text) controller.enqueue({ type: "text-delta", id: startTextBlock(), delta: text });
8385
+ if (text) controller.enqueue({ type: "text-delta", id: currentTextId ?? startTextBlock(), delta: text });
7393
8386
  } catch {
7394
8387
  }
7395
8388
  }
@@ -7446,9 +8439,25 @@ ${plan}
7446
8439
  });
7447
8440
  proc.on("error", procErrorHandler);
7448
8441
  if (options.abortSignal) {
8442
+ const releaseAbandonedProxyCalls = (reason) => {
8443
+ if (drainBuffer.length === 0 && getPendingProxyCalls(sk).length === 0) return;
8444
+ rejectAllPendingProxyCallsForSession(sk, new Error(reason));
8445
+ drainBuffer.length = 0;
8446
+ };
7449
8447
  options.abortSignal.addEventListener("abort", () => {
7450
8448
  autoContinueState.aborted = true;
7451
- if (turnCompleted || controllerClosed) return;
8449
+ if (turnCompleted || controllerClosed) {
8450
+ if (activeProcess && activeProcess.lineEmitter.listenerCount("line") === 0 && getPendingProxyCalls(sk).length > 0) {
8451
+ log.info("abort between proxy tool boundaries; releasing pending calls", { sk });
8452
+ void interruptTurn(activeProcess).then((idle) => {
8453
+ log.info("interrupt sent for aborted turn", { sk, idle });
8454
+ });
8455
+ releaseAbandonedProxyCalls(
8456
+ "Provider stream was aborted while opencode was running its proxy tool calls"
8457
+ );
8458
+ }
8459
+ return;
8460
+ }
7452
8461
  if (activeProcess) {
7453
8462
  void interruptTurn(activeProcess).then((idle) => {
7454
8463
  log.info("interrupt sent for aborted turn", { sk, idle });
@@ -7459,15 +8468,9 @@ ${plan}
7459
8468
  "abort signal received before content, closing stream immediately",
7460
8469
  { cwd }
7461
8470
  );
7462
- if (drainBuffer.length > 0 || getPendingProxyCalls(sk).length > 0) {
7463
- rejectAllPendingProxyCallsForSession(
7464
- sk,
7465
- new Error(
7466
- "Provider stream was aborted before pending proxy calls were emitted"
7467
- )
7468
- );
7469
- drainBuffer.length = 0;
7470
- }
8471
+ releaseAbandonedProxyCalls(
8472
+ "Provider stream was aborted before pending proxy calls were emitted"
8473
+ );
7471
8474
  controllerClosed = true;
7472
8475
  cleanupTurn();
7473
8476
  try {
@@ -7480,6 +8483,9 @@ ${plan}
7480
8483
  "abort signal received mid-turn, starting grace period",
7481
8484
  { cwd }
7482
8485
  );
8486
+ releaseAbandonedProxyCalls(
8487
+ "Provider stream was aborted while proxy tool calls were pending"
8488
+ );
7483
8489
  startResultFallback(5e3);
7484
8490
  });
7485
8491
  }
@@ -7573,7 +8579,7 @@ ${plan}
7573
8579
 
7574
8580
  // src/accounts.ts
7575
8581
  import { chmod, lstat, mkdir, readlink, symlink, writeFile } from "fs/promises";
7576
- import path7 from "path";
8582
+ import path8 from "path";
7577
8583
  var BASE_PROVIDER_ID = "claude-code";
7578
8584
  var DEFAULT_ACCOUNT = "default";
7579
8585
  var SHARED_CAPABILITY_ITEMS = [
@@ -7611,7 +8617,7 @@ function expandHome(value) {
7611
8617
  const home = process.env.HOME ?? process.env.USERPROFILE;
7612
8618
  if (value === "~") return home ?? value;
7613
8619
  if (value.startsWith("~/") || value.startsWith("~\\")) {
7614
- return home ? path7.join(home, value.slice(2)) : value;
8620
+ return home ? path8.join(home, value.slice(2)) : value;
7615
8621
  }
7616
8622
  return value;
7617
8623
  }
@@ -7643,8 +8649,8 @@ async function ensureSharedCapabilities(targetRoot) {
7643
8649
  }
7644
8650
  }
7645
8651
  async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
7646
- const source = path7.join(sourceRoot, item);
7647
- const target = path7.join(targetRoot, item);
8652
+ const source = path8.join(sourceRoot, item);
8653
+ const target = path8.join(targetRoot, item);
7648
8654
  let sourceStat;
7649
8655
  try {
7650
8656
  sourceStat = await lstat(source);
@@ -7655,8 +8661,8 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
7655
8661
  const targetStat = await lstat(target);
7656
8662
  if (targetStat.isSymbolicLink()) {
7657
8663
  const current = await readlink(target);
7658
- const resolvedCurrent = path7.resolve(path7.dirname(target), current);
7659
- const resolvedSource = path7.resolve(source);
8664
+ const resolvedCurrent = path8.resolve(path8.dirname(target), current);
8665
+ const resolvedSource = path8.resolve(source);
7660
8666
  if (resolvedCurrent === resolvedSource) return;
7661
8667
  }
7662
8668
  log.warn("shared Claude capability already exists; leaving untouched", {
@@ -7671,11 +8677,11 @@ async function ensureSharedCapabilityItem(sourceRoot, targetRoot, item) {
7671
8677
  await symlink(source, target, type);
7672
8678
  }
7673
8679
  async function writeAccountWrapper(account, baseCliPath, configDir) {
7674
- const cacheRoot = path7.join(
8680
+ const cacheRoot = path8.join(
7675
8681
  process.env.XDG_CACHE_HOME ?? expandHome("~/.cache"),
7676
8682
  "opencode-claude-code-plugin"
7677
8683
  );
7678
- const wrapperPath = path7.join(cacheRoot, `claude-${account}`);
8684
+ const wrapperPath = path8.join(cacheRoot, `claude-${account}`);
7679
8685
  const suffix = `@${account}`;
7680
8686
  await mkdir(cacheRoot, { recursive: true });
7681
8687
  const script = `#!/usr/bin/env bash
@@ -7716,34 +8722,34 @@ function titleizeAccount(account) {
7716
8722
  // src/cleanup-stale.ts
7717
8723
  import {
7718
8724
  existsSync as existsSync4,
7719
- readFileSync as readFileSync4,
8725
+ readFileSync as readFileSync5,
7720
8726
  realpathSync,
7721
8727
  rmSync as rmSync3,
7722
8728
  writeFileSync as writeFileSync5
7723
8729
  } from "fs";
7724
8730
  import { homedir as homedir6 } from "os";
7725
- import { join as join8, resolve as resolve4 } from "path";
7726
- import { fileURLToPath as fileURLToPath2 } from "url";
8731
+ import { join as join9, resolve as resolve4 } from "path";
8732
+ import { fileURLToPath as fileURLToPath3 } from "url";
7727
8733
  var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
7728
8734
  var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
7729
8735
  var alreadyRan = false;
7730
8736
  function candidateCacheRoots() {
7731
8737
  const xdg = process.env.XDG_CACHE_HOME;
7732
8738
  return [
7733
- xdg ? join8(xdg, "opencode") : null,
7734
- join8(homedir6(), ".cache", "opencode"),
7735
- join8(homedir6(), "Library", "Caches", "opencode")
8739
+ xdg ? join9(xdg, "opencode") : null,
8740
+ join9(homedir6(), ".cache", "opencode"),
8741
+ join9(homedir6(), "Library", "Caches", "opencode")
7736
8742
  ].filter((p) => Boolean(p));
7737
8743
  }
7738
8744
  function userOpencodeJsonPath() {
7739
- const xdgConfig = process.env.XDG_CONFIG_HOME ?? join8(homedir6(), ".config");
7740
- return join8(xdgConfig, "opencode", "opencode.json");
8745
+ const xdgConfig = process.env.XDG_CONFIG_HOME ?? join9(homedir6(), ".config");
8746
+ return join9(xdgConfig, "opencode", "opencode.json");
7741
8747
  }
7742
8748
  function userIntendsToUseUnscoped() {
7743
8749
  const cfg = userOpencodeJsonPath();
7744
8750
  if (!existsSync4(cfg)) return false;
7745
8751
  try {
7746
- const json = JSON.parse(readFileSync4(cfg, "utf8"));
8752
+ const json = JSON.parse(readFileSync5(cfg, "utf8"));
7747
8753
  const plugins = json.plugin;
7748
8754
  if (!Array.isArray(plugins)) return false;
7749
8755
  return plugins.some(
@@ -7755,7 +8761,7 @@ function userIntendsToUseUnscoped() {
7755
8761
  }
7756
8762
  function ourLoadedDir() {
7757
8763
  try {
7758
- const filePath = fileURLToPath2(import.meta.url);
8764
+ const filePath = fileURLToPath3(import.meta.url);
7759
8765
  return realpathSync(resolve4(filePath, "..", ".."));
7760
8766
  } catch {
7761
8767
  return null;
@@ -7780,7 +8786,7 @@ function cleanupStaleUnscopedInstall() {
7780
8786
  }
7781
8787
  function cleanupOne(cacheRoot, ourDir) {
7782
8788
  if (!existsSync4(cacheRoot)) return;
7783
- const stalePath = join8(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
8789
+ const stalePath = join9(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
7784
8790
  if (!existsSync4(stalePath)) return;
7785
8791
  let realStalePath = stalePath;
7786
8792
  try {
@@ -7788,11 +8794,11 @@ function cleanupOne(cacheRoot, ourDir) {
7788
8794
  } catch {
7789
8795
  }
7790
8796
  if (ourDir && realStalePath === ourDir) return;
7791
- const pkgJsonPath = join8(stalePath, "package.json");
8797
+ const pkgJsonPath = join9(stalePath, "package.json");
7792
8798
  if (!existsSync4(pkgJsonPath)) return;
7793
8799
  let pkg = {};
7794
8800
  try {
7795
- pkg = JSON.parse(readFileSync4(pkgJsonPath, "utf8"));
8801
+ pkg = JSON.parse(readFileSync5(pkgJsonPath, "utf8"));
7796
8802
  } catch {
7797
8803
  return;
7798
8804
  }
@@ -7808,10 +8814,10 @@ function cleanupOne(cacheRoot, ourDir) {
7808
8814
  });
7809
8815
  return;
7810
8816
  }
7811
- const cachePkgJson = join8(cacheRoot, "package.json");
8817
+ const cachePkgJson = join9(cacheRoot, "package.json");
7812
8818
  if (!existsSync4(cachePkgJson)) return;
7813
8819
  try {
7814
- const cfg = JSON.parse(readFileSync4(cachePkgJson, "utf8"));
8820
+ const cfg = JSON.parse(readFileSync5(cachePkgJson, "utf8"));
7815
8821
  if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
7816
8822
  delete cfg.dependencies[STALE_PACKAGE_NAME];
7817
8823
  writeFileSync5(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
@@ -7824,131 +8830,6 @@ function cleanupOne(cacheRoot, ourDir) {
7824
8830
  }
7825
8831
  }
7826
8832
 
7827
- // src/startup-diagnostics.ts
7828
- import { execFile as execFile2 } from "child_process";
7829
- import * as fs6 from "fs";
7830
- import * as path8 from "path";
7831
- import { promisify as promisify2 } from "util";
7832
- import { fileURLToPath as fileURLToPath3 } from "url";
7833
- var cachedPluginVersion;
7834
- function pluginVersion() {
7835
- if (cachedPluginVersion) return cachedPluginVersion;
7836
- try {
7837
- const here = path8.dirname(fileURLToPath3(import.meta.url));
7838
- const raw = fs6.readFileSync(path8.join(here, "..", "package.json"), "utf8");
7839
- const version = JSON.parse(raw).version;
7840
- cachedPluginVersion = typeof version === "string" ? version : "unknown";
7841
- } catch {
7842
- cachedPluginVersion = "unknown";
7843
- }
7844
- return cachedPluginVersion;
7845
- }
7846
- function pickOpencodeVersion(input) {
7847
- if (!input || typeof input !== "object") return void 0;
7848
- const app = input.app;
7849
- if (app && typeof app === "object") {
7850
- const version = app.version;
7851
- if (typeof version === "string" && version.length > 0) return version;
7852
- }
7853
- const direct = input.version;
7854
- if (typeof direct === "string" && direct.length > 0) return direct;
7855
- return void 0;
7856
- }
7857
- var execFileAsync2 = promisify2(execFile2);
7858
- var opencodeVersionProbe;
7859
- function detectOpencodeVersion(execPath = process.execPath) {
7860
- if (opencodeVersionProbe) return opencodeVersionProbe;
7861
- opencodeVersionProbe = (async () => {
7862
- if (!path8.basename(execPath).toLowerCase().includes("opencode")) {
7863
- log.debug("skipping opencode version probe: execPath is not opencode", { execPath });
7864
- return void 0;
7865
- }
7866
- try {
7867
- const { stdout } = await execFileAsync2(execPath, ["--version"], { timeout: 5e3 });
7868
- const match = /\d+\.\d+\.\d+\S*/.exec(stdout.trim());
7869
- return match ? match[0] : void 0;
7870
- } catch (err) {
7871
- log.debug("opencode version probe failed", {
7872
- execPath,
7873
- error: err instanceof Error ? err.message : String(err)
7874
- });
7875
- return void 0;
7876
- }
7877
- })();
7878
- return opencodeVersionProbe;
7879
- }
7880
- function describeSpawnCwd(configured, live = process.cwd(), captured = getOpencodeProjectDirectory()) {
7881
- if (typeof configured === "string" && configured.length > 0) {
7882
- return { resolved: configured, source: "configured" };
7883
- }
7884
- if (isUsableDirectory(live)) return { resolved: live, source: "process" };
7885
- if (isUsableDirectory(captured)) return { resolved: captured, source: "captured" };
7886
- return { resolved: live, source: "unresolved" };
7887
- }
7888
- function stringList(value) {
7889
- if (!Array.isArray(value)) return [];
7890
- return value.filter((entry) => typeof entry === "string");
7891
- }
7892
- function firstOption(providers, key) {
7893
- for (const entry of Object.values(providers)) {
7894
- const value = entry?.options?.[key];
7895
- if (value !== void 0) return value;
7896
- }
7897
- return void 0;
7898
- }
7899
- function collectStartupDiagnostics(providers, opencodeVersion) {
7900
- const accounts = [];
7901
- for (const entry of Object.values(providers)) {
7902
- const account = entry?.options?.account;
7903
- if (typeof account === "string" && account.length > 0) accounts.push(account);
7904
- }
7905
- const cwd = describeSpawnCwd(firstOption(providers, "cwd"));
7906
- let mcpServers = [];
7907
- try {
7908
- mcpServers = mergeOpencodeMcp(cwd.resolved).enabledServerNames;
7909
- } catch (err) {
7910
- log.debug("startup diagnostics could not read MCP config", {
7911
- error: err instanceof Error ? err.message : String(err)
7912
- });
7913
- }
7914
- return {
7915
- plugin: pluginVersion(),
7916
- opencode: opencodeVersion ?? process.env.OPENCODE_VERSION ?? "unknown",
7917
- claudeCliPath: String(firstOption(providers, "cliPath") ?? "claude"),
7918
- cwd,
7919
- providers: Object.keys(providers),
7920
- accounts,
7921
- proxyTools: stringList(firstOption(providers, "proxyTools")),
7922
- mcpServers,
7923
- interactiveTransport: firstOption(providers, "interactive") === true || process.env.CLAUDE_CODE_INTERACTIVE_TRANSPORT === "1",
7924
- planModeQuestion: firstOption(providers, "planModeQuestion") === true,
7925
- anthropicApiKeyInEnv: Boolean(
7926
- process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN
7927
- )
7928
- };
7929
- }
7930
- var logged = false;
7931
- function logStartupDiagnostics(providers, opencodeVersion) {
7932
- if (logged) return;
7933
- logged = true;
7934
- void (async () => {
7935
- try {
7936
- const version = opencodeVersion ?? process.env.OPENCODE_VERSION ?? await detectOpencodeVersion();
7937
- const { claudeCliPath, ...rest } = collectStartupDiagnostics(providers, version);
7938
- const cli = await detectCliVersion(claudeCliPath);
7939
- const diagnostics = {
7940
- ...rest,
7941
- claudeCli: { path: claudeCliPath, version: cli?.raw ?? "not detected" }
7942
- };
7943
- log.notice("claude-code plugin ready", { ...diagnostics });
7944
- } catch (err) {
7945
- log.debug("startup diagnostics failed", {
7946
- error: err instanceof Error ? err.message : String(err)
7947
- });
7948
- }
7949
- })();
7950
- }
7951
-
7952
8833
  // src/index.ts
7953
8834
  function pickOpencodeDirectory(input) {
7954
8835
  if (!input || typeof input !== "object") return void 0;
@@ -7975,6 +8856,15 @@ function registerSideQuestionCommand(config) {
7975
8856
  };
7976
8857
  return true;
7977
8858
  }
8859
+ function registerDoctorCommand(config) {
8860
+ config.command ??= {};
8861
+ if (config.command[DOCTOR_COMMAND]) return false;
8862
+ config.command[DOCTOR_COMMAND] = {
8863
+ template: `/${DOCTOR_COMMAND} $ARGUMENTS`,
8864
+ description: "Report what the Claude Code plugin sees: versions, cwd, live processes, pending proxy calls"
8865
+ };
8866
+ return true;
8867
+ }
7978
8868
  var ownsSideQuestionCommand = false;
7979
8869
  function warnIfAnthropicApiKey(ignore) {
7980
8870
  if (warnedAnthropicApiKey) return;
@@ -8045,6 +8935,7 @@ function createClaudeCode(settings = {}) {
8045
8935
  ignoreAnthropicApiKey: settings.ignoreAnthropicApiKey,
8046
8936
  idleProcessTimeoutMs: settings.idleProcessTimeoutMs,
8047
8937
  bridgeOpencodeSkills: settings.bridgeOpencodeSkills === true,
8938
+ turnStats: settings.turnStats === true,
8048
8939
  interactive: settings.interactive,
8049
8940
  interactiveBypass: settings.interactiveBypass,
8050
8941
  interactiveAllowTools: settings.interactiveAllowTools,
@@ -8234,8 +9125,16 @@ async function buildAgentRegistry(config) {
8234
9125
  defaultSubagentModel: getDefaultSubagentModel()
8235
9126
  });
8236
9127
  }
9128
+ function extractDeletedSessionId(event) {
9129
+ const payload = event?.payload ?? event;
9130
+ if (!payload || payload.type !== "session.deleted") return void 0;
9131
+ const properties = payload.properties;
9132
+ const id = properties?.info?.id;
9133
+ return typeof id === "string" && id.length > 0 ? id : void 0;
9134
+ }
8237
9135
  var server = async (input) => {
8238
9136
  cleanupStaleUnscopedInstall();
9137
+ ensureProcessExitCleanup();
8239
9138
  const opencodeVersion = pickOpencodeVersion(input);
8240
9139
  if (input && typeof input === "object" && "client" in input) {
8241
9140
  setOpencodeClient(input.client);
@@ -8244,6 +9143,7 @@ var server = async (input) => {
8244
9143
  return {
8245
9144
  config: async (config) => {
8246
9145
  if (registerSideQuestionCommand(config)) ownsSideQuestionCommand = true;
9146
+ registerDoctorCommand(config);
8247
9147
  registerBundledSkillPath(config);
8248
9148
  config.provider ??= {};
8249
9149
  await buildAgentRegistry(config);
@@ -8269,10 +9169,21 @@ var server = async (input) => {
8269
9169
  opencodeVersion
8270
9170
  );
8271
9171
  },
8272
- // No `event` hook: MCP config drift is detected at turn start by the
8273
- // hot-reload check in `claude-code-language-model.ts`, which respawns
8274
- // claude safely between turns. Eviction on `global.disposed` would kill
8275
- // an in-flight stream and abort the user's current turn.
9172
+ // Only `session.deleted` is acted on. MCP config drift is still detected
9173
+ // at turn start by the hot-reload check in `claude-code-language-model.ts`,
9174
+ // which respawns claude safely between turns, and eviction on
9175
+ // `global.disposed` would kill an in-flight stream and abort the user's
9176
+ // current turn. A deleted session has no turn left to abort, and its
9177
+ // `claude` child would otherwise linger until the idle timer or LRU
9178
+ // pressure took it, with its session id kept for a resume that never comes.
9179
+ event: async ({ event }) => {
9180
+ const sessionID = extractDeletedSessionId(event);
9181
+ if (!sessionID) return;
9182
+ const released = deleteActiveProcessesForSession(sessionID);
9183
+ if (released.length > 0) {
9184
+ log.info("released claude state for deleted session", { sessionID, released });
9185
+ }
9186
+ },
8276
9187
  provider: {
8277
9188
  id: PROVIDER_ID2,
8278
9189
  models: async (provider) => defaultModelsForProvider(provider.models)
@@ -8327,8 +9238,10 @@ export {
8327
9238
  createClaudeCode,
8328
9239
  index_default as default,
8329
9240
  defaultModels,
9241
+ extractDeletedSessionId,
8330
9242
  getAgentRegistry,
8331
9243
  getDefaultSubagentModel,
9244
+ registerDoctorCommand,
8332
9245
  registerSideQuestionCommand,
8333
9246
  resolveAgentModel,
8334
9247
  warnIfPlanModeCannotExit