@khalilgharbaoui/opencode-claude-code-plugin 0.4.4 → 0.4.6
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 +2 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +227 -23
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -172,6 +172,7 @@ The account model IDs are internally suffixed, for example `claude-sonnet-4-6@wo
|
|
|
172
172
|
| `strictMcpConfig` | boolean | `false` | Pass `--strict-mcp-config` so Claude loads **only** the configured servers and ignores `~/.claude/settings.json`. |
|
|
173
173
|
| `webSearch` | `"claude"` \| `"disabled"` \| `<tool>` | `"claude"` | Routing for Claude's built-in `WebSearch`. See [WebSearch routing](#websearch-routing). |
|
|
174
174
|
| `multiStepContinuation` | boolean | `true` | Append a system-prompt hint nudging Claude to chain tool calls within one turn instead of pausing between subtasks. Each opencode turn boundary requires the user to manually press "continue", so for multi-step tasks this reduces friction. Set `false` to disable. |
|
|
175
|
+
| `autoContinueIncompleteTurns` | boolean \| `"smart"` | `"smart"` | Smartly continue incomplete Claude CLI results inside the same opencode turn. Reduces manual "continue" presses when Claude ends after reasoning/tool activity without a useful final answer. Set `false` to disable. |
|
|
175
176
|
|
|
176
177
|
### Overriding model metadata
|
|
177
178
|
|
|
@@ -311,6 +312,7 @@ Set `permissionMode: "plan"` to forward `--permission-mode plan` to Claude. The
|
|
|
311
312
|
## Quirks worth knowing
|
|
312
313
|
|
|
313
314
|
- **Empty text blocks are dropped.** Claude sometimes opens a `content_block_start` for text but never sends a delta. The plugin no longer emits the empty block (which was triggering Anthropic 400s like `cache_control cannot be set for empty text blocks`).
|
|
315
|
+
- **Smart incomplete-turn continuation.** By default, the plugin keeps the current opencode stream open and feeds Claude CLI a small internal continuation message when Claude emits a `result` after reasoning/tool activity without a useful visible answer. It still stops normally on final-looking answers, questions, blockers, errors, aborts, or internal safety-budget exhaustion. Disable with `"autoContinueIncompleteTurns": false`.
|
|
314
316
|
- **`AskUserQuestion`** from the CLI is converted into plain text content rather than forwarded as a tool call.
|
|
315
317
|
- **Wire-inactivity watchdog.** Once the CLI has produced any content, the stream closes gracefully if stdout goes silent for 60 seconds without a `result` message arriving. Resets on every line received, so long mid-turn pauses (Sonnet between text-end and the next tool_use, for example) are tolerated. On a user-initiated abort, the watchdog shortens to 5 seconds.
|
|
316
318
|
- **Per-iteration usage.** When the CLI internally retries with tools, the plugin only counts the last iteration's usage so opencode's context accounting stays accurate.
|
package/dist/index.d.ts
CHANGED
|
@@ -116,6 +116,7 @@ interface ClaudeCodeConfig {
|
|
|
116
116
|
hotReloadMcp?: boolean;
|
|
117
117
|
proxyOpencodeMcpTools?: boolean;
|
|
118
118
|
multiStepContinuation?: boolean;
|
|
119
|
+
autoContinueIncompleteTurns?: boolean | "smart";
|
|
119
120
|
}
|
|
120
121
|
type WebSearchRouting = "claude" | "disabled" | (string & {});
|
|
121
122
|
interface ClaudeCodeProviderSettings {
|
|
@@ -219,6 +220,19 @@ interface ClaudeCodeProviderSettings {
|
|
|
219
220
|
* decides when to end the turn entirely on its own).
|
|
220
221
|
*/
|
|
221
222
|
multiStepContinuation?: boolean;
|
|
223
|
+
/**
|
|
224
|
+
* Smartly continue incomplete Claude CLI results inside the same opencode
|
|
225
|
+
* turn. Claude CLI sometimes emits `result` after reasoning/tool activity
|
|
226
|
+
* without a useful final answer, which makes opencode stop and wait for the
|
|
227
|
+
* user to type "continue". With the default `"smart"`, the plugin detects
|
|
228
|
+
* those incomplete result boundaries, feeds Claude a small continuation
|
|
229
|
+
* message internally, and keeps the opencode stream open. Final answers,
|
|
230
|
+
* questions, blockers, errors, aborts, and safety-budget exhaustion still
|
|
231
|
+
* stop normally.
|
|
232
|
+
*
|
|
233
|
+
* Set to `false` to disable.
|
|
234
|
+
*/
|
|
235
|
+
autoContinueIncompleteTurns?: boolean | "smart";
|
|
222
236
|
}
|
|
223
237
|
type PermissionMode = "acceptEdits" | "auto" | "bypassPermissions" | "default" | "dontAsk" | "plan";
|
|
224
238
|
type ControlRequestBehavior = "allow" | "deny";
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,33 @@
|
|
|
2
2
|
import { generateId } from "@ai-sdk/provider-utils";
|
|
3
3
|
|
|
4
4
|
// src/logger.ts
|
|
5
|
+
import { appendFileSync, mkdirSync, renameSync, statSync } from "fs";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
import { dirname, join } from "path";
|
|
5
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");
|
|
11
|
+
var MAX_LOG_BYTES = 5 * 1024 * 1024;
|
|
12
|
+
var fileLoggingDisabled = false;
|
|
13
|
+
function rotateIfNeeded() {
|
|
14
|
+
try {
|
|
15
|
+
const stat = statSync(LOG_FILE);
|
|
16
|
+
if (stat.size > MAX_LOG_BYTES) {
|
|
17
|
+
renameSync(LOG_FILE, `${LOG_FILE}.1`);
|
|
18
|
+
}
|
|
19
|
+
} catch {
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function writeToFile(line) {
|
|
23
|
+
if (fileLoggingDisabled) return;
|
|
24
|
+
try {
|
|
25
|
+
mkdirSync(dirname(LOG_FILE), { recursive: true });
|
|
26
|
+
rotateIfNeeded();
|
|
27
|
+
appendFileSync(LOG_FILE, line + "\n", "utf8");
|
|
28
|
+
} catch {
|
|
29
|
+
fileLoggingDisabled = true;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
6
32
|
function fmt(level, msg, data) {
|
|
7
33
|
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
8
34
|
const base = `[${ts}] [opencode-claude-code] ${level}: ${msg}`;
|
|
@@ -11,21 +37,30 @@ function fmt(level, msg, data) {
|
|
|
11
37
|
}
|
|
12
38
|
return base;
|
|
13
39
|
}
|
|
40
|
+
function emit(level, msg, data, alwaysStderr = false) {
|
|
41
|
+
const line = fmt(level, msg, data);
|
|
42
|
+
if (alwaysStderr || DEBUG) {
|
|
43
|
+
console.error(line);
|
|
44
|
+
}
|
|
45
|
+
writeToFile(line);
|
|
46
|
+
}
|
|
14
47
|
var log = {
|
|
15
48
|
info(msg, data) {
|
|
16
|
-
if (DEBUG)
|
|
49
|
+
if (DEBUG) emit("INFO", msg, data);
|
|
50
|
+
else writeToFile(fmt("INFO", msg, data));
|
|
17
51
|
},
|
|
18
52
|
notice(msg, data) {
|
|
19
|
-
|
|
53
|
+
emit("NOTICE", msg, data, true);
|
|
20
54
|
},
|
|
21
55
|
warn(msg, data) {
|
|
22
|
-
|
|
56
|
+
emit("WARN", msg, data, true);
|
|
23
57
|
},
|
|
24
58
|
error(msg, data) {
|
|
25
|
-
|
|
59
|
+
emit("ERROR", msg, data, true);
|
|
26
60
|
},
|
|
27
61
|
debug(msg, data) {
|
|
28
|
-
if (DEBUG)
|
|
62
|
+
if (DEBUG) emit("DEBUG", msg, data);
|
|
63
|
+
else writeToFile(fmt("DEBUG", msg, data));
|
|
29
64
|
}
|
|
30
65
|
};
|
|
31
66
|
|
|
@@ -1460,9 +1495,9 @@ function rejectAllPendingProxyCallsForSession(sessionKey2, error) {
|
|
|
1460
1495
|
// src/claude-code-language-model.ts
|
|
1461
1496
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
1462
1497
|
import { unlink as unlink2 } from "fs/promises";
|
|
1463
|
-
import { homedir as
|
|
1498
|
+
import { homedir as homedir3, tmpdir as tmpdir2 } from "os";
|
|
1464
1499
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
1465
|
-
import { dirname as
|
|
1500
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
1466
1501
|
function hasNewUserContent(prompt) {
|
|
1467
1502
|
for (let i = prompt.length - 1; i >= 0; i--) {
|
|
1468
1503
|
const msg = prompt[i];
|
|
@@ -1483,6 +1518,78 @@ function hasNewUserContent(prompt) {
|
|
|
1483
1518
|
}
|
|
1484
1519
|
return false;
|
|
1485
1520
|
}
|
|
1521
|
+
var AUTO_CONTINUE_MAX_ATTEMPTS = 8;
|
|
1522
|
+
var AUTO_CONTINUE_MAX_ELAPSED_MS = 10 * 60 * 1e3;
|
|
1523
|
+
var AUTO_CONTINUE_NO_PROGRESS_LIMIT = 2;
|
|
1524
|
+
var AUTO_CONTINUE_PROMPT = "Continue the task from where you stopped. Do not summarize; keep working until the requested task is complete, you need clarification, or you hit a real blocker.";
|
|
1525
|
+
function normalizeVisibleText(text) {
|
|
1526
|
+
return text.replace(/\s+/g, " ").trim();
|
|
1527
|
+
}
|
|
1528
|
+
function looksLikeQuestion(text) {
|
|
1529
|
+
const normalized = normalizeVisibleText(text).toLowerCase();
|
|
1530
|
+
if (!normalized) return false;
|
|
1531
|
+
if (normalized.endsWith("?")) return true;
|
|
1532
|
+
return /\b(please confirm|can you confirm|should i|would you like|do you want|which option|choose|pick one|need your|need you to|what would you like)\b/.test(normalized);
|
|
1533
|
+
}
|
|
1534
|
+
function looksLikeBlocker(text) {
|
|
1535
|
+
const normalized = normalizeVisibleText(text).toLowerCase();
|
|
1536
|
+
if (!normalized) return false;
|
|
1537
|
+
return /\b(blocked|blocker|cannot proceed|can't proceed|unable to proceed|need clarification|need more information|permission denied|failed and needs|requires your|manual step|required from you)\b/.test(normalized);
|
|
1538
|
+
}
|
|
1539
|
+
function looksLikeFinalAnswer(text) {
|
|
1540
|
+
const normalized = normalizeVisibleText(text).toLowerCase();
|
|
1541
|
+
if (normalized.length < 40) return false;
|
|
1542
|
+
if (looksLikeQuestion(normalized) || looksLikeBlocker(normalized)) return false;
|
|
1543
|
+
return /\b(done|completed|fixed|implemented|verified|published|released|sent|delivered|updated)\b/.test(normalized) || /\b(checks?|tests?) passed\b/.test(normalized) || /\b(summary|what changed|verification)\b/.test(normalized);
|
|
1544
|
+
}
|
|
1545
|
+
function continuationSignature(snapshot) {
|
|
1546
|
+
const text = normalizeVisibleText(snapshot.text).slice(-500);
|
|
1547
|
+
return JSON.stringify({
|
|
1548
|
+
text,
|
|
1549
|
+
reasoning: snapshot.hadReasoning,
|
|
1550
|
+
tools: snapshot.hadToolActivity,
|
|
1551
|
+
proxy: snapshot.hadProxyActivity
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1554
|
+
function shouldAutoContinueIncompleteTurn(state, snapshot) {
|
|
1555
|
+
if (state.enabled === false) return { continue: false, reason: "disabled" };
|
|
1556
|
+
if (snapshot.isError) return { continue: false, reason: "error" };
|
|
1557
|
+
if (state.aborted) return { continue: false, reason: "aborted" };
|
|
1558
|
+
if (state.attempts >= AUTO_CONTINUE_MAX_ATTEMPTS) {
|
|
1559
|
+
return { continue: false, reason: "max-attempts" };
|
|
1560
|
+
}
|
|
1561
|
+
const now = snapshot.now ?? Date.now();
|
|
1562
|
+
if (now - state.startedAt > AUTO_CONTINUE_MAX_ELAPSED_MS) {
|
|
1563
|
+
return { continue: false, reason: "max-elapsed" };
|
|
1564
|
+
}
|
|
1565
|
+
const text = normalizeVisibleText(snapshot.text);
|
|
1566
|
+
const lastText = normalizeVisibleText(snapshot.lastVisibleText);
|
|
1567
|
+
if (looksLikeQuestion(text)) return { continue: false, reason: "question" };
|
|
1568
|
+
if (looksLikeBlocker(text)) return { continue: false, reason: "blocker" };
|
|
1569
|
+
if (looksLikeFinalAnswer(lastText)) {
|
|
1570
|
+
return { continue: false, reason: "final-answer" };
|
|
1571
|
+
}
|
|
1572
|
+
const hadActivity = snapshot.hadReasoning || snapshot.hadToolActivity || snapshot.hadProxyActivity;
|
|
1573
|
+
if (!hadActivity) return { continue: false, reason: "no-activity" };
|
|
1574
|
+
const signature = continuationSignature(snapshot);
|
|
1575
|
+
const noProgress = signature === state.lastSignature;
|
|
1576
|
+
if (noProgress && state.noProgressCount + 1 >= AUTO_CONTINUE_NO_PROGRESS_LIMIT) {
|
|
1577
|
+
return { continue: false, reason: "no-progress" };
|
|
1578
|
+
}
|
|
1579
|
+
if (!text) {
|
|
1580
|
+
return { continue: true, reason: "activity-without-visible-answer" };
|
|
1581
|
+
}
|
|
1582
|
+
return { continue: true, reason: "non-final-progress" };
|
|
1583
|
+
}
|
|
1584
|
+
function makeAutoContinueMessage() {
|
|
1585
|
+
return JSON.stringify({
|
|
1586
|
+
type: "user",
|
|
1587
|
+
message: {
|
|
1588
|
+
role: "user",
|
|
1589
|
+
content: [{ type: "text", text: AUTO_CONTINUE_PROMPT }]
|
|
1590
|
+
}
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1486
1593
|
function readPromptFileIfPresent(path5) {
|
|
1487
1594
|
try {
|
|
1488
1595
|
const content = readFileSync2(path5, "utf8").trim();
|
|
@@ -1494,9 +1601,9 @@ function readPromptFileIfPresent(path5) {
|
|
|
1494
1601
|
function nearestWorkspaceAgentsPrompt(cwd) {
|
|
1495
1602
|
let dir = cwd;
|
|
1496
1603
|
while (true) {
|
|
1497
|
-
const content = readPromptFileIfPresent(
|
|
1604
|
+
const content = readPromptFileIfPresent(join5(dir, "AGENTS.md"));
|
|
1498
1605
|
if (content) return content;
|
|
1499
|
-
const parent =
|
|
1606
|
+
const parent = dirname3(dir);
|
|
1500
1607
|
if (parent === dir) return void 0;
|
|
1501
1608
|
dir = parent;
|
|
1502
1609
|
}
|
|
@@ -1511,15 +1618,15 @@ blocker. The user can interrupt or abort at any time; turn endings should
|
|
|
1511
1618
|
mark meaningful checkpoints, not every completed substep.`;
|
|
1512
1619
|
function buildAppendedSystemPrompt(cwd, includeMultiStepHint = true) {
|
|
1513
1620
|
const parts = [];
|
|
1514
|
-
const configRoot = process.env.XDG_CONFIG_HOME ??
|
|
1515
|
-
const globalAgents = readPromptFileIfPresent(
|
|
1621
|
+
const configRoot = process.env.XDG_CONFIG_HOME ?? join5(homedir3(), ".config");
|
|
1622
|
+
const globalAgents = readPromptFileIfPresent(join5(configRoot, "opencode", "AGENTS.md"));
|
|
1516
1623
|
const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
|
|
1517
1624
|
if (globalAgents) parts.push(globalAgents);
|
|
1518
1625
|
if (workspaceAgents && workspaceAgents !== globalAgents) parts.push(workspaceAgents);
|
|
1519
1626
|
if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
|
|
1520
1627
|
const content = parts.join("\n\n");
|
|
1521
1628
|
if (!content) return void 0;
|
|
1522
|
-
const path5 =
|
|
1629
|
+
const path5 = join5(tmpdir2(), `opencode-cc-sys-${randomUUID2()}.md`);
|
|
1523
1630
|
try {
|
|
1524
1631
|
writeFileSync3(path5, content, "utf8");
|
|
1525
1632
|
return path5;
|
|
@@ -2420,6 +2527,17 @@ ${plan}
|
|
|
2420
2527
|
let pendingProxyUnsubscribe = null;
|
|
2421
2528
|
let resultFallbackTimer = null;
|
|
2422
2529
|
let hasReceivedContent = false;
|
|
2530
|
+
let visibleTextSinceContinue = "";
|
|
2531
|
+
let lastVisibleTextSinceContinue = "";
|
|
2532
|
+
let hadReasoningSinceContinue = false;
|
|
2533
|
+
let hadToolActivitySinceContinue = false;
|
|
2534
|
+
let hadProxyActivitySinceContinue = false;
|
|
2535
|
+
const autoContinueState = {
|
|
2536
|
+
enabled: self.config.autoContinueIncompleteTurns,
|
|
2537
|
+
attempts: 0,
|
|
2538
|
+
startedAt: Date.now(),
|
|
2539
|
+
noProgressCount: 0
|
|
2540
|
+
};
|
|
2423
2541
|
const clearFallbackTimer = () => {
|
|
2424
2542
|
if (resultFallbackTimer) {
|
|
2425
2543
|
clearTimeout(resultFallbackTimer);
|
|
@@ -2492,6 +2610,29 @@ ${plan}
|
|
|
2492
2610
|
});
|
|
2493
2611
|
finishWithToolCalls(batch);
|
|
2494
2612
|
};
|
|
2613
|
+
const noteVisibleText = (text) => {
|
|
2614
|
+
visibleTextSinceContinue += text;
|
|
2615
|
+
lastVisibleTextSinceContinue += text;
|
|
2616
|
+
};
|
|
2617
|
+
const resetLastVisibleTextBlock = () => {
|
|
2618
|
+
lastVisibleTextSinceContinue = "";
|
|
2619
|
+
};
|
|
2620
|
+
const noteReasoning = () => {
|
|
2621
|
+
hadReasoningSinceContinue = true;
|
|
2622
|
+
};
|
|
2623
|
+
const noteToolActivity = () => {
|
|
2624
|
+
hadToolActivitySinceContinue = true;
|
|
2625
|
+
};
|
|
2626
|
+
const noteProxyActivity = () => {
|
|
2627
|
+
hadProxyActivitySinceContinue = true;
|
|
2628
|
+
};
|
|
2629
|
+
const resetAutoContinueWindow = () => {
|
|
2630
|
+
visibleTextSinceContinue = "";
|
|
2631
|
+
lastVisibleTextSinceContinue = "";
|
|
2632
|
+
hadReasoningSinceContinue = false;
|
|
2633
|
+
hadToolActivitySinceContinue = false;
|
|
2634
|
+
hadProxyActivitySinceContinue = false;
|
|
2635
|
+
};
|
|
2495
2636
|
let gotPartialEvents = false;
|
|
2496
2637
|
const lineHandler = (line) => {
|
|
2497
2638
|
if (!line.trim()) return;
|
|
@@ -2522,6 +2663,7 @@ ${plan}
|
|
|
2522
2663
|
const block = msg.content_block;
|
|
2523
2664
|
const idx = msg.index;
|
|
2524
2665
|
if (block.type === "thinking") {
|
|
2666
|
+
noteReasoning();
|
|
2525
2667
|
const reasoningId = generateId();
|
|
2526
2668
|
reasoningIds.set(idx, reasoningId);
|
|
2527
2669
|
controller.enqueue({
|
|
@@ -2532,6 +2674,7 @@ ${plan}
|
|
|
2532
2674
|
}
|
|
2533
2675
|
if (block.type === "text") {
|
|
2534
2676
|
textBlockIndices.add(idx);
|
|
2677
|
+
resetLastVisibleTextBlock();
|
|
2535
2678
|
if (block.text) {
|
|
2536
2679
|
if (!currentTextId) startTextBlock();
|
|
2537
2680
|
controller.enqueue({
|
|
@@ -2539,10 +2682,12 @@ ${plan}
|
|
|
2539
2682
|
id: currentTextId,
|
|
2540
2683
|
delta: block.text
|
|
2541
2684
|
});
|
|
2685
|
+
noteVisibleText(block.text);
|
|
2542
2686
|
hasReceivedContent = true;
|
|
2543
2687
|
}
|
|
2544
2688
|
}
|
|
2545
2689
|
if (block.type === "tool_use" && block.id && block.name) {
|
|
2690
|
+
noteToolActivity();
|
|
2546
2691
|
toolCallMap.set(idx, {
|
|
2547
2692
|
id: block.id,
|
|
2548
2693
|
name: block.name,
|
|
@@ -2574,6 +2719,7 @@ ${plan}
|
|
|
2574
2719
|
const delta = msg.delta;
|
|
2575
2720
|
const idx = msg.index;
|
|
2576
2721
|
if (delta.type === "thinking_delta" && delta.thinking) {
|
|
2722
|
+
noteReasoning();
|
|
2577
2723
|
const reasoningId = reasoningIds.get(idx);
|
|
2578
2724
|
if (reasoningId) {
|
|
2579
2725
|
controller.enqueue({
|
|
@@ -2590,6 +2736,7 @@ ${plan}
|
|
|
2590
2736
|
id: currentTextId,
|
|
2591
2737
|
delta: delta.text
|
|
2592
2738
|
});
|
|
2739
|
+
noteVisibleText(delta.text);
|
|
2593
2740
|
hasReceivedContent = true;
|
|
2594
2741
|
}
|
|
2595
2742
|
if (delta.type === "input_json_delta" && delta.partial_json) {
|
|
@@ -2712,6 +2859,7 @@ ${plan}
|
|
|
2712
2859
|
}
|
|
2713
2860
|
for (const block of msg.message.content) {
|
|
2714
2861
|
if (block.type === "text" && block.text) {
|
|
2862
|
+
resetLastVisibleTextBlock();
|
|
2715
2863
|
const blockId = startTextBlock();
|
|
2716
2864
|
controller.enqueue({
|
|
2717
2865
|
type: "text-delta",
|
|
@@ -2719,9 +2867,11 @@ ${plan}
|
|
|
2719
2867
|
delta: block.text
|
|
2720
2868
|
});
|
|
2721
2869
|
endTextBlock();
|
|
2870
|
+
noteVisibleText(block.text);
|
|
2722
2871
|
hasReceivedContent = true;
|
|
2723
2872
|
}
|
|
2724
2873
|
if (block.type === "thinking" && block.thinking) {
|
|
2874
|
+
noteReasoning();
|
|
2725
2875
|
const thinkingId = generateId();
|
|
2726
2876
|
controller.enqueue({
|
|
2727
2877
|
type: "reasoning-start",
|
|
@@ -2738,6 +2888,7 @@ ${plan}
|
|
|
2738
2888
|
});
|
|
2739
2889
|
}
|
|
2740
2890
|
if (block.type === "tool_use" && block.id && block.name) {
|
|
2891
|
+
noteToolActivity();
|
|
2741
2892
|
const parsedInput = block.input ?? {};
|
|
2742
2893
|
toolCallsById.set(block.id, {
|
|
2743
2894
|
id: block.id,
|
|
@@ -2851,6 +3002,7 @@ ${plan}
|
|
|
2851
3002
|
},
|
|
2852
3003
|
providerExecuted: true
|
|
2853
3004
|
});
|
|
3005
|
+
noteToolActivity();
|
|
2854
3006
|
log.info("tool result emitted", {
|
|
2855
3007
|
toolUseId: block.tool_use_id,
|
|
2856
3008
|
name: toolCall.name
|
|
@@ -2914,6 +3066,54 @@ ${plan}
|
|
|
2914
3066
|
)
|
|
2915
3067
|
);
|
|
2916
3068
|
}
|
|
3069
|
+
const autoDecision = shouldAutoContinueIncompleteTurn(
|
|
3070
|
+
autoContinueState,
|
|
3071
|
+
{
|
|
3072
|
+
text: visibleTextSinceContinue,
|
|
3073
|
+
lastVisibleText: lastVisibleTextSinceContinue,
|
|
3074
|
+
hadReasoning: hadReasoningSinceContinue,
|
|
3075
|
+
hadToolActivity: hadToolActivitySinceContinue,
|
|
3076
|
+
hadProxyActivity: hadProxyActivitySinceContinue,
|
|
3077
|
+
isError: msg.is_error
|
|
3078
|
+
}
|
|
3079
|
+
);
|
|
3080
|
+
if (autoDecision.continue) {
|
|
3081
|
+
const signature = continuationSignature({
|
|
3082
|
+
text: visibleTextSinceContinue,
|
|
3083
|
+
lastVisibleText: lastVisibleTextSinceContinue,
|
|
3084
|
+
hadReasoning: hadReasoningSinceContinue,
|
|
3085
|
+
hadToolActivity: hadToolActivitySinceContinue,
|
|
3086
|
+
hadProxyActivity: hadProxyActivitySinceContinue,
|
|
3087
|
+
isError: msg.is_error
|
|
3088
|
+
});
|
|
3089
|
+
autoContinueState.noProgressCount = signature === autoContinueState.lastSignature ? autoContinueState.noProgressCount + 1 : 0;
|
|
3090
|
+
autoContinueState.lastSignature = signature;
|
|
3091
|
+
autoContinueState.attempts++;
|
|
3092
|
+
log.notice("auto-continuing incomplete claude result", {
|
|
3093
|
+
sessionKey: sk,
|
|
3094
|
+
reason: autoDecision.reason,
|
|
3095
|
+
attempts: autoContinueState.attempts,
|
|
3096
|
+
textLength: visibleTextSinceContinue.length,
|
|
3097
|
+
lastTextLength: lastVisibleTextSinceContinue.length,
|
|
3098
|
+
hadReasoning: hadReasoningSinceContinue,
|
|
3099
|
+
hadToolActivity: hadToolActivitySinceContinue,
|
|
3100
|
+
hadProxyActivity: hadProxyActivitySinceContinue
|
|
3101
|
+
});
|
|
3102
|
+
turnCompleted = false;
|
|
3103
|
+
resetAutoContinueWindow();
|
|
3104
|
+
proc.stdin?.write(makeAutoContinueMessage() + "\n");
|
|
3105
|
+
return;
|
|
3106
|
+
}
|
|
3107
|
+
log.notice("auto-continuation stopped", {
|
|
3108
|
+
sessionKey: sk,
|
|
3109
|
+
reason: autoDecision.reason,
|
|
3110
|
+
attempts: autoContinueState.attempts,
|
|
3111
|
+
textLength: visibleTextSinceContinue.length,
|
|
3112
|
+
lastTextLength: lastVisibleTextSinceContinue.length,
|
|
3113
|
+
hadReasoning: hadReasoningSinceContinue,
|
|
3114
|
+
hadToolActivity: hadToolActivitySinceContinue,
|
|
3115
|
+
hadProxyActivity: hadProxyActivitySinceContinue
|
|
3116
|
+
});
|
|
2917
3117
|
for (const [idx, reasoningId] of reasoningIds) {
|
|
2918
3118
|
if (reasoningStarted.get(idx)) {
|
|
2919
3119
|
controller.enqueue({
|
|
@@ -3036,6 +3236,8 @@ ${plan}
|
|
|
3036
3236
|
toolCallId: call.toolCallId,
|
|
3037
3237
|
toolName: call.toolName
|
|
3038
3238
|
});
|
|
3239
|
+
noteProxyActivity();
|
|
3240
|
+
noteToolActivity();
|
|
3039
3241
|
drainBuffer.push(call);
|
|
3040
3242
|
if (drainTimer) clearTimeout(drainTimer);
|
|
3041
3243
|
drainTimer = setTimeout(drainNow, DRAIN_QUIET_MS);
|
|
@@ -3043,6 +3245,7 @@ ${plan}
|
|
|
3043
3245
|
proc.on("error", procErrorHandler);
|
|
3044
3246
|
if (options.abortSignal) {
|
|
3045
3247
|
options.abortSignal.addEventListener("abort", () => {
|
|
3248
|
+
autoContinueState.aborted = true;
|
|
3046
3249
|
if (turnCompleted || controllerClosed) return;
|
|
3047
3250
|
if (!hasReceivedContent) {
|
|
3048
3251
|
log.info(
|
|
@@ -3417,8 +3620,8 @@ import {
|
|
|
3417
3620
|
rmSync as rmSync2,
|
|
3418
3621
|
writeFileSync as writeFileSync4
|
|
3419
3622
|
} from "fs";
|
|
3420
|
-
import { homedir as
|
|
3421
|
-
import { join as
|
|
3623
|
+
import { homedir as homedir4 } from "os";
|
|
3624
|
+
import { join as join6, resolve as resolve2 } from "path";
|
|
3422
3625
|
import { fileURLToPath } from "url";
|
|
3423
3626
|
var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
|
|
3424
3627
|
var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
|
|
@@ -3426,14 +3629,14 @@ var alreadyRan = false;
|
|
|
3426
3629
|
function candidateCacheRoots() {
|
|
3427
3630
|
const xdg = process.env.XDG_CACHE_HOME;
|
|
3428
3631
|
return [
|
|
3429
|
-
xdg ?
|
|
3430
|
-
|
|
3431
|
-
|
|
3632
|
+
xdg ? join6(xdg, "opencode") : null,
|
|
3633
|
+
join6(homedir4(), ".cache", "opencode"),
|
|
3634
|
+
join6(homedir4(), "Library", "Caches", "opencode")
|
|
3432
3635
|
].filter((p) => Boolean(p));
|
|
3433
3636
|
}
|
|
3434
3637
|
function userOpencodeJsonPath() {
|
|
3435
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME ??
|
|
3436
|
-
return
|
|
3638
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME ?? join6(homedir4(), ".config");
|
|
3639
|
+
return join6(xdgConfig, "opencode", "opencode.json");
|
|
3437
3640
|
}
|
|
3438
3641
|
function userIntendsToUseUnscoped() {
|
|
3439
3642
|
const cfg = userOpencodeJsonPath();
|
|
@@ -3476,7 +3679,7 @@ function cleanupStaleUnscopedInstall() {
|
|
|
3476
3679
|
}
|
|
3477
3680
|
function cleanupOne(cacheRoot, ourDir) {
|
|
3478
3681
|
if (!existsSync3(cacheRoot)) return;
|
|
3479
|
-
const stalePath =
|
|
3682
|
+
const stalePath = join6(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
|
|
3480
3683
|
if (!existsSync3(stalePath)) return;
|
|
3481
3684
|
let realStalePath = stalePath;
|
|
3482
3685
|
try {
|
|
@@ -3484,7 +3687,7 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
3484
3687
|
} catch {
|
|
3485
3688
|
}
|
|
3486
3689
|
if (ourDir && realStalePath === ourDir) return;
|
|
3487
|
-
const pkgJsonPath =
|
|
3690
|
+
const pkgJsonPath = join6(stalePath, "package.json");
|
|
3488
3691
|
if (!existsSync3(pkgJsonPath)) return;
|
|
3489
3692
|
let pkg = {};
|
|
3490
3693
|
try {
|
|
@@ -3504,7 +3707,7 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
3504
3707
|
});
|
|
3505
3708
|
return;
|
|
3506
3709
|
}
|
|
3507
|
-
const cachePkgJson =
|
|
3710
|
+
const cachePkgJson = join6(cacheRoot, "package.json");
|
|
3508
3711
|
if (!existsSync3(cachePkgJson)) return;
|
|
3509
3712
|
try {
|
|
3510
3713
|
const cfg = JSON.parse(readFileSync3(cachePkgJson, "utf8"));
|
|
@@ -3556,7 +3759,8 @@ function createClaudeCode(settings = {}) {
|
|
|
3556
3759
|
webSearch: settings.webSearch,
|
|
3557
3760
|
hotReloadMcp: settings.hotReloadMcp ?? true,
|
|
3558
3761
|
proxyOpencodeMcpTools: settings.proxyOpencodeMcpTools ?? true,
|
|
3559
|
-
multiStepContinuation: settings.multiStepContinuation ?? true
|
|
3762
|
+
multiStepContinuation: settings.multiStepContinuation ?? true,
|
|
3763
|
+
autoContinueIncompleteTurns: settings.autoContinueIncompleteTurns ?? "smart"
|
|
3560
3764
|
});
|
|
3561
3765
|
};
|
|
3562
3766
|
const provider = function(modelId) {
|