@khalilgharbaoui/opencode-claude-code-plugin 0.4.16 → 0.4.18

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
@@ -96,6 +96,9 @@ type OpenCodeHooks = {
96
96
  };
97
97
  type OpenCodePlugin = (input: unknown, options?: Record<string, unknown>) => Promise<OpenCodeHooks>;
98
98
 
99
+ type LogLevel = "debug" | "info" | "notice" | "warn" | "error";
100
+ type LogMode = "silent" | "debug";
101
+
99
102
  interface ClaudeCodeConfig {
100
103
  provider: string;
101
104
  cliPath: string;
@@ -117,6 +120,35 @@ interface ClaudeCodeConfig {
117
120
  proxyOpencodeMcpTools?: boolean;
118
121
  multiStepContinuation?: boolean;
119
122
  autoContinueIncompleteTurns?: boolean | "smart";
123
+ logging?: LoggingConfig;
124
+ }
125
+ interface LoggingConfig {
126
+ /**
127
+ * Persist log activity (DEBUG / INFO / NOTICE / WARN / ERROR — those
128
+ * passing `level`) to a file. Default: `false`. When `false`, entries
129
+ * below WARN vanish entirely; WARN / ERROR still surface in the TUI via
130
+ * stderr. Set to `true` to capture the audit trail to disk for review
131
+ * via `tail` / `grep`.
132
+ */
133
+ file?: boolean;
134
+ /**
135
+ * Optional custom directory for the file log. Defaults to
136
+ * `~/.local/share/opencode-claude-code/`. Has no effect when `file:false`.
137
+ */
138
+ dir?: string;
139
+ /**
140
+ * TUI policy. `"silent"` (default) routes DEBUG / INFO / NOTICE to file
141
+ * only; WARN / ERROR still bubble in the TUI as they always do. `"debug"`
142
+ * additionally echoes every emitted level to stderr (which opencode's TUI
143
+ * surfaces as warning bubbles).
144
+ */
145
+ mode?: LogMode;
146
+ /**
147
+ * Minimum level to emit anywhere. Anything below the threshold is dropped
148
+ * before either destination decides what to do. Order:
149
+ * `debug` < `info` < `notice` < `warn` < `error`. Default: `"info"`.
150
+ */
151
+ level?: LogLevel;
120
152
  }
121
153
  type WebSearchRouting = "claude" | "disabled" | (string & {});
122
154
  interface ClaudeCodeProviderSettings {
@@ -233,6 +265,14 @@ interface ClaudeCodeProviderSettings {
233
265
  * Set to `false` to disable.
234
266
  */
235
267
  autoContinueIncompleteTurns?: boolean | "smart";
268
+ /**
269
+ * Logger configuration. See `LoggingConfig` for fields. Env vars
270
+ * (`OPENCODE_CLAUDE_CODE_LOG_FILE`, `OPENCODE_CLAUDE_CODE_LOG_DIR`,
271
+ * `OPENCODE_CLAUDE_CODE_LOG_LEVEL`, `DEBUG=opencode-claude-code`) override
272
+ * these values when explicitly set, so a developer can flip behavior for
273
+ * one process without editing opencode.jsonc.
274
+ */
275
+ logging?: LoggingConfig;
236
276
  }
237
277
  type PermissionMode = "acceptEdits" | "auto" | "bypassPermissions" | "default" | "dontAsk" | "plan";
238
278
  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;
25
+ const s = v.toLowerCase().trim();
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;
14
32
  const s = v.toLowerCase().trim();
15
- if (s === "") return false;
16
- return s !== "0" && s !== "false" && s !== "no" && s !== "off";
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
 
@@ -3791,6 +3843,14 @@ function pickOpencodeDirectory(input) {
3791
3843
  return void 0;
3792
3844
  }
3793
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
+ }
3794
3854
  const cliPath = settings.cliPath ?? process.env.CLAUDE_CLI_PATH ?? "claude";
3795
3855
  const providerName = settings.providerID ?? settings.name ?? "claude-code";
3796
3856
  const proxyTools = settings.proxyTools ?? ["Bash", "Edit", "Write", "WebFetch"];