@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 +55 -17
- package/dist/index.d.ts +39 -0
- package/dist/index.js +104 -28
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
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
|
-
##
|
|
322
|
+
## Logging
|
|
323
323
|
|
|
324
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
331
|
-
OPENCODE_CLAUDE_CODE_LOG_FILE=1 opencode
|
|
355
|
+
**Live TUI noise** (everything echoes to opencode's stderr → warning bubbles):
|
|
332
356
|
|
|
333
|
-
|
|
334
|
-
|
|
357
|
+
```jsonc
|
|
358
|
+
"logging": { "file": true, "mode": "debug" }
|
|
335
359
|
```
|
|
336
360
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
state, or other
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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
|
|
16
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
67
|
+
const stat = statSync(logFile);
|
|
23
68
|
if (stat.size > MAX_LOG_BYTES) {
|
|
24
|
-
renameSync(
|
|
69
|
+
renameSync(logFile, `${logFile}.1`);
|
|
25
70
|
}
|
|
26
71
|
} catch {
|
|
27
72
|
}
|
|
28
73
|
}
|
|
29
74
|
function writeToFile(line) {
|
|
30
|
-
if (!
|
|
75
|
+
if (!activeConfig.file) return;
|
|
31
76
|
if (fileLoggingDisabled) return;
|
|
32
77
|
try {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
58
|
-
else writeToFile(fmt("INFO", msg, data));
|
|
114
|
+
emit("info", msg, data);
|
|
59
115
|
},
|
|
60
116
|
notice(msg, data) {
|
|
61
|
-
emit("
|
|
117
|
+
emit("notice", msg, data);
|
|
62
118
|
},
|
|
63
119
|
warn(msg, data) {
|
|
64
|
-
emit("
|
|
120
|
+
emit("warn", msg, data);
|
|
65
121
|
},
|
|
66
122
|
error(msg, data) {
|
|
67
|
-
emit("
|
|
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"];
|