@khalilgharbaoui/opencode-claude-code-plugin 0.4.15 → 0.4.17

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
@@ -319,30 +319,68 @@ Set `permissionMode: "plan"` to forward `--permission-mode plan` to Claude. The
319
319
  - **Lazy `cwd`.** The working directory is re-resolved at every request, so opencode's project-aware behavior works without restarting the plugin.
320
320
  - **Variants survive merge.** opencode recalculates variant lists after the plugin loads; the plugin re-injects defaults into runtime config so your variants don't disappear.
321
321
 
322
- ## Debug logging
322
+ ## Logging
323
323
 
324
- Two independent knobs:
324
+ Configure via `opencode.jsonc` (launch-method-independent) or env vars
325
+ (temporary override for a single process). The plugin has four orthogonal
326
+ knobs:
325
327
 
326
- ```bash
327
- # Verbose logging to stderr (opencode surfaces stderr as UI warnings):
328
- DEBUG=opencode-claude-code opencode
328
+ | Field | Values | Default | Effect |
329
+ |---|---|---|---|
330
+ | `file` | `true \| false` | `false` | Persist log entries to disk |
331
+ | `dir` | path string | `~/.local/share/opencode-claude-code/` | Custom file location |
332
+ | `mode` | `"silent" \| "debug"` | `"silent"` | TUI policy |
333
+ | `level` | `"debug" \| "info" \| "notice" \| "warn" \| "error"` | `"info"` | Minimum level to emit |
334
+
335
+ Rails-style threshold: anything below `level` is dropped before either
336
+ destination decides what to do. `mode: "silent"` routes DEBUG/INFO/NOTICE
337
+ to file only and lets WARN/ERROR bubble in the TUI (they always do).
338
+ `mode: "debug"` additionally echoes every emitted level to the TUI (which
339
+ opencode surfaces as warning bubbles).
340
+
341
+ **Recommended dev setup** — capture audit trail to disk, keep TUI quiet:
342
+
343
+ ```jsonc
344
+ "@khalilgharbaoui/opencode-claude-code-plugin": {
345
+ "logging": { "file": true }
346
+ }
347
+ ```
348
+
349
+ **Full firehose for deep debugging** (every DEBUG stream event captured):
350
+
351
+ ```jsonc
352
+ "logging": { "file": true, "level": "debug" }
353
+ ```
329
354
 
330
- # Persistent file log (default: OFF file is not created at all):
331
- OPENCODE_CLAUDE_CODE_LOG_FILE=1 opencode
355
+ **Live TUI noise** (everything echoes to opencode's stderr warning bubbles):
332
356
 
333
- # Both:
334
- DEBUG=opencode-claude-code OPENCODE_CLAUDE_CODE_LOG_FILE=1 opencode
357
+ ```jsonc
358
+ "logging": { "file": true, "mode": "debug" }
335
359
  ```
336
360
 
337
- When `OPENCODE_CLAUDE_CODE_LOG_FILE` is set to any truthy value (`1`,
338
- `true`, `yes`, `on`), the plugin writes NOTICE/WARN/ERROR (plus INFO and
339
- DEBUG when `DEBUG=opencode-claude-code` is also set) to
340
- `~/.local/share/opencode-claude-code/plugin.log` with 5MB rotation. Override
341
- the directory with `OPENCODE_CLAUDE_CODE_LOG_DIR=/custom/path`.
361
+ ### Env-var overrides
362
+
363
+ Set explicitly to override config for one process — useful for one-off
364
+ debugging without editing `opencode.jsonc`:
365
+
366
+ ```bash
367
+ OPENCODE_CLAUDE_CODE_LOG_FILE=1 opencode # file on
368
+ OPENCODE_CLAUDE_CODE_LOG_FILE=0 opencode # file off (overrides config:true)
369
+ OPENCODE_CLAUDE_CODE_LOG_DIR=/tmp/cc opencode # custom dir
370
+ OPENCODE_CLAUDE_CODE_LOG_LEVEL=debug opencode # capture every level
371
+ DEBUG=opencode-claude-code opencode # promote to mode:"debug"
372
+ ```
373
+
374
+ Boolean env vars accept `1/true/on/yes` for on and `0/false/no/off` for
375
+ off; empty / unset falls through to config. Invalid `level` values fall
376
+ through to config.
377
+
378
+ ### Default behavior (no config, no env)
342
379
 
343
- Default is off so the plugin doesn't accrete a log file on every user's
344
- disk. Opt in when you need to inspect auto-continue decisions, broker
345
- state, or other plugin internals.
380
+ Nothing persists; only WARN and ERROR bubble in the TUI. The plugin
381
+ doesn't accrete a log file on every user's disk by default — opt in when
382
+ you need to inspect auto-continue decisions, broker state, or other
383
+ plugin internals.
346
384
 
347
385
  ## Known limitations
348
386
 
package/dist/index.d.ts CHANGED
@@ -117,7 +117,38 @@ interface ClaudeCodeConfig {
117
117
  proxyOpencodeMcpTools?: boolean;
118
118
  multiStepContinuation?: boolean;
119
119
  autoContinueIncompleteTurns?: boolean | "smart";
120
+ logging?: LoggingConfig;
120
121
  }
122
+ interface LoggingConfig {
123
+ /**
124
+ * Persist log activity (DEBUG / INFO / NOTICE / WARN / ERROR — those
125
+ * passing `level`) to a file. Default: `false`. When `false`, entries
126
+ * below WARN vanish entirely; WARN / ERROR still surface in the TUI via
127
+ * stderr. Set to `true` to capture the audit trail to disk for review
128
+ * via `tail` / `grep`.
129
+ */
130
+ file?: boolean;
131
+ /**
132
+ * Optional custom directory for the file log. Defaults to
133
+ * `~/.local/share/opencode-claude-code/`. Has no effect when `file:false`.
134
+ */
135
+ dir?: string;
136
+ /**
137
+ * TUI policy. `"silent"` (default) routes DEBUG / INFO / NOTICE to file
138
+ * only; WARN / ERROR still bubble in the TUI as they always do. `"debug"`
139
+ * additionally echoes every emitted level to stderr (which opencode's TUI
140
+ * surfaces as warning bubbles).
141
+ */
142
+ mode?: LogMode;
143
+ /**
144
+ * Minimum level to emit anywhere. Anything below the threshold is dropped
145
+ * before either destination decides what to do. Order:
146
+ * `debug` < `info` < `notice` < `warn` < `error`. Default: `"info"`.
147
+ */
148
+ level?: LogLevel;
149
+ }
150
+ type LogLevel = "debug" | "info" | "notice" | "warn" | "error";
151
+ type LogMode = "silent" | "debug";
121
152
  type WebSearchRouting = "claude" | "disabled" | (string & {});
122
153
  interface ClaudeCodeProviderSettings {
123
154
  cliPath?: string;
@@ -233,6 +264,14 @@ interface ClaudeCodeProviderSettings {
233
264
  * Set to `false` to disable.
234
265
  */
235
266
  autoContinueIncompleteTurns?: boolean | "smart";
267
+ /**
268
+ * Logger configuration. See `LoggingConfig` for fields. Env vars
269
+ * (`OPENCODE_CLAUDE_CODE_LOG_FILE`, `OPENCODE_CLAUDE_CODE_LOG_DIR`,
270
+ * `OPENCODE_CLAUDE_CODE_LOG_LEVEL`, `DEBUG=opencode-claude-code`) override
271
+ * these values when explicitly set, so a developer can flip behavior for
272
+ * one process without editing opencode.jsonc.
273
+ */
274
+ logging?: LoggingConfig;
236
275
  }
237
276
  type PermissionMode = "acceptEdits" | "auto" | "bypassPermissions" | "default" | "dontAsk" | "plan";
238
277
  type ControlRequestBehavior = "allow" | "deny";
package/dist/index.js CHANGED
@@ -5,34 +5,80 @@ import { generateId } from "@ai-sdk/provider-utils";
5
5
  import { appendFileSync, mkdirSync, renameSync, statSync } from "fs";
6
6
  import { homedir } from "os";
7
7
  import { dirname, join } from "path";
8
- var DEBUG = process.env.DEBUG?.includes("opencode-claude-code") ?? false;
9
- var LOG_DIR = process.env.OPENCODE_CLAUDE_CODE_LOG_DIR ?? join(homedir(), ".local", "share", "opencode-claude-code");
10
- var LOG_FILE = join(LOG_DIR, "plugin.log");
8
+ var LEVEL_RANK = {
9
+ debug: 0,
10
+ info: 1,
11
+ notice: 2,
12
+ warn: 3,
13
+ error: 4
14
+ };
11
15
  var MAX_LOG_BYTES = 5 * 1024 * 1024;
12
- function isTruthyEnv(v) {
13
- if (v == null) return false;
16
+ var DEFAULT_DIR = join(homedir(), ".local", "share", "opencode-claude-code");
17
+ var DEFAULT_CONFIG = {
18
+ file: false,
19
+ dir: null,
20
+ mode: "silent",
21
+ level: "info"
22
+ };
23
+ function parseBoolEnv(v) {
24
+ if (v == null) return void 0;
14
25
  const s = v.toLowerCase().trim();
15
- if (s === "") return false;
16
- return s !== "0" && s !== "false" && s !== "no" && s !== "off";
26
+ if (s === "") return void 0;
27
+ if (s === "0" || s === "false" || s === "no" || s === "off") return false;
28
+ return true;
29
+ }
30
+ function parseLevelEnv(v) {
31
+ if (v == null) return void 0;
32
+ const s = v.toLowerCase().trim();
33
+ if (s === "") return void 0;
34
+ if (s === "debug" || s === "info" || s === "notice" || s === "warn" || s === "error") {
35
+ return s;
36
+ }
37
+ return void 0;
17
38
  }
18
- var LOG_FILE_ENABLED = isTruthyEnv(process.env.OPENCODE_CLAUDE_CODE_LOG_FILE);
39
+ function parseModeFromDebugEnv(v) {
40
+ if (v == null || v === "") return void 0;
41
+ return v.includes("opencode-claude-code") ? "debug" : void 0;
42
+ }
43
+ function withEnvOverrides(base) {
44
+ const result = { ...base };
45
+ const envFile = parseBoolEnv(process.env.OPENCODE_CLAUDE_CODE_LOG_FILE);
46
+ if (envFile !== void 0) result.file = envFile;
47
+ const envDir = process.env.OPENCODE_CLAUDE_CODE_LOG_DIR;
48
+ if (envDir !== void 0 && envDir !== "") result.dir = envDir;
49
+ const envMode = parseModeFromDebugEnv(process.env.DEBUG);
50
+ if (envMode !== void 0) result.mode = envMode;
51
+ const envLevel = parseLevelEnv(process.env.OPENCODE_CLAUDE_CODE_LOG_LEVEL);
52
+ if (envLevel !== void 0) result.level = envLevel;
53
+ return result;
54
+ }
55
+ var activeConfig = withEnvOverrides(DEFAULT_CONFIG);
19
56
  var fileLoggingDisabled = false;
20
- function rotateIfNeeded() {
57
+ function configureLogger(input) {
58
+ const merged = { ...DEFAULT_CONFIG, ...input };
59
+ activeConfig = withEnvOverrides(merged);
60
+ fileLoggingDisabled = false;
61
+ }
62
+ function resolvedLogFile() {
63
+ return join(activeConfig.dir ?? DEFAULT_DIR, "plugin.log");
64
+ }
65
+ function rotateIfNeeded(logFile) {
21
66
  try {
22
- const stat = statSync(LOG_FILE);
67
+ const stat = statSync(logFile);
23
68
  if (stat.size > MAX_LOG_BYTES) {
24
- renameSync(LOG_FILE, `${LOG_FILE}.1`);
69
+ renameSync(logFile, `${logFile}.1`);
25
70
  }
26
71
  } catch {
27
72
  }
28
73
  }
29
74
  function writeToFile(line) {
30
- if (!LOG_FILE_ENABLED) return;
75
+ if (!activeConfig.file) return;
31
76
  if (fileLoggingDisabled) return;
32
77
  try {
33
- mkdirSync(dirname(LOG_FILE), { recursive: true });
34
- rotateIfNeeded();
35
- appendFileSync(LOG_FILE, line + "\n", "utf8");
78
+ const logFile = resolvedLogFile();
79
+ mkdirSync(dirname(logFile), { recursive: true });
80
+ rotateIfNeeded(logFile);
81
+ appendFileSync(logFile, line + "\n", "utf8");
36
82
  } catch {
37
83
  fileLoggingDisabled = true;
38
84
  }
@@ -45,30 +91,36 @@ function fmt(level, msg, data) {
45
91
  }
46
92
  return base;
47
93
  }
48
- function emit(level, msg, data, alwaysStderr = false) {
49
- const line = fmt(level, msg, data);
50
- if (alwaysStderr || DEBUG) {
94
+ function shouldEmit(level) {
95
+ return LEVEL_RANK[level] >= LEVEL_RANK[activeConfig.level];
96
+ }
97
+ function shouldTui(level) {
98
+ if (level === "warn" || level === "error") return true;
99
+ return activeConfig.mode === "debug";
100
+ }
101
+ function emit(level, msg, data) {
102
+ if (!shouldEmit(level)) return;
103
+ const line = fmt(level.toUpperCase(), msg, data);
104
+ if (shouldTui(level)) {
51
105
  console.error(line);
52
106
  }
53
107
  writeToFile(line);
54
108
  }
55
109
  var log = {
110
+ debug(msg, data) {
111
+ emit("debug", msg, data);
112
+ },
56
113
  info(msg, data) {
57
- if (DEBUG) emit("INFO", msg, data);
58
- else writeToFile(fmt("INFO", msg, data));
114
+ emit("info", msg, data);
59
115
  },
60
116
  notice(msg, data) {
61
- emit("NOTICE", msg, data, false);
117
+ emit("notice", msg, data);
62
118
  },
63
119
  warn(msg, data) {
64
- emit("WARN", msg, data, true);
120
+ emit("warn", msg, data);
65
121
  },
66
122
  error(msg, data) {
67
- emit("ERROR", msg, data, true);
68
- },
69
- debug(msg, data) {
70
- if (DEBUG) emit("DEBUG", msg, data);
71
- else writeToFile(fmt("DEBUG", msg, data));
123
+ emit("error", msg, data);
72
124
  }
73
125
  };
74
126
 
@@ -1594,6 +1646,12 @@ function shouldAutoContinueIncompleteTurn(state, snapshot) {
1594
1646
  if (state.enabled === false) return { continue: false, reason: "disabled" };
1595
1647
  if (snapshot.isError) return { continue: false, reason: "error" };
1596
1648
  if (state.aborted) return { continue: false, reason: "aborted" };
1649
+ if (snapshot.stopReason) {
1650
+ return {
1651
+ continue: false,
1652
+ reason: snapshot.stopReason.replace(/_/g, "-")
1653
+ };
1654
+ }
1597
1655
  if (state.attempts >= AUTO_CONTINUE_MAX_ATTEMPTS) {
1598
1656
  return { continue: false, reason: "max-attempts" };
1599
1657
  }
@@ -2571,6 +2629,7 @@ ${plan}
2571
2629
  let hadReasoningSinceContinue = false;
2572
2630
  let hadToolActivitySinceContinue = false;
2573
2631
  let hadProxyActivitySinceContinue = false;
2632
+ let lastStopReason = null;
2574
2633
  const autoContinueState = {
2575
2634
  enabled: self.config.autoContinueIncompleteTurns,
2576
2635
  attempts: 0,
@@ -2671,6 +2730,7 @@ ${plan}
2671
2730
  hadReasoningSinceContinue = false;
2672
2731
  hadToolActivitySinceContinue = false;
2673
2732
  hadProxyActivitySinceContinue = false;
2733
+ lastStopReason = null;
2674
2734
  };
2675
2735
  let gotPartialEvents = false;
2676
2736
  const lineHandler = (line) => {
@@ -2880,6 +2940,12 @@ ${plan}
2880
2940
  }
2881
2941
  }
2882
2942
  }
2943
+ if (gotPartialEvents && msg.type === "message_delta" && typeof msg.delta?.stop_reason === "string") {
2944
+ lastStopReason = msg.delta.stop_reason;
2945
+ }
2946
+ if (msg.type === "assistant" && msg.message && typeof msg.message.stop_reason === "string") {
2947
+ lastStopReason = msg.message.stop_reason;
2948
+ }
2883
2949
  if (msg.type === "assistant" && msg.message?.content && !gotPartialEvents) {
2884
2950
  const hasText = msg.message.content.some(
2885
2951
  (b) => b.type === "text" && b.text
@@ -3113,7 +3179,8 @@ ${plan}
3113
3179
  hadReasoning: hadReasoningSinceContinue,
3114
3180
  hadToolActivity: hadToolActivitySinceContinue,
3115
3181
  hadProxyActivity: hadProxyActivitySinceContinue,
3116
- isError: msg.is_error
3182
+ isError: msg.is_error,
3183
+ stopReason: lastStopReason
3117
3184
  }
3118
3185
  );
3119
3186
  if (autoDecision.continue) {
@@ -3146,6 +3213,7 @@ ${plan}
3146
3213
  log.notice("auto-continuation stopped", {
3147
3214
  sessionKey: sk,
3148
3215
  reason: autoDecision.reason,
3216
+ stopReason: lastStopReason,
3149
3217
  attempts: autoContinueState.attempts,
3150
3218
  textLength: visibleTextSinceContinue.length,
3151
3219
  lastTextLength: lastVisibleTextSinceContinue.length,
@@ -3775,6 +3843,14 @@ function pickOpencodeDirectory(input) {
3775
3843
  return void 0;
3776
3844
  }
3777
3845
  function createClaudeCode(settings = {}) {
3846
+ if (settings.logging) {
3847
+ configureLogger({
3848
+ file: settings.logging.file ?? false,
3849
+ dir: settings.logging.dir ?? null,
3850
+ mode: settings.logging.mode ?? "silent",
3851
+ level: settings.logging.level ?? "info"
3852
+ });
3853
+ }
3778
3854
  const cliPath = settings.cliPath ?? process.env.CLAUDE_CLI_PATH ?? "claude";
3779
3855
  const providerName = settings.providerID ?? settings.name ?? "claude-code";
3780
3856
  const proxyTools = settings.proxyTools ?? ["Bash", "Edit", "Write", "WebFetch"];