@khalilgharbaoui/opencode-claude-code-plugin 0.37.0 → 0.38.0
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 +1 -1
- package/dist/index.js +288 -28
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/skills/claude-code-plugin/SKILL.md +72 -16
package/README.md
CHANGED
|
@@ -55,7 +55,7 @@ Nothing in the picker? [Troubleshooting](./docs/troubleshooting/symptoms.md) is
|
|
|
55
55
|
|---|---|
|
|
56
56
|
| **Selective tool proxy** | Choose, per tool, whether Claude Code or opencode executes it. Proxied calls end on an event (the result, an abort, the next message, the child exiting), never on a clock. [Guide](./docs/guides/tool-proxy.md) |
|
|
57
57
|
| **Claude's own tools, MCP and skills** | `Read`, `Grep`, `Glob`, `WebSearch` run in Claude Code; your opencode MCP servers are bridged in; your opencode skills can be staged for Claude's `Skill` tool. [MCP](./docs/configuration/mcp.md) · [Skills](./docs/configuration/skills.md) |
|
|
58
|
-
| **Several accounts
|
|
58
|
+
| **Several accounts** | `"accounts": ["personal", "work"]` becomes one provider per account. Out of usage mid-task? One line says which account, which window and when it resets in your own time zone, and names the others to pick a model from. [Accounts](./docs/configuration/accounts.md) |
|
|
59
59
|
| **Subagents: your account, their model** | `forceModel`, `reasoningEffort` and `cacheTtl` in an agent file, inheriting the caller's account. `task_batch` runs several subagents at once. [Subagents](./docs/configuration/subagents.md) |
|
|
60
60
|
| **18 models, reasoning variants, fast mode** | Haiku 4.5 through Opus 5.5, Fable and Mythos, each with a `(N×)` list-price suffix, `low` to `max` effort variants, and a fallback chain for a model this account cannot run today. [Models](./docs/models.md) |
|
|
61
61
|
| **`/btw` and `/claude-code-doctor`** | Side questions on the live process, and a health report whose `bundle` form is redacted by allowlist so it is safe to paste into a public issue. [`/btw`](./docs/guides/btw.md) · [Doctor](./docs/guides/doctor.md) |
|
package/dist/index.js
CHANGED
|
@@ -149,6 +149,7 @@ import path from "path";
|
|
|
149
149
|
import { appendFileSync, mkdirSync, renameSync, statSync } from "fs";
|
|
150
150
|
import { homedir } from "os";
|
|
151
151
|
import { dirname, join } from "path";
|
|
152
|
+
import { isMainThread } from "worker_threads";
|
|
152
153
|
var LEVEL_RANK = {
|
|
153
154
|
debug: 0,
|
|
154
155
|
info: 1,
|
|
@@ -241,15 +242,110 @@ function fmt(level, msg, data) {
|
|
|
241
242
|
function shouldEmit(level) {
|
|
242
243
|
return LEVEL_RANK[level] >= LEVEL_RANK[activeConfig.level];
|
|
243
244
|
}
|
|
244
|
-
|
|
245
|
+
var tuiHostOverride;
|
|
246
|
+
function isTuiHost() {
|
|
247
|
+
return tuiHostOverride ?? !isMainThread;
|
|
248
|
+
}
|
|
249
|
+
var MAX_BUFFERED_SINK_ENTRIES = 50;
|
|
250
|
+
var MAX_TOASTED_MESSAGES = 200;
|
|
251
|
+
var toastBurstLimit = 3;
|
|
252
|
+
var toastBurstWindowMs = 2e3;
|
|
253
|
+
var activeSink = null;
|
|
254
|
+
var bufferedEntries = [];
|
|
255
|
+
var droppedBeforeSink = 0;
|
|
256
|
+
var toastedMessages = /* @__PURE__ */ new Set();
|
|
257
|
+
var burstWindowStart = 0;
|
|
258
|
+
var burstCount = 0;
|
|
259
|
+
var burstSuppressed = 0;
|
|
260
|
+
var burstSummaryTimer = null;
|
|
261
|
+
function setLogSink(sink) {
|
|
262
|
+
activeSink = sink;
|
|
263
|
+
if (!sink) return;
|
|
264
|
+
const pending = bufferedEntries;
|
|
265
|
+
const dropped = droppedBeforeSink;
|
|
266
|
+
bufferedEntries = [];
|
|
267
|
+
droppedBeforeSink = 0;
|
|
268
|
+
for (const entry of pending) deliverToSink(entry);
|
|
269
|
+
if (dropped > 0) {
|
|
270
|
+
const message = `${dropped} earlier log line(s) were dropped before opencode's log channel was available`;
|
|
271
|
+
deliverToSink({
|
|
272
|
+
level: "warn",
|
|
273
|
+
message,
|
|
274
|
+
line: fmt("WARN", message)
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function routeToSink(entry) {
|
|
279
|
+
if (!activeSink) {
|
|
280
|
+
if (bufferedEntries.length >= MAX_BUFFERED_SINK_ENTRIES) {
|
|
281
|
+
droppedBeforeSink += 1;
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
bufferedEntries.push(entry);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
deliverToSink(entry);
|
|
288
|
+
}
|
|
289
|
+
function deliverToSink(entry) {
|
|
290
|
+
const sink = activeSink;
|
|
291
|
+
if (!sink) return;
|
|
292
|
+
try {
|
|
293
|
+
sink.log(entry);
|
|
294
|
+
} catch {
|
|
295
|
+
}
|
|
296
|
+
if (entry.level !== "warn" && entry.level !== "error") return;
|
|
297
|
+
if (toastedMessages.has(entry.message)) return;
|
|
298
|
+
if (toastedMessages.size < MAX_TOASTED_MESSAGES) toastedMessages.add(entry.message);
|
|
299
|
+
if (!allowToastNow()) return;
|
|
300
|
+
sendToast(sink, entry);
|
|
301
|
+
}
|
|
302
|
+
function sendToast(sink, entry) {
|
|
303
|
+
try {
|
|
304
|
+
sink.toast(entry);
|
|
305
|
+
} catch {
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function allowToastNow() {
|
|
309
|
+
const now = Date.now();
|
|
310
|
+
if (now - burstWindowStart >= toastBurstWindowMs) {
|
|
311
|
+
burstWindowStart = now;
|
|
312
|
+
burstCount = 0;
|
|
313
|
+
}
|
|
314
|
+
if (burstCount < toastBurstLimit) {
|
|
315
|
+
burstCount += 1;
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
burstSuppressed += 1;
|
|
319
|
+
armBurstSummary();
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
function armBurstSummary() {
|
|
323
|
+
if (burstSummaryTimer) return;
|
|
324
|
+
burstSummaryTimer = setTimeout(() => {
|
|
325
|
+
burstSummaryTimer = null;
|
|
326
|
+
const suppressed = burstSuppressed;
|
|
327
|
+
burstSuppressed = 0;
|
|
328
|
+
if (suppressed <= 0) return;
|
|
329
|
+
const sink = activeSink;
|
|
330
|
+
if (!sink) return;
|
|
331
|
+
const message = `${suppressed} more claude-code warning(s) were not shown; the full text is in opencode's log`;
|
|
332
|
+
sendToast(sink, { level: "warn", message, line: fmt("WARN", message) });
|
|
333
|
+
}, toastBurstWindowMs);
|
|
334
|
+
burstSummaryTimer.unref?.();
|
|
335
|
+
}
|
|
336
|
+
function shouldSurface(level) {
|
|
245
337
|
if (level === "warn" || level === "error") return true;
|
|
246
338
|
return activeConfig.mode === "debug";
|
|
247
339
|
}
|
|
248
340
|
function emit(level, msg, data) {
|
|
249
341
|
if (!shouldEmit(level)) return;
|
|
250
342
|
const line = fmt(level.toUpperCase(), msg, data);
|
|
251
|
-
if (
|
|
252
|
-
|
|
343
|
+
if (shouldSurface(level)) {
|
|
344
|
+
if (isTuiHost()) {
|
|
345
|
+
routeToSink({ level, message: msg, data, line });
|
|
346
|
+
} else {
|
|
347
|
+
console.error(line);
|
|
348
|
+
}
|
|
253
349
|
}
|
|
254
350
|
writeToFile(line);
|
|
255
351
|
}
|
|
@@ -488,6 +584,20 @@ function formatResetsAt(resetsAt) {
|
|
|
488
584
|
const date = new Date(ms);
|
|
489
585
|
return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
|
|
490
586
|
}
|
|
587
|
+
function formatLocalMinute(ms) {
|
|
588
|
+
const when = new Date(ms);
|
|
589
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
590
|
+
return `${when.getFullYear()}-${pad(when.getMonth() + 1)}-${pad(when.getDate())} ${pad(when.getHours())}:${pad(when.getMinutes())}`;
|
|
591
|
+
}
|
|
592
|
+
function formatResetsAtLocal(resetsAt) {
|
|
593
|
+
const ms = resetsAtToMs(resetsAt);
|
|
594
|
+
if (ms === void 0 || Number.isNaN(new Date(ms).getTime())) return void 0;
|
|
595
|
+
return formatLocalMinute(ms);
|
|
596
|
+
}
|
|
597
|
+
function describeRateLimitWindow(rateLimitType) {
|
|
598
|
+
if (!rateLimitType) return void 0;
|
|
599
|
+
return RATE_LIMIT_WINDOWS[rateLimitType] ?? rateLimitType;
|
|
600
|
+
}
|
|
491
601
|
function rateLimitKey(info) {
|
|
492
602
|
return [
|
|
493
603
|
info.status ?? "?",
|
|
@@ -503,7 +613,7 @@ function isRateLimitRejected(info) {
|
|
|
503
613
|
}
|
|
504
614
|
function describeRateLimit(info) {
|
|
505
615
|
if (!info.status && !info.overageStatus) return null;
|
|
506
|
-
const window = info.rateLimitType
|
|
616
|
+
const window = describeRateLimitWindow(info.rateLimitType);
|
|
507
617
|
const resets = formatResetsAt(info.resetsAt);
|
|
508
618
|
const overageResets = formatResetsAt(info.overageResetsAt);
|
|
509
619
|
const key = rateLimitKey(info);
|
|
@@ -518,9 +628,7 @@ function describeRateLimit(info) {
|
|
|
518
628
|
if (resetAt) parts.push(`Resets at ${resetAt}.`);
|
|
519
629
|
parts.push(RATE_LIMIT_ACTION);
|
|
520
630
|
const message = parts.join(" ");
|
|
521
|
-
return { key, level: "warn", message, transcript:
|
|
522
|
-
${RATE_LIMIT_MARKER} ${message}
|
|
523
|
-
` };
|
|
631
|
+
return { key, level: "warn", message, transcript: null };
|
|
524
632
|
}
|
|
525
633
|
if (info.status === "allowed_warning" || info.overageStatus === "allowed_warning") {
|
|
526
634
|
const used = info.utilization === void 0 ? "" : ` (${Math.round(info.utilization * 100)}% used)`;
|
|
@@ -987,6 +1095,7 @@ var PLAN_MODE_APPROVAL_QUESTION = "Do you want to proceed with this plan?";
|
|
|
987
1095
|
var OPENCODE_QUESTION_RESULT_PREFIX = "User has answered your questions: ";
|
|
988
1096
|
var OPENCODE_QUESTION_RESULT_SUFFIX = ". You can now continue with the user's answers in mind.";
|
|
989
1097
|
var OPENCODE_UNANSWERED = "Unanswered";
|
|
1098
|
+
var DCP_MESSAGE_ID_SUFFIX = /(?:\s*<dcp-message-id>[^<>]*<\/dcp-message-id>)+\s*$/;
|
|
990
1099
|
var KEY_SEPARATOR = "\0";
|
|
991
1100
|
function isPlanModeQuestionActive(input) {
|
|
992
1101
|
if (input.compactionMode) return false;
|
|
@@ -1086,15 +1195,16 @@ function unwrapToolOutput(part) {
|
|
|
1086
1195
|
}
|
|
1087
1196
|
}
|
|
1088
1197
|
function unwrapOpencodeQuestionResult(value, question) {
|
|
1089
|
-
const
|
|
1198
|
+
const untagged = value.replace(DCP_MESSAGE_ID_SUFFIX, "");
|
|
1199
|
+
const sentence = untagged.trim();
|
|
1090
1200
|
if (!sentence.startsWith(OPENCODE_QUESTION_RESULT_PREFIX) || !sentence.endsWith(OPENCODE_QUESTION_RESULT_SUFFIX)) {
|
|
1091
|
-
return
|
|
1201
|
+
return untagged;
|
|
1092
1202
|
}
|
|
1093
1203
|
const body = sentence.slice(
|
|
1094
1204
|
OPENCODE_QUESTION_RESULT_PREFIX.length,
|
|
1095
1205
|
sentence.length - OPENCODE_QUESTION_RESULT_SUFFIX.length
|
|
1096
1206
|
);
|
|
1097
|
-
if (!body.startsWith('"') || !body.endsWith('"')) return
|
|
1207
|
+
if (!body.startsWith('"') || !body.endsWith('"')) return untagged;
|
|
1098
1208
|
let answer;
|
|
1099
1209
|
const head = question === void 0 ? void 0 : `"${question}"="`;
|
|
1100
1210
|
if (head && body.startsWith(head) && body.length > head.length) {
|
|
@@ -1103,7 +1213,7 @@ function unwrapOpencodeQuestionResult(value, question) {
|
|
|
1103
1213
|
const split = body.lastIndexOf('"="');
|
|
1104
1214
|
if (split > 0) answer = body.slice(split + 3, -1);
|
|
1105
1215
|
}
|
|
1106
|
-
if (answer === void 0) return
|
|
1216
|
+
if (answer === void 0) return untagged;
|
|
1107
1217
|
return answer === OPENCODE_UNANSWERED ? "" : answer;
|
|
1108
1218
|
}
|
|
1109
1219
|
function collectAnswerStrings(value, question) {
|
|
@@ -1161,6 +1271,13 @@ var ACCOUNT_LIMIT_PATTERNS = [
|
|
|
1161
1271
|
/you[’'`]?ve hit your individual spend limit/i
|
|
1162
1272
|
];
|
|
1163
1273
|
var ACCOUNT_BLOCK_MARKER = "\u258C **claude account:**";
|
|
1274
|
+
var USAGE_LIMIT_MARKER = "\u258C **usage limit:**";
|
|
1275
|
+
function describeOtherAccounts(candidates) {
|
|
1276
|
+
const names = candidates.map((candidate) => normalizeAccountName(candidate)).filter(Boolean).map((candidate) => `"${candidate}"`);
|
|
1277
|
+
if (names.length === 0) return "";
|
|
1278
|
+
const joined = names.length === 1 ? names[0] : `${names.slice(0, -1).join(", ")} or ${names[names.length - 1]}`;
|
|
1279
|
+
return `the ${joined} account`;
|
|
1280
|
+
}
|
|
1164
1281
|
var ACCOUNT_BLOCKS = {
|
|
1165
1282
|
authentication_failed: { what: "is not logged in (its login expired or was revoked)", login: true },
|
|
1166
1283
|
oauth_org_not_allowed: { what: "belongs to an organization that does not allow this login", login: false },
|
|
@@ -1184,17 +1301,51 @@ function formatAccountBlockNote(input) {
|
|
|
1184
1301
|
const block = ACCOUNT_BLOCKS[input.kind];
|
|
1185
1302
|
const account = normalizeAccountName(input.account || DEFAULT_ACCOUNT);
|
|
1186
1303
|
const fix = block.login ? `Log in again with \`${loginCommandFor(input.configDir)}\`, then resend your message.` : "Check the account at claude.ai, then resend your message.";
|
|
1187
|
-
const
|
|
1304
|
+
const others = input.offeringSwitch ? "" : describeOtherAccounts(input.candidates ?? []);
|
|
1305
|
+
const then = input.offeringSwitch ? " Or pick another account below." : others ? ` Or pick a model from ${others} and resend.` : "";
|
|
1188
1306
|
return `
|
|
1189
1307
|
${ACCOUNT_BLOCK_MARKER} the Claude account "${account}" ${block.what}. ${fix}${then}
|
|
1190
1308
|
`;
|
|
1191
1309
|
}
|
|
1310
|
+
function formatUsageLimitNote(input) {
|
|
1311
|
+
const account = normalizeAccountName(input.sourceAccount || DEFAULT_ACCOUNT);
|
|
1312
|
+
const window = describeRateLimitWindow(input.window);
|
|
1313
|
+
const resets = formatResetsAtLocal(input.resetsAt);
|
|
1314
|
+
const others = describeOtherAccounts(input.candidates);
|
|
1315
|
+
const what = [
|
|
1316
|
+
`the Claude account "${account}" is out of usage`,
|
|
1317
|
+
window ? ` in ${window}` : "",
|
|
1318
|
+
resets ? `, which resets at ${resets}` : "",
|
|
1319
|
+
"."
|
|
1320
|
+
].join("");
|
|
1321
|
+
const advice = others ? `Pick a model from ${others} and resend your message, or wait for the window to reset.` : "Wait for the window to reset, or enable extra usage on the account.";
|
|
1322
|
+
return `
|
|
1323
|
+
${USAGE_LIMIT_MARKER} ${what} ${advice}
|
|
1324
|
+
`;
|
|
1325
|
+
}
|
|
1192
1326
|
function isAccountLimitError(input) {
|
|
1193
1327
|
if (input.rateLimit && isRateLimitRejected(input.rateLimit)) return true;
|
|
1194
1328
|
const text = input.resultText;
|
|
1195
1329
|
if (typeof text !== "string" || text.length === 0) return false;
|
|
1196
1330
|
return ACCOUNT_LIMIT_PATTERNS.some((pattern) => pattern.test(text));
|
|
1197
1331
|
}
|
|
1332
|
+
var lastAccountLimits = /* @__PURE__ */ new Map();
|
|
1333
|
+
function rememberAccountLimit(account, facts) {
|
|
1334
|
+
const name = normalizeAccountName(account || DEFAULT_ACCOUNT);
|
|
1335
|
+
if (!name) return;
|
|
1336
|
+
lastAccountLimits.set(name, { resetsAt: facts.resetsAt, window: facts.window });
|
|
1337
|
+
}
|
|
1338
|
+
function recallAccountLimit(account, now = Date.now()) {
|
|
1339
|
+
const name = normalizeAccountName(account || DEFAULT_ACCOUNT);
|
|
1340
|
+
const facts = lastAccountLimits.get(name);
|
|
1341
|
+
if (!facts) return {};
|
|
1342
|
+
const resetsAtMs = resetsAtToMs(facts.resetsAt);
|
|
1343
|
+
const fresh = resetsAtMs !== void 0 && resetsAtMs > now;
|
|
1344
|
+
return {
|
|
1345
|
+
...fresh ? { resetsAt: facts.resetsAt } : {},
|
|
1346
|
+
...facts.window === void 0 ? {} : { window: facts.window }
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1198
1349
|
var accountOverrides = /* @__PURE__ */ new Map();
|
|
1199
1350
|
function setAccountOverride(source, target, until, now = Date.now()) {
|
|
1200
1351
|
const from = normalizeAccountName(source || DEFAULT_ACCOUNT);
|
|
@@ -1262,7 +1413,7 @@ function isAccountFailoverQuestionActive(input) {
|
|
|
1262
1413
|
if (input.compactionMode) return false;
|
|
1263
1414
|
if (input.interactive) return false;
|
|
1264
1415
|
if (input.childSession) return false;
|
|
1265
|
-
if (input.configured
|
|
1416
|
+
if (input.configured !== "ask") return false;
|
|
1266
1417
|
if (input.candidates.length === 0) return false;
|
|
1267
1418
|
return input.opencodeHasQuestion;
|
|
1268
1419
|
}
|
|
@@ -5378,6 +5529,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
5378
5529
|
"btw: early aside failed; the message will ask again",
|
|
5379
5530
|
"cannot interrupt this transport; waiting for the turn to end",
|
|
5380
5531
|
"claude --version output unparseable",
|
|
5532
|
+
"claude account is out of usage; the turn ends with a note",
|
|
5381
5533
|
"claude already registers a different skill under this name; not bridging ours",
|
|
5382
5534
|
"claude cli < 2.1.142 detected; Opus 4.7 thinking summaries unavailable. Run `npm i -g @anthropic-ai/claude-code` to upgrade.",
|
|
5383
5535
|
"claude cli does not support --plugin-dir; opencode skills will not be bridged. Run `npm i -g @anthropic-ai/claude-code` to upgrade.",
|
|
@@ -6232,11 +6384,6 @@ function compareBuilds(loaded, onDisk) {
|
|
|
6232
6384
|
rebuiltAt: onDisk.mtimeMs
|
|
6233
6385
|
};
|
|
6234
6386
|
}
|
|
6235
|
-
function formatLocalMinute(ms) {
|
|
6236
|
-
const when = new Date(ms);
|
|
6237
|
-
const pad = (value) => String(value).padStart(2, "0");
|
|
6238
|
-
return `${when.getFullYear()}-${pad(when.getMonth() + 1)}-${pad(when.getDate())} ${pad(when.getHours())}:${pad(when.getMinutes())}`;
|
|
6239
|
-
}
|
|
6240
6387
|
function formatStaleBuildNote(stale) {
|
|
6241
6388
|
const loadedAt = formatLocalMinute(stale.loadedAt);
|
|
6242
6389
|
if (stale.kind === "version") {
|
|
@@ -7462,6 +7609,7 @@ var PLUGIN_NOTE_MARKERS = [
|
|
|
7462
7609
|
SILENT_TURN_MARKER,
|
|
7463
7610
|
FAILOVER_MARKER,
|
|
7464
7611
|
ACCOUNT_BLOCK_MARKER,
|
|
7612
|
+
USAGE_LIMIT_MARKER,
|
|
7465
7613
|
MODEL_FALLBACK_MARKER,
|
|
7466
7614
|
UNATTENDED_REPLAY_MARKER,
|
|
7467
7615
|
STALE_BUILD_MARKER
|
|
@@ -9872,6 +10020,7 @@ function createTurnState(init) {
|
|
|
9872
10020
|
lastCallUsage: void 0,
|
|
9873
10021
|
resultFailure: void 0,
|
|
9874
10022
|
accountLimitHit: null,
|
|
10023
|
+
usageLimitNote: false,
|
|
9875
10024
|
accountBlock: null,
|
|
9876
10025
|
modelRefusal: null,
|
|
9877
10026
|
drainBuffer: [],
|
|
@@ -10652,6 +10801,7 @@ function createLineHandler(state, ctx) {
|
|
|
10652
10801
|
resetsAt: info.resetsAt ?? info.overageResetsAt,
|
|
10653
10802
|
window: info.rateLimitType
|
|
10654
10803
|
};
|
|
10804
|
+
rememberAccountLimit(ctx.sourceAccount, state.accountLimitHit);
|
|
10655
10805
|
}
|
|
10656
10806
|
const note = reportRateLimitEvent(msg);
|
|
10657
10807
|
if (note) {
|
|
@@ -10935,6 +11085,13 @@ ${plan}
|
|
|
10935
11085
|
if (hasText) {
|
|
10936
11086
|
state.hasReceivedContent = true;
|
|
10937
11087
|
}
|
|
11088
|
+
const apiErrorReply = msg.is_api_error_message === true;
|
|
11089
|
+
const limitErrorReply = apiErrorReply && ctx.usageLimitNoteActive && !state.accountBlock && (msg.error === "rate_limit" || isAccountLimitError({
|
|
11090
|
+
resultText: msg.message.content.filter((b) => b.type === "text" && b.text).map((b) => String(b.text)).join("\n")
|
|
11091
|
+
}));
|
|
11092
|
+
if (limitErrorReply) {
|
|
11093
|
+
state.accountLimitHit ??= recallAccountLimit(ctx.sourceAccount);
|
|
11094
|
+
}
|
|
10938
11095
|
if (hasText && !hasToolUse) {
|
|
10939
11096
|
startResultFallback(state);
|
|
10940
11097
|
}
|
|
@@ -10943,6 +11100,7 @@ ${plan}
|
|
|
10943
11100
|
}
|
|
10944
11101
|
for (const block2 of msg.message.content) {
|
|
10945
11102
|
if (block2.type === "text" && block2.text) {
|
|
11103
|
+
if (limitErrorReply) continue;
|
|
10946
11104
|
resetLastVisibleTextBlock(state);
|
|
10947
11105
|
const blockId = state.startTextBlock();
|
|
10948
11106
|
state.controller.enqueue({
|
|
@@ -11155,7 +11313,15 @@ ${plan}
|
|
|
11155
11313
|
if (deliverPendingCompletions(state)) {
|
|
11156
11314
|
return;
|
|
11157
11315
|
}
|
|
11158
|
-
if (!state.
|
|
11316
|
+
if (!state.accountLimitHit && msg.is_error && isAccountLimitError({
|
|
11317
|
+
resultText: typeof msg.result === "string" ? msg.result : null
|
|
11318
|
+
})) {
|
|
11319
|
+
state.accountLimitHit = recallAccountLimit(ctx.sourceAccount);
|
|
11320
|
+
}
|
|
11321
|
+
state.usageLimitNote = ctx.usageLimitNoteActive && !!state.accountLimitHit && // An expired login or a billing hold writes its own note just below
|
|
11322
|
+
// and says what to run; two notes about one failure is one too many.
|
|
11323
|
+
!state.accountBlock && msg.is_error === true;
|
|
11324
|
+
if (!state.currentTextId && msg.is_error && !state.usageLimitNote && typeof msg.result === "string" && msg.result.trim().length > 0) {
|
|
11159
11325
|
const errId = state.startTextBlock();
|
|
11160
11326
|
state.controller.enqueue({
|
|
11161
11327
|
type: "text-delta",
|
|
@@ -11163,11 +11329,6 @@ ${plan}
|
|
|
11163
11329
|
delta: msg.result
|
|
11164
11330
|
});
|
|
11165
11331
|
}
|
|
11166
|
-
if (!state.accountLimitHit && msg.is_error && isAccountLimitError({
|
|
11167
|
-
resultText: typeof msg.result === "string" ? msg.result : null
|
|
11168
|
-
})) {
|
|
11169
|
-
state.accountLimitHit = {};
|
|
11170
|
-
}
|
|
11171
11332
|
if (ctx.modelFallbackArmed && !state.modelRefusal) {
|
|
11172
11333
|
state.modelRefusal = modelRefusalFromResult(msg);
|
|
11173
11334
|
}
|
|
@@ -11181,7 +11342,11 @@ ${plan}
|
|
|
11181
11342
|
kind: state.accountBlock,
|
|
11182
11343
|
account: ctx.sourceAccount,
|
|
11183
11344
|
configDir: ctx.config.configDir,
|
|
11184
|
-
offeringSwitch
|
|
11345
|
+
offeringSwitch,
|
|
11346
|
+
// Read only when the form is not taking the turn, which is the
|
|
11347
|
+
// default: moving the work by hand is then the operator's only
|
|
11348
|
+
// route and nothing else on screen says it exists (h #g194).
|
|
11349
|
+
candidates: ctx.failoverAccounts
|
|
11185
11350
|
})
|
|
11186
11351
|
});
|
|
11187
11352
|
state.endTextBlock();
|
|
@@ -12060,7 +12225,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12060
12225
|
this.config.failoverAccounts,
|
|
12061
12226
|
sourceAccount
|
|
12062
12227
|
);
|
|
12063
|
-
const failoverAskActive = failoverAccounts.length > 0 && this.config.accountFailover
|
|
12228
|
+
const failoverAskActive = failoverAccounts.length > 0 && this.config.accountFailover === "ask" && !compactionMode && !useInteractive && // A `doGenerate` caller takes the override and reports the plain
|
|
12064
12229
|
// rate-limit error, never the form. See `TurnMode`.
|
|
12065
12230
|
mode !== "generate" && isAccountFailoverQuestionActive({
|
|
12066
12231
|
configured: this.config.accountFailover,
|
|
@@ -12073,6 +12238,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12073
12238
|
// the operator is usually not even looking at.
|
|
12074
12239
|
childSession: !!await fetchSessionParentId(affinity)
|
|
12075
12240
|
});
|
|
12241
|
+
const usageLimitNoteActive = !compactionMode && mode !== "generate" && !failoverAskActive;
|
|
12076
12242
|
let staleBuild = null;
|
|
12077
12243
|
if (!compactionMode && mode !== "generate" && affinity !== "default") {
|
|
12078
12244
|
staleBuild = staleBuildWatch().check();
|
|
@@ -12620,6 +12786,26 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12620
12786
|
return;
|
|
12621
12787
|
}
|
|
12622
12788
|
}
|
|
12789
|
+
if (state.usageLimitNote) {
|
|
12790
|
+
log.warn("claude account is out of usage; the turn ends with a note", {
|
|
12791
|
+
sessionKey: sk,
|
|
12792
|
+
account: sourceAccount,
|
|
12793
|
+
window: state.accountLimitHit?.window ?? null,
|
|
12794
|
+
resetsAt: state.accountLimitHit?.resetsAt ?? null,
|
|
12795
|
+
candidates: failoverAccounts
|
|
12796
|
+
});
|
|
12797
|
+
controller.enqueue({
|
|
12798
|
+
type: "text-delta",
|
|
12799
|
+
id: state.startTextBlock(),
|
|
12800
|
+
delta: formatUsageLimitNote({
|
|
12801
|
+
sourceAccount,
|
|
12802
|
+
candidates: failoverAccounts,
|
|
12803
|
+
resetsAt: state.accountLimitHit?.resetsAt,
|
|
12804
|
+
window: state.accountLimitHit?.window
|
|
12805
|
+
})
|
|
12806
|
+
});
|
|
12807
|
+
state.endTextBlock();
|
|
12808
|
+
}
|
|
12623
12809
|
if (runAutoContinue(state, msg)) return;
|
|
12624
12810
|
for (const [idx, reasoningId] of state.reasoningIds) {
|
|
12625
12811
|
if (state.reasoningStarted.get(idx)) {
|
|
@@ -12693,6 +12879,8 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12693
12879
|
planModeQuestionActive,
|
|
12694
12880
|
sourceAccount,
|
|
12695
12881
|
failoverAskActive,
|
|
12882
|
+
usageLimitNoteActive,
|
|
12883
|
+
failoverAccounts,
|
|
12696
12884
|
modelFallbackArmed,
|
|
12697
12885
|
attempt,
|
|
12698
12886
|
handleControlRequest: handleControlRequest2,
|
|
@@ -13142,6 +13330,72 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
13142
13330
|
}
|
|
13143
13331
|
}
|
|
13144
13332
|
|
|
13333
|
+
// src/tui-log-sink.ts
|
|
13334
|
+
var LOG_SERVICE_NAME = "opencode-claude-code";
|
|
13335
|
+
var TOAST_TITLE = "claude-code";
|
|
13336
|
+
var TOAST_DURATION_MS = 1e4;
|
|
13337
|
+
var MAX_TOAST_CHARS = 240;
|
|
13338
|
+
var APP_LOG_LEVEL = {
|
|
13339
|
+
debug: "debug",
|
|
13340
|
+
info: "info",
|
|
13341
|
+
notice: "info",
|
|
13342
|
+
warn: "warn",
|
|
13343
|
+
error: "error"
|
|
13344
|
+
};
|
|
13345
|
+
function toastMessage(message) {
|
|
13346
|
+
const flat = message.replace(/\s+/g, " ").trim();
|
|
13347
|
+
if (flat.length <= MAX_TOAST_CHARS) return flat;
|
|
13348
|
+
return `${flat.slice(0, MAX_TOAST_CHARS - 1)}\u2026`;
|
|
13349
|
+
}
|
|
13350
|
+
function appLogBody(entry) {
|
|
13351
|
+
return {
|
|
13352
|
+
service: LOG_SERVICE_NAME,
|
|
13353
|
+
level: APP_LOG_LEVEL[entry.level],
|
|
13354
|
+
message: entry.message,
|
|
13355
|
+
...entry.data && Object.keys(entry.data).length > 0 ? { extra: entry.data } : {}
|
|
13356
|
+
};
|
|
13357
|
+
}
|
|
13358
|
+
function toastBody(entry) {
|
|
13359
|
+
return {
|
|
13360
|
+
title: TOAST_TITLE,
|
|
13361
|
+
message: toastMessage(entry.message),
|
|
13362
|
+
variant: entry.level === "error" ? "error" : "warning",
|
|
13363
|
+
duration: TOAST_DURATION_MS
|
|
13364
|
+
};
|
|
13365
|
+
}
|
|
13366
|
+
function fireAndForget(call) {
|
|
13367
|
+
try {
|
|
13368
|
+
const result = call();
|
|
13369
|
+
if (result && typeof result.catch === "function") {
|
|
13370
|
+
void result.catch(() => void 0);
|
|
13371
|
+
}
|
|
13372
|
+
} catch {
|
|
13373
|
+
}
|
|
13374
|
+
}
|
|
13375
|
+
function createOpencodeLogSink(client) {
|
|
13376
|
+
if (!client || typeof client !== "object") return null;
|
|
13377
|
+
const sdk = client;
|
|
13378
|
+
const canLog = typeof sdk.app?.log === "function";
|
|
13379
|
+
const canToast = typeof sdk.tui?.showToast === "function";
|
|
13380
|
+
if (!canLog && !canToast) return null;
|
|
13381
|
+
return {
|
|
13382
|
+
log(entry) {
|
|
13383
|
+
if (!canLog) return;
|
|
13384
|
+
fireAndForget(() => sdk.app.log({ body: appLogBody(entry) }));
|
|
13385
|
+
},
|
|
13386
|
+
toast(entry) {
|
|
13387
|
+
if (!canToast) return;
|
|
13388
|
+
fireAndForget(() => sdk.tui.showToast({ body: toastBody(entry) }));
|
|
13389
|
+
}
|
|
13390
|
+
};
|
|
13391
|
+
}
|
|
13392
|
+
function registerOpencodeLogSink(client) {
|
|
13393
|
+
const sink = createOpencodeLogSink(client);
|
|
13394
|
+
if (!sink) return false;
|
|
13395
|
+
setLogSink(sink);
|
|
13396
|
+
return true;
|
|
13397
|
+
}
|
|
13398
|
+
|
|
13145
13399
|
// src/v2-client.ts
|
|
13146
13400
|
import path10 from "path";
|
|
13147
13401
|
function formatAgentTypeList(agents) {
|
|
@@ -13693,7 +13947,11 @@ function createClaudeCode(settings = {}) {
|
|
|
13693
13947
|
configDir: settings.configDir,
|
|
13694
13948
|
failoverAccounts: settings.failoverAccounts,
|
|
13695
13949
|
baseCliPath: settings.baseCliPath ?? cliPath,
|
|
13696
|
-
|
|
13950
|
+
// Passed through, with no default applied here: the form is opt-in and
|
|
13951
|
+
// `isAccountFailoverQuestionActive` asks for an explicit `"ask"`, so an
|
|
13952
|
+
// unset option must stay unset rather than being written to one mode or
|
|
13953
|
+
// the other in only one of the two entry points (h #g194).
|
|
13954
|
+
accountFailover: settings.accountFailover,
|
|
13697
13955
|
providerID: settings.providerID,
|
|
13698
13956
|
skipPermissions: preset?.skipPermissions ?? settings.skipPermissions ?? true,
|
|
13699
13957
|
permissionMode: preset?.permissionMode ?? settings.permissionMode,
|
|
@@ -13966,7 +14224,9 @@ var server = async (input) => {
|
|
|
13966
14224
|
ensureProcessExitCleanup();
|
|
13967
14225
|
const opencodeVersion = pickOpencodeVersion(input);
|
|
13968
14226
|
if (input && typeof input === "object" && "client" in input) {
|
|
13969
|
-
|
|
14227
|
+
const client = input.client;
|
|
14228
|
+
setOpencodeClient(client);
|
|
14229
|
+
registerOpencodeLogSink(client);
|
|
13970
14230
|
}
|
|
13971
14231
|
setOpencodeProjectDirectory(pickOpencodeDirectory(input));
|
|
13972
14232
|
return {
|