@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 +17 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +128 -16
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|