@khalilgharbaoui/opencode-claude-code-plugin 0.37.1 → 0.39.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 +4 -3
- package/dist/index.d.ts +18 -0
- package/dist/index.js +1028 -318
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/skills/claude-code-plugin/SKILL.md +58 -18
package/dist/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// src/claude-code-language-model.ts
|
|
2
|
+
import { join as join11 } from "path";
|
|
3
|
+
|
|
1
4
|
// src/ids.ts
|
|
2
5
|
import { randomInt } from "crypto";
|
|
3
6
|
var ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
@@ -72,6 +75,14 @@ function parseInput(raw) {
|
|
|
72
75
|
return {};
|
|
73
76
|
}
|
|
74
77
|
}
|
|
78
|
+
function hostedToolResult(part) {
|
|
79
|
+
const result = part.result;
|
|
80
|
+
if (part.providerExecuted !== true) return part.result;
|
|
81
|
+
if (!result || typeof result !== "object" || typeof result.output !== "string") {
|
|
82
|
+
return part.result;
|
|
83
|
+
}
|
|
84
|
+
return result.output;
|
|
85
|
+
}
|
|
75
86
|
function createHostToolPartTranslator(dialect = "v1") {
|
|
76
87
|
if (dialect !== "v2") return (part) => part;
|
|
77
88
|
const dropped = /* @__PURE__ */ new Set();
|
|
@@ -118,7 +129,8 @@ function createHostToolPartTranslator(dialect = "v1") {
|
|
|
118
129
|
case "tool-result": {
|
|
119
130
|
const tool = classify2(String(part.toolCallId), part.toolName);
|
|
120
131
|
if (tool === null) return null;
|
|
121
|
-
|
|
132
|
+
const result = hostedToolResult(part);
|
|
133
|
+
return tool ? { ...part, toolName: tool.name, result } : { ...part, result };
|
|
122
134
|
}
|
|
123
135
|
default:
|
|
124
136
|
return part;
|
|
@@ -584,6 +596,20 @@ function formatResetsAt(resetsAt) {
|
|
|
584
596
|
const date = new Date(ms);
|
|
585
597
|
return Number.isNaN(date.getTime()) ? void 0 : date.toISOString();
|
|
586
598
|
}
|
|
599
|
+
function formatLocalMinute(ms) {
|
|
600
|
+
const when = new Date(ms);
|
|
601
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
602
|
+
return `${when.getFullYear()}-${pad(when.getMonth() + 1)}-${pad(when.getDate())} ${pad(when.getHours())}:${pad(when.getMinutes())}`;
|
|
603
|
+
}
|
|
604
|
+
function formatResetsAtLocal(resetsAt) {
|
|
605
|
+
const ms = resetsAtToMs(resetsAt);
|
|
606
|
+
if (ms === void 0 || Number.isNaN(new Date(ms).getTime())) return void 0;
|
|
607
|
+
return formatLocalMinute(ms);
|
|
608
|
+
}
|
|
609
|
+
function describeRateLimitWindow(rateLimitType) {
|
|
610
|
+
if (!rateLimitType) return void 0;
|
|
611
|
+
return RATE_LIMIT_WINDOWS[rateLimitType] ?? rateLimitType;
|
|
612
|
+
}
|
|
587
613
|
function rateLimitKey(info) {
|
|
588
614
|
return [
|
|
589
615
|
info.status ?? "?",
|
|
@@ -599,7 +625,7 @@ function isRateLimitRejected(info) {
|
|
|
599
625
|
}
|
|
600
626
|
function describeRateLimit(info) {
|
|
601
627
|
if (!info.status && !info.overageStatus) return null;
|
|
602
|
-
const window = info.rateLimitType
|
|
628
|
+
const window = describeRateLimitWindow(info.rateLimitType);
|
|
603
629
|
const resets = formatResetsAt(info.resetsAt);
|
|
604
630
|
const overageResets = formatResetsAt(info.overageResetsAt);
|
|
605
631
|
const key = rateLimitKey(info);
|
|
@@ -614,9 +640,7 @@ function describeRateLimit(info) {
|
|
|
614
640
|
if (resetAt) parts.push(`Resets at ${resetAt}.`);
|
|
615
641
|
parts.push(RATE_LIMIT_ACTION);
|
|
616
642
|
const message = parts.join(" ");
|
|
617
|
-
return { key, level: "warn", message, transcript:
|
|
618
|
-
${RATE_LIMIT_MARKER} ${message}
|
|
619
|
-
` };
|
|
643
|
+
return { key, level: "warn", message, transcript: null };
|
|
620
644
|
}
|
|
621
645
|
if (info.status === "allowed_warning" || info.overageStatus === "allowed_warning") {
|
|
622
646
|
const used = info.utilization === void 0 ? "" : ` (${Math.round(info.utilization * 100)}% used)`;
|
|
@@ -1083,6 +1107,7 @@ var PLAN_MODE_APPROVAL_QUESTION = "Do you want to proceed with this plan?";
|
|
|
1083
1107
|
var OPENCODE_QUESTION_RESULT_PREFIX = "User has answered your questions: ";
|
|
1084
1108
|
var OPENCODE_QUESTION_RESULT_SUFFIX = ". You can now continue with the user's answers in mind.";
|
|
1085
1109
|
var OPENCODE_UNANSWERED = "Unanswered";
|
|
1110
|
+
var DCP_MESSAGE_ID_SUFFIX = /(?:\s*<dcp-message-id>[^<>]*<\/dcp-message-id>)+\s*$/;
|
|
1086
1111
|
var KEY_SEPARATOR = "\0";
|
|
1087
1112
|
function isPlanModeQuestionActive(input) {
|
|
1088
1113
|
if (input.compactionMode) return false;
|
|
@@ -1182,15 +1207,16 @@ function unwrapToolOutput(part) {
|
|
|
1182
1207
|
}
|
|
1183
1208
|
}
|
|
1184
1209
|
function unwrapOpencodeQuestionResult(value, question) {
|
|
1185
|
-
const
|
|
1210
|
+
const untagged = value.replace(DCP_MESSAGE_ID_SUFFIX, "");
|
|
1211
|
+
const sentence = untagged.trim();
|
|
1186
1212
|
if (!sentence.startsWith(OPENCODE_QUESTION_RESULT_PREFIX) || !sentence.endsWith(OPENCODE_QUESTION_RESULT_SUFFIX)) {
|
|
1187
|
-
return
|
|
1213
|
+
return untagged;
|
|
1188
1214
|
}
|
|
1189
1215
|
const body = sentence.slice(
|
|
1190
1216
|
OPENCODE_QUESTION_RESULT_PREFIX.length,
|
|
1191
1217
|
sentence.length - OPENCODE_QUESTION_RESULT_SUFFIX.length
|
|
1192
1218
|
);
|
|
1193
|
-
if (!body.startsWith('"') || !body.endsWith('"')) return
|
|
1219
|
+
if (!body.startsWith('"') || !body.endsWith('"')) return untagged;
|
|
1194
1220
|
let answer;
|
|
1195
1221
|
const head = question === void 0 ? void 0 : `"${question}"="`;
|
|
1196
1222
|
if (head && body.startsWith(head) && body.length > head.length) {
|
|
@@ -1199,7 +1225,7 @@ function unwrapOpencodeQuestionResult(value, question) {
|
|
|
1199
1225
|
const split = body.lastIndexOf('"="');
|
|
1200
1226
|
if (split > 0) answer = body.slice(split + 3, -1);
|
|
1201
1227
|
}
|
|
1202
|
-
if (answer === void 0) return
|
|
1228
|
+
if (answer === void 0) return untagged;
|
|
1203
1229
|
return answer === OPENCODE_UNANSWERED ? "" : answer;
|
|
1204
1230
|
}
|
|
1205
1231
|
function collectAnswerStrings(value, question) {
|
|
@@ -1257,6 +1283,13 @@ var ACCOUNT_LIMIT_PATTERNS = [
|
|
|
1257
1283
|
/you[’'`]?ve hit your individual spend limit/i
|
|
1258
1284
|
];
|
|
1259
1285
|
var ACCOUNT_BLOCK_MARKER = "\u258C **claude account:**";
|
|
1286
|
+
var USAGE_LIMIT_MARKER = "\u258C **usage limit:**";
|
|
1287
|
+
function describeOtherAccounts(candidates) {
|
|
1288
|
+
const names = candidates.map((candidate) => normalizeAccountName(candidate)).filter(Boolean).map((candidate) => `"${candidate}"`);
|
|
1289
|
+
if (names.length === 0) return "";
|
|
1290
|
+
const joined = names.length === 1 ? names[0] : `${names.slice(0, -1).join(", ")} or ${names[names.length - 1]}`;
|
|
1291
|
+
return `the ${joined} account`;
|
|
1292
|
+
}
|
|
1260
1293
|
var ACCOUNT_BLOCKS = {
|
|
1261
1294
|
authentication_failed: { what: "is not logged in (its login expired or was revoked)", login: true },
|
|
1262
1295
|
oauth_org_not_allowed: { what: "belongs to an organization that does not allow this login", login: false },
|
|
@@ -1280,17 +1313,51 @@ function formatAccountBlockNote(input) {
|
|
|
1280
1313
|
const block = ACCOUNT_BLOCKS[input.kind];
|
|
1281
1314
|
const account = normalizeAccountName(input.account || DEFAULT_ACCOUNT);
|
|
1282
1315
|
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.";
|
|
1283
|
-
const
|
|
1316
|
+
const others = input.offeringSwitch ? "" : describeOtherAccounts(input.candidates ?? []);
|
|
1317
|
+
const then = input.offeringSwitch ? " Or pick another account below." : others ? ` Or pick a model from ${others} and resend.` : "";
|
|
1284
1318
|
return `
|
|
1285
1319
|
${ACCOUNT_BLOCK_MARKER} the Claude account "${account}" ${block.what}. ${fix}${then}
|
|
1286
1320
|
`;
|
|
1287
1321
|
}
|
|
1322
|
+
function formatUsageLimitNote(input) {
|
|
1323
|
+
const account = normalizeAccountName(input.sourceAccount || DEFAULT_ACCOUNT);
|
|
1324
|
+
const window = describeRateLimitWindow(input.window);
|
|
1325
|
+
const resets = formatResetsAtLocal(input.resetsAt);
|
|
1326
|
+
const others = describeOtherAccounts(input.candidates);
|
|
1327
|
+
const what = [
|
|
1328
|
+
`the Claude account "${account}" is out of usage`,
|
|
1329
|
+
window ? ` in ${window}` : "",
|
|
1330
|
+
resets ? `, which resets at ${resets}` : "",
|
|
1331
|
+
"."
|
|
1332
|
+
].join("");
|
|
1333
|
+
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.";
|
|
1334
|
+
return `
|
|
1335
|
+
${USAGE_LIMIT_MARKER} ${what} ${advice}
|
|
1336
|
+
`;
|
|
1337
|
+
}
|
|
1288
1338
|
function isAccountLimitError(input) {
|
|
1289
1339
|
if (input.rateLimit && isRateLimitRejected(input.rateLimit)) return true;
|
|
1290
1340
|
const text = input.resultText;
|
|
1291
1341
|
if (typeof text !== "string" || text.length === 0) return false;
|
|
1292
1342
|
return ACCOUNT_LIMIT_PATTERNS.some((pattern) => pattern.test(text));
|
|
1293
1343
|
}
|
|
1344
|
+
var lastAccountLimits = /* @__PURE__ */ new Map();
|
|
1345
|
+
function rememberAccountLimit(account, facts) {
|
|
1346
|
+
const name = normalizeAccountName(account || DEFAULT_ACCOUNT);
|
|
1347
|
+
if (!name) return;
|
|
1348
|
+
lastAccountLimits.set(name, { resetsAt: facts.resetsAt, window: facts.window });
|
|
1349
|
+
}
|
|
1350
|
+
function recallAccountLimit(account, now = Date.now()) {
|
|
1351
|
+
const name = normalizeAccountName(account || DEFAULT_ACCOUNT);
|
|
1352
|
+
const facts = lastAccountLimits.get(name);
|
|
1353
|
+
if (!facts) return {};
|
|
1354
|
+
const resetsAtMs = resetsAtToMs(facts.resetsAt);
|
|
1355
|
+
const fresh = resetsAtMs !== void 0 && resetsAtMs > now;
|
|
1356
|
+
return {
|
|
1357
|
+
...fresh ? { resetsAt: facts.resetsAt } : {},
|
|
1358
|
+
...facts.window === void 0 ? {} : { window: facts.window }
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1294
1361
|
var accountOverrides = /* @__PURE__ */ new Map();
|
|
1295
1362
|
function setAccountOverride(source, target, until, now = Date.now()) {
|
|
1296
1363
|
const from = normalizeAccountName(source || DEFAULT_ACCOUNT);
|
|
@@ -1358,7 +1425,7 @@ function isAccountFailoverQuestionActive(input) {
|
|
|
1358
1425
|
if (input.compactionMode) return false;
|
|
1359
1426
|
if (input.interactive) return false;
|
|
1360
1427
|
if (input.childSession) return false;
|
|
1361
|
-
if (input.configured
|
|
1428
|
+
if (input.configured !== "ask") return false;
|
|
1362
1429
|
if (input.candidates.length === 0) return false;
|
|
1363
1430
|
return input.opencodeHasQuestion;
|
|
1364
1431
|
}
|
|
@@ -4204,6 +4271,11 @@ function findForkParent(opts) {
|
|
|
4204
4271
|
}
|
|
4205
4272
|
return best;
|
|
4206
4273
|
}
|
|
4274
|
+
function continuesRecordedConversation(recorded, prompt) {
|
|
4275
|
+
const { history, forkable } = splitForkHistory(prompt);
|
|
4276
|
+
if (!forkable || recorded.length === 0 || recorded.length > history.length) return false;
|
|
4277
|
+
return isPrefix(recorded, history) && tailIsReplyOnly(history, recorded.length);
|
|
4278
|
+
}
|
|
4207
4279
|
function isPrefix(recorded, history) {
|
|
4208
4280
|
for (let i = 0; i < recorded.length; i++) {
|
|
4209
4281
|
if (recorded[i].role !== history[i].role) return false;
|
|
@@ -4218,6 +4290,109 @@ function tailIsReplyOnly(history, from) {
|
|
|
4218
4290
|
return true;
|
|
4219
4291
|
}
|
|
4220
4292
|
|
|
4293
|
+
// src/session-resume-store.ts
|
|
4294
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync2, renameSync as renameSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
4295
|
+
import { homedir as homedir4 } from "os";
|
|
4296
|
+
import { join as join5 } from "path";
|
|
4297
|
+
var MAX_RESUME_RECORDS = 256;
|
|
4298
|
+
var RESUME_RECORD_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
4299
|
+
var records = null;
|
|
4300
|
+
var pathOverride = null;
|
|
4301
|
+
function resumeStorePath() {
|
|
4302
|
+
if (pathOverride) return pathOverride;
|
|
4303
|
+
const stateRoot = process.env.XDG_STATE_HOME || join5(homedir4(), ".local", "state");
|
|
4304
|
+
return join5(stateRoot, "opencode-claude-code-plugin", "claude-sessions.json");
|
|
4305
|
+
}
|
|
4306
|
+
function isDigest(value) {
|
|
4307
|
+
const entry = value;
|
|
4308
|
+
return !!entry && (entry.role === "user" || entry.role === "assistant" || entry.role === "tool") && typeof entry.digest === "string";
|
|
4309
|
+
}
|
|
4310
|
+
function isRecord2(value) {
|
|
4311
|
+
const record = value;
|
|
4312
|
+
return !!record && typeof record.claudeSessionId === "string" && typeof record.cliPath === "string" && typeof record.updatedAt === "number" && Array.isArray(record.chain) && record.chain.every(isDigest);
|
|
4313
|
+
}
|
|
4314
|
+
function load() {
|
|
4315
|
+
if (records) return records;
|
|
4316
|
+
records = /* @__PURE__ */ new Map();
|
|
4317
|
+
try {
|
|
4318
|
+
const parsed = JSON.parse(readFileSync2(resumeStorePath(), "utf8"));
|
|
4319
|
+
const entries = parsed && typeof parsed === "object" ? parsed.sessions : null;
|
|
4320
|
+
if (entries && typeof entries === "object") {
|
|
4321
|
+
const cutoff = Date.now() - RESUME_RECORD_MAX_AGE_MS;
|
|
4322
|
+
const valid = Object.entries(entries).filter((entry) => isRecord2(entry[1])).filter(([, record]) => record.updatedAt >= cutoff).sort((a, b) => a[1].updatedAt - b[1].updatedAt);
|
|
4323
|
+
for (const [key, record] of valid) records.set(key, record);
|
|
4324
|
+
}
|
|
4325
|
+
} catch {
|
|
4326
|
+
}
|
|
4327
|
+
return records;
|
|
4328
|
+
}
|
|
4329
|
+
function save(update) {
|
|
4330
|
+
const file = resumeStorePath();
|
|
4331
|
+
const merged = /* @__PURE__ */ new Map();
|
|
4332
|
+
try {
|
|
4333
|
+
const onDisk = JSON.parse(readFileSync2(file, "utf8"))?.sessions;
|
|
4334
|
+
if (onDisk && typeof onDisk === "object") {
|
|
4335
|
+
for (const [key, record] of Object.entries(onDisk)) {
|
|
4336
|
+
if (isRecord2(record)) merged.set(key, record);
|
|
4337
|
+
}
|
|
4338
|
+
}
|
|
4339
|
+
} catch {
|
|
4340
|
+
}
|
|
4341
|
+
update(merged);
|
|
4342
|
+
const cutoff = Date.now() - RESUME_RECORD_MAX_AGE_MS;
|
|
4343
|
+
const kept = [...merged.entries()].filter(([, record]) => record.updatedAt >= cutoff).sort((a, b) => a[1].updatedAt - b[1].updatedAt).slice(-MAX_RESUME_RECORDS);
|
|
4344
|
+
records = new Map(kept);
|
|
4345
|
+
try {
|
|
4346
|
+
mkdirSync3(join5(file, ".."), { recursive: true, mode: 448 });
|
|
4347
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
4348
|
+
writeFileSync3(tmp, JSON.stringify({ version: 1, sessions: Object.fromEntries(kept) }), {
|
|
4349
|
+
mode: 384
|
|
4350
|
+
});
|
|
4351
|
+
renameSync2(tmp, file);
|
|
4352
|
+
} catch (err) {
|
|
4353
|
+
log.debug("could not write the claude session resume store", {
|
|
4354
|
+
error: err instanceof Error ? err.message : String(err)
|
|
4355
|
+
});
|
|
4356
|
+
}
|
|
4357
|
+
}
|
|
4358
|
+
function recordResumePoint(sessionKey2, claudeSessionId, prompt, cliPath) {
|
|
4359
|
+
const chain = conversationDigests(prompt);
|
|
4360
|
+
if (chain.length === 0) return;
|
|
4361
|
+
const current = load().get(sessionKey2);
|
|
4362
|
+
if (current && current.claudeSessionId === claudeSessionId && current.cliPath === cliPath && current.chain.length === chain.length && current.chain.every((entry, i) => entry.digest === chain[i].digest)) {
|
|
4363
|
+
return;
|
|
4364
|
+
}
|
|
4365
|
+
save((merged) => {
|
|
4366
|
+
merged.delete(sessionKey2);
|
|
4367
|
+
merged.set(sessionKey2, { claudeSessionId, cliPath, chain, updatedAt: Date.now() });
|
|
4368
|
+
});
|
|
4369
|
+
}
|
|
4370
|
+
function forgetResumePoint(sessionKey2) {
|
|
4371
|
+
if (!load().has(sessionKey2)) return;
|
|
4372
|
+
save((merged) => {
|
|
4373
|
+
merged.delete(sessionKey2);
|
|
4374
|
+
});
|
|
4375
|
+
}
|
|
4376
|
+
function findResumePoint(opts) {
|
|
4377
|
+
const record = load().get(opts.sessionKey);
|
|
4378
|
+
if (!record) return void 0;
|
|
4379
|
+
const refuse = (reason) => {
|
|
4380
|
+
log.info("not resuming the claude session from before the restart", {
|
|
4381
|
+
sessionKey: opts.sessionKey,
|
|
4382
|
+
reason
|
|
4383
|
+
});
|
|
4384
|
+
return void 0;
|
|
4385
|
+
};
|
|
4386
|
+
if (record.cliPath !== opts.cliPath) return refuse("another claude binary");
|
|
4387
|
+
if (!continuesRecordedConversation(record.chain, opts.prompt)) {
|
|
4388
|
+
return refuse("the conversation changed");
|
|
4389
|
+
}
|
|
4390
|
+
if (!existsSync2(opts.transcriptPath(record.claudeSessionId))) {
|
|
4391
|
+
return refuse("the transcript is gone");
|
|
4392
|
+
}
|
|
4393
|
+
return { claudeSessionId: record.claudeSessionId, matched: record.chain.length };
|
|
4394
|
+
}
|
|
4395
|
+
|
|
4221
4396
|
// src/permission-presets.ts
|
|
4222
4397
|
var READ_ONLY_DISALLOWED_CLI_TOOLS = [
|
|
4223
4398
|
"Bash",
|
|
@@ -4330,7 +4505,7 @@ import { randomUUID as randomUUID2 } from "crypto";
|
|
|
4330
4505
|
var MAX_HISTORY_EXCHANGES = 20;
|
|
4331
4506
|
var SIDE_QUESTION_USAGE = "Usage: /btw <question>. Ask a side question about the current conversation without adding it to the main context.";
|
|
4332
4507
|
var pendingProcesses = /* @__PURE__ */ new WeakSet();
|
|
4333
|
-
function
|
|
4508
|
+
function isRecord3(value) {
|
|
4334
4509
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4335
4510
|
}
|
|
4336
4511
|
var SYSTEM_REMINDER_BLOCK = /<system-reminder>[\s\S]*?<\/system-reminder>/g;
|
|
@@ -4341,7 +4516,7 @@ function parseSideQuestionContent(content) {
|
|
|
4341
4516
|
} else if (Array.isArray(content)) {
|
|
4342
4517
|
const parts = [];
|
|
4343
4518
|
for (const part of content) {
|
|
4344
|
-
if (!
|
|
4519
|
+
if (!isRecord3(part) || part.type !== "text" || typeof part.text !== "string") return null;
|
|
4345
4520
|
parts.push(part.text);
|
|
4346
4521
|
}
|
|
4347
4522
|
text = parts.join("\n");
|
|
@@ -4360,7 +4535,7 @@ function assistantText(content) {
|
|
|
4360
4535
|
if (!Array.isArray(content)) return "";
|
|
4361
4536
|
const parts = [];
|
|
4362
4537
|
for (const part of content) {
|
|
4363
|
-
if (
|
|
4538
|
+
if (isRecord3(part) && part.type === "text" && typeof part.text === "string") parts.push(part.text);
|
|
4364
4539
|
}
|
|
4365
4540
|
return parts.join("\n").trim();
|
|
4366
4541
|
}
|
|
@@ -4390,9 +4565,9 @@ function dispatchSideQuestionResponse(activeProcess, line) {
|
|
|
4390
4565
|
} catch {
|
|
4391
4566
|
return false;
|
|
4392
4567
|
}
|
|
4393
|
-
if (!
|
|
4568
|
+
if (!isRecord3(message) || message.type !== "control_response") return false;
|
|
4394
4569
|
const response = message.response;
|
|
4395
|
-
if (!
|
|
4570
|
+
if (!isRecord3(response) || typeof response.request_id !== "string") return false;
|
|
4396
4571
|
return activeProcess.lineEmitter.emit(`side-question:${response.request_id}`, response);
|
|
4397
4572
|
}
|
|
4398
4573
|
async function requestSideQuestion(activeProcess, question, options) {
|
|
@@ -4477,7 +4652,7 @@ async function requestSideQuestion(activeProcess, question, options) {
|
|
|
4477
4652
|
return;
|
|
4478
4653
|
}
|
|
4479
4654
|
const result = response.response;
|
|
4480
|
-
if (response.subtype !== "success" || !
|
|
4655
|
+
if (response.subtype !== "success" || !isRecord3(result) || typeof result.response !== "string" || typeof result.synthetic !== "boolean") {
|
|
4481
4656
|
fail(new Error("Claude Code returned an invalid /btw response."));
|
|
4482
4657
|
return;
|
|
4483
4658
|
}
|
|
@@ -4647,6 +4822,7 @@ function noteTurnLine(ap, line) {
|
|
|
4647
4822
|
if (isTerminalResultLine(line)) settleTurn(ap);
|
|
4648
4823
|
}
|
|
4649
4824
|
function isTurnInFlight(ap) {
|
|
4825
|
+
if (ap.interactiveControl) return ap.interactiveControl.turnRunning();
|
|
4650
4826
|
return ap.turnInFlight === true;
|
|
4651
4827
|
}
|
|
4652
4828
|
function awaitTurnIdle(ap, timeoutMs) {
|
|
@@ -4666,6 +4842,7 @@ function awaitTurnIdle(ap, timeoutMs) {
|
|
|
4666
4842
|
});
|
|
4667
4843
|
}
|
|
4668
4844
|
function interruptTurn(ap, timeoutMs = TURN_INTERRUPT_TIMEOUT_MS) {
|
|
4845
|
+
if (ap.interactiveControl) return ap.interactiveControl.interrupt(timeoutMs);
|
|
4669
4846
|
if (!ap.turnInFlight) return Promise.resolve(true);
|
|
4670
4847
|
const stdin = ap.proc.stdin;
|
|
4671
4848
|
if (ap.asideTransport?.interactive || !stdin || !stdin.writable) {
|
|
@@ -4706,6 +4883,15 @@ function setActiveProcess(key, ap) {
|
|
|
4706
4883
|
cancelIdleProcessEviction(key);
|
|
4707
4884
|
activeProcesses.set(key, ap);
|
|
4708
4885
|
}
|
|
4886
|
+
function noteInteractiveProcessExit(sessionKey2, ap, code) {
|
|
4887
|
+
if (activeProcesses.get(sessionKey2) !== ap) return;
|
|
4888
|
+
cancelIdleProcessEviction(sessionKey2);
|
|
4889
|
+
activeProcesses.delete(sessionKey2);
|
|
4890
|
+
if (code !== 0 && code !== null) {
|
|
4891
|
+
claudeSessions.delete(sessionKey2);
|
|
4892
|
+
forgetResumePoint(sessionKey2);
|
|
4893
|
+
}
|
|
4894
|
+
}
|
|
4709
4895
|
function scheduleIdleProcessEviction(key, timeoutMs) {
|
|
4710
4896
|
cancelIdleProcessEviction(key);
|
|
4711
4897
|
if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_IDLE_TIMEOUT_MS) {
|
|
@@ -4851,6 +5037,7 @@ function deleteClaudeSessionId(key) {
|
|
|
4851
5037
|
if (claudeSessionId) clearLedger(claudeSessionId);
|
|
4852
5038
|
claudeSessions.delete(key);
|
|
4853
5039
|
forgetForkFingerprint(key);
|
|
5040
|
+
forgetResumePoint(key);
|
|
4854
5041
|
}
|
|
4855
5042
|
function effortSessionKey(baseKey, effort) {
|
|
4856
5043
|
return effort ? `${baseKey}::effort=${effort}` : baseKey;
|
|
@@ -4963,6 +5150,7 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
|
|
|
4963
5150
|
sessionKey: sessionKey2
|
|
4964
5151
|
});
|
|
4965
5152
|
claudeSessions.delete(sessionKey2);
|
|
5153
|
+
forgetResumePoint(sessionKey2);
|
|
4966
5154
|
}
|
|
4967
5155
|
});
|
|
4968
5156
|
proc.stderr?.on("data", (data) => {
|
|
@@ -4977,6 +5165,7 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
|
|
|
4977
5165
|
error: stderr.slice(0, 200)
|
|
4978
5166
|
});
|
|
4979
5167
|
claudeSessions.delete(sessionKey2);
|
|
5168
|
+
forgetResumePoint(sessionKey2);
|
|
4980
5169
|
} else {
|
|
4981
5170
|
log.debug("ignoring session ID error from stale claude process", {
|
|
4982
5171
|
sessionKey: sessionKey2
|
|
@@ -5455,7 +5644,7 @@ async function handleBtwCommand(client, input, options = {}) {
|
|
|
5455
5644
|
// src/diagnostic-bundle.ts
|
|
5456
5645
|
import { createHash as createHash4, randomBytes as randomBytes2 } from "crypto";
|
|
5457
5646
|
import { openSync, readSync, closeSync, fstatSync, realpathSync } from "fs";
|
|
5458
|
-
import { homedir as
|
|
5647
|
+
import { homedir as homedir5 } from "os";
|
|
5459
5648
|
|
|
5460
5649
|
// src/log-messages.ts
|
|
5461
5650
|
var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
@@ -5474,6 +5663,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
5474
5663
|
"btw: early aside failed; the message will ask again",
|
|
5475
5664
|
"cannot interrupt this transport; waiting for the turn to end",
|
|
5476
5665
|
"claude --version output unparseable",
|
|
5666
|
+
"claude account is out of usage; the turn ends with a note",
|
|
5477
5667
|
"claude already registers a different skill under this name; not bridging ours",
|
|
5478
5668
|
"claude cli < 2.1.142 detected; Opus 4.7 thinking summaries unavailable. Run `npm i -g @anthropic-ai/claude-code` to upgrade.",
|
|
5479
5669
|
"claude cli does not support --plugin-dir; opencode skills will not be bridged. Run `npm i -g @anthropic-ai/claude-code` to upgrade.",
|
|
@@ -5530,6 +5720,9 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
5530
5720
|
"ignoring unknown proxyTools entries",
|
|
5531
5721
|
"interactive permissionMode bypassPermissions ignored: Claude Code prompts for confirmation in the TUI",
|
|
5532
5722
|
"interactive system prompt disabled; opencode agent prompts will not be appended",
|
|
5723
|
+
"interactive transport cancelled the usage-limit auto-continue",
|
|
5724
|
+
"interactive transport cannot continue",
|
|
5725
|
+
"interactive transport denied a permission prompt",
|
|
5533
5726
|
"interactive transport dropped non-text content blocks",
|
|
5534
5727
|
"interactive turn failed",
|
|
5535
5728
|
"interactiveBypass ignored: Claude Code prompts for bypassPermissions confirmation in the interactive TUI",
|
|
@@ -5561,6 +5754,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
5561
5754
|
"remote URL images are not supported by Claude CLI, skipping",
|
|
5562
5755
|
"replaying stdout the child emitted between turns",
|
|
5563
5756
|
"result fallback timer fired \u2014 closing stream without result event",
|
|
5757
|
+
"resuming the claude session from before the restart instead of replaying it",
|
|
5564
5758
|
"shared Claude capability already exists; leaving untouched",
|
|
5565
5759
|
"skipping MCP server with unknown type",
|
|
5566
5760
|
"skipping local MCP server with no command",
|
|
@@ -5784,7 +5978,7 @@ function createIdHasher(salt = randomBytes2(16).toString("hex")) {
|
|
|
5784
5978
|
return (raw) => createHash4("sha256").update(salt).update("\0").update(raw).digest("hex").slice(0, 8);
|
|
5785
5979
|
}
|
|
5786
5980
|
function createRedactionContext(options = {}) {
|
|
5787
|
-
const home = options.home ??
|
|
5981
|
+
const home = options.home ?? homedir5();
|
|
5788
5982
|
const homes = /* @__PURE__ */ new Set();
|
|
5789
5983
|
if (home) homes.add(home);
|
|
5790
5984
|
if (home) {
|
|
@@ -6328,11 +6522,6 @@ function compareBuilds(loaded, onDisk) {
|
|
|
6328
6522
|
rebuiltAt: onDisk.mtimeMs
|
|
6329
6523
|
};
|
|
6330
6524
|
}
|
|
6331
|
-
function formatLocalMinute(ms) {
|
|
6332
|
-
const when = new Date(ms);
|
|
6333
|
-
const pad = (value) => String(value).padStart(2, "0");
|
|
6334
|
-
return `${when.getFullYear()}-${pad(when.getMonth() + 1)}-${pad(when.getDate())} ${pad(when.getHours())}:${pad(when.getMinutes())}`;
|
|
6335
|
-
}
|
|
6336
6525
|
function formatStaleBuildNote(stale) {
|
|
6337
6526
|
const loadedAt = formatLocalMinute(stale.loadedAt);
|
|
6338
6527
|
if (stale.kind === "version") {
|
|
@@ -6463,7 +6652,7 @@ function staleBuildWatch() {
|
|
|
6463
6652
|
var DOCTOR_COMMAND = "claude-code-doctor";
|
|
6464
6653
|
var DOCTOR_COMMAND_DESCRIPTION = "Report what the Claude Code plugin sees: versions, cwd, live processes, pending proxy calls. Add `usage` for plan windows, or `bundle` for a redacted log you can paste into an issue";
|
|
6465
6654
|
var DOCTOR_MARKER = "\u258C **claude-code doctor**";
|
|
6466
|
-
function
|
|
6655
|
+
function isRecord4(value) {
|
|
6467
6656
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
6468
6657
|
}
|
|
6469
6658
|
var SYSTEM_REMINDER_BLOCK2 = /<system-reminder>[\s\S]*?<\/system-reminder>/g;
|
|
@@ -6474,7 +6663,7 @@ function parseDoctorCommandContent(content) {
|
|
|
6474
6663
|
} else if (Array.isArray(content)) {
|
|
6475
6664
|
const parts = [];
|
|
6476
6665
|
for (const part of content) {
|
|
6477
|
-
if (!
|
|
6666
|
+
if (!isRecord4(part) || part.type !== "text" || typeof part.text !== "string") return null;
|
|
6478
6667
|
parts.push(part.text);
|
|
6479
6668
|
}
|
|
6480
6669
|
text = parts.join("\n");
|
|
@@ -7215,8 +7404,8 @@ var registry = {};
|
|
|
7215
7404
|
var defaultSubagentModel;
|
|
7216
7405
|
var defaultSubagentCacheTtl;
|
|
7217
7406
|
var providerFallbackModels = [];
|
|
7218
|
-
function setAgentRegistry(
|
|
7219
|
-
registry =
|
|
7407
|
+
function setAgentRegistry(records2) {
|
|
7408
|
+
registry = records2;
|
|
7220
7409
|
}
|
|
7221
7410
|
function getAgentRegistry() {
|
|
7222
7411
|
return registry;
|
|
@@ -7356,7 +7545,7 @@ function parseFallbackModelList(value) {
|
|
|
7356
7545
|
return value.trim().replace(/^\[|\]$/g, "").split(/[,\s]+/).map((entry) => entry.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
|
|
7357
7546
|
}
|
|
7358
7547
|
async function readAgentMarkdownRecords(directories) {
|
|
7359
|
-
const
|
|
7548
|
+
const records2 = {};
|
|
7360
7549
|
for (const directory of directories) {
|
|
7361
7550
|
let entries;
|
|
7362
7551
|
try {
|
|
@@ -7367,10 +7556,10 @@ async function readAgentMarkdownRecords(directories) {
|
|
|
7367
7556
|
for (const entry of entries) {
|
|
7368
7557
|
if (!entry.endsWith(".md")) continue;
|
|
7369
7558
|
const name = entry.slice(0, -3);
|
|
7370
|
-
if (
|
|
7559
|
+
if (records2[name]) continue;
|
|
7371
7560
|
try {
|
|
7372
7561
|
const text = await readFile(path7.join(directory, entry), "utf8");
|
|
7373
|
-
|
|
7562
|
+
records2[name] = parseAgentFrontmatter(text);
|
|
7374
7563
|
} catch (err) {
|
|
7375
7564
|
log.debug("failed to read agent markdown", {
|
|
7376
7565
|
file: path7.join(directory, entry),
|
|
@@ -7379,7 +7568,7 @@ async function readAgentMarkdownRecords(directories) {
|
|
|
7379
7568
|
}
|
|
7380
7569
|
}
|
|
7381
7570
|
}
|
|
7382
|
-
return
|
|
7571
|
+
return records2;
|
|
7383
7572
|
}
|
|
7384
7573
|
function agentDirectories(home, projectDirectory) {
|
|
7385
7574
|
const directories = [];
|
|
@@ -7558,6 +7747,7 @@ var PLUGIN_NOTE_MARKERS = [
|
|
|
7558
7747
|
SILENT_TURN_MARKER,
|
|
7559
7748
|
FAILOVER_MARKER,
|
|
7560
7749
|
ACCOUNT_BLOCK_MARKER,
|
|
7750
|
+
USAGE_LIMIT_MARKER,
|
|
7561
7751
|
MODEL_FALLBACK_MARKER,
|
|
7562
7752
|
UNATTENDED_REPLAY_MARKER,
|
|
7563
7753
|
STALE_BUILD_MARKER
|
|
@@ -8317,6 +8507,7 @@ async function resolveSkillPluginDirs(opts) {
|
|
|
8317
8507
|
|
|
8318
8508
|
// src/claude-session-wrapper.ts
|
|
8319
8509
|
import { EventEmitter as EventEmitter4 } from "events";
|
|
8510
|
+
import { existsSync as existsSync4 } from "fs";
|
|
8320
8511
|
import { unlink as unlink2 } from "fs/promises";
|
|
8321
8512
|
|
|
8322
8513
|
// src/claude-session-bun.ts
|
|
@@ -8350,10 +8541,99 @@ function encodeCwd(cwd) {
|
|
|
8350
8541
|
}
|
|
8351
8542
|
return real.replace(/[^a-zA-Z0-9]/g, "-");
|
|
8352
8543
|
}
|
|
8544
|
+
var bunPtySpawner = (argv, opts) => {
|
|
8545
|
+
const [command, ...args] = argv;
|
|
8546
|
+
return Bun.spawn([resolveClaude(command ?? "claude"), ...args], {
|
|
8547
|
+
cwd: opts.cwd,
|
|
8548
|
+
env: opts.env,
|
|
8549
|
+
terminal: {
|
|
8550
|
+
cols: opts.cols,
|
|
8551
|
+
rows: opts.rows,
|
|
8552
|
+
data: (_term, data) => opts.onData(Buffer.from(data).toString("utf8"))
|
|
8553
|
+
}
|
|
8554
|
+
});
|
|
8555
|
+
};
|
|
8556
|
+
function words(text) {
|
|
8557
|
+
return text.split(/\s+/).map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s*");
|
|
8558
|
+
}
|
|
8559
|
+
var SCREEN_PATTERNS = [
|
|
8560
|
+
{
|
|
8561
|
+
kind: "login",
|
|
8562
|
+
pattern: new RegExp(
|
|
8563
|
+
[
|
|
8564
|
+
words("Select login method"),
|
|
8565
|
+
words("Please run /login"),
|
|
8566
|
+
words("Invalid API key"),
|
|
8567
|
+
words("OAuth token has expired"),
|
|
8568
|
+
words("Not logged in")
|
|
8569
|
+
].join("|"),
|
|
8570
|
+
"i"
|
|
8571
|
+
)
|
|
8572
|
+
},
|
|
8573
|
+
{
|
|
8574
|
+
kind: "onboarding",
|
|
8575
|
+
pattern: new RegExp(
|
|
8576
|
+
[words("Choose the text style that looks best"), words("Let's get started.")].join("|"),
|
|
8577
|
+
"i"
|
|
8578
|
+
)
|
|
8579
|
+
},
|
|
8580
|
+
{
|
|
8581
|
+
kind: "trust",
|
|
8582
|
+
pattern: new RegExp(
|
|
8583
|
+
[
|
|
8584
|
+
words("Do you trust the files in this folder?"),
|
|
8585
|
+
words("Is this a project you created or one you trust?"),
|
|
8586
|
+
words("Yes, I trust this folder")
|
|
8587
|
+
].join("|"),
|
|
8588
|
+
"i"
|
|
8589
|
+
)
|
|
8590
|
+
},
|
|
8591
|
+
{
|
|
8592
|
+
// The question alone is not enough: Claude can write "Do you want to
|
|
8593
|
+
// proceed?" in a reply. The dialog always numbers its options.
|
|
8594
|
+
kind: "permission",
|
|
8595
|
+
pattern: new RegExp(
|
|
8596
|
+
`${words("Do you want to")}\\s*(?:proceed|make\\s*this\\s*edit|create|overwrite|allow|run)[^?]{0,160}\\?[\\s\\S]{0,600}?1\\.\\s*Yes`,
|
|
8597
|
+
"i"
|
|
8598
|
+
)
|
|
8599
|
+
},
|
|
8600
|
+
{
|
|
8601
|
+
kind: "auto-continue",
|
|
8602
|
+
pattern: new RegExp(
|
|
8603
|
+
`${words("continuing automatically at")}[^\xB7\\n]{1,40}\xB7\\s*${words("esc to cancel")}`,
|
|
8604
|
+
"i"
|
|
8605
|
+
)
|
|
8606
|
+
}
|
|
8607
|
+
];
|
|
8608
|
+
function stripTerminal(raw) {
|
|
8609
|
+
return raw.replace(
|
|
8610
|
+
/\x1B\[(\d*)C/g,
|
|
8611
|
+
(_match, count) => " ".repeat(Math.min(Number(count) || 1, 200))
|
|
8612
|
+
).replace(/\x1B\[[0-9;?]*[ABDEFGHJKfd]/g, "\n").replace(/\x1B\][^\x07\x1B]*(?:\x07|\x1B\\)/g, "").replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "").replace(/[\x00-\x08\x0B-\x1F\x7F]/g, "").replace(/[ \t]+/g, " ");
|
|
8613
|
+
}
|
|
8614
|
+
function highlightedChoice(text) {
|
|
8615
|
+
let last = null;
|
|
8616
|
+
for (const match of text.matchAll(/❯\s*(?:\d+\s*\.\s*)?(Yes|No)\b/gi)) {
|
|
8617
|
+
last = match[1].toLowerCase();
|
|
8618
|
+
}
|
|
8619
|
+
return last === "yes" || last === "no" ? last : null;
|
|
8620
|
+
}
|
|
8621
|
+
function classifyScreen(text) {
|
|
8622
|
+
for (const { kind, pattern } of SCREEN_PATTERNS) {
|
|
8623
|
+
const match = pattern.exec(text);
|
|
8624
|
+
if (match) {
|
|
8625
|
+
return { kind, detail: match[0].replace(/\s+/g, " ").trim().slice(0, 200) };
|
|
8626
|
+
}
|
|
8627
|
+
}
|
|
8628
|
+
return null;
|
|
8629
|
+
}
|
|
8630
|
+
function configDirEnv(configDir) {
|
|
8631
|
+
return configDir ? { CLAUDE_CONFIG_DIR: configDir } : {};
|
|
8632
|
+
}
|
|
8353
8633
|
function interactiveSpawnEnv(opts) {
|
|
8354
8634
|
return {
|
|
8355
8635
|
...process.env,
|
|
8356
|
-
|
|
8636
|
+
...configDirEnv(opts.configDir),
|
|
8357
8637
|
TERM: "xterm-256color",
|
|
8358
8638
|
// Pin the binary so a mid-session autoupdate cannot invalidate the
|
|
8359
8639
|
// detected version the flag gates read, and skip non-essential traffic.
|
|
@@ -8428,6 +8708,21 @@ var TurnUsageAccumulator = class {
|
|
|
8428
8708
|
};
|
|
8429
8709
|
var TERMINAL_STOP = /* @__PURE__ */ new Set(["end_turn", "stop_sequence", "max_tokens"]);
|
|
8430
8710
|
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
8711
|
+
var RAW_KEEP_CHARS = 64 * 1024;
|
|
8712
|
+
var SCREEN_WINDOW_CHARS = 16 * 1024;
|
|
8713
|
+
var INTERRUPT_MARKER = "[Request interrupted by user";
|
|
8714
|
+
function isInterruptRecord(rec) {
|
|
8715
|
+
if (!rec || rec.type !== "user" || !rec.message) return false;
|
|
8716
|
+
const content = rec.message.content;
|
|
8717
|
+
if (typeof content === "string") return content.startsWith(INTERRUPT_MARKER);
|
|
8718
|
+
if (!Array.isArray(content)) return false;
|
|
8719
|
+
return content.some(
|
|
8720
|
+
(block) => block?.type === "text" && typeof block.text === "string" && block.text.startsWith(INTERRUPT_MARKER)
|
|
8721
|
+
);
|
|
8722
|
+
}
|
|
8723
|
+
function isTurnDurationRecord(rec) {
|
|
8724
|
+
return rec?.type === "system" && rec.subtype === "turn_duration";
|
|
8725
|
+
}
|
|
8431
8726
|
function resolveConfigDir(configDir) {
|
|
8432
8727
|
const value = configDir ?? process.env.CLAUDE_CONFIG_DIR;
|
|
8433
8728
|
if (!value) return path9.join(os4.homedir(), ".claude");
|
|
@@ -8437,11 +8732,21 @@ function resolveConfigDir(configDir) {
|
|
|
8437
8732
|
}
|
|
8438
8733
|
return path9.resolve(value);
|
|
8439
8734
|
}
|
|
8735
|
+
function fatalScreenMessage(kind, configDir, cwd) {
|
|
8736
|
+
if (kind === "trust") {
|
|
8737
|
+
return `Claude Code's folder trust dialog for ${cwd} could not be answered. Run \`claude\` in that folder once by hand and trust it, then retry.`;
|
|
8738
|
+
}
|
|
8739
|
+
if (kind === "login") {
|
|
8740
|
+
return `Claude Code is not logged in for ${configDir}. Run \`claude\` there once by hand and log in (or \`claude auth login\`), then retry.`;
|
|
8741
|
+
}
|
|
8742
|
+
return `Claude Code has not finished its first-run setup for ${configDir}. Run \`claude\` there once by hand, then retry.`;
|
|
8743
|
+
}
|
|
8440
8744
|
var ClaudeSession = class {
|
|
8441
8745
|
sessionId;
|
|
8442
8746
|
cwd;
|
|
8443
8747
|
configDir;
|
|
8444
8748
|
jsonlPath;
|
|
8749
|
+
/** The newest terminal output, ANSI included, for diagnostics. */
|
|
8445
8750
|
raw = "";
|
|
8446
8751
|
proc = null;
|
|
8447
8752
|
cursor = 0;
|
|
@@ -8451,12 +8756,36 @@ var ClaudeSession = class {
|
|
|
8451
8756
|
exitCode = null;
|
|
8452
8757
|
aborted = false;
|
|
8453
8758
|
signal;
|
|
8759
|
+
resumeSessionId;
|
|
8760
|
+
env;
|
|
8761
|
+
onScreen;
|
|
8762
|
+
onExit;
|
|
8763
|
+
spawnPty;
|
|
8764
|
+
/** The configured config dir, which alone is exported to the child. */
|
|
8765
|
+
explicitConfigDir;
|
|
8766
|
+
/** Output since the screen was last acted on, read by `checkScreen`. */
|
|
8767
|
+
screen = "";
|
|
8768
|
+
screenTimer = null;
|
|
8769
|
+
fatal = null;
|
|
8770
|
+
/** The folder trust dialog is up and not answered yet. */
|
|
8771
|
+
trustPending = false;
|
|
8772
|
+
trustMoves = 0;
|
|
8773
|
+
trustMovedAt = 0;
|
|
8774
|
+
turn = null;
|
|
8775
|
+
turnDenied = [];
|
|
8776
|
+
abandonTurn = false;
|
|
8454
8777
|
o;
|
|
8455
8778
|
constructor(opts = {}) {
|
|
8456
8779
|
this.cwd = path9.resolve(opts.cwd ?? process.cwd());
|
|
8457
8780
|
this.configDir = resolveConfigDir(opts.configDir);
|
|
8781
|
+
this.explicitConfigDir = opts.configDir ? this.configDir : void 0;
|
|
8458
8782
|
this.signal = opts.signal;
|
|
8459
|
-
this.
|
|
8783
|
+
this.resumeSessionId = opts.resumeSessionId;
|
|
8784
|
+
this.env = opts.env;
|
|
8785
|
+
this.onScreen = opts.onScreen;
|
|
8786
|
+
this.onExit = opts.onExit;
|
|
8787
|
+
this.spawnPty = opts.spawnPty ?? bunPtySpawner;
|
|
8788
|
+
this.sessionId = opts.resumeSessionId ?? randomUUID4();
|
|
8460
8789
|
this.jsonlPath = path9.join(
|
|
8461
8790
|
this.configDir,
|
|
8462
8791
|
"projects",
|
|
@@ -8486,9 +8815,47 @@ var ClaudeSession = class {
|
|
|
8486
8815
|
submitMinMs: opts.submitMinMs ?? 200,
|
|
8487
8816
|
submitConfirmMs: opts.submitConfirmMs ?? 1500,
|
|
8488
8817
|
submitMaxRetries: opts.submitMaxRetries ?? 8,
|
|
8489
|
-
debug: opts.debug ?? false
|
|
8818
|
+
debug: opts.debug ?? false,
|
|
8819
|
+
// The headless CLI's `tool_progress` heartbeat runs every 30 s, and the
|
|
8820
|
+
// wire-inactivity watchdog it keeps quiet fires at 60 s.
|
|
8821
|
+
heartbeatMs: opts.heartbeatMs ?? 3e4,
|
|
8822
|
+
interruptGraceMs: opts.interruptGraceMs ?? 5e3,
|
|
8823
|
+
// Shorter than `bootQuietMs`, so a trust dialog is answered before boot
|
|
8824
|
+
// could declare the TUI ready and paste a prompt into it.
|
|
8825
|
+
permissionQuietMs: opts.permissionQuietMs ?? 1e3,
|
|
8826
|
+
stopSettleMs: opts.stopSettleMs ?? 750
|
|
8490
8827
|
};
|
|
8491
8828
|
}
|
|
8829
|
+
/** The CLI arguments this session spawns with. */
|
|
8830
|
+
spawnArgs() {
|
|
8831
|
+
const args = this.resumeSessionId ? ["--resume", this.resumeSessionId] : ["--session-id", this.sessionId];
|
|
8832
|
+
if (this.o.model) args.push("--model", this.o.model);
|
|
8833
|
+
if (this.o.settingSources !== null && this.o.settingSources !== void 0) {
|
|
8834
|
+
args.push("--setting-sources", this.o.settingSources);
|
|
8835
|
+
}
|
|
8836
|
+
if (this.o.extraArgs && this.o.extraArgs.length) args.push(...this.o.extraArgs);
|
|
8837
|
+
return args;
|
|
8838
|
+
}
|
|
8839
|
+
/** The child environment: the caller's (or the default one) plus `TERM`,
|
|
8840
|
+
* and `CLAUDE_CONFIG_DIR` only for a configured one (`configDirEnv`). */
|
|
8841
|
+
spawnEnv() {
|
|
8842
|
+
if (!this.env) {
|
|
8843
|
+
return interactiveSpawnEnv({
|
|
8844
|
+
configDir: this.explicitConfigDir,
|
|
8845
|
+
ignoreAnthropicApiKey: this.o.ignoreAnthropicApiKey,
|
|
8846
|
+
effort: this.o.effort
|
|
8847
|
+
});
|
|
8848
|
+
}
|
|
8849
|
+
return { ...this.env, ...configDirEnv(this.explicitConfigDir), TERM: "xterm-256color" };
|
|
8850
|
+
}
|
|
8851
|
+
/** True while a turn is between its paste and its end. */
|
|
8852
|
+
get turnRunning() {
|
|
8853
|
+
return this.turn !== null;
|
|
8854
|
+
}
|
|
8855
|
+
/** True once the child is gone (exited, killed or never started). */
|
|
8856
|
+
get hasExited() {
|
|
8857
|
+
return this.exited;
|
|
8858
|
+
}
|
|
8492
8859
|
async start() {
|
|
8493
8860
|
if (this.signal?.aborted) throw new Error("aborted before start");
|
|
8494
8861
|
this.signal?.addEventListener(
|
|
@@ -8499,61 +8866,147 @@ var ClaudeSession = class {
|
|
|
8499
8866
|
},
|
|
8500
8867
|
{ once: true }
|
|
8501
8868
|
);
|
|
8502
|
-
const claude =
|
|
8503
|
-
const args =
|
|
8504
|
-
if (this.o.model) args.push("--model", this.o.model);
|
|
8505
|
-
if (this.o.settingSources !== null && this.o.settingSources !== void 0) {
|
|
8506
|
-
args.push("--setting-sources", this.o.settingSources);
|
|
8507
|
-
}
|
|
8508
|
-
if (this.o.extraArgs && this.o.extraArgs.length) args.push(...this.o.extraArgs);
|
|
8869
|
+
const claude = this.o.cliPath ?? "claude";
|
|
8870
|
+
const args = this.spawnArgs();
|
|
8509
8871
|
if (this.o.debug)
|
|
8510
8872
|
process.stderr.write(`[session] spawn: ${claude} ${args.join(" ")}
|
|
8511
8873
|
`);
|
|
8512
8874
|
this.lastDataAt = Date.now();
|
|
8513
|
-
|
|
8875
|
+
const proc = this.spawnPty([claude, ...args], {
|
|
8514
8876
|
cwd: this.cwd,
|
|
8515
|
-
env:
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
ignoreAnthropicApiKey: this.o.ignoreAnthropicApiKey,
|
|
8520
|
-
effort: this.o.effort
|
|
8521
|
-
}),
|
|
8522
|
-
terminal: {
|
|
8523
|
-
cols: this.o.cols,
|
|
8524
|
-
rows: this.o.rows,
|
|
8525
|
-
data: (_term, d) => {
|
|
8526
|
-
this.lastDataAt = Date.now();
|
|
8527
|
-
const chunk = Buffer.from(d).toString("utf8");
|
|
8528
|
-
this.raw += chunk;
|
|
8529
|
-
if (this.o.debug) process.stdout.write(chunk);
|
|
8530
|
-
}
|
|
8531
|
-
}
|
|
8877
|
+
env: this.spawnEnv(),
|
|
8878
|
+
cols: this.o.cols,
|
|
8879
|
+
rows: this.o.rows,
|
|
8880
|
+
onData: (chunk) => this.onData(chunk)
|
|
8532
8881
|
});
|
|
8533
|
-
this.proc
|
|
8534
|
-
|
|
8535
|
-
this.exited
|
|
8536
|
-
this.
|
|
8537
|
-
}).catch(() => {
|
|
8882
|
+
this.proc = proc;
|
|
8883
|
+
const markExited = (code) => {
|
|
8884
|
+
if (this.exited) return;
|
|
8885
|
+
this.exitCode = code;
|
|
8538
8886
|
this.exited = true;
|
|
8539
8887
|
this.proc = null;
|
|
8540
|
-
|
|
8888
|
+
this.clearScreenTimer();
|
|
8889
|
+
this.onExit?.(code);
|
|
8890
|
+
};
|
|
8891
|
+
proc.exited.then((code) => markExited(typeof code === "number" ? code : null)).catch(() => markExited(null));
|
|
8541
8892
|
await this.waitForBoot();
|
|
8542
8893
|
this.cursor = this.lineCount();
|
|
8543
8894
|
}
|
|
8895
|
+
onData(chunk) {
|
|
8896
|
+
this.lastDataAt = Date.now();
|
|
8897
|
+
this.raw = (this.raw + chunk).slice(-RAW_KEEP_CHARS);
|
|
8898
|
+
this.screen = (this.screen + chunk).slice(-SCREEN_WINDOW_CHARS);
|
|
8899
|
+
if (this.o.debug) process.stdout.write(chunk);
|
|
8900
|
+
this.clearScreenTimer();
|
|
8901
|
+
this.screenTimer = setTimeout(() => {
|
|
8902
|
+
this.screenTimer = null;
|
|
8903
|
+
this.checkScreen();
|
|
8904
|
+
}, this.o.permissionQuietMs);
|
|
8905
|
+
}
|
|
8906
|
+
clearScreenTimer() {
|
|
8907
|
+
if (this.screenTimer) {
|
|
8908
|
+
clearTimeout(this.screenTimer);
|
|
8909
|
+
this.screenTimer = null;
|
|
8910
|
+
}
|
|
8911
|
+
}
|
|
8912
|
+
/**
|
|
8913
|
+
* Answer the screen the TUI is blocked on, if it is one of the few this
|
|
8914
|
+
* session recognises. Returns what it did, or null. Each screen is acted on
|
|
8915
|
+
* once: the window is cleared afterwards, so the next check only reads what
|
|
8916
|
+
* the TUI drew since.
|
|
8917
|
+
*/
|
|
8918
|
+
checkScreen() {
|
|
8919
|
+
if (!this.proc || this.exited) return null;
|
|
8920
|
+
const text = stripTerminal(this.screen);
|
|
8921
|
+
const state = this.trustPending ? { kind: "trust", detail: "folder trust dialog" } : classifyScreen(text);
|
|
8922
|
+
if (!state) return null;
|
|
8923
|
+
let action;
|
|
8924
|
+
switch (state.kind) {
|
|
8925
|
+
case "trust": {
|
|
8926
|
+
this.trustPending = true;
|
|
8927
|
+
const choice = highlightedChoice(text);
|
|
8928
|
+
if (choice === "yes") {
|
|
8929
|
+
this.write("\r");
|
|
8930
|
+
this.trustPending = false;
|
|
8931
|
+
action = "accepted";
|
|
8932
|
+
break;
|
|
8933
|
+
}
|
|
8934
|
+
if (choice === null) return null;
|
|
8935
|
+
if (this.trustMovedAt && this.lastDataAt <= this.trustMovedAt) return null;
|
|
8936
|
+
if (this.trustMoves >= 4) {
|
|
8937
|
+
action = "fatal";
|
|
8938
|
+
break;
|
|
8939
|
+
}
|
|
8940
|
+
this.trustMoves++;
|
|
8941
|
+
this.trustMovedAt = Date.now();
|
|
8942
|
+
this.write("\x1B[B");
|
|
8943
|
+
return null;
|
|
8944
|
+
}
|
|
8945
|
+
case "permission":
|
|
8946
|
+
if (!this.turn) return null;
|
|
8947
|
+
this.write("\x1B");
|
|
8948
|
+
this.turnDenied.push(state);
|
|
8949
|
+
action = "denied";
|
|
8950
|
+
break;
|
|
8951
|
+
case "auto-continue":
|
|
8952
|
+
this.write("\x1B");
|
|
8953
|
+
action = "cancelled";
|
|
8954
|
+
break;
|
|
8955
|
+
case "login":
|
|
8956
|
+
case "onboarding":
|
|
8957
|
+
action = "fatal";
|
|
8958
|
+
break;
|
|
8959
|
+
}
|
|
8960
|
+
this.screen = "";
|
|
8961
|
+
const event = { ...state, action };
|
|
8962
|
+
if (action === "fatal") {
|
|
8963
|
+
this.fatal = event;
|
|
8964
|
+
this.killProcess();
|
|
8965
|
+
}
|
|
8966
|
+
try {
|
|
8967
|
+
this.onScreen?.(event);
|
|
8968
|
+
} catch {
|
|
8969
|
+
}
|
|
8970
|
+
return event;
|
|
8971
|
+
}
|
|
8972
|
+
write(data) {
|
|
8973
|
+
try {
|
|
8974
|
+
this.proc?.terminal.write(data);
|
|
8975
|
+
} catch {
|
|
8976
|
+
}
|
|
8977
|
+
}
|
|
8978
|
+
fatalError() {
|
|
8979
|
+
if (!this.fatal) return null;
|
|
8980
|
+
return new Error(
|
|
8981
|
+
this.failureMessage(fatalScreenMessage(this.fatal.kind, this.configDir, this.cwd), true)
|
|
8982
|
+
);
|
|
8983
|
+
}
|
|
8544
8984
|
/** Wait until the TUI has been quiet for bootQuietMs (Ink ready), bounded by
|
|
8545
|
-
* bootMinMs..bootMaxMs.
|
|
8985
|
+
* bootMinMs..bootMaxMs. A screen still waiting to be answered when the TUI
|
|
8986
|
+
* goes quiet is answered first, and the wait goes on. */
|
|
8546
8987
|
async waitForBoot() {
|
|
8547
8988
|
const start = Date.now();
|
|
8548
8989
|
while (Date.now() - start < this.o.bootMaxMs) {
|
|
8549
8990
|
await delay(150);
|
|
8550
8991
|
if (this.aborted) throw new Error("aborted during boot");
|
|
8992
|
+
const fatal = this.fatalError();
|
|
8993
|
+
if (fatal) throw fatal;
|
|
8551
8994
|
if (this.exited) {
|
|
8552
8995
|
throw new Error(this.failureMessage("claude exited during boot", true));
|
|
8553
8996
|
}
|
|
8554
8997
|
const elapsed = Date.now() - start;
|
|
8555
8998
|
const sinceData = Date.now() - this.lastDataAt;
|
|
8556
|
-
if (elapsed >= this.o.bootMinMs && sinceData >= this.o.bootQuietMs)
|
|
8999
|
+
if (elapsed >= this.o.bootMinMs && sinceData >= this.o.bootQuietMs) {
|
|
9000
|
+
const acted = this.checkScreen();
|
|
9001
|
+
if (!acted && !this.trustPending) return;
|
|
9002
|
+
const fatalNow = this.fatalError();
|
|
9003
|
+
if (fatalNow) throw fatalNow;
|
|
9004
|
+
}
|
|
9005
|
+
}
|
|
9006
|
+
if (this.trustPending) {
|
|
9007
|
+
this.fatal = { kind: "trust", detail: "folder trust dialog", action: "fatal" };
|
|
9008
|
+
this.killProcess();
|
|
9009
|
+
throw this.fatalError();
|
|
8557
9010
|
}
|
|
8558
9011
|
}
|
|
8559
9012
|
/** Submit the freshly-injected prompt and confirm the turn was actually
|
|
@@ -8591,7 +9044,7 @@ var ClaudeSession = class {
|
|
|
8591
9044
|
return lines.length > 0 ? lines.length - 1 : 0;
|
|
8592
9045
|
}
|
|
8593
9046
|
rawTail(max = 600) {
|
|
8594
|
-
const clean = this.raw
|
|
9047
|
+
const clean = stripTerminal(this.raw).replace(/\s+/g, " ").trim();
|
|
8595
9048
|
return clean.length > max ? clean.slice(-max) : clean;
|
|
8596
9049
|
}
|
|
8597
9050
|
failureMessage(reason, includeRaw = false) {
|
|
@@ -8605,61 +9058,133 @@ var ClaudeSession = class {
|
|
|
8605
9058
|
return parts.join("; ");
|
|
8606
9059
|
}
|
|
8607
9060
|
/**
|
|
8608
|
-
*
|
|
8609
|
-
*
|
|
9061
|
+
* Paste a turn into the live session and follow the transcript until the
|
|
9062
|
+
* turn ends, handing every new record to `onRecord` (raw line and parsed
|
|
9063
|
+
* record, the latter null when the line is not JSON). One turn at a time:
|
|
9064
|
+
* a second call while one runs is refused, because both would read the
|
|
9065
|
+
* same cursor.
|
|
8610
9066
|
*/
|
|
8611
|
-
async
|
|
9067
|
+
async runTurnLoop(prompt, onRecord, timeout, onHeartbeat) {
|
|
8612
9068
|
if (this.aborted) throw new Error("aborted");
|
|
9069
|
+
const fatal = this.fatalError();
|
|
9070
|
+
if (fatal) throw fatal;
|
|
8613
9071
|
if (!this.proc || this.exited)
|
|
8614
9072
|
throw new Error("session not started or already exited");
|
|
8615
|
-
|
|
8616
|
-
|
|
8617
|
-
|
|
8618
|
-
|
|
8619
|
-
|
|
8620
|
-
|
|
8621
|
-
|
|
8622
|
-
|
|
8623
|
-
|
|
8624
|
-
|
|
8625
|
-
|
|
8626
|
-
|
|
8627
|
-
while (Date.now() < deadline) {
|
|
8628
|
-
await delay(this.o.pollMs);
|
|
8629
|
-
if (this.aborted) throw new Error("aborted mid-turn");
|
|
8630
|
-
const lines = this.readRawLines();
|
|
8631
|
-
const lastComplete = lines.length - 1;
|
|
8632
|
-
if (lastComplete <= this.cursor) {
|
|
8633
|
-
if (this.exited) throw new Error(this.failureMessage("claude exited mid-turn", true));
|
|
8634
|
-
continue;
|
|
9073
|
+
if (this.turn) throw new Error("a turn is already running in this session");
|
|
9074
|
+
let settle;
|
|
9075
|
+
this.turn = new Promise((resolve6) => settle = resolve6);
|
|
9076
|
+
this.turnDenied = [];
|
|
9077
|
+
this.abandonTurn = false;
|
|
9078
|
+
this.cursor = Math.max(this.cursor, this.lineCount());
|
|
9079
|
+
this.screen = "";
|
|
9080
|
+
try {
|
|
9081
|
+
if (this.o.bracketedPaste) {
|
|
9082
|
+
this.proc.terminal.write("\x1B[200~" + prompt + "\x1B[201~");
|
|
9083
|
+
} else {
|
|
9084
|
+
this.proc.terminal.write(prompt);
|
|
8635
9085
|
}
|
|
8636
|
-
|
|
8637
|
-
|
|
8638
|
-
|
|
8639
|
-
|
|
8640
|
-
|
|
8641
|
-
|
|
8642
|
-
|
|
9086
|
+
await this.submitTurn();
|
|
9087
|
+
let stopReason = null;
|
|
9088
|
+
let stopSeenAt = 0;
|
|
9089
|
+
let end = null;
|
|
9090
|
+
let sawUserRecord = false;
|
|
9091
|
+
let lastBeat = Date.now();
|
|
9092
|
+
const deadline = Date.now() + timeout;
|
|
9093
|
+
while (Date.now() < deadline) {
|
|
9094
|
+
await delay(this.o.pollMs);
|
|
9095
|
+
if (this.aborted) throw new Error("aborted mid-turn");
|
|
9096
|
+
const fatalNow = this.fatalError();
|
|
9097
|
+
if (fatalNow) throw fatalNow;
|
|
9098
|
+
const lines = this.readRawLines();
|
|
9099
|
+
const lastComplete = lines.length - 1;
|
|
9100
|
+
if (lastComplete > this.cursor) {
|
|
9101
|
+
for (let i = this.cursor; i < lastComplete && !end; i++) {
|
|
9102
|
+
const s = lines[i];
|
|
9103
|
+
if (!s || !s.trim()) continue;
|
|
9104
|
+
let rec = null;
|
|
9105
|
+
try {
|
|
9106
|
+
rec = JSON.parse(s);
|
|
9107
|
+
} catch {
|
|
9108
|
+
}
|
|
9109
|
+
onRecord(s, rec);
|
|
9110
|
+
if (!rec) continue;
|
|
9111
|
+
if (rec.type === "assistant" && rec.message) {
|
|
9112
|
+
const reason = rec.message.stop_reason;
|
|
9113
|
+
if (typeof reason === "string") {
|
|
9114
|
+
stopReason = TERMINAL_STOP.has(reason) ? reason : null;
|
|
9115
|
+
}
|
|
9116
|
+
} else if (rec.type === "user") {
|
|
9117
|
+
if (sawUserRecord && isInterruptRecord(rec)) end = "interrupted";
|
|
9118
|
+
sawUserRecord = true;
|
|
9119
|
+
} else if ((sawUserRecord || stopReason) && isTurnDurationRecord(rec)) {
|
|
9120
|
+
end = stopReason ? "stop" : "ended";
|
|
9121
|
+
}
|
|
9122
|
+
this.cursor = i + 1;
|
|
9123
|
+
}
|
|
9124
|
+
if (!end) this.cursor = lastComplete;
|
|
9125
|
+
lastBeat = Date.now();
|
|
9126
|
+
stopSeenAt = Date.now();
|
|
9127
|
+
if (end) return { stopReason: end === "stop" ? stopReason : null, end, denied: this.turnDenied };
|
|
8643
9128
|
continue;
|
|
8644
9129
|
}
|
|
8645
|
-
if (
|
|
8646
|
-
|
|
8647
|
-
|
|
8648
|
-
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
|
|
9130
|
+
if (stopReason && Date.now() - stopSeenAt >= this.o.stopSettleMs) {
|
|
9131
|
+
return { stopReason, end: "stop", denied: this.turnDenied };
|
|
9132
|
+
}
|
|
9133
|
+
if (this.exited) throw new Error(this.failureMessage("claude exited mid-turn", true));
|
|
9134
|
+
if (this.abandonTurn) return { stopReason: null, end: "interrupted", denied: this.turnDenied };
|
|
9135
|
+
const now = Date.now();
|
|
9136
|
+
if (onHeartbeat && now - lastBeat >= this.o.heartbeatMs) {
|
|
9137
|
+
lastBeat = now;
|
|
9138
|
+
if (now - this.lastDataAt < this.o.heartbeatMs) onHeartbeat();
|
|
8654
9139
|
}
|
|
8655
9140
|
}
|
|
8656
|
-
this.
|
|
8657
|
-
|
|
9141
|
+
return { stopReason: null, end: null, denied: this.turnDenied };
|
|
9142
|
+
} finally {
|
|
9143
|
+
this.turn = null;
|
|
9144
|
+
this.abandonTurn = false;
|
|
9145
|
+
settle();
|
|
8658
9146
|
}
|
|
9147
|
+
}
|
|
9148
|
+
/**
|
|
9149
|
+
* Stop the running turn the way the TUI's own Esc does, and keep the
|
|
9150
|
+
* session alive for the next one. Resolves true once the turn ended, false
|
|
9151
|
+
* when it was abandoned after `interruptGraceMs` without the TUI saying so.
|
|
9152
|
+
*/
|
|
9153
|
+
async interrupt(graceMs = this.o.interruptGraceMs) {
|
|
9154
|
+
const turn = this.turn;
|
|
9155
|
+
if (!turn) return true;
|
|
9156
|
+
this.write("\x1B");
|
|
9157
|
+
const ended = await Promise.race([turn.then(() => true), delay(graceMs).then(() => false)]);
|
|
9158
|
+
if (ended) return true;
|
|
9159
|
+
this.write("\x1B");
|
|
9160
|
+
this.abandonTurn = true;
|
|
9161
|
+
await Promise.race([turn, delay(this.o.pollMs * 4)]);
|
|
9162
|
+
return false;
|
|
9163
|
+
}
|
|
9164
|
+
/**
|
|
9165
|
+
* Inject a turn into the live session and return the assistant reply once a
|
|
9166
|
+
* terminal stop_reason is observed in the transcript.
|
|
9167
|
+
*/
|
|
9168
|
+
async ask(prompt, perTurnTimeoutMs) {
|
|
9169
|
+
const timeout = perTurnTimeoutMs ?? this.o.turnTimeoutMs;
|
|
9170
|
+
const t0 = Date.now();
|
|
9171
|
+
const collected2 = [];
|
|
9172
|
+
const usage = new TurnUsageAccumulator();
|
|
9173
|
+
const { stopReason } = await this.runTurnLoop(
|
|
9174
|
+
prompt,
|
|
9175
|
+
(_raw, rec) => {
|
|
9176
|
+
if (rec?.type !== "assistant" || !rec.message) return;
|
|
9177
|
+
for (const b of rec.message.content ?? []) {
|
|
9178
|
+
if (b?.type === "text" && typeof b.text === "string") collected2.push(b.text);
|
|
9179
|
+
}
|
|
9180
|
+
usage.add(rec);
|
|
9181
|
+
},
|
|
9182
|
+
timeout
|
|
9183
|
+
);
|
|
8659
9184
|
if (!stopReason) {
|
|
8660
9185
|
throw new Error(
|
|
8661
9186
|
this.failureMessage(
|
|
8662
|
-
`turn
|
|
9187
|
+
`turn ended without a terminal assistant record after ${Date.now() - t0}ms (collected ${collected2.length} text block(s))`
|
|
8663
9188
|
)
|
|
8664
9189
|
);
|
|
8665
9190
|
}
|
|
@@ -8681,9 +9206,9 @@ var ClaudeSession = class {
|
|
|
8681
9206
|
}
|
|
8682
9207
|
/**
|
|
8683
9208
|
* Like ask(), but instead of collecting the reply text it re-emits each NEW
|
|
8684
|
-
* raw JSONL transcript line via onLine (verbatim) until
|
|
8685
|
-
*
|
|
8686
|
-
*
|
|
9209
|
+
* raw JSONL transcript line via onLine (verbatim) until the turn ends. Used
|
|
9210
|
+
* by the opencode plugin transport shim, which feeds these raw lines into
|
|
9211
|
+
* the existing stream-json line handler unchanged.
|
|
8687
9212
|
*
|
|
8688
9213
|
* `usage` is the turn summed over DISTINCT API calls, which is exactly what
|
|
8689
9214
|
* a headless `result` frame reports, so the shim can synthesize one and
|
|
@@ -8691,53 +9216,24 @@ var ClaudeSession = class {
|
|
|
8691
9216
|
* `turnStats`) behaves as it does on the headless path. `lastCallUsage` is
|
|
8692
9217
|
* the newest real call, returned because a caller with no stream parser
|
|
8693
9218
|
* (`askOnce`) has no other way to get the context side.
|
|
9219
|
+
*
|
|
9220
|
+
* A turn that ends any way other than a terminal stop (an interrupt, a
|
|
9221
|
+
* denied permission dialog, `turn_duration`) resolves with that `end` and a
|
|
9222
|
+
* null `stopReason`; only a timeout throws.
|
|
8694
9223
|
*/
|
|
8695
|
-
async tailTurn(prompt, onLine, perTurnTimeoutMs) {
|
|
8696
|
-
if (this.aborted) throw new Error("aborted");
|
|
8697
|
-
if (!this.proc || this.exited)
|
|
8698
|
-
throw new Error("session not started or already exited");
|
|
9224
|
+
async tailTurn(prompt, onLine, perTurnTimeoutMs, onHeartbeat) {
|
|
8699
9225
|
const timeout = perTurnTimeoutMs ?? this.o.turnTimeoutMs;
|
|
8700
|
-
if (this.o.bracketedPaste) {
|
|
8701
|
-
this.proc.terminal.write("\x1B[200~" + prompt + "\x1B[201~");
|
|
8702
|
-
} else {
|
|
8703
|
-
this.proc.terminal.write(prompt);
|
|
8704
|
-
}
|
|
8705
|
-
await this.submitTurn();
|
|
8706
9226
|
const usage = new TurnUsageAccumulator();
|
|
8707
|
-
|
|
8708
|
-
|
|
8709
|
-
|
|
8710
|
-
|
|
8711
|
-
|
|
8712
|
-
|
|
8713
|
-
|
|
8714
|
-
|
|
8715
|
-
|
|
8716
|
-
|
|
8717
|
-
}
|
|
8718
|
-
continue;
|
|
8719
|
-
}
|
|
8720
|
-
for (let i = this.cursor; i < lastComplete; i++) {
|
|
8721
|
-
const s = lines[i];
|
|
8722
|
-
if (!s || !s.trim()) continue;
|
|
8723
|
-
onLine(s);
|
|
8724
|
-
let rec;
|
|
8725
|
-
try {
|
|
8726
|
-
rec = JSON.parse(s);
|
|
8727
|
-
} catch {
|
|
8728
|
-
continue;
|
|
8729
|
-
}
|
|
8730
|
-
usage.add(rec);
|
|
8731
|
-
if (rec.type === "assistant" && rec.message) {
|
|
8732
|
-
if (rec.message.stop_reason && TERMINAL_STOP.has(rec.message.stop_reason)) {
|
|
8733
|
-
stopReason = rec.message.stop_reason;
|
|
8734
|
-
}
|
|
8735
|
-
}
|
|
8736
|
-
}
|
|
8737
|
-
this.cursor = lastComplete;
|
|
8738
|
-
if (stopReason) break;
|
|
8739
|
-
}
|
|
8740
|
-
if (!stopReason) {
|
|
9227
|
+
const { stopReason, end, denied } = await this.runTurnLoop(
|
|
9228
|
+
prompt,
|
|
9229
|
+
(raw, rec) => {
|
|
9230
|
+
onLine(raw);
|
|
9231
|
+
if (rec) usage.add(rec);
|
|
9232
|
+
},
|
|
9233
|
+
timeout,
|
|
9234
|
+
onHeartbeat
|
|
9235
|
+
);
|
|
9236
|
+
if (!end) {
|
|
8741
9237
|
throw new Error(
|
|
8742
9238
|
this.failureMessage(
|
|
8743
9239
|
`turn timed out after ${timeout}ms (no terminal assistant record)`
|
|
@@ -8746,26 +9242,32 @@ var ClaudeSession = class {
|
|
|
8746
9242
|
}
|
|
8747
9243
|
return {
|
|
8748
9244
|
stopReason,
|
|
9245
|
+
end,
|
|
8749
9246
|
usage: usage.turnTotal,
|
|
8750
9247
|
lastCallUsage: usage.lastCall,
|
|
8751
|
-
callCount: usage.callCount
|
|
9248
|
+
callCount: usage.callCount,
|
|
9249
|
+
denied
|
|
8752
9250
|
};
|
|
8753
9251
|
}
|
|
8754
|
-
|
|
8755
|
-
|
|
8756
|
-
|
|
8757
|
-
|
|
8758
|
-
|
|
8759
|
-
|
|
8760
|
-
|
|
8761
|
-
|
|
8762
|
-
|
|
8763
|
-
|
|
8764
|
-
try {
|
|
8765
|
-
this.proc.terminal.close();
|
|
8766
|
-
} catch {
|
|
8767
|
-
}
|
|
9252
|
+
killProcess() {
|
|
9253
|
+
const proc = this.proc;
|
|
9254
|
+
if (!proc) return;
|
|
9255
|
+
try {
|
|
9256
|
+
proc.terminal.write("");
|
|
9257
|
+
} catch {
|
|
9258
|
+
}
|
|
9259
|
+
try {
|
|
9260
|
+
proc.kill();
|
|
9261
|
+
} catch {
|
|
8768
9262
|
}
|
|
9263
|
+
try {
|
|
9264
|
+
proc.terminal.close();
|
|
9265
|
+
} catch {
|
|
9266
|
+
}
|
|
9267
|
+
}
|
|
9268
|
+
dispose() {
|
|
9269
|
+
this.clearScreenTimer();
|
|
9270
|
+
this.killProcess();
|
|
8769
9271
|
this.proc = null;
|
|
8770
9272
|
}
|
|
8771
9273
|
};
|
|
@@ -8839,9 +9341,88 @@ function interactiveExtraArgs(opts) {
|
|
|
8839
9341
|
}
|
|
8840
9342
|
return extraArgs;
|
|
8841
9343
|
}
|
|
9344
|
+
var OpenToolCalls = class {
|
|
9345
|
+
open = /* @__PURE__ */ new Map();
|
|
9346
|
+
add(rec, now = Date.now()) {
|
|
9347
|
+
const content = rec?.message?.content;
|
|
9348
|
+
if (!Array.isArray(content)) return;
|
|
9349
|
+
for (const block of content) {
|
|
9350
|
+
if (rec.type === "assistant" && block?.type === "tool_use" && typeof block.id === "string") {
|
|
9351
|
+
this.open.set(block.id, { name: String(block.name ?? "tool"), startedAt: now });
|
|
9352
|
+
} else if (rec.type === "user" && block?.type === "tool_result") {
|
|
9353
|
+
this.open.delete(block.tool_use_id);
|
|
9354
|
+
}
|
|
9355
|
+
}
|
|
9356
|
+
}
|
|
9357
|
+
newest() {
|
|
9358
|
+
let newest = null;
|
|
9359
|
+
for (const [id, call] of this.open) newest = { id, ...call };
|
|
9360
|
+
return newest;
|
|
9361
|
+
}
|
|
9362
|
+
clear() {
|
|
9363
|
+
this.open.clear();
|
|
9364
|
+
}
|
|
9365
|
+
};
|
|
9366
|
+
function heartbeatFrame(sessionId, openCall, now = Date.now()) {
|
|
9367
|
+
if (openCall) {
|
|
9368
|
+
return JSON.stringify({
|
|
9369
|
+
type: "tool_progress",
|
|
9370
|
+
tool_use_id: `${openCall.id}-heartbeat`,
|
|
9371
|
+
tool_name: openCall.name,
|
|
9372
|
+
parent_tool_use_id: openCall.id,
|
|
9373
|
+
heartbeat: true,
|
|
9374
|
+
elapsed_time_seconds: Math.round((now - openCall.startedAt) / 1e3),
|
|
9375
|
+
session_id: sessionId
|
|
9376
|
+
});
|
|
9377
|
+
}
|
|
9378
|
+
return JSON.stringify({
|
|
9379
|
+
type: "system",
|
|
9380
|
+
subtype: "status",
|
|
9381
|
+
status: "requesting",
|
|
9382
|
+
session_id: sessionId
|
|
9383
|
+
});
|
|
9384
|
+
}
|
|
9385
|
+
function interactiveResultFrame(opts) {
|
|
9386
|
+
const completed = opts.end === "stop" && !!opts.stopReason;
|
|
9387
|
+
let result;
|
|
9388
|
+
let terminalReason = "completed";
|
|
9389
|
+
if (!completed) {
|
|
9390
|
+
if (opts.end === "failed") {
|
|
9391
|
+
terminalReason = "error_during_execution";
|
|
9392
|
+
result = `Interactive transport failed: ${opts.error ?? "unknown error"}`;
|
|
9393
|
+
} else if (opts.end === "interrupted") {
|
|
9394
|
+
terminalReason = "aborted";
|
|
9395
|
+
result = "Interactive transport: the turn was interrupted.";
|
|
9396
|
+
} else {
|
|
9397
|
+
terminalReason = "error_during_execution";
|
|
9398
|
+
result = "Interactive transport: the turn ended without a terminal stop_reason. Output above may be incomplete.";
|
|
9399
|
+
}
|
|
9400
|
+
}
|
|
9401
|
+
return JSON.stringify({
|
|
9402
|
+
type: "result",
|
|
9403
|
+
subtype: completed ? "success" : "error_during_execution",
|
|
9404
|
+
is_error: !completed,
|
|
9405
|
+
stop_reason: completed ? opts.stopReason : null,
|
|
9406
|
+
terminal_reason: terminalReason,
|
|
9407
|
+
result,
|
|
9408
|
+
session_id: opts.sessionId,
|
|
9409
|
+
usage: opts.usage ?? {},
|
|
9410
|
+
// The TUI has no `can_use_tool` channel, so a permission dialog is the
|
|
9411
|
+
// only way it asks; each one the session denied is reported in the
|
|
9412
|
+
// headless field's place, names and ids only (h #g109).
|
|
9413
|
+
permission_denials: opts.denials ?? [],
|
|
9414
|
+
total_cost_usd: null,
|
|
9415
|
+
duration_ms: 0
|
|
9416
|
+
});
|
|
9417
|
+
}
|
|
8842
9418
|
function spawnInteractiveProcess(opts) {
|
|
8843
9419
|
const extraArgs = interactiveExtraArgs(opts);
|
|
9420
|
+
const openCalls = new OpenToolCalls();
|
|
9421
|
+
let denials = [];
|
|
9422
|
+
const lineEmitter = new EventEmitter4();
|
|
9423
|
+
const proc = new EventEmitter4();
|
|
8844
9424
|
const session = new ClaudeSession({
|
|
9425
|
+
...opts.tuning,
|
|
8845
9426
|
cwd: opts.cwd,
|
|
8846
9427
|
cliPath: opts.cliPath,
|
|
8847
9428
|
configDir: opts.configDir,
|
|
@@ -8851,7 +9432,28 @@ function spawnInteractiveProcess(opts) {
|
|
|
8851
9432
|
settingSources: opts.settingSources === void 0 ? null : opts.settingSources,
|
|
8852
9433
|
extraArgs,
|
|
8853
9434
|
ignoreAnthropicApiKey: opts.ignoreAnthropicApiKey,
|
|
8854
|
-
effort: opts.effort ? cliEffortLevel(opts.effort) : void 0
|
|
9435
|
+
effort: opts.effort ? cliEffortLevel(opts.effort) : void 0,
|
|
9436
|
+
resumeSessionId: opts.resumeSessionId,
|
|
9437
|
+
env: opts.env,
|
|
9438
|
+
spawnPty: opts.spawnPty,
|
|
9439
|
+
onScreen: (event) => {
|
|
9440
|
+
if (event.action === "denied") {
|
|
9441
|
+
const call = openCalls.newest();
|
|
9442
|
+
denials.push(call ? { tool_name: call.name, tool_use_id: call.id } : { tool_name: "unknown" });
|
|
9443
|
+
}
|
|
9444
|
+
const data = { sessionId: session.sessionId, screen: event.kind, detail: event.detail };
|
|
9445
|
+
if (event.action === "accepted") log.info("interactive transport accepted a prompt", data);
|
|
9446
|
+
else if (event.action === "fatal") log.error("interactive transport cannot continue", data);
|
|
9447
|
+
else if (event.action === "denied") log.warn("interactive transport denied a permission prompt", data);
|
|
9448
|
+
else log.warn("interactive transport cancelled the usage-limit auto-continue", data);
|
|
9449
|
+
},
|
|
9450
|
+
onExit: (code) => {
|
|
9451
|
+
proc.exitCode = code;
|
|
9452
|
+
proc.signalCode = code === null ? "SIGTERM" : null;
|
|
9453
|
+
log.info("interactive claude exited", { sessionId: session.sessionId, code });
|
|
9454
|
+
proc.emit("exit", proc.exitCode, proc.signalCode);
|
|
9455
|
+
proc.emit("close", proc.exitCode, proc.signalCode);
|
|
9456
|
+
}
|
|
8855
9457
|
});
|
|
8856
9458
|
log.info("prepared interactive claude session", {
|
|
8857
9459
|
cwd: opts.cwd,
|
|
@@ -8860,80 +9462,95 @@ function spawnInteractiveProcess(opts) {
|
|
|
8860
9462
|
model: opts.model,
|
|
8861
9463
|
effort: opts.effort,
|
|
8862
9464
|
sessionId: session.sessionId,
|
|
9465
|
+
resumed: !!opts.resumeSessionId,
|
|
8863
9466
|
jsonlPath: session.jsonlPath
|
|
8864
9467
|
});
|
|
8865
|
-
const lineEmitter = new EventEmitter4();
|
|
8866
|
-
const errorHandlers = /* @__PURE__ */ new Set();
|
|
8867
9468
|
let startPromise = null;
|
|
8868
9469
|
const ensureStarted = () => {
|
|
8869
9470
|
if (!startPromise) startPromise = session.start();
|
|
8870
9471
|
return startPromise;
|
|
8871
9472
|
};
|
|
8872
|
-
|
|
8873
|
-
|
|
8874
|
-
|
|
8875
|
-
|
|
8876
|
-
|
|
8877
|
-
|
|
8878
|
-
|
|
8879
|
-
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
|
|
8883
|
-
|
|
8884
|
-
|
|
8885
|
-
|
|
8886
|
-
})
|
|
8887
|
-
);
|
|
8888
|
-
};
|
|
8889
|
-
const runTurn = (userMsg) => {
|
|
8890
|
-
void (async () => {
|
|
8891
|
-
try {
|
|
8892
|
-
await ensureStarted();
|
|
8893
|
-
const { stopReason, usage, lastCallUsage, callCount } = await session.tailTurn(userMsg, (raw) => {
|
|
8894
|
-
lineEmitter.emit("line", raw);
|
|
8895
|
-
});
|
|
8896
|
-
log.info("interactive turn usage", {
|
|
8897
|
-
apiCalls: callCount,
|
|
8898
|
-
turnOutputTokens: usage?.output_tokens,
|
|
8899
|
-
turnInputTokens: usage?.input_tokens,
|
|
8900
|
-
turnCacheReadTokens: usage?.cache_read_input_tokens,
|
|
8901
|
-
lastCallInputTokens: lastCallUsage?.input_tokens,
|
|
8902
|
-
lastCallCacheReadTokens: lastCallUsage?.cache_read_input_tokens,
|
|
8903
|
-
lastCallCacheWriteTokens: lastCallUsage?.cache_creation_input_tokens
|
|
8904
|
-
});
|
|
8905
|
-
const timedOut = !stopReason;
|
|
8906
|
-
emitResult({
|
|
8907
|
-
subtype: timedOut ? "error_during_execution" : "success",
|
|
8908
|
-
isError: timedOut,
|
|
8909
|
-
stopReason: timedOut ? null : stopReason,
|
|
8910
|
-
terminalReason: timedOut ? "error_during_execution" : "completed",
|
|
8911
|
-
result: timedOut ? "Interactive transport: the turn ended without a terminal stop_reason (turn timeout or claude exit). Output above may be incomplete." : void 0,
|
|
8912
|
-
usage
|
|
8913
|
-
});
|
|
8914
|
-
} catch (err) {
|
|
8915
|
-
const e = err instanceof Error ? err : new Error(String(err));
|
|
8916
|
-
log.error("interactive turn failed", { error: e.message });
|
|
8917
|
-
emitResult({
|
|
8918
|
-
subtype: "error_during_execution",
|
|
8919
|
-
isError: true,
|
|
8920
|
-
stopReason: null,
|
|
8921
|
-
terminalReason: "error_during_execution",
|
|
8922
|
-
result: `Interactive transport failed: ${e.message}`
|
|
8923
|
-
});
|
|
8924
|
-
if (errorHandlers.size > 0) {
|
|
8925
|
-
for (const h of errorHandlers) h(e);
|
|
8926
|
-
} else {
|
|
8927
|
-
lineEmitter.emit("close");
|
|
8928
|
-
}
|
|
9473
|
+
let writes = 0;
|
|
9474
|
+
let cancelledThrough = 0;
|
|
9475
|
+
let queued = 0;
|
|
9476
|
+
let chain = Promise.resolve();
|
|
9477
|
+
const runTurn = async (userMsg, turnNumber) => {
|
|
9478
|
+
const current = () => turnNumber === writes;
|
|
9479
|
+
const emit2 = (line) => {
|
|
9480
|
+
if (current()) lineEmitter.emit("line", line);
|
|
9481
|
+
};
|
|
9482
|
+
try {
|
|
9483
|
+
await ensureStarted();
|
|
9484
|
+
if (turnNumber <= cancelledThrough) {
|
|
9485
|
+
log.info("interactive turn cancelled before it started", { sessionId: session.sessionId });
|
|
9486
|
+
return;
|
|
8929
9487
|
}
|
|
8930
|
-
|
|
9488
|
+
openCalls.clear();
|
|
9489
|
+
denials = [];
|
|
9490
|
+
const { stopReason, end, usage, lastCallUsage, callCount, denied } = await session.tailTurn(
|
|
9491
|
+
userMsg,
|
|
9492
|
+
(raw) => {
|
|
9493
|
+
try {
|
|
9494
|
+
openCalls.add(JSON.parse(raw));
|
|
9495
|
+
} catch {
|
|
9496
|
+
}
|
|
9497
|
+
emit2(raw);
|
|
9498
|
+
},
|
|
9499
|
+
void 0,
|
|
9500
|
+
() => emit2(heartbeatFrame(session.sessionId, openCalls.newest()))
|
|
9501
|
+
);
|
|
9502
|
+
log.info("interactive turn ended", {
|
|
9503
|
+
end,
|
|
9504
|
+
stopReason,
|
|
9505
|
+
superseded: !current(),
|
|
9506
|
+
apiCalls: callCount,
|
|
9507
|
+
turnOutputTokens: usage?.output_tokens,
|
|
9508
|
+
turnInputTokens: usage?.input_tokens,
|
|
9509
|
+
turnCacheReadTokens: usage?.cache_read_input_tokens,
|
|
9510
|
+
lastCallInputTokens: lastCallUsage?.input_tokens,
|
|
9511
|
+
lastCallCacheReadTokens: lastCallUsage?.cache_read_input_tokens,
|
|
9512
|
+
lastCallCacheWriteTokens: lastCallUsage?.cache_creation_input_tokens,
|
|
9513
|
+
deniedPrompts: denied.length,
|
|
9514
|
+
cancelled: turnNumber <= cancelledThrough
|
|
9515
|
+
});
|
|
9516
|
+
emit2(
|
|
9517
|
+
interactiveResultFrame({
|
|
9518
|
+
sessionId: session.sessionId,
|
|
9519
|
+
end,
|
|
9520
|
+
stopReason,
|
|
9521
|
+
usage,
|
|
9522
|
+
denials
|
|
9523
|
+
})
|
|
9524
|
+
);
|
|
9525
|
+
} catch (err) {
|
|
9526
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
9527
|
+
log.error("interactive turn failed", { error: e.message, superseded: !current() });
|
|
9528
|
+
if (!current()) return;
|
|
9529
|
+
emit2(
|
|
9530
|
+
interactiveResultFrame({
|
|
9531
|
+
sessionId: existsSync4(session.jsonlPath) ? session.sessionId : void 0,
|
|
9532
|
+
end: "failed",
|
|
9533
|
+
stopReason: null,
|
|
9534
|
+
error: e.message
|
|
9535
|
+
})
|
|
9536
|
+
);
|
|
9537
|
+
if (proc.listenerCount("error") > 0) proc.emit("error", e);
|
|
9538
|
+
else lineEmitter.emit("close");
|
|
9539
|
+
}
|
|
9540
|
+
};
|
|
9541
|
+
const enqueueTurn = (userMsg) => {
|
|
9542
|
+
const turnNumber = ++writes;
|
|
9543
|
+
queued++;
|
|
9544
|
+
chain = chain.then(() => runTurn(userMsg, turnNumber)).finally(() => {
|
|
9545
|
+
queued--;
|
|
9546
|
+
});
|
|
8931
9547
|
};
|
|
8932
|
-
|
|
9548
|
+
Object.assign(proc, {
|
|
8933
9549
|
stdin: {
|
|
9550
|
+
writable: true,
|
|
8934
9551
|
write(chunk) {
|
|
8935
9552
|
const raw = typeof chunk === "string" && chunk.endsWith("\n") ? chunk.slice(0, -1) : chunk;
|
|
8936
|
-
|
|
9553
|
+
enqueueTurn(decodeUserEnvelope(raw));
|
|
8937
9554
|
return true;
|
|
8938
9555
|
},
|
|
8939
9556
|
end() {
|
|
@@ -8943,47 +9560,56 @@ function spawnInteractiveProcess(opts) {
|
|
|
8943
9560
|
stderr: null,
|
|
8944
9561
|
pid: -1,
|
|
8945
9562
|
killed: false,
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
return proc;
|
|
8949
|
-
},
|
|
8950
|
-
once() {
|
|
8951
|
-
return proc;
|
|
8952
|
-
},
|
|
8953
|
-
off(event, fn) {
|
|
8954
|
-
if (event === "error") errorHandlers.delete(fn);
|
|
8955
|
-
return proc;
|
|
8956
|
-
},
|
|
9563
|
+
exitCode: null,
|
|
9564
|
+
signalCode: null,
|
|
8957
9565
|
kill() {
|
|
9566
|
+
if (!proc.killed) {
|
|
9567
|
+
proc.killed = true;
|
|
9568
|
+
if (opts.systemPromptFile) {
|
|
9569
|
+
void unlink2(opts.systemPromptFile).catch(() => {
|
|
9570
|
+
});
|
|
9571
|
+
}
|
|
9572
|
+
}
|
|
9573
|
+
const started = startPromise !== null;
|
|
8958
9574
|
try {
|
|
8959
9575
|
session.dispose();
|
|
8960
9576
|
} catch {
|
|
8961
9577
|
}
|
|
8962
|
-
if (
|
|
8963
|
-
|
|
8964
|
-
|
|
9578
|
+
if (!started && proc.exitCode === null && proc.signalCode === null) {
|
|
9579
|
+
proc.signalCode = "SIGTERM";
|
|
9580
|
+
proc.emit("exit", null, "SIGTERM");
|
|
9581
|
+
proc.emit("close", null, "SIGTERM");
|
|
8965
9582
|
}
|
|
8966
|
-
proc.killed = true;
|
|
8967
9583
|
return true;
|
|
8968
9584
|
}
|
|
8969
|
-
};
|
|
9585
|
+
});
|
|
8970
9586
|
return {
|
|
8971
9587
|
proc,
|
|
8972
9588
|
lineEmitter,
|
|
8973
9589
|
proxyServer: null,
|
|
8974
9590
|
mcpHash: void 0,
|
|
8975
|
-
systemPromptFile: opts.systemPromptFile
|
|
9591
|
+
systemPromptFile: opts.systemPromptFile,
|
|
9592
|
+
cliPath: opts.cliPath,
|
|
9593
|
+
interactiveControl: {
|
|
9594
|
+
turnRunning: () => queued > 0,
|
|
9595
|
+
// Stops the running turn and every queued one: they were all written
|
|
9596
|
+
// by the turn being aborted, or by one before it.
|
|
9597
|
+
interrupt: (timeoutMs) => {
|
|
9598
|
+
cancelledThrough = writes;
|
|
9599
|
+
return session.interrupt(timeoutMs);
|
|
9600
|
+
}
|
|
9601
|
+
}
|
|
8976
9602
|
};
|
|
8977
9603
|
}
|
|
8978
9604
|
|
|
8979
9605
|
// src/prompts.ts
|
|
8980
|
-
import { readFileSync as
|
|
8981
|
-
import { homedir as
|
|
9606
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
9607
|
+
import { homedir as homedir8 } from "os";
|
|
8982
9608
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
8983
|
-
import { dirname as dirname6, join as
|
|
9609
|
+
import { dirname as dirname6, join as join10 } from "path";
|
|
8984
9610
|
function readPromptFileIfPresent(path11) {
|
|
8985
9611
|
try {
|
|
8986
|
-
const content =
|
|
9612
|
+
const content = readFileSync7(path11, "utf8").trim();
|
|
8987
9613
|
return content || void 0;
|
|
8988
9614
|
} catch {
|
|
8989
9615
|
return void 0;
|
|
@@ -8992,7 +9618,7 @@ function readPromptFileIfPresent(path11) {
|
|
|
8992
9618
|
function nearestWorkspaceAgentsPrompt(cwd) {
|
|
8993
9619
|
let dir = cwd;
|
|
8994
9620
|
while (true) {
|
|
8995
|
-
const content = readPromptFileIfPresent(
|
|
9621
|
+
const content = readPromptFileIfPresent(join10(dir, "AGENTS.md"));
|
|
8996
9622
|
if (content) return content;
|
|
8997
9623
|
const parent = dirname6(dir);
|
|
8998
9624
|
if (parent === dir) return void 0;
|
|
@@ -9101,8 +9727,8 @@ ${options.compressionSummary.trim()}`
|
|
|
9101
9727
|
for (const s of extraSystemContent) {
|
|
9102
9728
|
if (s.trim()) parts.push(s.trim());
|
|
9103
9729
|
}
|
|
9104
|
-
const configRoot = process.env.XDG_CONFIG_HOME ??
|
|
9105
|
-
const globalAgents = readPromptFileIfPresent(
|
|
9730
|
+
const configRoot = process.env.XDG_CONFIG_HOME ?? join10(homedir8(), ".config");
|
|
9731
|
+
const globalAgents = readPromptFileIfPresent(join10(configRoot, "opencode", "AGENTS.md"));
|
|
9106
9732
|
const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
|
|
9107
9733
|
const forwarded = extraSystemContent.join("\n\n");
|
|
9108
9734
|
const pushGlobal = !!globalAgents && !forwarded.includes(globalAgents);
|
|
@@ -9113,9 +9739,9 @@ ${options.compressionSummary.trim()}`
|
|
|
9113
9739
|
if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
|
|
9114
9740
|
const content = parts.join("\n\n");
|
|
9115
9741
|
if (!content) return void 0;
|
|
9116
|
-
const path11 =
|
|
9742
|
+
const path11 = join10(pluginTmpDir(), `opencode-cc-sys-${randomUUID5()}.md`);
|
|
9117
9743
|
try {
|
|
9118
|
-
|
|
9744
|
+
writeFileSync5(path11, content, { encoding: "utf8", mode: 384 });
|
|
9119
9745
|
return path11;
|
|
9120
9746
|
} catch (err) {
|
|
9121
9747
|
log.warn("failed to write system prompt file", { error: String(err) });
|
|
@@ -9833,8 +10459,8 @@ function synthesizeTitle(prompt) {
|
|
|
9833
10459
|
"summary",
|
|
9834
10460
|
"project"
|
|
9835
10461
|
]);
|
|
9836
|
-
const
|
|
9837
|
-
const picked = (
|
|
10462
|
+
const words2 = source.split(" ").map((word) => word.trim()).filter(Boolean).filter((word) => !stop.has(word.toLowerCase()));
|
|
10463
|
+
const picked = (words2.length > 0 ? words2 : source.split(" ").filter(Boolean)).slice(0, 6).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
9838
10464
|
return picked || "New Session";
|
|
9839
10465
|
}
|
|
9840
10466
|
|
|
@@ -9968,6 +10594,7 @@ function createTurnState(init) {
|
|
|
9968
10594
|
lastCallUsage: void 0,
|
|
9969
10595
|
resultFailure: void 0,
|
|
9970
10596
|
accountLimitHit: null,
|
|
10597
|
+
usageLimitNote: false,
|
|
9971
10598
|
accountBlock: null,
|
|
9972
10599
|
modelRefusal: null,
|
|
9973
10600
|
drainBuffer: [],
|
|
@@ -10297,6 +10924,11 @@ function clearStartWatchdog(state) {
|
|
|
10297
10924
|
function onStartWatchdogFire(state) {
|
|
10298
10925
|
state.startWatchdog = null;
|
|
10299
10926
|
if (state.controllerClosed || state.hasReceivedContent || state.hasReceivedProgress) return;
|
|
10927
|
+
const interactive = state.activeProcess?.interactiveControl;
|
|
10928
|
+
if (interactive) {
|
|
10929
|
+
if (interactive.turnRunning()) armStartWatchdog(state);
|
|
10930
|
+
return;
|
|
10931
|
+
}
|
|
10300
10932
|
if (state.respawnAttempted) {
|
|
10301
10933
|
log.error(
|
|
10302
10934
|
"claude process still silent after respawn; ending turn",
|
|
@@ -10748,6 +11380,7 @@ function createLineHandler(state, ctx) {
|
|
|
10748
11380
|
resetsAt: info.resetsAt ?? info.overageResetsAt,
|
|
10749
11381
|
window: info.rateLimitType
|
|
10750
11382
|
};
|
|
11383
|
+
rememberAccountLimit(ctx.sourceAccount, state.accountLimitHit);
|
|
10751
11384
|
}
|
|
10752
11385
|
const note = reportRateLimitEvent(msg);
|
|
10753
11386
|
if (note) {
|
|
@@ -11031,6 +11664,13 @@ ${plan}
|
|
|
11031
11664
|
if (hasText) {
|
|
11032
11665
|
state.hasReceivedContent = true;
|
|
11033
11666
|
}
|
|
11667
|
+
const apiErrorReply = msg.is_api_error_message === true;
|
|
11668
|
+
const limitErrorReply = apiErrorReply && ctx.usageLimitNoteActive && !state.accountBlock && (msg.error === "rate_limit" || isAccountLimitError({
|
|
11669
|
+
resultText: msg.message.content.filter((b) => b.type === "text" && b.text).map((b) => String(b.text)).join("\n")
|
|
11670
|
+
}));
|
|
11671
|
+
if (limitErrorReply) {
|
|
11672
|
+
state.accountLimitHit ??= recallAccountLimit(ctx.sourceAccount);
|
|
11673
|
+
}
|
|
11034
11674
|
if (hasText && !hasToolUse) {
|
|
11035
11675
|
startResultFallback(state);
|
|
11036
11676
|
}
|
|
@@ -11039,6 +11679,7 @@ ${plan}
|
|
|
11039
11679
|
}
|
|
11040
11680
|
for (const block2 of msg.message.content) {
|
|
11041
11681
|
if (block2.type === "text" && block2.text) {
|
|
11682
|
+
if (limitErrorReply) continue;
|
|
11042
11683
|
resetLastVisibleTextBlock(state);
|
|
11043
11684
|
const blockId = state.startTextBlock();
|
|
11044
11685
|
state.controller.enqueue({
|
|
@@ -11251,7 +11892,15 @@ ${plan}
|
|
|
11251
11892
|
if (deliverPendingCompletions(state)) {
|
|
11252
11893
|
return;
|
|
11253
11894
|
}
|
|
11254
|
-
if (!state.
|
|
11895
|
+
if (!state.accountLimitHit && msg.is_error && isAccountLimitError({
|
|
11896
|
+
resultText: typeof msg.result === "string" ? msg.result : null
|
|
11897
|
+
})) {
|
|
11898
|
+
state.accountLimitHit = recallAccountLimit(ctx.sourceAccount);
|
|
11899
|
+
}
|
|
11900
|
+
state.usageLimitNote = ctx.usageLimitNoteActive && !!state.accountLimitHit && // An expired login or a billing hold writes its own note just below
|
|
11901
|
+
// and says what to run; two notes about one failure is one too many.
|
|
11902
|
+
!state.accountBlock && msg.is_error === true;
|
|
11903
|
+
if (!state.currentTextId && msg.is_error && !state.usageLimitNote && typeof msg.result === "string" && msg.result.trim().length > 0) {
|
|
11255
11904
|
const errId = state.startTextBlock();
|
|
11256
11905
|
state.controller.enqueue({
|
|
11257
11906
|
type: "text-delta",
|
|
@@ -11259,11 +11908,6 @@ ${plan}
|
|
|
11259
11908
|
delta: msg.result
|
|
11260
11909
|
});
|
|
11261
11910
|
}
|
|
11262
|
-
if (!state.accountLimitHit && msg.is_error && isAccountLimitError({
|
|
11263
|
-
resultText: typeof msg.result === "string" ? msg.result : null
|
|
11264
|
-
})) {
|
|
11265
|
-
state.accountLimitHit = {};
|
|
11266
|
-
}
|
|
11267
11911
|
if (ctx.modelFallbackArmed && !state.modelRefusal) {
|
|
11268
11912
|
state.modelRefusal = modelRefusalFromResult(msg);
|
|
11269
11913
|
}
|
|
@@ -11277,7 +11921,11 @@ ${plan}
|
|
|
11277
11921
|
kind: state.accountBlock,
|
|
11278
11922
|
account: ctx.sourceAccount,
|
|
11279
11923
|
configDir: ctx.config.configDir,
|
|
11280
|
-
offeringSwitch
|
|
11924
|
+
offeringSwitch,
|
|
11925
|
+
// Read only when the form is not taking the turn, which is the
|
|
11926
|
+
// default: moving the work by hand is then the operator's only
|
|
11927
|
+
// route and nothing else on screen says it exists (h #g194).
|
|
11928
|
+
candidates: ctx.failoverAccounts
|
|
11281
11929
|
})
|
|
11282
11930
|
});
|
|
11283
11931
|
state.endTextBlock();
|
|
@@ -12101,6 +12749,23 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12101
12749
|
}
|
|
12102
12750
|
}
|
|
12103
12751
|
}
|
|
12752
|
+
if (this.config.resumeAfterRestart !== false && includeHistoryContext && !compactionMode && !forkFromClaudeSessionId && failoverAnswer?.kind !== "switch") {
|
|
12753
|
+
const configDir = resolveConfigDir(this.config.configDir);
|
|
12754
|
+
const resumePoint = findResumePoint({
|
|
12755
|
+
sessionKey: sk,
|
|
12756
|
+
prompt: options.prompt,
|
|
12757
|
+
cliPath,
|
|
12758
|
+
transcriptPath: (id) => join11(configDir, "projects", encodeCwd(cwd), `${id}.jsonl`)
|
|
12759
|
+
});
|
|
12760
|
+
if (resumePoint) {
|
|
12761
|
+
setClaudeSessionId(sk, resumePoint.claudeSessionId);
|
|
12762
|
+
includeHistoryContext = false;
|
|
12763
|
+
log.notice("resuming the claude session from before the restart instead of replaying it", {
|
|
12764
|
+
sessionKey: sk,
|
|
12765
|
+
matchedMessages: resumePoint.matched
|
|
12766
|
+
});
|
|
12767
|
+
}
|
|
12768
|
+
}
|
|
12104
12769
|
if (this.config.forkSessions === true && !compactionMode && !useInteractive) {
|
|
12105
12770
|
recordForkFingerprint(sk, options.prompt, cliPath);
|
|
12106
12771
|
}
|
|
@@ -12156,7 +12821,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12156
12821
|
this.config.failoverAccounts,
|
|
12157
12822
|
sourceAccount
|
|
12158
12823
|
);
|
|
12159
|
-
const failoverAskActive = failoverAccounts.length > 0 && this.config.accountFailover
|
|
12824
|
+
const failoverAskActive = failoverAccounts.length > 0 && this.config.accountFailover === "ask" && !compactionMode && !useInteractive && // A `doGenerate` caller takes the override and reports the plain
|
|
12160
12825
|
// rate-limit error, never the form. See `TurnMode`.
|
|
12161
12826
|
mode !== "generate" && isAccountFailoverQuestionActive({
|
|
12162
12827
|
configured: this.config.accountFailover,
|
|
@@ -12169,6 +12834,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12169
12834
|
// the operator is usually not even looking at.
|
|
12170
12835
|
childSession: !!await fetchSessionParentId(affinity)
|
|
12171
12836
|
});
|
|
12837
|
+
const usageLimitNoteActive = !compactionMode && mode !== "generate" && !failoverAskActive;
|
|
12172
12838
|
let staleBuild = null;
|
|
12173
12839
|
if (!compactionMode && mode !== "generate" && affinity !== "default") {
|
|
12174
12840
|
staleBuild = staleBuildWatch().check();
|
|
@@ -12393,6 +13059,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12393
13059
|
...self.skillBridgeSpawn(failover)
|
|
12394
13060
|
});
|
|
12395
13061
|
if (stoppedBeforeWork()) return;
|
|
13062
|
+
const resumeSessionId = getClaudeSessionId(sk);
|
|
12396
13063
|
const ap = spawnInteractiveProcess({
|
|
12397
13064
|
cwd,
|
|
12398
13065
|
cliPath,
|
|
@@ -12404,8 +13071,20 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12404
13071
|
permissionsAllow: allow,
|
|
12405
13072
|
systemPromptFile,
|
|
12406
13073
|
ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey,
|
|
12407
|
-
effort: reasoningEffort
|
|
13074
|
+
effort: reasoningEffort,
|
|
13075
|
+
resumeSessionId,
|
|
13076
|
+
// The headless spawn's env, so hygiene, effort and the
|
|
13077
|
+
// agent's prompt cache TTL reach both transports alike.
|
|
13078
|
+
env: claudeSpawnEnv({
|
|
13079
|
+
ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey,
|
|
13080
|
+
effort: reasoningEffort,
|
|
13081
|
+
promptCacheTtl
|
|
13082
|
+
})
|
|
12408
13083
|
});
|
|
13084
|
+
ap.proc.once(
|
|
13085
|
+
"exit",
|
|
13086
|
+
(code) => noteInteractiveProcessExit(sk, ap, code)
|
|
13087
|
+
);
|
|
12409
13088
|
ap.mcpHash = mcp.bridgedHash;
|
|
12410
13089
|
ap.mcpServers = mcp.allEnabledServerNames;
|
|
12411
13090
|
setActiveProcess(sk, ap);
|
|
@@ -12414,6 +13093,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12414
13093
|
state.activeProcess = ap;
|
|
12415
13094
|
log.info("spawned interactive claude session", {
|
|
12416
13095
|
sk,
|
|
13096
|
+
resumed: !!resumeSessionId,
|
|
12417
13097
|
cliPath,
|
|
12418
13098
|
configDir: self.config.configDir,
|
|
12419
13099
|
model: effectiveModelId
|
|
@@ -12653,6 +13333,9 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12653
13333
|
state.autoContinueState.startedAt = Date.now();
|
|
12654
13334
|
const completeResult = (msg) => {
|
|
12655
13335
|
if (state.controllerClosed) return;
|
|
13336
|
+
if (self.config.resumeAfterRestart !== false && !compactionMode && msg.is_error !== true && typeof msg.session_id === "string") {
|
|
13337
|
+
recordResumePoint(sk, msg.session_id, options.prompt, cliPath);
|
|
13338
|
+
}
|
|
12656
13339
|
if (deliverPendingCompletions(state)) {
|
|
12657
13340
|
if (state.drainBuffer.length > 0) drainNow(state);
|
|
12658
13341
|
return;
|
|
@@ -12716,6 +13399,26 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12716
13399
|
return;
|
|
12717
13400
|
}
|
|
12718
13401
|
}
|
|
13402
|
+
if (state.usageLimitNote) {
|
|
13403
|
+
log.warn("claude account is out of usage; the turn ends with a note", {
|
|
13404
|
+
sessionKey: sk,
|
|
13405
|
+
account: sourceAccount,
|
|
13406
|
+
window: state.accountLimitHit?.window ?? null,
|
|
13407
|
+
resetsAt: state.accountLimitHit?.resetsAt ?? null,
|
|
13408
|
+
candidates: failoverAccounts
|
|
13409
|
+
});
|
|
13410
|
+
controller.enqueue({
|
|
13411
|
+
type: "text-delta",
|
|
13412
|
+
id: state.startTextBlock(),
|
|
13413
|
+
delta: formatUsageLimitNote({
|
|
13414
|
+
sourceAccount,
|
|
13415
|
+
candidates: failoverAccounts,
|
|
13416
|
+
resetsAt: state.accountLimitHit?.resetsAt,
|
|
13417
|
+
window: state.accountLimitHit?.window
|
|
13418
|
+
})
|
|
13419
|
+
});
|
|
13420
|
+
state.endTextBlock();
|
|
13421
|
+
}
|
|
12719
13422
|
if (runAutoContinue(state, msg)) return;
|
|
12720
13423
|
for (const [idx, reasoningId] of state.reasoningIds) {
|
|
12721
13424
|
if (state.reasoningStarted.get(idx)) {
|
|
@@ -12789,6 +13492,8 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12789
13492
|
planModeQuestionActive,
|
|
12790
13493
|
sourceAccount,
|
|
12791
13494
|
failoverAskActive,
|
|
13495
|
+
usageLimitNoteActive,
|
|
13496
|
+
failoverAccounts,
|
|
12792
13497
|
modelFallbackArmed,
|
|
12793
13498
|
attempt,
|
|
12794
13499
|
handleControlRequest: handleControlRequest2,
|
|
@@ -13087,15 +13792,15 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13087
13792
|
|
|
13088
13793
|
// src/cleanup-stale.ts
|
|
13089
13794
|
import {
|
|
13090
|
-
existsSync as
|
|
13091
|
-
mkdirSync as
|
|
13092
|
-
readFileSync as
|
|
13795
|
+
existsSync as existsSync5,
|
|
13796
|
+
mkdirSync as mkdirSync5,
|
|
13797
|
+
readFileSync as readFileSync8,
|
|
13093
13798
|
realpathSync as realpathSync4,
|
|
13094
13799
|
rmSync as rmSync3,
|
|
13095
|
-
writeFileSync as
|
|
13800
|
+
writeFileSync as writeFileSync6
|
|
13096
13801
|
} from "fs";
|
|
13097
|
-
import { homedir as
|
|
13098
|
-
import { dirname as dirname7, join as
|
|
13802
|
+
import { homedir as homedir9 } from "os";
|
|
13803
|
+
import { dirname as dirname7, join as join12, resolve as resolve5 } from "path";
|
|
13099
13804
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
13100
13805
|
var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
|
|
13101
13806
|
var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
|
|
@@ -13103,20 +13808,20 @@ var alreadyRan = false;
|
|
|
13103
13808
|
function candidateCacheRoots() {
|
|
13104
13809
|
const xdg = process.env.XDG_CACHE_HOME;
|
|
13105
13810
|
return [
|
|
13106
|
-
xdg ?
|
|
13107
|
-
|
|
13108
|
-
|
|
13811
|
+
xdg ? join12(xdg, "opencode") : null,
|
|
13812
|
+
join12(homedir9(), ".cache", "opencode"),
|
|
13813
|
+
join12(homedir9(), "Library", "Caches", "opencode")
|
|
13109
13814
|
].filter((p) => Boolean(p));
|
|
13110
13815
|
}
|
|
13111
13816
|
function userOpencodeJsonPath() {
|
|
13112
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME ??
|
|
13113
|
-
return
|
|
13817
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME ?? join12(homedir9(), ".config");
|
|
13818
|
+
return join12(xdgConfig, "opencode", "opencode.json");
|
|
13114
13819
|
}
|
|
13115
13820
|
function userIntendsToUseUnscoped() {
|
|
13116
13821
|
const cfg = userOpencodeJsonPath();
|
|
13117
|
-
if (!
|
|
13822
|
+
if (!existsSync5(cfg)) return false;
|
|
13118
13823
|
try {
|
|
13119
|
-
const json = JSON.parse(
|
|
13824
|
+
const json = JSON.parse(readFileSync8(cfg, "utf8"));
|
|
13120
13825
|
const plugins = json.plugin;
|
|
13121
13826
|
if (!Array.isArray(plugins)) return false;
|
|
13122
13827
|
return plugins.some(
|
|
@@ -13137,19 +13842,19 @@ function ourLoadedDir() {
|
|
|
13137
13842
|
function ourVersion(dir) {
|
|
13138
13843
|
if (!dir) return null;
|
|
13139
13844
|
try {
|
|
13140
|
-
const pkg = JSON.parse(
|
|
13845
|
+
const pkg = JSON.parse(readFileSync8(join12(dir, "package.json"), "utf8"));
|
|
13141
13846
|
return typeof pkg.version === "string" && pkg.version ? pkg.version : null;
|
|
13142
13847
|
} catch {
|
|
13143
13848
|
return null;
|
|
13144
13849
|
}
|
|
13145
13850
|
}
|
|
13146
13851
|
function markerPath() {
|
|
13147
|
-
const stateRoot = process.env.XDG_STATE_HOME ??
|
|
13148
|
-
return
|
|
13852
|
+
const stateRoot = process.env.XDG_STATE_HOME ?? join12(homedir9(), ".local", "state");
|
|
13853
|
+
return join12(stateRoot, "opencode-claude-code-plugin", "cleanup-stale.json");
|
|
13149
13854
|
}
|
|
13150
13855
|
function markerVersion() {
|
|
13151
13856
|
try {
|
|
13152
|
-
const marker = JSON.parse(
|
|
13857
|
+
const marker = JSON.parse(readFileSync8(markerPath(), "utf8"));
|
|
13153
13858
|
return typeof marker.version === "string" ? marker.version : null;
|
|
13154
13859
|
} catch {
|
|
13155
13860
|
return null;
|
|
@@ -13158,8 +13863,8 @@ function markerVersion() {
|
|
|
13158
13863
|
function writeMarker(version) {
|
|
13159
13864
|
const file = markerPath();
|
|
13160
13865
|
try {
|
|
13161
|
-
|
|
13162
|
-
|
|
13866
|
+
mkdirSync5(dirname7(file), { recursive: true });
|
|
13867
|
+
writeFileSync6(
|
|
13163
13868
|
file,
|
|
13164
13869
|
JSON.stringify({ version, at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n",
|
|
13165
13870
|
"utf8"
|
|
@@ -13193,20 +13898,20 @@ function cleanupStaleUnscopedInstall() {
|
|
|
13193
13898
|
if (version) writeMarker(version);
|
|
13194
13899
|
}
|
|
13195
13900
|
function cleanupOne(cacheRoot, ourDir) {
|
|
13196
|
-
if (!
|
|
13197
|
-
const stalePath =
|
|
13198
|
-
if (!
|
|
13901
|
+
if (!existsSync5(cacheRoot)) return;
|
|
13902
|
+
const stalePath = join12(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
|
|
13903
|
+
if (!existsSync5(stalePath)) return;
|
|
13199
13904
|
let realStalePath = stalePath;
|
|
13200
13905
|
try {
|
|
13201
13906
|
realStalePath = realpathSync4(stalePath);
|
|
13202
13907
|
} catch {
|
|
13203
13908
|
}
|
|
13204
13909
|
if (ourDir && realStalePath === ourDir) return;
|
|
13205
|
-
const pkgJsonPath =
|
|
13206
|
-
if (!
|
|
13910
|
+
const pkgJsonPath = join12(stalePath, "package.json");
|
|
13911
|
+
if (!existsSync5(pkgJsonPath)) return;
|
|
13207
13912
|
let pkg = {};
|
|
13208
13913
|
try {
|
|
13209
|
-
pkg = JSON.parse(
|
|
13914
|
+
pkg = JSON.parse(readFileSync8(pkgJsonPath, "utf8"));
|
|
13210
13915
|
} catch {
|
|
13211
13916
|
return;
|
|
13212
13917
|
}
|
|
@@ -13222,13 +13927,13 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
13222
13927
|
});
|
|
13223
13928
|
return;
|
|
13224
13929
|
}
|
|
13225
|
-
const cachePkgJson =
|
|
13226
|
-
if (!
|
|
13930
|
+
const cachePkgJson = join12(cacheRoot, "package.json");
|
|
13931
|
+
if (!existsSync5(cachePkgJson)) return;
|
|
13227
13932
|
try {
|
|
13228
|
-
const cfg = JSON.parse(
|
|
13933
|
+
const cfg = JSON.parse(readFileSync8(cachePkgJson, "utf8"));
|
|
13229
13934
|
if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
|
|
13230
13935
|
delete cfg.dependencies[STALE_PACKAGE_NAME];
|
|
13231
|
-
|
|
13936
|
+
writeFileSync6(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
|
|
13232
13937
|
log.info("cleanup-stale: pruned dep from cache package.json");
|
|
13233
13938
|
}
|
|
13234
13939
|
} catch (err) {
|
|
@@ -13855,7 +14560,11 @@ function createClaudeCode(settings = {}) {
|
|
|
13855
14560
|
configDir: settings.configDir,
|
|
13856
14561
|
failoverAccounts: settings.failoverAccounts,
|
|
13857
14562
|
baseCliPath: settings.baseCliPath ?? cliPath,
|
|
13858
|
-
|
|
14563
|
+
// Passed through, with no default applied here: the form is opt-in and
|
|
14564
|
+
// `isAccountFailoverQuestionActive` asks for an explicit `"ask"`, so an
|
|
14565
|
+
// unset option must stay unset rather than being written to one mode or
|
|
14566
|
+
// the other in only one of the two entry points (h #g194).
|
|
14567
|
+
accountFailover: settings.accountFailover,
|
|
13859
14568
|
providerID: settings.providerID,
|
|
13860
14569
|
skipPermissions: preset?.skipPermissions ?? settings.skipPermissions ?? true,
|
|
13861
14570
|
permissionMode: preset?.permissionMode ?? settings.permissionMode,
|
|
@@ -13885,6 +14594,7 @@ function createClaudeCode(settings = {}) {
|
|
|
13885
14594
|
bridgeSkipNativeSkills: settings.bridgeSkipNativeSkills !== false,
|
|
13886
14595
|
turnStats: settings.turnStats === true,
|
|
13887
14596
|
forkSessions: settings.forkSessions === true,
|
|
14597
|
+
resumeAfterRestart: settings.resumeAfterRestart !== false,
|
|
13888
14598
|
interactive: settings.interactive,
|
|
13889
14599
|
interactiveBypass: settings.interactiveBypass,
|
|
13890
14600
|
interactiveAllowTools: settings.interactiveAllowTools,
|
|
@@ -13970,12 +14680,12 @@ function configModelsForProvider(providerModels, providerID, modelSuffix) {
|
|
|
13970
14680
|
}
|
|
13971
14681
|
return models;
|
|
13972
14682
|
}
|
|
13973
|
-
function
|
|
14683
|
+
function isRecord5(value) {
|
|
13974
14684
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13975
14685
|
}
|
|
13976
14686
|
function completeUserModel(id, entry, providerID) {
|
|
13977
|
-
const record =
|
|
13978
|
-
const runtimeShaped =
|
|
14687
|
+
const record = isRecord5(entry) ? entry : {};
|
|
14688
|
+
const runtimeShaped = isRecord5(record.capabilities) && isRecord5(record.capabilities.input) && isRecord5(record.capabilities.output) && isRecord5(record.cost) && isRecord5(record.cost.cache) && isRecord5(record.api);
|
|
13979
14689
|
if (runtimeShaped) {
|
|
13980
14690
|
return toConfigModel({ ...record, providerID });
|
|
13981
14691
|
}
|
|
@@ -13988,8 +14698,8 @@ function completeUserModel(id, entry, providerID) {
|
|
|
13988
14698
|
const merged = { ...base, ...record, id };
|
|
13989
14699
|
for (const key of ["cost", "limit", "variants"]) {
|
|
13990
14700
|
merged[key] = {
|
|
13991
|
-
...
|
|
13992
|
-
...
|
|
14701
|
+
...isRecord5(base[key]) ? base[key] : {},
|
|
14702
|
+
...isRecord5(record[key]) ? record[key] : {}
|
|
13993
14703
|
};
|
|
13994
14704
|
}
|
|
13995
14705
|
return merged;
|
|
@@ -14086,7 +14796,7 @@ async function buildAgentRegistry(config) {
|
|
|
14086
14796
|
typeof configuredTtl === "string" ? configuredTtl : void 0
|
|
14087
14797
|
);
|
|
14088
14798
|
setProviderFallbackModels(parseFallbackModelList(options?.fallbackModels));
|
|
14089
|
-
const
|
|
14799
|
+
const records2 = await readAgentMarkdownRecords(
|
|
14090
14800
|
agentDirectories(
|
|
14091
14801
|
process.env.HOME ?? process.env.USERPROFILE,
|
|
14092
14802
|
getOpencodeProjectDirectory()
|
|
@@ -14101,18 +14811,18 @@ async function buildAgentRegistry(config) {
|
|
|
14101
14811
|
const declaredChain = parseFallbackModelList(
|
|
14102
14812
|
agent.fallbackModels ?? bag.fallbackModels
|
|
14103
14813
|
);
|
|
14104
|
-
|
|
14105
|
-
mode: pick("mode") ??
|
|
14106
|
-
model: pick("model") ??
|
|
14107
|
-
forceModel: pick("forceModel") ??
|
|
14108
|
-
reasoningEffort: pick("reasoningEffort") ??
|
|
14109
|
-
cacheTtl: pick("cacheTtl") ??
|
|
14110
|
-
fallbackModels: declaredChain.length ? declaredChain :
|
|
14814
|
+
records2[name] = {
|
|
14815
|
+
mode: pick("mode") ?? records2[name]?.mode,
|
|
14816
|
+
model: pick("model") ?? records2[name]?.model,
|
|
14817
|
+
forceModel: pick("forceModel") ?? records2[name]?.forceModel,
|
|
14818
|
+
reasoningEffort: pick("reasoningEffort") ?? records2[name]?.reasoningEffort,
|
|
14819
|
+
cacheTtl: pick("cacheTtl") ?? records2[name]?.cacheTtl,
|
|
14820
|
+
fallbackModels: declaredChain.length ? declaredChain : records2[name]?.fallbackModels
|
|
14111
14821
|
};
|
|
14112
14822
|
}
|
|
14113
|
-
setAgentRegistry(
|
|
14823
|
+
setAgentRegistry(records2);
|
|
14114
14824
|
log.debug("agent registry built", {
|
|
14115
|
-
agents: Object.keys(
|
|
14825
|
+
agents: Object.keys(records2).length,
|
|
14116
14826
|
defaultSubagentModel: getDefaultSubagentModel()
|
|
14117
14827
|
});
|
|
14118
14828
|
}
|