@khalilgharbaoui/opencode-claude-code-plugin 0.26.1 → 0.26.2
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 +3 -1
- package/dist/index.js +153 -49
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/claude-code-plugin/SKILL.md +8 -3
package/README.md
CHANGED
|
@@ -677,7 +677,9 @@ The same events are also what let a legitimately long call complete, which is th
|
|
|
677
677
|
|
|
678
678
|
### Per-tool proxy timeouts
|
|
679
679
|
|
|
680
|
-
Deadlines still exist, as an explicit backstop rather than the mechanism that decides when a call is over. If a tool with one has not been resolved within that many milliseconds, the call is rejected and Claude receives a timeout error.
|
|
680
|
+
Deadlines still exist, as an explicit backstop rather than the mechanism that decides when a call is over. If a tool with one has not been resolved within that many milliseconds, the call is rejected and Claude receives a timeout error.
|
|
681
|
+
|
|
682
|
+
A deadline does not count time opencode is still spending on the call. When it passes, the plugin asks opencode whether the session is still busy. If it is (a permission prompt waiting for your answer, or the tool itself still running), the call keeps waiting and is checked again every minute. The deadline only applies once opencode is idle, or when opencode cannot be asked. Before this, answering a permission prompt after ten minutes meant Claude had already been told the command timed out. Your late approval then cancelled Claude's next action, which it reported as you rejecting it. Resolved per tool, most-specific layer winning:
|
|
681
683
|
|
|
682
684
|
1. flat default — 10 min (matches Claude CLI's own Bash ceiling)
|
|
683
685
|
2. per-tool default: **`task` / `task_batch`: none**, **`question`: 30 min**, everything else: 10 min
|
package/dist/index.js
CHANGED
|
@@ -907,8 +907,9 @@ var QUESTION_TOOL_NAME = "question";
|
|
|
907
907
|
var APPROVED_EXIT_PLAN_MODE_MESSAGE = "User has approved your plan. You can now start coding. Start with updating your todo list if applicable.";
|
|
908
908
|
var REJECTED_EXIT_PLAN_MODE_PREFIX = "The user doesn't want to proceed with this tool use. The tool use was rejected. To tell you how to proceed, the user said:";
|
|
909
909
|
var PLAN_MODE_APPROVAL_QUESTION = "Do you want to proceed with this plan?";
|
|
910
|
-
var OPENCODE_QUESTION_RESULT_PREFIX =
|
|
911
|
-
var OPENCODE_QUESTION_RESULT_SUFFIX =
|
|
910
|
+
var OPENCODE_QUESTION_RESULT_PREFIX = "User has answered your questions: ";
|
|
911
|
+
var OPENCODE_QUESTION_RESULT_SUFFIX = ". You can now continue with the user's answers in mind.";
|
|
912
|
+
var OPENCODE_UNANSWERED = "Unanswered";
|
|
912
913
|
var KEY_SEPARATOR = "\0";
|
|
913
914
|
function isPlanModeQuestionActive(input) {
|
|
914
915
|
if (input.compactionMode) return false;
|
|
@@ -1007,29 +1008,40 @@ function unwrapToolOutput(part) {
|
|
|
1007
1008
|
return output;
|
|
1008
1009
|
}
|
|
1009
1010
|
}
|
|
1010
|
-
function unwrapOpencodeQuestionResult(value) {
|
|
1011
|
-
if (value.startsWith(OPENCODE_QUESTION_RESULT_PREFIX)
|
|
1012
|
-
return value
|
|
1013
|
-
OPENCODE_QUESTION_RESULT_PREFIX.length,
|
|
1014
|
-
-OPENCODE_QUESTION_RESULT_SUFFIX.length
|
|
1015
|
-
);
|
|
1011
|
+
function unwrapOpencodeQuestionResult(value, question) {
|
|
1012
|
+
if (!value.startsWith(OPENCODE_QUESTION_RESULT_PREFIX) || !value.endsWith(OPENCODE_QUESTION_RESULT_SUFFIX)) {
|
|
1013
|
+
return value;
|
|
1016
1014
|
}
|
|
1017
|
-
|
|
1015
|
+
const body = value.slice(
|
|
1016
|
+
OPENCODE_QUESTION_RESULT_PREFIX.length,
|
|
1017
|
+
value.length - OPENCODE_QUESTION_RESULT_SUFFIX.length
|
|
1018
|
+
);
|
|
1019
|
+
if (!body.startsWith('"') || !body.endsWith('"')) return value;
|
|
1020
|
+
let answer;
|
|
1021
|
+
const head = question === void 0 ? void 0 : `"${question}"="`;
|
|
1022
|
+
if (head && body.startsWith(head) && body.length > head.length) {
|
|
1023
|
+
answer = body.slice(head.length, -1);
|
|
1024
|
+
} else {
|
|
1025
|
+
const split = body.lastIndexOf('"="');
|
|
1026
|
+
if (split > 0) answer = body.slice(split + 3, -1);
|
|
1027
|
+
}
|
|
1028
|
+
if (answer === void 0) return value;
|
|
1029
|
+
return answer === OPENCODE_UNANSWERED ? "" : answer;
|
|
1018
1030
|
}
|
|
1019
|
-
function collectAnswerStrings(value) {
|
|
1020
|
-
if (typeof value === "string") return [unwrapOpencodeQuestionResult(value)];
|
|
1021
|
-
if (Array.isArray(value)) return value.flatMap(collectAnswerStrings);
|
|
1031
|
+
function collectAnswerStrings(value, question) {
|
|
1032
|
+
if (typeof value === "string") return [unwrapOpencodeQuestionResult(value, question)];
|
|
1033
|
+
if (Array.isArray(value)) return value.flatMap((item) => collectAnswerStrings(item, question));
|
|
1022
1034
|
if (!value || typeof value !== "object") return [];
|
|
1023
1035
|
const obj = value;
|
|
1024
1036
|
if (obj.denied === true) return [String(obj.reason ?? "question rejected")];
|
|
1025
1037
|
for (const key of ["answers", "answer", "selected", "selection", "value"]) {
|
|
1026
|
-
if (key in obj) return collectAnswerStrings(obj[key]);
|
|
1038
|
+
if (key in obj) return collectAnswerStrings(obj[key], question);
|
|
1027
1039
|
}
|
|
1028
1040
|
return [];
|
|
1029
1041
|
}
|
|
1030
1042
|
function classifyQuestionResult(part) {
|
|
1031
1043
|
const output = unwrapToolOutput(part);
|
|
1032
|
-
const answers = collectAnswerStrings(output).map((answer) => answer.trim()).filter(Boolean);
|
|
1044
|
+
const answers = collectAnswerStrings(output, PLAN_MODE_APPROVAL_QUESTION).map((answer) => answer.trim()).filter(Boolean);
|
|
1033
1045
|
if (answers.length === 1 && answers[0].toLowerCase() === "yes") {
|
|
1034
1046
|
return { approved: true, feedback: "" };
|
|
1035
1047
|
}
|
|
@@ -1175,17 +1187,18 @@ function createAccountFailoverQuestionCall(sessionKey2, input, toolCallId = `${A
|
|
|
1175
1187
|
const candidates = input.candidates.map((c) => normalizeAccountName(c));
|
|
1176
1188
|
const resets = describeReset(input.resetsAt);
|
|
1177
1189
|
const until = resets ?? "opencode restarts";
|
|
1178
|
-
pendingQuestions2.set(pendingKey2(sessionKey2, toolCallId), {
|
|
1179
|
-
sourceAccount: source,
|
|
1180
|
-
candidates: [...candidates],
|
|
1181
|
-
resetsAt: input.resetsAt
|
|
1182
|
-
});
|
|
1183
1190
|
const question = [
|
|
1184
1191
|
`The Claude account "${source}" is out of usage`,
|
|
1185
1192
|
input.window ? ` in ${input.window}` : "",
|
|
1186
1193
|
resets ? `, which resets at ${resets}` : "",
|
|
1187
1194
|
". Continue this task on another configured account? Leaving this unanswered waits, at no cost."
|
|
1188
1195
|
].join("");
|
|
1196
|
+
pendingQuestions2.set(pendingKey2(sessionKey2, toolCallId), {
|
|
1197
|
+
sourceAccount: source,
|
|
1198
|
+
candidates: [...candidates],
|
|
1199
|
+
resetsAt: input.resetsAt,
|
|
1200
|
+
question
|
|
1201
|
+
});
|
|
1189
1202
|
return {
|
|
1190
1203
|
toolCallId,
|
|
1191
1204
|
toolName: QUESTION_TOOL_NAME,
|
|
@@ -1223,7 +1236,11 @@ function classify(pending, part) {
|
|
|
1223
1236
|
reason: String(output.reason ?? "question rejected")
|
|
1224
1237
|
};
|
|
1225
1238
|
}
|
|
1226
|
-
const
|
|
1239
|
+
const outputType = part?.output?.type;
|
|
1240
|
+
if (outputType === "error-text" || outputType === "error-json") {
|
|
1241
|
+
return { kind: "stop", reason: String(output ?? "the form was dismissed") };
|
|
1242
|
+
}
|
|
1243
|
+
const answers = collectAnswerStrings(output, pending.question).map((answer) => answer.trim()).filter(Boolean);
|
|
1227
1244
|
if (answers.length === 0) return { kind: "stop", reason: "no answer" };
|
|
1228
1245
|
const picked = normalizeAccountName(answers[0]);
|
|
1229
1246
|
if (picked === STOP_ANSWER) {
|
|
@@ -1240,7 +1257,7 @@ function classify(pending, part) {
|
|
|
1240
1257
|
resetsAt: pending.resetsAt
|
|
1241
1258
|
};
|
|
1242
1259
|
}
|
|
1243
|
-
function consumeAccountFailoverAnswer(sessionKey2, prompt) {
|
|
1260
|
+
function consumeAccountFailoverAnswer(sessionKey2, prompt, fallback) {
|
|
1244
1261
|
for (let i = prompt.length - 1; i >= 0; i--) {
|
|
1245
1262
|
const msg = prompt[i];
|
|
1246
1263
|
if (!Array.isArray(msg.content)) continue;
|
|
@@ -1255,6 +1272,21 @@ function consumeAccountFailoverAnswer(sessionKey2, prompt) {
|
|
|
1255
1272
|
return classify(pending, part);
|
|
1256
1273
|
}
|
|
1257
1274
|
}
|
|
1275
|
+
const last = prompt[prompt.length - 1];
|
|
1276
|
+
if (fallback && fallback.candidates.length > 0 && Array.isArray(last?.content)) {
|
|
1277
|
+
const orphan = last.content.find(
|
|
1278
|
+
(part) => part?.type === "tool-result" && typeof part.toolCallId === "string" && part.toolCallId.startsWith(ACCOUNT_FAILOVER_TOOL_CALL_PREFIX)
|
|
1279
|
+
);
|
|
1280
|
+
if (orphan) {
|
|
1281
|
+
return classify(
|
|
1282
|
+
{
|
|
1283
|
+
sourceAccount: normalizeAccountName(fallback.sourceAccount || DEFAULT_ACCOUNT),
|
|
1284
|
+
candidates: fallback.candidates.map((c) => normalizeAccountName(c))
|
|
1285
|
+
},
|
|
1286
|
+
orphan
|
|
1287
|
+
);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1258
1290
|
return null;
|
|
1259
1291
|
}
|
|
1260
1292
|
function formatFailoverNote(input) {
|
|
@@ -1515,6 +1547,53 @@ function resolveProxyClientCeilingMs(overrides) {
|
|
|
1515
1547
|
}
|
|
1516
1548
|
return Math.min(ms, MAX_PROXY_TIMEOUT_MS);
|
|
1517
1549
|
}
|
|
1550
|
+
var proxyDeadlineGuard = null;
|
|
1551
|
+
function setProxyDeadlineGuard(guard) {
|
|
1552
|
+
proxyDeadlineGuard = guard;
|
|
1553
|
+
}
|
|
1554
|
+
var PROXY_DEADLINE_RECHECK_MS = 6e4;
|
|
1555
|
+
var deadlineRecheckMs = PROXY_DEADLINE_RECHECK_MS;
|
|
1556
|
+
function armProxyDeadline(opts) {
|
|
1557
|
+
let cancelled = false;
|
|
1558
|
+
let extended = false;
|
|
1559
|
+
let timer = null;
|
|
1560
|
+
const check = async () => {
|
|
1561
|
+
timer = null;
|
|
1562
|
+
if (cancelled) return;
|
|
1563
|
+
let keep = false;
|
|
1564
|
+
const guard = proxyDeadlineGuard;
|
|
1565
|
+
if (guard) {
|
|
1566
|
+
try {
|
|
1567
|
+
keep = await guard({ callId: opts.callId, toolName: opts.toolName });
|
|
1568
|
+
} catch {
|
|
1569
|
+
keep = false;
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
if (cancelled) return;
|
|
1573
|
+
if (keep) {
|
|
1574
|
+
if (!extended && opts.logExtension) {
|
|
1575
|
+
log.warn("proxy call past its deadline, but opencode is still serving it; waiting", {
|
|
1576
|
+
callId: opts.callId,
|
|
1577
|
+
toolName: opts.toolName,
|
|
1578
|
+
deadlineMs: opts.deadlineMs,
|
|
1579
|
+
note: "usually a permission prompt in opencode; the call ends when it is answered, you abort, or the claude process goes"
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
extended = true;
|
|
1583
|
+
timer = setTimeout(() => void check(), opts.recheckMs ?? deadlineRecheckMs);
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
opts.onExpire();
|
|
1587
|
+
};
|
|
1588
|
+
timer = setTimeout(() => void check(), opts.deadlineMs);
|
|
1589
|
+
return {
|
|
1590
|
+
cancel() {
|
|
1591
|
+
cancelled = true;
|
|
1592
|
+
if (timer) clearTimeout(timer);
|
|
1593
|
+
timer = null;
|
|
1594
|
+
}
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1518
1597
|
function buildProxyTimeoutError(toolName, ms) {
|
|
1519
1598
|
const key = toolName.toLowerCase();
|
|
1520
1599
|
const base = `Proxy tool '${toolName}' timed out after ${ms}ms waiting for opencode to resolve the call`;
|
|
@@ -2000,21 +2079,26 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS, timeoutOverride
|
|
|
2000
2079
|
timeoutOverrides
|
|
2001
2080
|
);
|
|
2002
2081
|
if (deadlineMs > PROXY_NO_DEADLINE_MS) {
|
|
2003
|
-
timer =
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2082
|
+
timer = armProxyDeadline({
|
|
2083
|
+
callId,
|
|
2084
|
+
toolName,
|
|
2085
|
+
deadlineMs,
|
|
2086
|
+
onExpire: () => {
|
|
2087
|
+
if (!pending.has(callId)) return;
|
|
2088
|
+
pending.delete(callId);
|
|
2089
|
+
log.notice("proxy-mcp tool call timed out", {
|
|
2090
|
+
callId,
|
|
2091
|
+
toolName,
|
|
2092
|
+
deadlineMs
|
|
2093
|
+
});
|
|
2094
|
+
reject2(buildProxyTimeoutError(toolName, deadlineMs));
|
|
2095
|
+
}
|
|
2096
|
+
});
|
|
2013
2097
|
}
|
|
2014
2098
|
calls.emit("call", entry);
|
|
2015
2099
|
}
|
|
2016
2100
|
).finally(() => {
|
|
2017
|
-
|
|
2101
|
+
timer?.cancel();
|
|
2018
2102
|
pending.delete(callId);
|
|
2019
2103
|
});
|
|
2020
2104
|
if (channel.closed) {
|
|
@@ -2381,7 +2465,7 @@ var PROXY_STALL_WARNING_MS = 5 * 6e4;
|
|
|
2381
2465
|
var PROXY_DEADLINE_WARNING_FRACTION = 0.6;
|
|
2382
2466
|
var PROXY_DEADLINE_WARNING_MIN_MS = 6e4;
|
|
2383
2467
|
function clearPendingTimers(pending) {
|
|
2384
|
-
|
|
2468
|
+
pending.timer?.cancel();
|
|
2385
2469
|
if (pending.stallTimer) clearInterval(pending.stallTimer);
|
|
2386
2470
|
if (pending.deadlineWarnTimer) clearTimeout(pending.deadlineWarnTimer);
|
|
2387
2471
|
}
|
|
@@ -2425,20 +2509,26 @@ function queuePendingProxyCall(sessionKey2, call, timeoutOverrides, stallWarning
|
|
|
2425
2509
|
call.input,
|
|
2426
2510
|
timeoutOverrides
|
|
2427
2511
|
);
|
|
2428
|
-
const timer = deadlineMs > PROXY_NO_DEADLINE_MS ?
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
deadlineMs
|
|
2440
|
-
|
|
2441
|
-
|
|
2512
|
+
const timer = deadlineMs > PROXY_NO_DEADLINE_MS ? armProxyDeadline({
|
|
2513
|
+
callId: call.id,
|
|
2514
|
+
toolName: call.toolName,
|
|
2515
|
+
deadlineMs,
|
|
2516
|
+
logExtension: true,
|
|
2517
|
+
onExpire: () => {
|
|
2518
|
+
const current = pendingByCallId.get(call.id);
|
|
2519
|
+
if (!current) return;
|
|
2520
|
+
pendingByCallId.delete(call.id);
|
|
2521
|
+
indexRemove(current.sessionKey, call.id);
|
|
2522
|
+
clearPendingTimers(current);
|
|
2523
|
+
current.reject(buildProxyTimeoutError(call.toolName, deadlineMs));
|
|
2524
|
+
log.notice("timed out pending proxy call", {
|
|
2525
|
+
sessionKey: current.sessionKey,
|
|
2526
|
+
toolCallId: call.id,
|
|
2527
|
+
toolName: call.toolName,
|
|
2528
|
+
deadlineMs
|
|
2529
|
+
});
|
|
2530
|
+
}
|
|
2531
|
+
}) : null;
|
|
2442
2532
|
const stallTimer = deadlineMs === PROXY_NO_DEADLINE_MS && stallWarningMs > 0 ? setInterval(() => {
|
|
2443
2533
|
const current = pendingByCallId.get(call.id);
|
|
2444
2534
|
if (!current) return;
|
|
@@ -2467,7 +2557,7 @@ function queuePendingProxyCall(sessionKey2, call, timeoutOverrides, stallWarning
|
|
|
2467
2557
|
remainingMs: Math.max(0, deadlineMs - waitedMs),
|
|
2468
2558
|
emitted: current.emitted === true,
|
|
2469
2559
|
channelClosed: current.channel?.closed === true,
|
|
2470
|
-
note: "it will be rejected when the deadline passes; raise this tool's proxyToolTimeoutMs if the work is legitimately this long"
|
|
2560
|
+
note: "it will be rejected when the deadline passes unless opencode is still serving it (a permission prompt is waiting); raise this tool's proxyToolTimeoutMs if the work is legitimately this long"
|
|
2471
2561
|
});
|
|
2472
2562
|
}, warnAtMs) : null;
|
|
2473
2563
|
deadlineWarnTimer?.unref?.();
|
|
@@ -2502,6 +2592,9 @@ function markPendingProxyCallEmitted(toolCallId) {
|
|
|
2502
2592
|
function isPendingProxyCallChannelClosed(call) {
|
|
2503
2593
|
return call.channel?.closed === true;
|
|
2504
2594
|
}
|
|
2595
|
+
function findPendingProxyCall(toolCallId) {
|
|
2596
|
+
return pendingByCallId.get(toolCallId);
|
|
2597
|
+
}
|
|
2505
2598
|
function getPendingProxyCalls(sessionKey2) {
|
|
2506
2599
|
const s = callIdsBySession.get(sessionKey2);
|
|
2507
2600
|
if (!s || s.size === 0) return [];
|
|
@@ -4854,6 +4947,7 @@ function getClaudeUserMessage(prompt, includeHistoryContext = false, opts = {})
|
|
|
4854
4947
|
const compactionMode = opts.compactionMode === true;
|
|
4855
4948
|
const cliToolCallIds = opts.cliToolCallIds;
|
|
4856
4949
|
const content = [];
|
|
4950
|
+
prompt = stripAccountFailoverParts(prompt);
|
|
4857
4951
|
if (opts.stripContextReminders) {
|
|
4858
4952
|
const stripped = stripContextReminders(prompt);
|
|
4859
4953
|
if (stripped.removed > 0) {
|
|
@@ -6198,6 +6292,13 @@ import { unlink as unlink3 } from "fs/promises";
|
|
|
6198
6292
|
import { homedir as homedir5, tmpdir as tmpdir2 } from "os";
|
|
6199
6293
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
6200
6294
|
import { dirname as dirname5, join as join8 } from "path";
|
|
6295
|
+
async function isProxyCallStillServed(callId) {
|
|
6296
|
+
const pending = findPendingProxyCall(callId);
|
|
6297
|
+
if (!pending || pending.emitted !== true) return false;
|
|
6298
|
+
const session = describeSessionKey(pending.sessionKey).session;
|
|
6299
|
+
return await fetchSessionRunState(session) === "busy";
|
|
6300
|
+
}
|
|
6301
|
+
setProxyDeadlineGuard(({ callId }) => isProxyCallStillServed(callId));
|
|
6201
6302
|
var DEFAULT_COMPACTION_MODEL = "claude-haiku-4-5";
|
|
6202
6303
|
function resolveCompactionModel(configured) {
|
|
6203
6304
|
const env = process.env.CLAUDE_CODE_COMPACTION_MODEL?.trim();
|
|
@@ -7855,7 +7956,10 @@ ${plan}
|
|
|
7855
7956
|
deleteActiveProcess(sk);
|
|
7856
7957
|
clearCompression(sk);
|
|
7857
7958
|
}
|
|
7858
|
-
const failoverAnswer = compactionMode || useInteractive ? null : consumeAccountFailoverAnswer(sk, options.prompt
|
|
7959
|
+
const failoverAnswer = compactionMode || useInteractive ? null : consumeAccountFailoverAnswer(sk, options.prompt, {
|
|
7960
|
+
sourceAccount,
|
|
7961
|
+
candidates: failoverCandidates(this.config.failoverAccounts, sourceAccount)
|
|
7962
|
+
});
|
|
7859
7963
|
if (failoverAnswer?.kind === "stop") {
|
|
7860
7964
|
log.warn("account failover declined; ending the turn", {
|
|
7861
7965
|
sessionKey: sk,
|