@khalilgharbaoui/opencode-claude-code-plugin 0.20.0 → 0.22.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
@@ -1248,6 +1248,102 @@ function resolveDisallowedTools(options) {
1248
1248
  if (options.disableWebSearch) push("WebSearch");
1249
1249
  return out;
1250
1250
  }
1251
+ function resolveProxyOpencodeToolDefs(options) {
1252
+ const requested = options.requested ?? [];
1253
+ if (requested.length === 0) return [];
1254
+ const items = options.items;
1255
+ if (!items) {
1256
+ log.warn(
1257
+ "proxyOpencodeTools is set but opencode's tool registry did not answer; forwarding nothing this spawn",
1258
+ { requested: requested.map(String) }
1259
+ );
1260
+ return [];
1261
+ }
1262
+ const byLowerId = /* @__PURE__ */ new Map();
1263
+ for (const item of items) {
1264
+ const key = item.id.toLowerCase();
1265
+ if (!byLowerId.has(key)) byLowerId.set(key, item);
1266
+ }
1267
+ const taken = options.taken ?? /* @__PURE__ */ new Set();
1268
+ const out = [];
1269
+ const seen = /* @__PURE__ */ new Set();
1270
+ const unknown = [];
1271
+ const collided = [];
1272
+ for (const raw of requested) {
1273
+ const name = String(raw).trim();
1274
+ if (!name) continue;
1275
+ const item = byLowerId.get(name.toLowerCase());
1276
+ if (!item) {
1277
+ unknown.push(name);
1278
+ continue;
1279
+ }
1280
+ if (taken.has(item.id)) {
1281
+ collided.push(item.id);
1282
+ continue;
1283
+ }
1284
+ if (seen.has(item.id)) continue;
1285
+ seen.add(item.id);
1286
+ out.push({
1287
+ name: item.id,
1288
+ description: typeof item.description === "string" ? item.description : "",
1289
+ inputSchema: item.parameters && typeof item.parameters === "object" ? item.parameters : { type: "object", properties: {} }
1290
+ });
1291
+ }
1292
+ if (unknown.length > 0) {
1293
+ log.warn("ignoring unknown proxyOpencodeTools entries", {
1294
+ unknown,
1295
+ known: [...byLowerId.values()].map((item) => item.id).join(", ")
1296
+ });
1297
+ }
1298
+ if (collided.length > 0) {
1299
+ log.warn(
1300
+ "proxyOpencodeTools entry dropped: a proxy tool already holds that name, and it keeps it",
1301
+ { collided }
1302
+ );
1303
+ }
1304
+ return out;
1305
+ }
1306
+ function resolveMcpProxyToolDefs(options) {
1307
+ const empty = { defs: [], coveredServers: /* @__PURE__ */ new Set() };
1308
+ const serverNames = options.serverNames ?? [];
1309
+ if (serverNames.length === 0) return empty;
1310
+ const tools = options.tools;
1311
+ if (!tools || tools.length === 0) return empty;
1312
+ const serversByLengthDesc = [...serverNames].sort((a, b) => b.length - a.length);
1313
+ const taken = options.taken ?? /* @__PURE__ */ new Set();
1314
+ const defs = [];
1315
+ const coveredServers = /* @__PURE__ */ new Set();
1316
+ const seen = /* @__PURE__ */ new Set();
1317
+ const collided = [];
1318
+ for (const tool of tools) {
1319
+ if (tool?.type !== void 0 && tool.type !== "function") continue;
1320
+ const name = typeof tool?.name === "string" ? tool.name.trim() : "";
1321
+ if (!name) continue;
1322
+ const matchedServer = serversByLengthDesc.find(
1323
+ (server2) => name === server2 || name.startsWith(`${server2}_`)
1324
+ );
1325
+ if (!matchedServer) continue;
1326
+ if (seen.has(name)) continue;
1327
+ if (taken.has(name)) {
1328
+ collided.push(name);
1329
+ continue;
1330
+ }
1331
+ seen.add(name);
1332
+ coveredServers.add(matchedServer);
1333
+ defs.push({
1334
+ name,
1335
+ description: typeof tool.description === "string" ? tool.description : "",
1336
+ inputSchema: tool.inputSchema && typeof tool.inputSchema === "object" ? tool.inputSchema : { type: "object", properties: {} }
1337
+ });
1338
+ }
1339
+ if (collided.length > 0) {
1340
+ log.warn(
1341
+ "MCP tool not routed through the proxy: another proxy tool already holds that name, and it keeps it",
1342
+ { collided }
1343
+ );
1344
+ }
1345
+ return { defs, coveredServers };
1346
+ }
1251
1347
  function readBody(req) {
1252
1348
  return new Promise((resolve5, reject) => {
1253
1349
  const chunks = [];
@@ -1336,6 +1432,14 @@ function writeJson(res, body) {
1336
1432
 
1337
1433
  // src/proxy-broker.ts
1338
1434
  import { EventEmitter as EventEmitter2 } from "events";
1435
+ var PROXY_STALL_WARNING_MS = 5 * 6e4;
1436
+ var PROXY_DEADLINE_WARNING_FRACTION = 0.6;
1437
+ var PROXY_DEADLINE_WARNING_MIN_MS = 6e4;
1438
+ function clearPendingTimers(pending) {
1439
+ if (pending.timer) clearTimeout(pending.timer);
1440
+ if (pending.stallTimer) clearInterval(pending.stallTimer);
1441
+ if (pending.deadlineWarnTimer) clearTimeout(pending.deadlineWarnTimer);
1442
+ }
1339
1443
  var pendingByCallId = /* @__PURE__ */ new Map();
1340
1444
  var callIdsBySession = /* @__PURE__ */ new Map();
1341
1445
  var emitter = new EventEmitter2();
@@ -1361,10 +1465,10 @@ function onPendingProxyCall(sessionKey2, handler) {
1361
1465
  emitter.on(name, handler);
1362
1466
  return () => emitter.off(name, handler);
1363
1467
  }
1364
- function queuePendingProxyCall(sessionKey2, call, timeoutOverrides) {
1468
+ function queuePendingProxyCall(sessionKey2, call, timeoutOverrides, stallWarningMs = PROXY_STALL_WARNING_MS, deadlineWarningMinMs = PROXY_DEADLINE_WARNING_MIN_MS) {
1365
1469
  const previous = pendingByCallId.get(call.id);
1366
1470
  if (previous) {
1367
- if (previous.timer) clearTimeout(previous.timer);
1471
+ clearPendingTimers(previous);
1368
1472
  previous.reject(
1369
1473
  new Error(`Replaced pending proxy call ${call.id} with a fresh one`)
1370
1474
  );
@@ -1381,6 +1485,7 @@ function queuePendingProxyCall(sessionKey2, call, timeoutOverrides) {
1381
1485
  if (!current) return;
1382
1486
  pendingByCallId.delete(call.id);
1383
1487
  indexRemove(current.sessionKey, call.id);
1488
+ clearPendingTimers(current);
1384
1489
  current.reject(buildProxyTimeoutError(call.toolName, deadlineMs));
1385
1490
  log.notice("timed out pending proxy call", {
1386
1491
  sessionKey: current.sessionKey,
@@ -1389,6 +1494,38 @@ function queuePendingProxyCall(sessionKey2, call, timeoutOverrides) {
1389
1494
  deadlineMs
1390
1495
  });
1391
1496
  }, deadlineMs) : null;
1497
+ const stallTimer = deadlineMs === PROXY_NO_DEADLINE_MS && stallWarningMs > 0 ? setInterval(() => {
1498
+ const current = pendingByCallId.get(call.id);
1499
+ if (!current) return;
1500
+ log.warn("proxy call still waiting, no deadline", {
1501
+ sessionKey: current.sessionKey,
1502
+ toolCallId: current.toolCallId,
1503
+ toolName: current.toolName,
1504
+ waitedMs: Date.now() - current.createdAt,
1505
+ emitted: current.emitted === true,
1506
+ channelClosed: current.channel?.closed === true,
1507
+ note: "nothing will time this out; it ends when opencode returns a result, you abort, you send another message, or the claude process goes"
1508
+ });
1509
+ }, stallWarningMs) : null;
1510
+ stallTimer?.unref?.();
1511
+ const warnAtMs = Math.floor(deadlineMs * PROXY_DEADLINE_WARNING_FRACTION);
1512
+ const deadlineWarnTimer = deadlineMs >= deadlineWarningMinMs && deadlineMs > 0 && warnAtMs > 0 ? setTimeout(() => {
1513
+ const current = pendingByCallId.get(call.id);
1514
+ if (!current) return;
1515
+ const waitedMs = Date.now() - current.createdAt;
1516
+ log.warn("proxy call still waiting, deadline approaching", {
1517
+ sessionKey: current.sessionKey,
1518
+ toolCallId: current.toolCallId,
1519
+ toolName: current.toolName,
1520
+ waitedMs,
1521
+ deadlineMs,
1522
+ remainingMs: Math.max(0, deadlineMs - waitedMs),
1523
+ emitted: current.emitted === true,
1524
+ channelClosed: current.channel?.closed === true,
1525
+ note: "it will be rejected when the deadline passes; raise this tool's proxyToolTimeoutMs if the work is legitimately this long"
1526
+ });
1527
+ }, warnAtMs) : null;
1528
+ deadlineWarnTimer?.unref?.();
1392
1529
  const pending = {
1393
1530
  sessionKey: sessionKey2,
1394
1531
  toolCallId: call.id,
@@ -1398,6 +1535,8 @@ function queuePendingProxyCall(sessionKey2, call, timeoutOverrides) {
1398
1535
  createdAt: Date.now(),
1399
1536
  deadlineMs,
1400
1537
  timer,
1538
+ stallTimer,
1539
+ deadlineWarnTimer,
1401
1540
  resolve: call.resolve,
1402
1541
  reject: call.reject
1403
1542
  };
@@ -1448,7 +1587,7 @@ function resolvePendingProxyCallById(toolCallId, result) {
1448
1587
  if (!pending) return false;
1449
1588
  pendingByCallId.delete(toolCallId);
1450
1589
  indexRemove(pending.sessionKey, toolCallId);
1451
- if (pending.timer) clearTimeout(pending.timer);
1590
+ clearPendingTimers(pending);
1452
1591
  pending.resolve(result);
1453
1592
  log.info("resolved pending proxy call", {
1454
1593
  sessionKey: pending.sessionKey,
@@ -1462,7 +1601,7 @@ function rejectPendingProxyCallById(toolCallId, error) {
1462
1601
  if (!pending) return false;
1463
1602
  pendingByCallId.delete(toolCallId);
1464
1603
  indexRemove(pending.sessionKey, toolCallId);
1465
- if (pending.timer) clearTimeout(pending.timer);
1604
+ clearPendingTimers(pending);
1466
1605
  pending.reject(error);
1467
1606
  log.notice("rejected pending proxy call", {
1468
1607
  sessionKey: pending.sessionKey,
@@ -3840,6 +3979,35 @@ function filterSideQuestionHistory(prompt) {
3840
3979
  (message) => message.role === "assistant" ? { ...message, content: stripPluginNotes(message.content) } : message
3841
3980
  );
3842
3981
  }
3982
+ var DCP_REMINDER_BLOCK = /<dcp-system-reminder\b[^>]*>[\s\S]*?<\/dcp-system-reminder>/gi;
3983
+ function stripContextReminderBlocks(text) {
3984
+ if (!text.includes("<dcp-system-reminder")) return text;
3985
+ return text.replace(DCP_REMINDER_BLOCK, "").replace(/\n{3,}/g, "\n\n").trim();
3986
+ }
3987
+ function shouldStripContextReminders(options) {
3988
+ if (options.enabled !== true) return false;
3989
+ const namesCompress = (list2) => (list2 ?? []).some((name) => String(name).trim().toLowerCase() === "compress");
3990
+ return !namesCompress(options.proxyTools) && !namesCompress(options.proxyOpencodeTools);
3991
+ }
3992
+ function stripContextReminders(prompt) {
3993
+ let removed = 0;
3994
+ const countIn = (text) => (text.match(DCP_REMINDER_BLOCK) ?? []).length;
3995
+ const out = prompt.map((message) => {
3996
+ if (message.role !== "user" && message.role !== "assistant") return message;
3997
+ if (!Array.isArray(message.content)) return message;
3998
+ let touched = false;
3999
+ const parts = message.content.map((part) => {
4000
+ if (!part || part.type !== "text" || typeof part.text !== "string") return part;
4001
+ const hits = countIn(part.text);
4002
+ if (hits === 0) return part;
4003
+ removed += hits;
4004
+ touched = true;
4005
+ return { ...part, text: stripContextReminderBlocks(part.text) };
4006
+ });
4007
+ return touched ? { ...message, content: parts } : message;
4008
+ });
4009
+ return removed > 0 ? { prompt: out, removed } : { prompt, removed: 0 };
4010
+ }
3843
4011
  var SUPPORTED_IMAGE_TYPES = /* @__PURE__ */ new Set([
3844
4012
  "image/jpeg",
3845
4013
  "image/png",
@@ -4039,6 +4207,15 @@ function getClaudeUserMessage(prompt, includeHistoryContext = false, opts = {})
4039
4207
  const compactionMode = opts.compactionMode === true;
4040
4208
  const cliToolCallIds = opts.cliToolCallIds;
4041
4209
  const content = [];
4210
+ if (opts.stripContextReminders) {
4211
+ const stripped = stripContextReminders(prompt);
4212
+ if (stripped.removed > 0) {
4213
+ log.info("stripped unsatisfiable context reminders", {
4214
+ blocks: stripped.removed
4215
+ });
4216
+ prompt = stripped.prompt;
4217
+ }
4218
+ }
4042
4219
  const pushToolResult = (part) => {
4043
4220
  const id = part.toolCallId;
4044
4221
  const text = getToolResultText(part);
@@ -5592,6 +5769,14 @@ You are running via the Claude Code CLI (not a direct API call). This affects co
5592
5769
  - Everything outside the summary is gone after the reset \u2014 tool output, files you read, and the earlier conversation are not replayed. Write the summary as the authoritative record.
5593
5770
  - The \`distill\`, \`prune\`, and \`extract\` tools are NOT available.
5594
5771
  - DCP context injections (AGENTS.md, dynamic state) arrive via the system prompt and are already applied.`;
5772
+ var CLAUDE_CLI_OPENCODE_COMPRESS_NOTE = `## Runtime environment: Claude Code CLI
5773
+
5774
+ You are running via the Claude Code CLI (not a direct API call). This affects context management:
5775
+
5776
+ - To compress context, call \`mcp__opencode_proxy__compress\`. Use that exact full name. It runs opencode's own \`compress\` tool, which is what a "MAX CONTEXT LIMIT REACHED" reminder is asking you to do.
5777
+ - It compresses opencode's stored conversation, NOT this Claude Code session. Your current session keeps the context it already has, so do not assume earlier detail is gone after the call.
5778
+ - The \`distill\`, \`prune\`, and \`extract\` tools are NOT available.
5779
+ - DCP context injections (AGENTS.md, dynamic state) arrive via the system prompt and are already applied.`;
5595
5780
  function extractSystemMessages(prompt) {
5596
5781
  const out = [];
5597
5782
  for (const msg of prompt) {
@@ -5618,7 +5803,7 @@ ${options.compressionSummary.trim()}`
5618
5803
  );
5619
5804
  }
5620
5805
  parts.push(
5621
- options.compressEnabled ? CLAUDE_CLI_COMPRESS_NOTE : CLAUDE_CLI_CONTEXT_NOTE
5806
+ options.compressEnabled ? CLAUDE_CLI_COMPRESS_NOTE : options.opencodeCompressEnabled ? CLAUDE_CLI_OPENCODE_COMPRESS_NOTE : CLAUDE_CLI_CONTEXT_NOTE
5622
5807
  );
5623
5808
  for (const s of extraSystemContent) {
5624
5809
  if (s.trim()) parts.push(s.trim());
@@ -5798,43 +5983,46 @@ var ClaudeCodeLanguageModel = class {
5798
5983
  return picked.length > 0 ? picked : null;
5799
5984
  }
5800
5985
  /**
5801
- * Resolve ProxyToolDef[] for opencode's MCP-bridged tools so they go
5986
+ * Resolve ProxyToolDef[] for opencode's MCP-backed tools so they go
5802
5987
  * through the in-process proxy instead of being bridged into Claude CLI's
5803
- * `--mcp-config`. Direct bridging causes double execution because both
5804
- * Claude CLI's own MCP child and opencode hold their own connection to
5805
- * the same server; routing through the proxy keeps a single execution
5806
- * site (opencode). Returns null when the feature is disabled, the SDK
5807
- * client is unavailable, or no MCP servers are configured.
5988
+ * `--mcp-config`. Routing through the proxy keeps a single execution site
5989
+ * (opencode), so the call is permission-prompted and rendered as an
5990
+ * opencode tool call.
5991
+ *
5992
+ * Opt-in (`proxyOpencodeMcpTools: true`) and off by default. It used to
5993
+ * default to true while finding nothing, because it discovered tools via
5994
+ * `client.tool.list()`, which enumerates opencode's `ToolRegistry` and not
5995
+ * the MCP tools merged into the model's tool set afterwards. Discovery now
5996
+ * reads that merged set, the `tools` array opencode passes `doStream`, so
5997
+ * the option does what it says. Turning it on by default at the same time
5998
+ * would have silently moved every existing user's MCP traffic off the
5999
+ * working direct bridge, so the default went to false instead: today's
6000
+ * behaviour is preserved exactly and crossing over is the operator's call.
6001
+ *
6002
+ * Returns null when the feature is off or nothing matched, which leaves
6003
+ * every server on the direct bridge.
5808
6004
  */
5809
- async resolvedProxyMcpTools(allEnabledServerNames) {
5810
- if (this.config.proxyOpencodeMcpTools === false) return null;
6005
+ resolvedProxyMcpTools(allEnabledServerNames, modelTools, taken) {
6006
+ if (this.config.proxyOpencodeMcpTools !== true) return null;
5811
6007
  if (this.config.bridgeOpencodeMcp === false) return null;
5812
6008
  if (allEnabledServerNames.length === 0) return null;
5813
- const items = await fetchOpencodeToolList(
5814
- this.config.provider,
5815
- this.modelId,
5816
- this.config.cwd
5817
- );
5818
- if (!items || items.length === 0) return null;
5819
- const serversByLengthDesc = [...allEnabledServerNames].sort(
5820
- (a, b) => b.length - a.length
5821
- );
5822
- const out = [];
5823
- const seen = /* @__PURE__ */ new Set();
5824
- for (const item of items) {
5825
- const matchedServer = serversByLengthDesc.find(
5826
- (name) => item.id === name || item.id.startsWith(`${name}_`)
6009
+ const resolution = resolveMcpProxyToolDefs({
6010
+ serverNames: allEnabledServerNames,
6011
+ tools: modelTools,
6012
+ taken
6013
+ });
6014
+ if (resolution.defs.length === 0) {
6015
+ log.warn(
6016
+ "proxyOpencodeMcpTools is on but no MCP tool was found in opencode's tool set; those servers stay on the direct bridge this spawn",
6017
+ { servers: allEnabledServerNames, modelTools: modelTools?.length ?? 0 }
5827
6018
  );
5828
- if (!matchedServer) continue;
5829
- if (seen.has(item.id)) continue;
5830
- seen.add(item.id);
5831
- out.push({
5832
- name: item.id,
5833
- description: item.description ?? "",
5834
- inputSchema: item.parameters && typeof item.parameters === "object" ? item.parameters : { type: "object", properties: {} }
5835
- });
6019
+ return null;
5836
6020
  }
5837
- return out.length > 0 ? out : null;
6021
+ log.debug("routing opencode MCP tools through the proxy", {
6022
+ servers: [...resolution.coveredServers],
6023
+ tools: resolution.defs.map((def) => def.name)
6024
+ });
6025
+ return resolution;
5838
6026
  }
5839
6027
  /**
5840
6028
  * Live tool info derived from a single `client.tool.list()` fetch:
@@ -5865,9 +6053,22 @@ var ClaudeCodeLanguageModel = class {
5865
6053
  resolved: items !== void 0,
5866
6054
  taskDescription: items?.find((item) => item.id === "task")?.description,
5867
6055
  questionDescription: question?.description,
5868
- hasQuestion: !!question
6056
+ hasQuestion: !!question,
6057
+ items
5869
6058
  };
5870
6059
  }
6060
+ /**
6061
+ * Whether dcp-style context reminders should be stripped from this turn's
6062
+ * messages. Config-only and synchronous, so it can be answered before the
6063
+ * spawn block resolves anything: `userMsg` is built well ahead of it.
6064
+ */
6065
+ stripContextRemindersEnabled() {
6066
+ return shouldStripContextReminders({
6067
+ enabled: this.config.stripContextReminders,
6068
+ proxyTools: this.config.proxyTools,
6069
+ proxyOpencodeTools: this.config.proxyOpencodeTools
6070
+ });
6071
+ }
5871
6072
  /** Share one lazy registry request within a turn without making it stale. */
5872
6073
  createLiveToolInfoLoader() {
5873
6074
  let pending;
@@ -5903,10 +6104,10 @@ var ClaudeCodeLanguageModel = class {
5903
6104
  * Create a proxy MCP server for a single active Claude process/session.
5904
6105
  * The process lifecycle owns the server lifecycle via session-manager.
5905
6106
  */
5906
- async ensureProxyServer(tools, sessionKeyForCalls) {
6107
+ async ensureProxyServer(tools, sessionKeyForCalls, interceptCompress) {
5907
6108
  const timeoutOverrides = this.config.proxyToolTimeoutMs;
5908
6109
  const interceptors = /* @__PURE__ */ new Map();
5909
- if (tools.some((t) => t.name === "compress")) {
6110
+ if (interceptCompress && tools.some((t) => t.name === "compress")) {
5910
6111
  interceptors.set("compress", (input) => {
5911
6112
  const summary = typeof input.summary === "string" ? input.summary.trim() : "";
5912
6113
  if (!summary) {
@@ -6284,7 +6485,7 @@ var ClaudeCodeLanguageModel = class {
6284
6485
  );
6285
6486
  const sk = effortSessionKey(baseKey, reasoningEffort);
6286
6487
  const compactionMode = this.isCompactionCall(options);
6287
- if (scope === "tools" && (this.resolvedProxyTools() || this.config.proxyOpencodeMcpTools !== false && this.config.bridgeOpencodeMcp !== false)) {
6488
+ if (scope === "tools" && (this.resolvedProxyTools() || this.config.proxyOpencodeMcpTools === true && this.config.bridgeOpencodeMcp !== false)) {
6288
6489
  return this.doGenerateViaStream(options);
6289
6490
  }
6290
6491
  if (compactionMode) {
@@ -6347,7 +6548,8 @@ var ClaudeCodeLanguageModel = class {
6347
6548
  // at all: every tool result reaching it belongs to opencode and must be
6348
6549
  // rendered as text rather than an orphaned `tool_result` (issue #29).
6349
6550
  getClaudeUserMessage(options.prompt, includeHistoryContext, {
6350
- cliToolCallIds: /* @__PURE__ */ new Set()
6551
+ cliToolCallIds: /* @__PURE__ */ new Set(),
6552
+ stripContextReminders: this.stripContextRemindersEnabled()
6351
6553
  });
6352
6554
  const [runtimeStatus, cliVersion, planModeQuestionActive] = await Promise.all([
6353
6555
  getRuntimeMcpStatus(),
@@ -6866,7 +7068,8 @@ ${plan}
6866
7068
  const previousPendingProxyCalls = compactionMode ? [] : getPendingProxyCalls(sk);
6867
7069
  const userMsg = exitPlanModeQuestionResult ?? getClaudeUserMessage(options.prompt, includeHistoryContext, {
6868
7070
  compactionMode,
6869
- cliToolCallIds: new Set(previousPendingProxyCalls.map((c) => c.toolCallId))
7071
+ cliToolCallIds: new Set(previousPendingProxyCalls.map((c) => c.toolCallId)),
7072
+ stripContextReminders: this.stripContextRemindersEnabled()
6870
7073
  });
6871
7074
  const resolvedProxy = compactionMode ? null : this.resolvedProxyTools();
6872
7075
  const loadLiveToolInfo = this.createLiveToolInfoLoader();
@@ -7028,13 +7231,21 @@ ${plan}
7028
7231
  void 0,
7029
7232
  runtimeStatus
7030
7233
  );
7031
- const proxyMcpTools = await self.resolvedProxyMcpTools(
7032
- discovery.allEnabledServerNames
7234
+ const mcpResolution = self.resolvedProxyMcpTools(
7235
+ discovery.allEnabledServerNames,
7236
+ options.tools,
7237
+ new Set((resolvedProxy ?? []).map((def) => def.name))
7033
7238
  );
7034
- const excludeServers = proxyMcpTools ? new Set(discovery.allEnabledServerNames) : void 0;
7239
+ const proxyMcpTools = mcpResolution?.defs ?? null;
7240
+ const excludeServers = mcpResolution ? mcpResolution.coveredServers : void 0;
7035
7241
  const taskProxyEnabled = resolvedProxy?.some((t) => t.name === "task") ?? false;
7036
7242
  const questionProxyEnabled = resolvedProxy?.some((t) => t.name === "question") ?? false;
7037
- const liveToolInfo = taskProxyEnabled || questionProxyEnabled ? await loadLiveToolInfo() : {
7243
+ const opencodeToolsRequested = (self.config.proxyOpencodeTools?.length ?? 0) > 0;
7244
+ log.debug("opencode tool forwarding gate", {
7245
+ requested: self.config.proxyOpencodeTools ?? null,
7246
+ opencodeToolsRequested
7247
+ });
7248
+ const liveToolInfo = taskProxyEnabled || questionProxyEnabled || opencodeToolsRequested ? await loadLiveToolInfo() : {
7038
7249
  resolved: false,
7039
7250
  taskDescription: void 0,
7040
7251
  questionDescription: void 0,
@@ -7070,13 +7281,33 @@ ${plan}
7070
7281
  kept: liveToolInfo.hasQuestion
7071
7282
  });
7072
7283
  }
7284
+ const opencodeToolDefs = resolveProxyOpencodeToolDefs({
7285
+ requested: self.config.proxyOpencodeTools,
7286
+ items: liveToolInfo.items,
7287
+ taken: new Set(
7288
+ [...enrichedProxy ?? [], ...proxyMcpTools ?? []].map(
7289
+ (t) => t.name
7290
+ )
7291
+ )
7292
+ });
7293
+ if (opencodeToolDefs.length > 0) {
7294
+ log.info("forwarding opencode tools through the proxy", {
7295
+ tools: opencodeToolDefs.map((t) => t.name)
7296
+ });
7297
+ }
7073
7298
  const combinedList = [
7074
7299
  ...enrichedProxy ?? [],
7075
- ...proxyMcpTools ?? []
7300
+ ...proxyMcpTools ?? [],
7301
+ ...opencodeToolDefs
7076
7302
  ];
7077
7303
  const combinedProxyTools = combinedList.length > 0 ? combinedList : null;
7304
+ const pluginCompressEnabled = enrichedProxy?.some((t) => t.name === "compress") ?? false;
7078
7305
  if (!proxyServer && combinedProxyTools) {
7079
- proxyServer = await self.ensureProxyServer(combinedProxyTools, sk);
7306
+ proxyServer = await self.ensureProxyServer(
7307
+ combinedProxyTools,
7308
+ sk,
7309
+ pluginCompressEnabled
7310
+ );
7080
7311
  }
7081
7312
  const questionProxyActive = enrichedProxy?.some((t) => t.name === "question") ?? false;
7082
7313
  const allDisallowed = resolveDisallowedTools({
@@ -7099,7 +7330,10 @@ ${plan}
7099
7330
  ...questionProxyActive ? [QUESTION_PROXY_HINT] : []
7100
7331
  ],
7101
7332
  {
7102
- compressEnabled: enrichedProxy?.some((t) => t.name === "compress") ?? false,
7333
+ compressEnabled: pluginCompressEnabled,
7334
+ opencodeCompressEnabled: opencodeToolDefs.some(
7335
+ (t) => t.name === "compress"
7336
+ ),
7103
7337
  compressionSummary: getCompressionSummary(sk)
7104
7338
  }
7105
7339
  );
@@ -8923,12 +9157,14 @@ function createClaudeCode(settings = {}) {
8923
9157
  controlRequestToolBehaviors: settings.controlRequestToolBehaviors,
8924
9158
  controlRequestDenyMessage: settings.controlRequestDenyMessage,
8925
9159
  proxyTools,
9160
+ proxyOpencodeTools: settings.proxyOpencodeTools,
9161
+ stripContextReminders: settings.stripContextReminders === true,
8926
9162
  extraDisallowedTools: settings.extraDisallowedTools,
8927
9163
  proxyToolTimeoutMs: settings.proxyToolTimeoutMs,
8928
9164
  planModeQuestion: settings.planModeQuestion ?? false,
8929
9165
  webSearch: settings.webSearch,
8930
9166
  hotReloadMcp: settings.hotReloadMcp ?? true,
8931
- proxyOpencodeMcpTools: settings.proxyOpencodeMcpTools ?? true,
9167
+ proxyOpencodeMcpTools: settings.proxyOpencodeMcpTools === true,
8932
9168
  multiStepContinuation: settings.multiStepContinuation ?? true,
8933
9169
  autoContinueIncompleteTurns: settings.autoContinueIncompleteTurns ?? "smart",
8934
9170
  compactionModel: settings.compactionModel,