@khalilgharbaoui/opencode-claude-code-plugin 0.12.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -181,7 +181,7 @@ The account model IDs are internally suffixed, for example `claude-sonnet-4-6@wo
181
181
  | `cwd` | string | `process.cwd()` | Working directory for the spawned CLI. Resolved **lazily per request**, so opencode's project switching works. |
182
182
  | `skipPermissions` | boolean | `true` | Pass `--dangerously-skip-permissions` to `claude`. Ignored when `proxyTools` is set — the proxy handles permissions through opencode instead. |
183
183
  | `permissionMode` | `acceptEdits` \| `auto` \| `bypassPermissions` \| `default` \| `dontAsk` \| `plan` | – | Forwarded to `claude --permission-mode`. |
184
- | `proxyTools` | string[] | `["Bash", "Edit", "Write", "WebFetch", "Task"]` | Claude built-in tools to route through opencode's executor + permission UI. See [Selective tool proxy](#selective-tool-proxy). |
184
+ | `proxyTools` | string[] | `["Bash", "Edit", "Write", "WebFetch", "Task"]` | Claude built-in tools to route through opencode's executor + permission UI. Opt-in extras: `"Question"`, `"Compress"`. See [Selective tool proxy](#selective-tool-proxy). |
185
185
  | `proxyToolTimeoutMs` | `Record<string, number>` | – | Per-tool proxy call deadline in ms, keyed by proxy tool name (`bash`, `task`, …). Defaults: 10 min flat, `task` → 60 min. For `bash`, the call's own `input.timeout` is honoured on top (`max(resolved, input.timeout)`). See [Selective tool proxy](#selective-tool-proxy). |
186
186
  | `planModeQuestion` | boolean | `false` | Route `ExitPlanMode` approval through opencode's native `question` tool instead of a text "(yes/no)" prompt. Off because opencode's question form is currently broken upstream. See [Plan mode](#plan-mode). |
187
187
  | `controlRequestBehavior` | `allow` \| `deny` | `allow` | Default response when `skipPermissions: false` and Claude sends a `can_use_tool` control request. |
@@ -273,6 +273,7 @@ By default, the plugin proxies `Bash`, `Edit`, `Write`, `WebFetch`, and `Task`.
273
273
  | `"WebFetch"` | `WebFetch` | `mcp__opencode_proxy__webfetch` |
274
274
  | `"Task"` | `Agent` | `mcp__opencode_proxy__task` |
275
275
  | `"Question"` | `AskUserQuestion` | `mcp__opencode_proxy__question` |
276
+ | `"Compress"` | none | `mcp__opencode_proxy__compress` |
276
277
 
277
278
  ### OpenCode-native subagents
278
279
 
@@ -297,7 +298,21 @@ recovery step for harnesses that defer MCP tool schemas. Both apply per Claude
297
298
  process at spawn, and provider options are read once at opencode startup, so
298
299
  `proxyTools` changes need a full opencode restart.
299
300
 
300
- Only those six values are actually proxied; anything else you put in `proxyTools` is ignored. Proxying `Edit` also disables `MultiEdit` — opencode has no batched-edit equivalent, so Claude is forced to fan out into single `Edit` calls that each flow through the permission UI. The `"Question"` proxy is version-gated on opencode's built-in `question` tool: on builds that lack the registry entry the def is silently dropped (a forwarded call would otherwise render as `⚙ invalid`), so add it only on opencode versions that ship the `question` tool.
301
+ ### Context compression
302
+
303
+ `"Compress"` is off by default. Add it when you run a harness that expects the model to manage its own context (opencode-dcp injects exactly those instructions), and the plugin exposes `mcp__opencode_proxy__compress`:
304
+
305
+ ```json
306
+ "options": {
307
+ "proxyTools": ["Bash", "Edit", "Write", "WebFetch", "Task", "Compress"]
308
+ }
309
+ ```
310
+
311
+ It is the one proxy tool opencode never sees. The call is answered inside the plugin: the model passes a `summary`, the plugin stores it, and the turn continues normally. At the start of the **next** turn the Claude Code session is discarded and a fresh `claude` starts with that summary prepended to its system prompt, and nothing else. The earlier conversation is not replayed, so a thin summary means real lost context. The reset waits if the incoming turn is carrying tool results for the running process.
312
+
313
+ Without it, the appended system prompt tells the model that `compress` is unavailable and to ignore instructions that ask for it, which is the right answer when nothing implements it.
314
+
315
+ Only those seven values are actually proxied; anything else you put in `proxyTools` is ignored. Proxying `Edit` also disables `MultiEdit` — opencode has no batched-edit equivalent, so Claude is forced to fan out into single `Edit` calls that each flow through the permission UI. The `"Question"` proxy is version-gated on opencode's built-in `question` tool: on builds that lack the registry entry the def is silently dropped (a forwarded call would otherwise render as `⚙ invalid`), so add it only on opencode versions that ship the `question` tool.
301
316
 
302
317
  Without `"Task"` in `proxyTools`, Claude's built-in `Agent` tool stays enabled and Claude orchestrates subagents internally with no opencode child-session visibility. To opt out of all proxying, including Task, use an explicit empty list:
303
318
 
package/dist/index.d.ts CHANGED
@@ -655,6 +655,7 @@ interface ClaudeCodeProvider {
655
655
  (modelId: string): LanguageModelV3;
656
656
  languageModel(modelId: string): LanguageModelV3;
657
657
  }
658
+ declare const DEFAULT_PROXY_TOOL_NAMES: string[];
658
659
  declare function createClaudeCode(settings?: ClaudeCodeProviderSettings): ClaudeCodeProvider;
659
660
  /**
660
661
  * Build models in OpenCode's config schema format (flat properties like
@@ -673,4 +674,4 @@ declare const _default: {
673
674
  server: OpenCodePlugin;
674
675
  };
675
676
 
676
- export { type ClaudeCodeConfig, ClaudeCodeLanguageModel, type ClaudeCodeProvider, type ClaudeCodeProviderSettings, type ClaudeStreamMessage, type OpenCodeHooks, type OpenCodeModel, type OpenCodePlugin, bridgeOpencodeMcp, claudeCodeProviders, configModelsForProvider, createClaudeCode, _default as default, defaultModels };
677
+ export { type ClaudeCodeConfig, ClaudeCodeLanguageModel, type ClaudeCodeProvider, type ClaudeCodeProviderSettings, type ClaudeStreamMessage, DEFAULT_PROXY_TOOL_NAMES, type OpenCodeHooks, type OpenCodeModel, type OpenCodePlugin, bridgeOpencodeMcp, claudeCodeProviders, configModelsForProvider, createClaudeCode, _default as default, defaultModels };
package/dist/index.js CHANGED
@@ -2212,6 +2212,31 @@ function spawnInteractiveProcess(opts) {
2212
2212
  };
2213
2213
  }
2214
2214
 
2215
+ // src/compression-store.ts
2216
+ var MAX_COMPRESSION_ENTRIES = 32;
2217
+ var compressions = /* @__PURE__ */ new Map();
2218
+ function storeCompressionSummary(sessionKey2, summary) {
2219
+ compressions.set(sessionKey2, { summary, restartPending: true });
2220
+ while (compressions.size > MAX_COMPRESSION_ENTRIES) {
2221
+ const oldest = compressions.keys().next();
2222
+ if (oldest.done) break;
2223
+ compressions.delete(oldest.value);
2224
+ log.info("compression store evicted oldest entry", { sessionKey: oldest.value });
2225
+ }
2226
+ }
2227
+ function getCompressionSummary(sessionKey2) {
2228
+ return compressions.get(sessionKey2)?.summary;
2229
+ }
2230
+ function consumeCompressionRestart(sessionKey2) {
2231
+ const state = compressions.get(sessionKey2);
2232
+ if (!state?.restartPending) return false;
2233
+ state.restartPending = false;
2234
+ return true;
2235
+ }
2236
+ function clearCompression(sessionKey2) {
2237
+ compressions.delete(sessionKey2);
2238
+ }
2239
+
2215
2240
  // src/proxy-mcp.ts
2216
2241
  import { createServer } from "http";
2217
2242
  import * as fs4 from "fs";
@@ -2279,6 +2304,7 @@ var TASK_PROXY_NOTE = "This is the ONLY tool that dispatches opencode subagents
2279
2304
  var AGENT_TYPES_HEADING = "Available agent types";
2280
2305
  var AGENT_BLURB_LIMIT = 140;
2281
2306
  var QUESTION_PROXY_NOTE = "This routes structured questions through opencode's native `question` tool, which renders a TUI form with the options you provide and blocks until the operator answers. Claude Code's built-in AskUserQuestion is disabled in this environment; this proxy is the ONLY way to ask the operator for a decision or clarification. Answers come back as arrays of selected labels (set `multiple: true` to allow more than one). If the operator dismisses the form the call returns an error \u2014 treat that as 'no answer' and stop, do not guess. Question calls get a 30-minute proxy deadline by default (configurable via proxyToolTimeoutMs); for long-AFK scenarios prefer fewer, high-signal questions.";
2307
+ var COMPRESS_PROXY_NOTE = "The current turn continues normally after this call \u2014 finish what you are doing. The reset happens at the START of the next turn: the Claude Code session is discarded and a fresh one begins with your summary as its only prior context. Everything else, including tool output and files you read, is gone, so write the summary as the authoritative record. Call this once per compression, when older resolved work no longer needs full detail.";
2282
2308
  function extractAgentTypeList(liveDescription) {
2283
2309
  const live = liveDescription?.trim();
2284
2310
  if (!live) return void 0;
@@ -2493,9 +2519,23 @@ var DEFAULT_PROXY_TOOLS = [
2493
2519
  },
2494
2520
  required: ["questions"]
2495
2521
  }
2522
+ },
2523
+ {
2524
+ name: "compress",
2525
+ description: "Replace older conversation detail with a summary you write, then continue in a fresh Claude Code session. Handled inside the plugin, so it never prompts the operator. " + COMPRESS_PROXY_NOTE,
2526
+ inputSchema: {
2527
+ type: "object",
2528
+ properties: {
2529
+ summary: {
2530
+ type: "string",
2531
+ description: "Dense technical summary of the work being compressed: decisions made, files changed, commands run and their outcomes, and what is still open. This is the ONLY prior context that survives, so anything omitted is lost."
2532
+ }
2533
+ },
2534
+ required: ["summary"]
2535
+ }
2496
2536
  }
2497
2537
  ];
2498
- async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverrides) {
2538
+ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverrides, interceptors) {
2499
2539
  const calls = new EventEmitter3();
2500
2540
  const pending = /* @__PURE__ */ new Map();
2501
2541
  const server2 = createServer(async (req, res) => {
@@ -2572,6 +2612,19 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
2572
2612
  });
2573
2613
  return;
2574
2614
  }
2615
+ const interceptor = interceptors?.get(toolName);
2616
+ if (interceptor) {
2617
+ let intercepted;
2618
+ try {
2619
+ intercepted = await interceptor(input);
2620
+ } catch (interceptorError) {
2621
+ const message = interceptorError instanceof Error ? interceptorError.message : String(interceptorError);
2622
+ log.warn("proxy-mcp interceptor failed", { toolName, error: message });
2623
+ intercepted = { kind: "error", message };
2624
+ }
2625
+ writeToolCallResult(res, requestId, intercepted);
2626
+ return;
2627
+ }
2575
2628
  const callId = crypto2.randomUUID();
2576
2629
  log.info("proxy-mcp tool call received", {
2577
2630
  callId,
@@ -2610,16 +2663,7 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
2610
2663
  if (timer) clearTimeout(timer);
2611
2664
  pending.delete(callId);
2612
2665
  });
2613
- const text = result.kind === "error" ? result.message : result.text;
2614
- const isError = result.kind === "error" || result.isError === true;
2615
- writeJson(res, {
2616
- jsonrpc: "2.0",
2617
- id: requestId,
2618
- result: {
2619
- content: [{ type: "text", text }],
2620
- isError
2621
- }
2622
- });
2666
+ writeToolCallResult(res, requestId, result);
2623
2667
  return;
2624
2668
  }
2625
2669
  writeJson(res, {
@@ -2774,6 +2818,18 @@ function readBody(req) {
2774
2818
  req.on("error", reject);
2775
2819
  });
2776
2820
  }
2821
+ function writeToolCallResult(res, requestId, result) {
2822
+ const text = result.kind === "error" ? result.message : result.text;
2823
+ const isError = result.kind === "error" || result.isError === true;
2824
+ writeJson(res, {
2825
+ jsonrpc: "2.0",
2826
+ id: requestId ?? null,
2827
+ result: {
2828
+ content: [{ type: "text", text }],
2829
+ isError
2830
+ }
2831
+ });
2832
+ }
2777
2833
  function writeJson(res, body) {
2778
2834
  const payload = JSON.stringify(body);
2779
2835
  res.statusCode = 200;
@@ -3159,6 +3215,15 @@ You are running via the Claude Code CLI (not a direct API call). This affects co
3159
3215
  - Context window management is handled automatically by Claude CLI's own session history.
3160
3216
  - Ignore any system instructions that tell you to call \`compress\` \u2014 they are intended for direct API providers, not this environment.
3161
3217
  - DCP context injections (AGENTS.md, dynamic state) arrive via the system prompt and are already applied.`;
3218
+ var CLAUDE_CLI_COMPRESS_NOTE = `## Runtime environment: Claude Code CLI
3219
+
3220
+ You are running via the Claude Code CLI (not a direct API call). This affects context management:
3221
+
3222
+ - To compress context, call \`mcp__opencode_proxy__compress\` with a \`summary\` argument. Use that exact full name.
3223
+ - The reset happens at the start of your NEXT turn: this Claude Code session is discarded and a fresh one starts with your summary as its only prior context. Keep working normally after the call.
3224
+ - 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.
3225
+ - The \`distill\`, \`prune\`, and \`extract\` tools are NOT available.
3226
+ - DCP context injections (AGENTS.md, dynamic state) arrive via the system prompt and are already applied.`;
3162
3227
  function extractSystemMessages(prompt) {
3163
3228
  const out = [];
3164
3229
  for (const msg of prompt) {
@@ -3175,9 +3240,18 @@ function extractSystemMessages(prompt) {
3175
3240
  }
3176
3241
  return out;
3177
3242
  }
3178
- function buildAppendedSystemPrompt(cwd, includeMultiStepHint = true, extraSystemContent = []) {
3243
+ function buildAppendedSystemPrompt(cwd, includeMultiStepHint = true, extraSystemContent = [], options = {}) {
3179
3244
  const parts = [];
3180
- parts.push(CLAUDE_CLI_CONTEXT_NOTE);
3245
+ if (options.compressionSummary?.trim()) {
3246
+ parts.push(
3247
+ `## Summary of earlier work (context was compressed)
3248
+
3249
+ ${options.compressionSummary.trim()}`
3250
+ );
3251
+ }
3252
+ parts.push(
3253
+ options.compressEnabled ? CLAUDE_CLI_COMPRESS_NOTE : CLAUDE_CLI_CONTEXT_NOTE
3254
+ );
3181
3255
  for (const s of extraSystemContent) {
3182
3256
  if (s.trim()) parts.push(s.trim());
3183
3257
  }
@@ -3393,7 +3467,28 @@ var ClaudeCodeLanguageModel = class {
3393
3467
  */
3394
3468
  async ensureProxyServer(tools, sessionKeyForCalls) {
3395
3469
  const timeoutOverrides = this.config.proxyToolTimeoutMs;
3396
- const srv = await createProxyMcpServer(tools, timeoutOverrides);
3470
+ const interceptors = /* @__PURE__ */ new Map();
3471
+ if (tools.some((t) => t.name === "compress")) {
3472
+ interceptors.set("compress", (input) => {
3473
+ const summary = typeof input.summary === "string" ? input.summary.trim() : "";
3474
+ if (!summary) {
3475
+ return {
3476
+ kind: "error",
3477
+ message: "compress needs a non-empty `summary`: it becomes the only prior context after the reset. Nothing was compressed."
3478
+ };
3479
+ }
3480
+ storeCompressionSummary(sessionKeyForCalls, summary);
3481
+ log.info("compress stored summary; session resets next turn", {
3482
+ sessionKey: sessionKeyForCalls,
3483
+ summaryLength: summary.length
3484
+ });
3485
+ return {
3486
+ kind: "text",
3487
+ text: "Summary stored. Finish this turn as normal; the next turn starts a fresh Claude Code session with this summary as its only prior context."
3488
+ };
3489
+ });
3490
+ }
3491
+ const srv = await createProxyMcpServer(tools, timeoutOverrides, interceptors);
3397
3492
  srv.calls.on("call", (call) => {
3398
3493
  queuePendingProxyCall(sessionKeyForCalls, call, timeoutOverrides);
3399
3494
  });
@@ -3755,6 +3850,7 @@ var ClaudeCodeLanguageModel = class {
3755
3850
  if (!hasPriorConversation) {
3756
3851
  deleteClaudeSessionId(sk);
3757
3852
  deleteActiveProcess(sk);
3853
+ clearCompression(sk);
3758
3854
  }
3759
3855
  const hasExistingSession = !!getClaudeSessionId(sk);
3760
3856
  const includeHistoryContext = !hasExistingSession && hasPriorConversation;
@@ -3768,7 +3864,10 @@ var ClaudeCodeLanguageModel = class {
3768
3864
  const systemPromptFile = buildAppendedSystemPrompt(
3769
3865
  cwd,
3770
3866
  this.config.multiStepContinuation !== false,
3771
- extractSystemMessages(options.prompt)
3867
+ extractSystemMessages(options.prompt),
3868
+ // doGenerate has no proxy wiring, so `compress` is not callable here.
3869
+ // An existing summary still carries: it is this key's prior context.
3870
+ { compressEnabled: false, compressionSummary: getCompressionSummary(sk) }
3772
3871
  );
3773
3872
  const cliArgs = buildCliArgs({
3774
3873
  sessionKey: sk,
@@ -4138,6 +4237,7 @@ ${plan}
4138
4237
  if (!hasPriorConversation) {
4139
4238
  deleteClaudeSessionId(sk);
4140
4239
  deleteActiveProcess(sk);
4240
+ clearCompression(sk);
4141
4241
  }
4142
4242
  const hasExistingSession = !!getClaudeSessionId(sk);
4143
4243
  const hasActiveProcess = !!getActiveProcess(sk);
@@ -4188,6 +4288,13 @@ ${plan}
4188
4288
  deleteActiveProcess(sk);
4189
4289
  deleteClaudeSessionId(sk);
4190
4290
  }
4291
+ if (!compactionMode && !hasMatchedPendingResults && consumeCompressionRestart(sk)) {
4292
+ deleteActiveProcess(sk);
4293
+ deleteClaudeSessionId(sk);
4294
+ log.info("compress reset: dropped claude process and session id", {
4295
+ sessionKey: sk
4296
+ });
4297
+ }
4191
4298
  let activeProcess = getActiveProcess(sk);
4192
4299
  let proc;
4193
4300
  let lineEmitter;
@@ -4363,7 +4470,11 @@ ${plan}
4363
4470
  ...extractSystemMessages(options.prompt),
4364
4471
  ...taskProxyEnabled ? [SUBAGENT_DISPATCH_HINT] : [],
4365
4472
  ...questionProxyActive ? [QUESTION_PROXY_HINT] : []
4366
- ]
4473
+ ],
4474
+ {
4475
+ compressEnabled: enrichedProxy?.some((t) => t.name === "compress") ?? false,
4476
+ compressionSummary: getCompressionSummary(sk)
4477
+ }
4367
4478
  );
4368
4479
  cliArgs = buildCliArgs({
4369
4480
  sessionKey: sk,
@@ -6439,6 +6550,7 @@ var index_default = {
6439
6550
  };
6440
6551
  export {
6441
6552
  ClaudeCodeLanguageModel,
6553
+ DEFAULT_PROXY_TOOL_NAMES,
6442
6554
  bridgeOpencodeMcp,
6443
6555
  claudeCodeProviders,
6444
6556
  configModelsForProvider,