@khalilgharbaoui/opencode-claude-code-plugin 0.5.0 → 0.6.2

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
@@ -23,7 +23,7 @@ claude --version
23
23
 
24
24
  That's it. Restart opencode, pick a `claude-code` model, done.
25
25
 
26
- The plugin self-registers the `claude-code` provider, all current Claude Code models (Haiku 4.5, Sonnet 4.5/4.6, Opus 4.5/4.6/4.7) with reasoning variants (`low` / `medium` / `high` / `xhigh` / `max`), and sensible defaults for tool proxying. You don't need to write a `provider` block at all unless you want to override something.
26
+ The plugin self-registers the `claude-code` provider, all current Claude Code models (Haiku 4.5, Sonnet 4.5/4.6, Opus 4.5/4.6/4.7/4.8) with reasoning variants (`low` / `medium` / `high` / `xhigh` / `max`), and sensible defaults for tool proxying. You don't need to write a `provider` block at all unless you want to override something.
27
27
 
28
28
  ---
29
29
 
@@ -74,6 +74,7 @@ The plugin auto-registers the following. They appear in the model picker without
74
74
  | `claude-opus-4-5` | Claude Code Opus 4.5 | 1M | 16,384 | low/medium/high/xhigh/max |
75
75
  | `claude-opus-4-6` | Claude Code Opus 4.6 | 1M | 16,384 | low/medium/high/xhigh/max |
76
76
  | `claude-opus-4-7` | Claude Code Opus 4.7 | 1M | 16,384 | low/medium/high/xhigh/max |
77
+ | `claude-opus-4-8` | Claude Code Opus 4.8 | 1M | 16,384 | low/medium/high/xhigh/max |
77
78
 
78
79
  Capabilities for every model: text + image input, text output, tool use, attachments. No temperature control, no PDF/audio/video, no interleaved streaming.
79
80
 
@@ -456,6 +457,24 @@ doesn't accrete a log file on every user's disk by default — opt in when
456
457
  you need to inspect auto-continue decisions, broker state, or other
457
458
  plugin internals.
458
459
 
460
+ ## Compatibility with other opencode plugins
461
+
462
+ ### [opencode-dcp](https://github.com/Opencode-DCP/opencode-dynamic-context-pruning) (Dynamic Context Pruning)
463
+
464
+ Partial support since v0.5.1. DCP runs in a useful degraded mode: automatic strategies and slash commands work, autonomous model-driven compression does not.
465
+
466
+ | DCP feature | Status | Notes |
467
+ |---|---|---|
468
+ | `experimental.chat.messages.transform` (compression placeholders, dedup, error purge) | ✅ Works | Transforms run inside opencode before reaching this plugin. |
469
+ | `experimental.chat.system.transform` (context-limit nudges, iteration reminders) | ✅ Works | `extractSystemMessages` forwards system-role content to Claude CLI via `--append-system-prompt-file`. |
470
+ | `/dcp compress`, `/dcp sweep`, `/dcp manual`, `/dcp context`, `/dcp stats` slash commands | ✅ Works | Handled by opencode's `command.execute.before` hook, not the model. |
471
+ | Automatic `deduplication` + `purgeErrors` strategies | ✅ Works | Message-transform only, no model tool calls. |
472
+ | Autonomous model-driven `compress` tool calls | ❌ Not supported | DCP registers `compress` as an opencode-native tool. Claude CLI only sees its own built-ins and MCP-bridged servers, so the model never sees `compress`. The plugin prepends a runtime note instructing Claude to ignore any system instruction that asks it to call `compress`/`distill`/`prune`. |
473
+
474
+ Workaround for autonomous compression: trigger it manually with `/dcp compress` whenever you'd want the model to call it. Full autonomous support would require exposing `compress` as an MCP-bridged tool, which is upstream of this plugin.
475
+
476
+ ---
477
+
459
478
  ## Known limitations
460
479
 
461
480
  - No streaming of tool inputs as they're being constructed (Anthropic's `input_json_delta`); the plugin emits them once complete.
package/dist/index.d.ts CHANGED
@@ -89,6 +89,13 @@ type OpenCodeEvent = {
89
89
  * for the current call ("default", "compaction", "title", etc.), the
90
90
  * resolved model, and the user message. Output is the mutable params bag
91
91
  * the hook can adjust before opencode forwards them to the LM.
92
+ *
93
+ * The plugin injects `input.agent` as `opencodeAgent` and `input.sessionID`
94
+ * as `opencodeSessionID` into `output.options` so the language model can
95
+ * read them from `providerOptions[providerID]` on every LLM request.
96
+ * `opencodeSessionID` serves as a fallback affinity token when the
97
+ * `x-session-affinity` request header is absent (provider switch
98
+ * mid-session, title synthesis paths, older opencode versions).
92
99
  */
93
100
  type OpenCodeChatParamsInput = {
94
101
  sessionID?: string;
@@ -450,11 +457,14 @@ declare class ClaudeCodeLanguageModel implements LanguageModelV3 {
450
457
  private ensureProxyServer;
451
458
  private extractPendingProxyResult;
452
459
  /**
453
- * Opencode sets `x-session-affinity: <sessionID>` on LLM calls for
454
- * third-party providers (packages/opencode/src/session/llm.ts). Use it so
455
- * two chats in the same cwd+model get separate CLI processes instead of
456
- * stomping on each other. Falls back to "default" when absent (older
457
- * opencode, direct AI-SDK use, title synthesis paths, etc).
460
+ * Resolve the session affinity token for this LLM call. Delegates to the
461
+ * exported `resolveSessionAffinity` helper so the logic is unit-testable.
462
+ * Priority:
463
+ * 1. `x-session-affinity` request header (primary).
464
+ * 2. `opencodeSessionID` in providerOptions (chat.params hook fallback
465
+ * covers provider switches mid-session and title synthesis paths
466
+ * where the header is absent).
467
+ * 3. `"default"`.
458
468
  */
459
469
  private sessionAffinity;
460
470
  private controlRequestBehaviorForTool;
package/dist/index.js CHANGED
@@ -1955,6 +1955,22 @@ function resolveCompactionModel(configured) {
1955
1955
  if (trimmed) return trimmed;
1956
1956
  return DEFAULT_COMPACTION_MODEL;
1957
1957
  }
1958
+ function resolveSessionAffinity(headers, providerOptions, providerKey) {
1959
+ if (headers) {
1960
+ for (const key of Object.keys(headers)) {
1961
+ if (key.toLowerCase() === "x-session-affinity") {
1962
+ const v = headers[key];
1963
+ if (typeof v === "string" && v.length > 0) return v;
1964
+ }
1965
+ }
1966
+ }
1967
+ if (providerOptions) {
1968
+ const bag = providerOptions[providerKey] ?? providerOptions["claude-code"];
1969
+ const sid = bag?.opencodeSessionID;
1970
+ if (typeof sid === "string" && sid.length > 0) return sid;
1971
+ }
1972
+ return "default";
1973
+ }
1958
1974
  var KNOWN_DELTA_TYPES = /* @__PURE__ */ new Set([
1959
1975
  "thinking_delta",
1960
1976
  "text_delta",
@@ -2132,6 +2148,11 @@ function nearestWorkspaceAgentsPrompt(cwd) {
2132
2148
  dir = parent;
2133
2149
  }
2134
2150
  }
2151
+ var AGENTS_MAINTENANCE_HINT = `## Keeping AGENTS.md up to date
2152
+
2153
+ When you complete a task, phase, or to-do item that is listed in AGENTS.md, update the file
2154
+ immediately after the work is done \u2014 mark it \u2705, check it off, or remove it. Do this inside
2155
+ the same turn so the next session does not repeat work that is already finished.`;
2135
2156
  var MULTI_STEP_TASK_HINT = `## Continuing through multi-step tasks
2136
2157
 
2137
2158
  opencode requires the user to press "continue" after each turn ends. When a
@@ -2140,13 +2161,43 @@ than pausing for user confirmation between subtasks. End the turn only
2140
2161
  when the task is done, you need clarification on intent, or you hit a real
2141
2162
  blocker. The user can interrupt or abort at any time; turn endings should
2142
2163
  mark meaningful checkpoints, not every completed substep.`;
2143
- function buildAppendedSystemPrompt(cwd, includeMultiStepHint = true) {
2164
+ var CLAUDE_CLI_CONTEXT_NOTE = `## Runtime environment: Claude Code CLI
2165
+
2166
+ You are running via the Claude Code CLI (not a direct API call). This affects context management:
2167
+
2168
+ - The \`compress\` tool is NOT available. Do not attempt to call it.
2169
+ - The \`distill\`, \`prune\`, and \`extract\` tools are NOT available.
2170
+ - Context window management is handled automatically by Claude CLI's own session history.
2171
+ - Ignore any system instructions that tell you to call \`compress\` \u2014 they are intended for direct API providers, not this environment.
2172
+ - DCP context injections (AGENTS.md, dynamic state) arrive via the system prompt and are already applied.`;
2173
+ function extractSystemMessages(prompt) {
2174
+ const out = [];
2175
+ for (const msg of prompt) {
2176
+ if (msg.role !== "system") continue;
2177
+ if (typeof msg.content === "string") {
2178
+ if (msg.content.trim()) out.push(msg.content.trim());
2179
+ } else if (Array.isArray(msg.content)) {
2180
+ for (const part of msg.content) {
2181
+ if (part?.type === "text" && typeof part.text === "string" && part.text.trim()) {
2182
+ out.push(part.text.trim());
2183
+ }
2184
+ }
2185
+ }
2186
+ }
2187
+ return out;
2188
+ }
2189
+ function buildAppendedSystemPrompt(cwd, includeMultiStepHint = true, extraSystemContent = []) {
2144
2190
  const parts = [];
2191
+ parts.push(CLAUDE_CLI_CONTEXT_NOTE);
2192
+ for (const s of extraSystemContent) {
2193
+ if (s.trim()) parts.push(s.trim());
2194
+ }
2145
2195
  const configRoot = process.env.XDG_CONFIG_HOME ?? join5(homedir3(), ".config");
2146
2196
  const globalAgents = readPromptFileIfPresent(join5(configRoot, "opencode", "AGENTS.md"));
2147
2197
  const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
2148
2198
  if (globalAgents) parts.push(globalAgents);
2149
2199
  if (workspaceAgents && workspaceAgents !== globalAgents) parts.push(workspaceAgents);
2200
+ if (globalAgents || workspaceAgents) parts.push(AGENTS_MAINTENANCE_HINT);
2150
2201
  if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
2151
2202
  const content = parts.join("\n\n");
2152
2203
  if (!content) return void 0;
@@ -2336,22 +2387,22 @@ var ClaudeCodeLanguageModel = class {
2336
2387
  return null;
2337
2388
  }
2338
2389
  /**
2339
- * Opencode sets `x-session-affinity: <sessionID>` on LLM calls for
2340
- * third-party providers (packages/opencode/src/session/llm.ts). Use it so
2341
- * two chats in the same cwd+model get separate CLI processes instead of
2342
- * stomping on each other. Falls back to "default" when absent (older
2343
- * opencode, direct AI-SDK use, title synthesis paths, etc).
2390
+ * Resolve the session affinity token for this LLM call. Delegates to the
2391
+ * exported `resolveSessionAffinity` helper so the logic is unit-testable.
2392
+ * Priority:
2393
+ * 1. `x-session-affinity` request header (primary).
2394
+ * 2. `opencodeSessionID` in providerOptions (chat.params hook fallback
2395
+ * covers provider switches mid-session and title synthesis paths
2396
+ * where the header is absent).
2397
+ * 3. `"default"`.
2344
2398
  */
2345
2399
  sessionAffinity(options) {
2346
2400
  const headers = options?.headers;
2347
- if (!headers) return "default";
2348
- for (const key of Object.keys(headers)) {
2349
- if (key.toLowerCase() === "x-session-affinity") {
2350
- const v = headers[key];
2351
- if (typeof v === "string" && v.length > 0) return v;
2352
- }
2353
- }
2354
- return "default";
2401
+ return resolveSessionAffinity(
2402
+ headers,
2403
+ options.providerOptions,
2404
+ this.config.provider
2405
+ );
2355
2406
  }
2356
2407
  controlRequestBehaviorForTool(toolName) {
2357
2408
  const configured = this.config.controlRequestToolBehaviors;
@@ -2663,7 +2714,8 @@ var ClaudeCodeLanguageModel = class {
2663
2714
  ]);
2664
2715
  const systemPromptFile = buildAppendedSystemPrompt(
2665
2716
  cwd,
2666
- this.config.multiStepContinuation !== false
2717
+ this.config.multiStepContinuation !== false,
2718
+ extractSystemMessages(options.prompt)
2667
2719
  );
2668
2720
  const cliArgs = buildCliArgs({
2669
2721
  sessionKey: sk,
@@ -2703,8 +2755,15 @@ var ClaudeCodeLanguageModel = class {
2703
2755
  let thinkingText = "";
2704
2756
  let resultMeta = {};
2705
2757
  const toolCalls = [];
2758
+ const toolCallStreams = /* @__PURE__ */ new Map();
2706
2759
  let gotPartialEvents = false;
2707
2760
  const result = await new Promise((resolve3, reject) => {
2761
+ const cleanup = () => {
2762
+ try {
2763
+ if (!proc.killed && proc.exitCode === null) proc.kill();
2764
+ } catch {
2765
+ }
2766
+ };
2708
2767
  rl.on("line", (line) => {
2709
2768
  if (!line.trim()) return;
2710
2769
  try {
@@ -2755,30 +2814,41 @@ ${plan}
2755
2814
  }
2756
2815
  }
2757
2816
  }
2758
- if (msg.type === "content_block_start" && msg.content_block) {
2817
+ if (msg.type === "content_block_start" && msg.content_block && msg.index !== void 0) {
2759
2818
  if (msg.content_block.type === "tool_use" && msg.content_block.id && msg.content_block.name) {
2760
- toolCalls.push({
2819
+ toolCallStreams.set(msg.index, {
2761
2820
  id: msg.content_block.id,
2762
2821
  name: msg.content_block.name,
2763
- args: {}
2822
+ inputJson: ""
2764
2823
  });
2765
2824
  }
2766
2825
  }
2767
- if (msg.type === "content_block_delta" && msg.delta) {
2826
+ if (msg.type === "content_block_delta" && msg.delta && msg.index !== void 0) {
2768
2827
  if (msg.delta.type === "text_delta" && msg.delta.text) {
2769
2828
  responseText += msg.delta.text;
2770
2829
  }
2771
2830
  if (msg.delta.type === "thinking_delta" && msg.delta.thinking) {
2772
2831
  thinkingText += msg.delta.thinking;
2773
2832
  }
2774
- if (msg.delta.type === "input_json_delta" && msg.delta.partial_json && msg.index !== void 0) {
2775
- const tc = toolCalls[msg.index];
2776
- if (tc) {
2777
- try {
2778
- tc.args = JSON.parse(msg.delta.partial_json);
2779
- } catch {
2780
- }
2833
+ if (msg.delta.type === "input_json_delta" && msg.delta.partial_json) {
2834
+ const tc = toolCallStreams.get(msg.index);
2835
+ if (tc) tc.inputJson += msg.delta.partial_json;
2836
+ }
2837
+ }
2838
+ if (msg.type === "content_block_stop" && msg.index !== void 0) {
2839
+ const tc = toolCallStreams.get(msg.index);
2840
+ if (tc) {
2841
+ let args = {};
2842
+ try {
2843
+ args = tc.inputJson ? JSON.parse(tc.inputJson) : {};
2844
+ } catch (err) {
2845
+ log.warn("tool input JSON parse failed", {
2846
+ name: tc.name,
2847
+ error: String(err)
2848
+ });
2781
2849
  }
2850
+ toolCalls.push({ id: tc.id, name: tc.name, args });
2851
+ toolCallStreams.delete(msg.index);
2782
2852
  }
2783
2853
  }
2784
2854
  if (msg.type === "result") {
@@ -2794,6 +2864,7 @@ ${plan}
2794
2864
  durationMs: msg.duration_ms,
2795
2865
  usage: msg.usage
2796
2866
  };
2867
+ cleanup();
2797
2868
  resolve3({
2798
2869
  ...resultMeta,
2799
2870
  text: responseText,
@@ -2805,6 +2876,7 @@ ${plan}
2805
2876
  }
2806
2877
  });
2807
2878
  rl.on("close", () => {
2879
+ cleanup();
2808
2880
  resolve3({
2809
2881
  ...resultMeta,
2810
2882
  text: responseText,
@@ -2814,6 +2886,7 @@ ${plan}
2814
2886
  });
2815
2887
  proc.on("error", (err) => {
2816
2888
  log.error("process error", { error: err.message });
2889
+ cleanup();
2817
2890
  reject(err);
2818
2891
  });
2819
2892
  proc.stderr?.on("data", (data) => {
@@ -3070,7 +3143,8 @@ ${plan}
3070
3143
  );
3071
3144
  const systemPromptFile = activeProcess ? void 0 : buildAppendedSystemPrompt(
3072
3145
  cwd,
3073
- self.config.multiStepContinuation !== false
3146
+ self.config.multiStepContinuation !== false,
3147
+ extractSystemMessages(options.prompt)
3074
3148
  );
3075
3149
  cliArgs = buildCliArgs({
3076
3150
  sessionKey: sk,
@@ -4157,6 +4231,16 @@ var defaultModels = {
4157
4231
  output: 16384,
4158
4232
  cost: opusCost,
4159
4233
  releaseDate: "2025-07-16"
4234
+ }),
4235
+ "claude-opus-4-8": defineModel({
4236
+ id: "claude-opus-4-8",
4237
+ name: "Claude Opus 4.8",
4238
+ family: "opus",
4239
+ reasoning: true,
4240
+ context: 1e6,
4241
+ output: 16384,
4242
+ cost: opusCost,
4243
+ releaseDate: "2026-05-28"
4160
4244
  })
4161
4245
  };
4162
4246
 
@@ -4662,11 +4746,16 @@ var server = async (input) => {
4662
4746
  });
4663
4747
  if (typeof providerID !== "string") return;
4664
4748
  if (providerID !== PROVIDER_ID2 && !providerID.startsWith(`${PROVIDER_ID2}-`)) return;
4749
+ if (typeof input2.sessionID === "string" && input2.sessionID.length > 0) {
4750
+ output.options ??= {};
4751
+ output.options.opencodeSessionID = input2.sessionID;
4752
+ }
4665
4753
  if (!input2.agent) return;
4666
4754
  output.options ??= {};
4667
4755
  output.options.opencodeAgent = input2.agent;
4668
4756
  log.debug("chat.params tagged providerOptions", {
4669
4757
  agent: input2.agent,
4758
+ sessionID: input2.sessionID,
4670
4759
  providerID
4671
4760
  });
4672
4761
  }