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

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.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,
@@ -4662,11 +4736,16 @@ var server = async (input) => {
4662
4736
  });
4663
4737
  if (typeof providerID !== "string") return;
4664
4738
  if (providerID !== PROVIDER_ID2 && !providerID.startsWith(`${PROVIDER_ID2}-`)) return;
4739
+ if (typeof input2.sessionID === "string" && input2.sessionID.length > 0) {
4740
+ output.options ??= {};
4741
+ output.options.opencodeSessionID = input2.sessionID;
4742
+ }
4665
4743
  if (!input2.agent) return;
4666
4744
  output.options ??= {};
4667
4745
  output.options.opencodeAgent = input2.agent;
4668
4746
  log.debug("chat.params tagged providerOptions", {
4669
4747
  agent: input2.agent,
4748
+ sessionID: input2.sessionID,
4670
4749
  providerID
4671
4750
  });
4672
4751
  }