@khalilgharbaoui/opencode-claude-code-plugin 0.38.0 → 0.40.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 +3 -2
- package/dist/index.d.ts +18 -0
- package/dist/index.js +1150 -412
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/skills/claude-code-plugin/SKILL.md +6 -5
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;
|
|
@@ -4259,6 +4271,11 @@ function findForkParent(opts) {
|
|
|
4259
4271
|
}
|
|
4260
4272
|
return best;
|
|
4261
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
|
+
}
|
|
4262
4279
|
function isPrefix(recorded, history) {
|
|
4263
4280
|
for (let i = 0; i < recorded.length; i++) {
|
|
4264
4281
|
if (recorded[i].role !== history[i].role) return false;
|
|
@@ -4273,6 +4290,109 @@ function tailIsReplyOnly(history, from) {
|
|
|
4273
4290
|
return true;
|
|
4274
4291
|
}
|
|
4275
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
|
+
|
|
4276
4396
|
// src/permission-presets.ts
|
|
4277
4397
|
var READ_ONLY_DISALLOWED_CLI_TOOLS = [
|
|
4278
4398
|
"Bash",
|
|
@@ -4385,7 +4505,7 @@ import { randomUUID as randomUUID2 } from "crypto";
|
|
|
4385
4505
|
var MAX_HISTORY_EXCHANGES = 20;
|
|
4386
4506
|
var SIDE_QUESTION_USAGE = "Usage: /btw <question>. Ask a side question about the current conversation without adding it to the main context.";
|
|
4387
4507
|
var pendingProcesses = /* @__PURE__ */ new WeakSet();
|
|
4388
|
-
function
|
|
4508
|
+
function isRecord3(value) {
|
|
4389
4509
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4390
4510
|
}
|
|
4391
4511
|
var SYSTEM_REMINDER_BLOCK = /<system-reminder>[\s\S]*?<\/system-reminder>/g;
|
|
@@ -4396,7 +4516,7 @@ function parseSideQuestionContent(content) {
|
|
|
4396
4516
|
} else if (Array.isArray(content)) {
|
|
4397
4517
|
const parts = [];
|
|
4398
4518
|
for (const part of content) {
|
|
4399
|
-
if (!
|
|
4519
|
+
if (!isRecord3(part) || part.type !== "text" || typeof part.text !== "string") return null;
|
|
4400
4520
|
parts.push(part.text);
|
|
4401
4521
|
}
|
|
4402
4522
|
text = parts.join("\n");
|
|
@@ -4415,7 +4535,7 @@ function assistantText(content) {
|
|
|
4415
4535
|
if (!Array.isArray(content)) return "";
|
|
4416
4536
|
const parts = [];
|
|
4417
4537
|
for (const part of content) {
|
|
4418
|
-
if (
|
|
4538
|
+
if (isRecord3(part) && part.type === "text" && typeof part.text === "string") parts.push(part.text);
|
|
4419
4539
|
}
|
|
4420
4540
|
return parts.join("\n").trim();
|
|
4421
4541
|
}
|
|
@@ -4445,9 +4565,9 @@ function dispatchSideQuestionResponse(activeProcess, line) {
|
|
|
4445
4565
|
} catch {
|
|
4446
4566
|
return false;
|
|
4447
4567
|
}
|
|
4448
|
-
if (!
|
|
4568
|
+
if (!isRecord3(message) || message.type !== "control_response") return false;
|
|
4449
4569
|
const response = message.response;
|
|
4450
|
-
if (!
|
|
4570
|
+
if (!isRecord3(response) || typeof response.request_id !== "string") return false;
|
|
4451
4571
|
return activeProcess.lineEmitter.emit(`side-question:${response.request_id}`, response);
|
|
4452
4572
|
}
|
|
4453
4573
|
async function requestSideQuestion(activeProcess, question, options) {
|
|
@@ -4532,7 +4652,7 @@ async function requestSideQuestion(activeProcess, question, options) {
|
|
|
4532
4652
|
return;
|
|
4533
4653
|
}
|
|
4534
4654
|
const result = response.response;
|
|
4535
|
-
if (response.subtype !== "success" || !
|
|
4655
|
+
if (response.subtype !== "success" || !isRecord3(result) || typeof result.response !== "string" || typeof result.synthetic !== "boolean") {
|
|
4536
4656
|
fail(new Error("Claude Code returned an invalid /btw response."));
|
|
4537
4657
|
return;
|
|
4538
4658
|
}
|
|
@@ -4702,6 +4822,7 @@ function noteTurnLine(ap, line) {
|
|
|
4702
4822
|
if (isTerminalResultLine(line)) settleTurn(ap);
|
|
4703
4823
|
}
|
|
4704
4824
|
function isTurnInFlight(ap) {
|
|
4825
|
+
if (ap.interactiveControl) return ap.interactiveControl.turnRunning();
|
|
4705
4826
|
return ap.turnInFlight === true;
|
|
4706
4827
|
}
|
|
4707
4828
|
function awaitTurnIdle(ap, timeoutMs) {
|
|
@@ -4721,6 +4842,7 @@ function awaitTurnIdle(ap, timeoutMs) {
|
|
|
4721
4842
|
});
|
|
4722
4843
|
}
|
|
4723
4844
|
function interruptTurn(ap, timeoutMs = TURN_INTERRUPT_TIMEOUT_MS) {
|
|
4845
|
+
if (ap.interactiveControl) return ap.interactiveControl.interrupt(timeoutMs);
|
|
4724
4846
|
if (!ap.turnInFlight) return Promise.resolve(true);
|
|
4725
4847
|
const stdin = ap.proc.stdin;
|
|
4726
4848
|
if (ap.asideTransport?.interactive || !stdin || !stdin.writable) {
|
|
@@ -4761,6 +4883,18 @@ function setActiveProcess(key, ap) {
|
|
|
4761
4883
|
cancelIdleProcessEviction(key);
|
|
4762
4884
|
activeProcesses.set(key, ap);
|
|
4763
4885
|
}
|
|
4886
|
+
function noteInteractiveProcessExit(sessionKey2, ap, code) {
|
|
4887
|
+
if (activeProcesses.get(sessionKey2) !== ap) return;
|
|
4888
|
+
cancelIdleProcessEviction(sessionKey2);
|
|
4889
|
+
activeProcesses.delete(sessionKey2);
|
|
4890
|
+
if (getPendingProxyCalls(sessionKey2).length > 0) {
|
|
4891
|
+
rejectAllPendingProxyCallsForSession(sessionKey2, new Error(CHILD_EXITED_MESSAGE));
|
|
4892
|
+
}
|
|
4893
|
+
if (code !== 0 && code !== null) {
|
|
4894
|
+
claudeSessions.delete(sessionKey2);
|
|
4895
|
+
forgetResumePoint(sessionKey2);
|
|
4896
|
+
}
|
|
4897
|
+
}
|
|
4764
4898
|
function scheduleIdleProcessEviction(key, timeoutMs) {
|
|
4765
4899
|
cancelIdleProcessEviction(key);
|
|
4766
4900
|
if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_IDLE_TIMEOUT_MS) {
|
|
@@ -4906,6 +5040,7 @@ function deleteClaudeSessionId(key) {
|
|
|
4906
5040
|
if (claudeSessionId) clearLedger(claudeSessionId);
|
|
4907
5041
|
claudeSessions.delete(key);
|
|
4908
5042
|
forgetForkFingerprint(key);
|
|
5043
|
+
forgetResumePoint(key);
|
|
4909
5044
|
}
|
|
4910
5045
|
function effortSessionKey(baseKey, effort) {
|
|
4911
5046
|
return effort ? `${baseKey}::effort=${effort}` : baseKey;
|
|
@@ -5018,6 +5153,7 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
|
|
|
5018
5153
|
sessionKey: sessionKey2
|
|
5019
5154
|
});
|
|
5020
5155
|
claudeSessions.delete(sessionKey2);
|
|
5156
|
+
forgetResumePoint(sessionKey2);
|
|
5021
5157
|
}
|
|
5022
5158
|
});
|
|
5023
5159
|
proc.stderr?.on("data", (data) => {
|
|
@@ -5032,6 +5168,7 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
|
|
|
5032
5168
|
error: stderr.slice(0, 200)
|
|
5033
5169
|
});
|
|
5034
5170
|
claudeSessions.delete(sessionKey2);
|
|
5171
|
+
forgetResumePoint(sessionKey2);
|
|
5035
5172
|
} else {
|
|
5036
5173
|
log.debug("ignoring session ID error from stale claude process", {
|
|
5037
5174
|
sessionKey: sessionKey2
|
|
@@ -5510,7 +5647,7 @@ async function handleBtwCommand(client, input, options = {}) {
|
|
|
5510
5647
|
// src/diagnostic-bundle.ts
|
|
5511
5648
|
import { createHash as createHash4, randomBytes as randomBytes2 } from "crypto";
|
|
5512
5649
|
import { openSync, readSync, closeSync, fstatSync, realpathSync } from "fs";
|
|
5513
|
-
import { homedir as
|
|
5650
|
+
import { homedir as homedir5 } from "os";
|
|
5514
5651
|
|
|
5515
5652
|
// src/log-messages.ts
|
|
5516
5653
|
var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
@@ -5586,6 +5723,9 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
5586
5723
|
"ignoring unknown proxyTools entries",
|
|
5587
5724
|
"interactive permissionMode bypassPermissions ignored: Claude Code prompts for confirmation in the TUI",
|
|
5588
5725
|
"interactive system prompt disabled; opencode agent prompts will not be appended",
|
|
5726
|
+
"interactive transport cancelled the usage-limit auto-continue",
|
|
5727
|
+
"interactive transport cannot continue",
|
|
5728
|
+
"interactive transport denied a permission prompt",
|
|
5589
5729
|
"interactive transport dropped non-text content blocks",
|
|
5590
5730
|
"interactive turn failed",
|
|
5591
5731
|
"interactiveBypass ignored: Claude Code prompts for bypassPermissions confirmation in the interactive TUI",
|
|
@@ -5617,6 +5757,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
5617
5757
|
"remote URL images are not supported by Claude CLI, skipping",
|
|
5618
5758
|
"replaying stdout the child emitted between turns",
|
|
5619
5759
|
"result fallback timer fired \u2014 closing stream without result event",
|
|
5760
|
+
"resuming the claude session from before the restart instead of replaying it",
|
|
5620
5761
|
"shared Claude capability already exists; leaving untouched",
|
|
5621
5762
|
"skipping MCP server with unknown type",
|
|
5622
5763
|
"skipping local MCP server with no command",
|
|
@@ -5840,7 +5981,7 @@ function createIdHasher(salt = randomBytes2(16).toString("hex")) {
|
|
|
5840
5981
|
return (raw) => createHash4("sha256").update(salt).update("\0").update(raw).digest("hex").slice(0, 8);
|
|
5841
5982
|
}
|
|
5842
5983
|
function createRedactionContext(options = {}) {
|
|
5843
|
-
const home = options.home ??
|
|
5984
|
+
const home = options.home ?? homedir5();
|
|
5844
5985
|
const homes = /* @__PURE__ */ new Set();
|
|
5845
5986
|
if (home) homes.add(home);
|
|
5846
5987
|
if (home) {
|
|
@@ -6514,7 +6655,7 @@ function staleBuildWatch() {
|
|
|
6514
6655
|
var DOCTOR_COMMAND = "claude-code-doctor";
|
|
6515
6656
|
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";
|
|
6516
6657
|
var DOCTOR_MARKER = "\u258C **claude-code doctor**";
|
|
6517
|
-
function
|
|
6658
|
+
function isRecord4(value) {
|
|
6518
6659
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
6519
6660
|
}
|
|
6520
6661
|
var SYSTEM_REMINDER_BLOCK2 = /<system-reminder>[\s\S]*?<\/system-reminder>/g;
|
|
@@ -6525,7 +6666,7 @@ function parseDoctorCommandContent(content) {
|
|
|
6525
6666
|
} else if (Array.isArray(content)) {
|
|
6526
6667
|
const parts = [];
|
|
6527
6668
|
for (const part of content) {
|
|
6528
|
-
if (!
|
|
6669
|
+
if (!isRecord4(part) || part.type !== "text" || typeof part.text !== "string") return null;
|
|
6529
6670
|
parts.push(part.text);
|
|
6530
6671
|
}
|
|
6531
6672
|
text = parts.join("\n");
|
|
@@ -7266,8 +7407,8 @@ var registry = {};
|
|
|
7266
7407
|
var defaultSubagentModel;
|
|
7267
7408
|
var defaultSubagentCacheTtl;
|
|
7268
7409
|
var providerFallbackModels = [];
|
|
7269
|
-
function setAgentRegistry(
|
|
7270
|
-
registry =
|
|
7410
|
+
function setAgentRegistry(records2) {
|
|
7411
|
+
registry = records2;
|
|
7271
7412
|
}
|
|
7272
7413
|
function getAgentRegistry() {
|
|
7273
7414
|
return registry;
|
|
@@ -7407,7 +7548,7 @@ function parseFallbackModelList(value) {
|
|
|
7407
7548
|
return value.trim().replace(/^\[|\]$/g, "").split(/[,\s]+/).map((entry) => entry.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
|
|
7408
7549
|
}
|
|
7409
7550
|
async function readAgentMarkdownRecords(directories) {
|
|
7410
|
-
const
|
|
7551
|
+
const records2 = {};
|
|
7411
7552
|
for (const directory of directories) {
|
|
7412
7553
|
let entries;
|
|
7413
7554
|
try {
|
|
@@ -7418,10 +7559,10 @@ async function readAgentMarkdownRecords(directories) {
|
|
|
7418
7559
|
for (const entry of entries) {
|
|
7419
7560
|
if (!entry.endsWith(".md")) continue;
|
|
7420
7561
|
const name = entry.slice(0, -3);
|
|
7421
|
-
if (
|
|
7562
|
+
if (records2[name]) continue;
|
|
7422
7563
|
try {
|
|
7423
7564
|
const text = await readFile(path7.join(directory, entry), "utf8");
|
|
7424
|
-
|
|
7565
|
+
records2[name] = parseAgentFrontmatter(text);
|
|
7425
7566
|
} catch (err) {
|
|
7426
7567
|
log.debug("failed to read agent markdown", {
|
|
7427
7568
|
file: path7.join(directory, entry),
|
|
@@ -7430,7 +7571,7 @@ async function readAgentMarkdownRecords(directories) {
|
|
|
7430
7571
|
}
|
|
7431
7572
|
}
|
|
7432
7573
|
}
|
|
7433
|
-
return
|
|
7574
|
+
return records2;
|
|
7434
7575
|
}
|
|
7435
7576
|
function agentDirectories(home, projectDirectory) {
|
|
7436
7577
|
const directories = [];
|
|
@@ -8369,6 +8510,7 @@ async function resolveSkillPluginDirs(opts) {
|
|
|
8369
8510
|
|
|
8370
8511
|
// src/claude-session-wrapper.ts
|
|
8371
8512
|
import { EventEmitter as EventEmitter4 } from "events";
|
|
8513
|
+
import { existsSync as existsSync4 } from "fs";
|
|
8372
8514
|
import { unlink as unlink2 } from "fs/promises";
|
|
8373
8515
|
|
|
8374
8516
|
// src/claude-session-bun.ts
|
|
@@ -8402,10 +8544,99 @@ function encodeCwd(cwd) {
|
|
|
8402
8544
|
}
|
|
8403
8545
|
return real.replace(/[^a-zA-Z0-9]/g, "-");
|
|
8404
8546
|
}
|
|
8547
|
+
var bunPtySpawner = (argv, opts) => {
|
|
8548
|
+
const [command, ...args] = argv;
|
|
8549
|
+
return Bun.spawn([resolveClaude(command ?? "claude"), ...args], {
|
|
8550
|
+
cwd: opts.cwd,
|
|
8551
|
+
env: opts.env,
|
|
8552
|
+
terminal: {
|
|
8553
|
+
cols: opts.cols,
|
|
8554
|
+
rows: opts.rows,
|
|
8555
|
+
data: (_term, data) => opts.onData(Buffer.from(data).toString("utf8"))
|
|
8556
|
+
}
|
|
8557
|
+
});
|
|
8558
|
+
};
|
|
8559
|
+
function words(text) {
|
|
8560
|
+
return text.split(/\s+/).map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s*");
|
|
8561
|
+
}
|
|
8562
|
+
var SCREEN_PATTERNS = [
|
|
8563
|
+
{
|
|
8564
|
+
kind: "login",
|
|
8565
|
+
pattern: new RegExp(
|
|
8566
|
+
[
|
|
8567
|
+
words("Select login method"),
|
|
8568
|
+
words("Please run /login"),
|
|
8569
|
+
words("Invalid API key"),
|
|
8570
|
+
words("OAuth token has expired"),
|
|
8571
|
+
words("Not logged in")
|
|
8572
|
+
].join("|"),
|
|
8573
|
+
"i"
|
|
8574
|
+
)
|
|
8575
|
+
},
|
|
8576
|
+
{
|
|
8577
|
+
kind: "onboarding",
|
|
8578
|
+
pattern: new RegExp(
|
|
8579
|
+
[words("Choose the text style that looks best"), words("Let's get started.")].join("|"),
|
|
8580
|
+
"i"
|
|
8581
|
+
)
|
|
8582
|
+
},
|
|
8583
|
+
{
|
|
8584
|
+
kind: "trust",
|
|
8585
|
+
pattern: new RegExp(
|
|
8586
|
+
[
|
|
8587
|
+
words("Do you trust the files in this folder?"),
|
|
8588
|
+
words("Is this a project you created or one you trust?"),
|
|
8589
|
+
words("Yes, I trust this folder")
|
|
8590
|
+
].join("|"),
|
|
8591
|
+
"i"
|
|
8592
|
+
)
|
|
8593
|
+
},
|
|
8594
|
+
{
|
|
8595
|
+
// The question alone is not enough: Claude can write "Do you want to
|
|
8596
|
+
// proceed?" in a reply. The dialog always numbers its options.
|
|
8597
|
+
kind: "permission",
|
|
8598
|
+
pattern: new RegExp(
|
|
8599
|
+
`${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`,
|
|
8600
|
+
"i"
|
|
8601
|
+
)
|
|
8602
|
+
},
|
|
8603
|
+
{
|
|
8604
|
+
kind: "auto-continue",
|
|
8605
|
+
pattern: new RegExp(
|
|
8606
|
+
`${words("continuing automatically at")}[^\xB7\\n]{1,40}\xB7\\s*${words("esc to cancel")}`,
|
|
8607
|
+
"i"
|
|
8608
|
+
)
|
|
8609
|
+
}
|
|
8610
|
+
];
|
|
8611
|
+
function stripTerminal(raw) {
|
|
8612
|
+
return raw.replace(
|
|
8613
|
+
/\x1B\[(\d*)C/g,
|
|
8614
|
+
(_match, count) => " ".repeat(Math.min(Number(count) || 1, 200))
|
|
8615
|
+
).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, " ");
|
|
8616
|
+
}
|
|
8617
|
+
function highlightedChoice(text) {
|
|
8618
|
+
let last = null;
|
|
8619
|
+
for (const match of text.matchAll(/❯\s*(?:\d+\s*\.\s*)?(Yes|No)\b/gi)) {
|
|
8620
|
+
last = match[1].toLowerCase();
|
|
8621
|
+
}
|
|
8622
|
+
return last === "yes" || last === "no" ? last : null;
|
|
8623
|
+
}
|
|
8624
|
+
function classifyScreen(text) {
|
|
8625
|
+
for (const { kind, pattern } of SCREEN_PATTERNS) {
|
|
8626
|
+
const match = pattern.exec(text);
|
|
8627
|
+
if (match) {
|
|
8628
|
+
return { kind, detail: match[0].replace(/\s+/g, " ").trim().slice(0, 200) };
|
|
8629
|
+
}
|
|
8630
|
+
}
|
|
8631
|
+
return null;
|
|
8632
|
+
}
|
|
8633
|
+
function configDirEnv(configDir) {
|
|
8634
|
+
return configDir ? { CLAUDE_CONFIG_DIR: configDir } : {};
|
|
8635
|
+
}
|
|
8405
8636
|
function interactiveSpawnEnv(opts) {
|
|
8406
8637
|
return {
|
|
8407
8638
|
...process.env,
|
|
8408
|
-
|
|
8639
|
+
...configDirEnv(opts.configDir),
|
|
8409
8640
|
TERM: "xterm-256color",
|
|
8410
8641
|
// Pin the binary so a mid-session autoupdate cannot invalidate the
|
|
8411
8642
|
// detected version the flag gates read, and skip non-essential traffic.
|
|
@@ -8480,6 +8711,21 @@ var TurnUsageAccumulator = class {
|
|
|
8480
8711
|
};
|
|
8481
8712
|
var TERMINAL_STOP = /* @__PURE__ */ new Set(["end_turn", "stop_sequence", "max_tokens"]);
|
|
8482
8713
|
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
8714
|
+
var RAW_KEEP_CHARS = 64 * 1024;
|
|
8715
|
+
var SCREEN_WINDOW_CHARS = 16 * 1024;
|
|
8716
|
+
var INTERRUPT_MARKER = "[Request interrupted by user";
|
|
8717
|
+
function isInterruptRecord(rec) {
|
|
8718
|
+
if (!rec || rec.type !== "user" || !rec.message) return false;
|
|
8719
|
+
const content = rec.message.content;
|
|
8720
|
+
if (typeof content === "string") return content.startsWith(INTERRUPT_MARKER);
|
|
8721
|
+
if (!Array.isArray(content)) return false;
|
|
8722
|
+
return content.some(
|
|
8723
|
+
(block) => block?.type === "text" && typeof block.text === "string" && block.text.startsWith(INTERRUPT_MARKER)
|
|
8724
|
+
);
|
|
8725
|
+
}
|
|
8726
|
+
function isTurnDurationRecord(rec) {
|
|
8727
|
+
return rec?.type === "system" && rec.subtype === "turn_duration";
|
|
8728
|
+
}
|
|
8483
8729
|
function resolveConfigDir(configDir) {
|
|
8484
8730
|
const value = configDir ?? process.env.CLAUDE_CONFIG_DIR;
|
|
8485
8731
|
if (!value) return path9.join(os4.homedir(), ".claude");
|
|
@@ -8489,11 +8735,21 @@ function resolveConfigDir(configDir) {
|
|
|
8489
8735
|
}
|
|
8490
8736
|
return path9.resolve(value);
|
|
8491
8737
|
}
|
|
8738
|
+
function fatalScreenMessage(kind, configDir, cwd) {
|
|
8739
|
+
if (kind === "trust") {
|
|
8740
|
+
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.`;
|
|
8741
|
+
}
|
|
8742
|
+
if (kind === "login") {
|
|
8743
|
+
return `Claude Code is not logged in for ${configDir}. Run \`claude\` there once by hand and log in (or \`claude auth login\`), then retry.`;
|
|
8744
|
+
}
|
|
8745
|
+
return `Claude Code has not finished its first-run setup for ${configDir}. Run \`claude\` there once by hand, then retry.`;
|
|
8746
|
+
}
|
|
8492
8747
|
var ClaudeSession = class {
|
|
8493
8748
|
sessionId;
|
|
8494
8749
|
cwd;
|
|
8495
8750
|
configDir;
|
|
8496
8751
|
jsonlPath;
|
|
8752
|
+
/** The newest terminal output, ANSI included, for diagnostics. */
|
|
8497
8753
|
raw = "";
|
|
8498
8754
|
proc = null;
|
|
8499
8755
|
cursor = 0;
|
|
@@ -8503,12 +8759,38 @@ var ClaudeSession = class {
|
|
|
8503
8759
|
exitCode = null;
|
|
8504
8760
|
aborted = false;
|
|
8505
8761
|
signal;
|
|
8762
|
+
resumeSessionId;
|
|
8763
|
+
env;
|
|
8764
|
+
onScreen;
|
|
8765
|
+
onExit;
|
|
8766
|
+
spawnPty;
|
|
8767
|
+
/** The configured config dir, which alone is exported to the child. */
|
|
8768
|
+
explicitConfigDir;
|
|
8769
|
+
/** Output since the screen was last acted on, read by `checkScreen`. */
|
|
8770
|
+
screen = "";
|
|
8771
|
+
screenTimer = null;
|
|
8772
|
+
fatal = null;
|
|
8773
|
+
/** The folder trust dialog is up and not answered yet. */
|
|
8774
|
+
trustPending = false;
|
|
8775
|
+
trustMoves = 0;
|
|
8776
|
+
trustMovedAt = 0;
|
|
8777
|
+
turn = null;
|
|
8778
|
+
/** The running turn's read state, shared with `flushTranscript`. */
|
|
8779
|
+
scan = null;
|
|
8780
|
+
turnDenied = [];
|
|
8781
|
+
abandonTurn = false;
|
|
8506
8782
|
o;
|
|
8507
8783
|
constructor(opts = {}) {
|
|
8508
8784
|
this.cwd = path9.resolve(opts.cwd ?? process.cwd());
|
|
8509
8785
|
this.configDir = resolveConfigDir(opts.configDir);
|
|
8786
|
+
this.explicitConfigDir = opts.configDir ? this.configDir : void 0;
|
|
8510
8787
|
this.signal = opts.signal;
|
|
8511
|
-
this.
|
|
8788
|
+
this.resumeSessionId = opts.resumeSessionId;
|
|
8789
|
+
this.env = opts.env;
|
|
8790
|
+
this.onScreen = opts.onScreen;
|
|
8791
|
+
this.onExit = opts.onExit;
|
|
8792
|
+
this.spawnPty = opts.spawnPty ?? bunPtySpawner;
|
|
8793
|
+
this.sessionId = opts.resumeSessionId ?? randomUUID4();
|
|
8512
8794
|
this.jsonlPath = path9.join(
|
|
8513
8795
|
this.configDir,
|
|
8514
8796
|
"projects",
|
|
@@ -8538,9 +8820,47 @@ var ClaudeSession = class {
|
|
|
8538
8820
|
submitMinMs: opts.submitMinMs ?? 200,
|
|
8539
8821
|
submitConfirmMs: opts.submitConfirmMs ?? 1500,
|
|
8540
8822
|
submitMaxRetries: opts.submitMaxRetries ?? 8,
|
|
8541
|
-
debug: opts.debug ?? false
|
|
8823
|
+
debug: opts.debug ?? false,
|
|
8824
|
+
// The headless CLI's `tool_progress` heartbeat runs every 30 s, and the
|
|
8825
|
+
// wire-inactivity watchdog it keeps quiet fires at 60 s.
|
|
8826
|
+
heartbeatMs: opts.heartbeatMs ?? 3e4,
|
|
8827
|
+
interruptGraceMs: opts.interruptGraceMs ?? 5e3,
|
|
8828
|
+
// Shorter than `bootQuietMs`, so a trust dialog is answered before boot
|
|
8829
|
+
// could declare the TUI ready and paste a prompt into it.
|
|
8830
|
+
permissionQuietMs: opts.permissionQuietMs ?? 1e3,
|
|
8831
|
+
stopSettleMs: opts.stopSettleMs ?? 750
|
|
8542
8832
|
};
|
|
8543
8833
|
}
|
|
8834
|
+
/** The CLI arguments this session spawns with. */
|
|
8835
|
+
spawnArgs() {
|
|
8836
|
+
const args = this.resumeSessionId ? ["--resume", this.resumeSessionId] : ["--session-id", this.sessionId];
|
|
8837
|
+
if (this.o.model) args.push("--model", this.o.model);
|
|
8838
|
+
if (this.o.settingSources !== null && this.o.settingSources !== void 0) {
|
|
8839
|
+
args.push("--setting-sources", this.o.settingSources);
|
|
8840
|
+
}
|
|
8841
|
+
if (this.o.extraArgs && this.o.extraArgs.length) args.push(...this.o.extraArgs);
|
|
8842
|
+
return args;
|
|
8843
|
+
}
|
|
8844
|
+
/** The child environment: the caller's (or the default one) plus `TERM`,
|
|
8845
|
+
* and `CLAUDE_CONFIG_DIR` only for a configured one (`configDirEnv`). */
|
|
8846
|
+
spawnEnv() {
|
|
8847
|
+
if (!this.env) {
|
|
8848
|
+
return interactiveSpawnEnv({
|
|
8849
|
+
configDir: this.explicitConfigDir,
|
|
8850
|
+
ignoreAnthropicApiKey: this.o.ignoreAnthropicApiKey,
|
|
8851
|
+
effort: this.o.effort
|
|
8852
|
+
});
|
|
8853
|
+
}
|
|
8854
|
+
return { ...this.env, ...configDirEnv(this.explicitConfigDir), TERM: "xterm-256color" };
|
|
8855
|
+
}
|
|
8856
|
+
/** True while a turn is between its paste and its end. */
|
|
8857
|
+
get turnRunning() {
|
|
8858
|
+
return this.turn !== null;
|
|
8859
|
+
}
|
|
8860
|
+
/** True once the child is gone (exited, killed or never started). */
|
|
8861
|
+
get hasExited() {
|
|
8862
|
+
return this.exited;
|
|
8863
|
+
}
|
|
8544
8864
|
async start() {
|
|
8545
8865
|
if (this.signal?.aborted) throw new Error("aborted before start");
|
|
8546
8866
|
this.signal?.addEventListener(
|
|
@@ -8551,61 +8871,147 @@ var ClaudeSession = class {
|
|
|
8551
8871
|
},
|
|
8552
8872
|
{ once: true }
|
|
8553
8873
|
);
|
|
8554
|
-
const claude =
|
|
8555
|
-
const args =
|
|
8556
|
-
if (this.o.model) args.push("--model", this.o.model);
|
|
8557
|
-
if (this.o.settingSources !== null && this.o.settingSources !== void 0) {
|
|
8558
|
-
args.push("--setting-sources", this.o.settingSources);
|
|
8559
|
-
}
|
|
8560
|
-
if (this.o.extraArgs && this.o.extraArgs.length) args.push(...this.o.extraArgs);
|
|
8874
|
+
const claude = this.o.cliPath ?? "claude";
|
|
8875
|
+
const args = this.spawnArgs();
|
|
8561
8876
|
if (this.o.debug)
|
|
8562
8877
|
process.stderr.write(`[session] spawn: ${claude} ${args.join(" ")}
|
|
8563
8878
|
`);
|
|
8564
8879
|
this.lastDataAt = Date.now();
|
|
8565
|
-
|
|
8880
|
+
const proc = this.spawnPty([claude, ...args], {
|
|
8566
8881
|
cwd: this.cwd,
|
|
8567
|
-
env:
|
|
8568
|
-
|
|
8569
|
-
|
|
8570
|
-
|
|
8571
|
-
ignoreAnthropicApiKey: this.o.ignoreAnthropicApiKey,
|
|
8572
|
-
effort: this.o.effort
|
|
8573
|
-
}),
|
|
8574
|
-
terminal: {
|
|
8575
|
-
cols: this.o.cols,
|
|
8576
|
-
rows: this.o.rows,
|
|
8577
|
-
data: (_term, d) => {
|
|
8578
|
-
this.lastDataAt = Date.now();
|
|
8579
|
-
const chunk = Buffer.from(d).toString("utf8");
|
|
8580
|
-
this.raw += chunk;
|
|
8581
|
-
if (this.o.debug) process.stdout.write(chunk);
|
|
8582
|
-
}
|
|
8583
|
-
}
|
|
8882
|
+
env: this.spawnEnv(),
|
|
8883
|
+
cols: this.o.cols,
|
|
8884
|
+
rows: this.o.rows,
|
|
8885
|
+
onData: (chunk) => this.onData(chunk)
|
|
8584
8886
|
});
|
|
8585
|
-
this.proc
|
|
8586
|
-
|
|
8587
|
-
this.exited
|
|
8588
|
-
this.
|
|
8589
|
-
}).catch(() => {
|
|
8887
|
+
this.proc = proc;
|
|
8888
|
+
const markExited = (code) => {
|
|
8889
|
+
if (this.exited) return;
|
|
8890
|
+
this.exitCode = code;
|
|
8590
8891
|
this.exited = true;
|
|
8591
8892
|
this.proc = null;
|
|
8592
|
-
|
|
8893
|
+
this.clearScreenTimer();
|
|
8894
|
+
this.onExit?.(code);
|
|
8895
|
+
};
|
|
8896
|
+
proc.exited.then((code) => markExited(typeof code === "number" ? code : null)).catch(() => markExited(null));
|
|
8593
8897
|
await this.waitForBoot();
|
|
8594
8898
|
this.cursor = this.lineCount();
|
|
8595
8899
|
}
|
|
8900
|
+
onData(chunk) {
|
|
8901
|
+
this.lastDataAt = Date.now();
|
|
8902
|
+
this.raw = (this.raw + chunk).slice(-RAW_KEEP_CHARS);
|
|
8903
|
+
this.screen = (this.screen + chunk).slice(-SCREEN_WINDOW_CHARS);
|
|
8904
|
+
if (this.o.debug) process.stdout.write(chunk);
|
|
8905
|
+
this.clearScreenTimer();
|
|
8906
|
+
this.screenTimer = setTimeout(() => {
|
|
8907
|
+
this.screenTimer = null;
|
|
8908
|
+
this.checkScreen();
|
|
8909
|
+
}, this.o.permissionQuietMs);
|
|
8910
|
+
}
|
|
8911
|
+
clearScreenTimer() {
|
|
8912
|
+
if (this.screenTimer) {
|
|
8913
|
+
clearTimeout(this.screenTimer);
|
|
8914
|
+
this.screenTimer = null;
|
|
8915
|
+
}
|
|
8916
|
+
}
|
|
8917
|
+
/**
|
|
8918
|
+
* Answer the screen the TUI is blocked on, if it is one of the few this
|
|
8919
|
+
* session recognises. Returns what it did, or null. Each screen is acted on
|
|
8920
|
+
* once: the window is cleared afterwards, so the next check only reads what
|
|
8921
|
+
* the TUI drew since.
|
|
8922
|
+
*/
|
|
8923
|
+
checkScreen() {
|
|
8924
|
+
if (!this.proc || this.exited) return null;
|
|
8925
|
+
const text = stripTerminal(this.screen);
|
|
8926
|
+
const state = this.trustPending ? { kind: "trust", detail: "folder trust dialog" } : classifyScreen(text);
|
|
8927
|
+
if (!state) return null;
|
|
8928
|
+
let action;
|
|
8929
|
+
switch (state.kind) {
|
|
8930
|
+
case "trust": {
|
|
8931
|
+
this.trustPending = true;
|
|
8932
|
+
const choice = highlightedChoice(text);
|
|
8933
|
+
if (choice === "yes") {
|
|
8934
|
+
this.write("\r");
|
|
8935
|
+
this.trustPending = false;
|
|
8936
|
+
action = "accepted";
|
|
8937
|
+
break;
|
|
8938
|
+
}
|
|
8939
|
+
if (choice === null) return null;
|
|
8940
|
+
if (this.trustMovedAt && this.lastDataAt <= this.trustMovedAt) return null;
|
|
8941
|
+
if (this.trustMoves >= 4) {
|
|
8942
|
+
action = "fatal";
|
|
8943
|
+
break;
|
|
8944
|
+
}
|
|
8945
|
+
this.trustMoves++;
|
|
8946
|
+
this.trustMovedAt = Date.now();
|
|
8947
|
+
this.write("\x1B[B");
|
|
8948
|
+
return null;
|
|
8949
|
+
}
|
|
8950
|
+
case "permission":
|
|
8951
|
+
if (!this.turn) return null;
|
|
8952
|
+
this.write("\x1B");
|
|
8953
|
+
this.turnDenied.push(state);
|
|
8954
|
+
action = "denied";
|
|
8955
|
+
break;
|
|
8956
|
+
case "auto-continue":
|
|
8957
|
+
this.write("\x1B");
|
|
8958
|
+
action = "cancelled";
|
|
8959
|
+
break;
|
|
8960
|
+
case "login":
|
|
8961
|
+
case "onboarding":
|
|
8962
|
+
action = "fatal";
|
|
8963
|
+
break;
|
|
8964
|
+
}
|
|
8965
|
+
this.screen = "";
|
|
8966
|
+
const event = { ...state, action };
|
|
8967
|
+
if (action === "fatal") {
|
|
8968
|
+
this.fatal = event;
|
|
8969
|
+
this.killProcess();
|
|
8970
|
+
}
|
|
8971
|
+
try {
|
|
8972
|
+
this.onScreen?.(event);
|
|
8973
|
+
} catch {
|
|
8974
|
+
}
|
|
8975
|
+
return event;
|
|
8976
|
+
}
|
|
8977
|
+
write(data) {
|
|
8978
|
+
try {
|
|
8979
|
+
this.proc?.terminal.write(data);
|
|
8980
|
+
} catch {
|
|
8981
|
+
}
|
|
8982
|
+
}
|
|
8983
|
+
fatalError() {
|
|
8984
|
+
if (!this.fatal) return null;
|
|
8985
|
+
return new Error(
|
|
8986
|
+
this.failureMessage(fatalScreenMessage(this.fatal.kind, this.configDir, this.cwd), true)
|
|
8987
|
+
);
|
|
8988
|
+
}
|
|
8596
8989
|
/** Wait until the TUI has been quiet for bootQuietMs (Ink ready), bounded by
|
|
8597
|
-
* bootMinMs..bootMaxMs.
|
|
8990
|
+
* bootMinMs..bootMaxMs. A screen still waiting to be answered when the TUI
|
|
8991
|
+
* goes quiet is answered first, and the wait goes on. */
|
|
8598
8992
|
async waitForBoot() {
|
|
8599
8993
|
const start = Date.now();
|
|
8600
8994
|
while (Date.now() - start < this.o.bootMaxMs) {
|
|
8601
8995
|
await delay(150);
|
|
8602
8996
|
if (this.aborted) throw new Error("aborted during boot");
|
|
8997
|
+
const fatal = this.fatalError();
|
|
8998
|
+
if (fatal) throw fatal;
|
|
8603
8999
|
if (this.exited) {
|
|
8604
9000
|
throw new Error(this.failureMessage("claude exited during boot", true));
|
|
8605
9001
|
}
|
|
8606
9002
|
const elapsed = Date.now() - start;
|
|
8607
9003
|
const sinceData = Date.now() - this.lastDataAt;
|
|
8608
|
-
if (elapsed >= this.o.bootMinMs && sinceData >= this.o.bootQuietMs)
|
|
9004
|
+
if (elapsed >= this.o.bootMinMs && sinceData >= this.o.bootQuietMs) {
|
|
9005
|
+
const acted = this.checkScreen();
|
|
9006
|
+
if (!acted && !this.trustPending) return;
|
|
9007
|
+
const fatalNow = this.fatalError();
|
|
9008
|
+
if (fatalNow) throw fatalNow;
|
|
9009
|
+
}
|
|
9010
|
+
}
|
|
9011
|
+
if (this.trustPending) {
|
|
9012
|
+
this.fatal = { kind: "trust", detail: "folder trust dialog", action: "fatal" };
|
|
9013
|
+
this.killProcess();
|
|
9014
|
+
throw this.fatalError();
|
|
8609
9015
|
}
|
|
8610
9016
|
}
|
|
8611
9017
|
/** Submit the freshly-injected prompt and confirm the turn was actually
|
|
@@ -8643,7 +9049,7 @@ var ClaudeSession = class {
|
|
|
8643
9049
|
return lines.length > 0 ? lines.length - 1 : 0;
|
|
8644
9050
|
}
|
|
8645
9051
|
rawTail(max = 600) {
|
|
8646
|
-
const clean = this.raw
|
|
9052
|
+
const clean = stripTerminal(this.raw).replace(/\s+/g, " ").trim();
|
|
8647
9053
|
return clean.length > max ? clean.slice(-max) : clean;
|
|
8648
9054
|
}
|
|
8649
9055
|
failureMessage(reason, includeRaw = false) {
|
|
@@ -8657,61 +9063,169 @@ var ClaudeSession = class {
|
|
|
8657
9063
|
return parts.join("; ");
|
|
8658
9064
|
}
|
|
8659
9065
|
/**
|
|
8660
|
-
*
|
|
8661
|
-
*
|
|
9066
|
+
* Paste a turn into the live session and follow the transcript until the
|
|
9067
|
+
* turn ends, handing every new record to `onRecord` (raw line and parsed
|
|
9068
|
+
* record, the latter null when the line is not JSON). One turn at a time:
|
|
9069
|
+
* a second call while one runs is refused, because both would read the
|
|
9070
|
+
* same cursor.
|
|
8662
9071
|
*/
|
|
8663
|
-
async
|
|
9072
|
+
async runTurnLoop(prompt, onRecord, timeout, onHeartbeat) {
|
|
8664
9073
|
if (this.aborted) throw new Error("aborted");
|
|
9074
|
+
const fatal = this.fatalError();
|
|
9075
|
+
if (fatal) throw fatal;
|
|
8665
9076
|
if (!this.proc || this.exited)
|
|
8666
9077
|
throw new Error("session not started or already exited");
|
|
8667
|
-
|
|
8668
|
-
|
|
8669
|
-
|
|
8670
|
-
|
|
8671
|
-
|
|
8672
|
-
|
|
8673
|
-
|
|
8674
|
-
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
|
|
8679
|
-
while (Date.now() < deadline) {
|
|
8680
|
-
await delay(this.o.pollMs);
|
|
8681
|
-
if (this.aborted) throw new Error("aborted mid-turn");
|
|
8682
|
-
const lines = this.readRawLines();
|
|
8683
|
-
const lastComplete = lines.length - 1;
|
|
8684
|
-
if (lastComplete <= this.cursor) {
|
|
8685
|
-
if (this.exited) throw new Error(this.failureMessage("claude exited mid-turn", true));
|
|
8686
|
-
continue;
|
|
9078
|
+
if (this.turn) throw new Error("a turn is already running in this session");
|
|
9079
|
+
let settle;
|
|
9080
|
+
this.turn = new Promise((resolve6) => settle = resolve6);
|
|
9081
|
+
this.turnDenied = [];
|
|
9082
|
+
this.abandonTurn = false;
|
|
9083
|
+
this.cursor = Math.max(this.cursor, this.lineCount());
|
|
9084
|
+
this.screen = "";
|
|
9085
|
+
try {
|
|
9086
|
+
if (this.o.bracketedPaste) {
|
|
9087
|
+
this.proc.terminal.write("\x1B[200~" + prompt + "\x1B[201~");
|
|
9088
|
+
} else {
|
|
9089
|
+
this.proc.terminal.write(prompt);
|
|
8687
9090
|
}
|
|
8688
|
-
|
|
8689
|
-
|
|
8690
|
-
|
|
8691
|
-
|
|
8692
|
-
|
|
8693
|
-
|
|
8694
|
-
|
|
9091
|
+
await this.submitTurn();
|
|
9092
|
+
const scan = {
|
|
9093
|
+
onRecord,
|
|
9094
|
+
stopReason: null,
|
|
9095
|
+
stopSeenAt: 0,
|
|
9096
|
+
end: null,
|
|
9097
|
+
sawUserRecord: false,
|
|
9098
|
+
lastBeat: Date.now()
|
|
9099
|
+
};
|
|
9100
|
+
this.scan = scan;
|
|
9101
|
+
const deadline = Date.now() + timeout;
|
|
9102
|
+
while (Date.now() < deadline) {
|
|
9103
|
+
await delay(this.o.pollMs);
|
|
9104
|
+
if (this.aborted) throw new Error("aborted mid-turn");
|
|
9105
|
+
const fatalNow = this.fatalError();
|
|
9106
|
+
if (fatalNow) throw fatalNow;
|
|
9107
|
+
if (this.scanTranscript() || scan.end) {
|
|
9108
|
+
if (scan.end) {
|
|
9109
|
+
return {
|
|
9110
|
+
stopReason: scan.end === "stop" ? scan.stopReason : null,
|
|
9111
|
+
end: scan.end,
|
|
9112
|
+
denied: this.turnDenied
|
|
9113
|
+
};
|
|
9114
|
+
}
|
|
8695
9115
|
continue;
|
|
8696
9116
|
}
|
|
9117
|
+
if (scan.stopReason && Date.now() - scan.stopSeenAt >= this.o.stopSettleMs) {
|
|
9118
|
+
return { stopReason: scan.stopReason, end: "stop", denied: this.turnDenied };
|
|
9119
|
+
}
|
|
9120
|
+
if (this.exited) throw new Error(this.failureMessage("claude exited mid-turn", true));
|
|
9121
|
+
if (this.abandonTurn) return { stopReason: null, end: "interrupted", denied: this.turnDenied };
|
|
9122
|
+
const now = Date.now();
|
|
9123
|
+
if (onHeartbeat && now - scan.lastBeat >= this.o.heartbeatMs) {
|
|
9124
|
+
scan.lastBeat = now;
|
|
9125
|
+
if (now - this.lastDataAt < this.o.heartbeatMs) onHeartbeat();
|
|
9126
|
+
}
|
|
9127
|
+
}
|
|
9128
|
+
return { stopReason: null, end: null, denied: this.turnDenied };
|
|
9129
|
+
} finally {
|
|
9130
|
+
this.turn = null;
|
|
9131
|
+
this.scan = null;
|
|
9132
|
+
this.abandonTurn = false;
|
|
9133
|
+
settle();
|
|
9134
|
+
}
|
|
9135
|
+
}
|
|
9136
|
+
/**
|
|
9137
|
+
* Read every complete transcript record past the cursor into the running
|
|
9138
|
+
* turn. Returns whether anything new was read. The one place a record is
|
|
9139
|
+
* interpreted, shared by the poll loop and `flushTranscript`.
|
|
9140
|
+
*/
|
|
9141
|
+
scanTranscript() {
|
|
9142
|
+
const scan = this.scan;
|
|
9143
|
+
if (!scan || scan.end) return false;
|
|
9144
|
+
const lines = this.readRawLines();
|
|
9145
|
+
const lastComplete = lines.length - 1;
|
|
9146
|
+
if (lastComplete <= this.cursor) return false;
|
|
9147
|
+
for (let i = this.cursor; i < lastComplete && !scan.end; i++) {
|
|
9148
|
+
const s = lines[i];
|
|
9149
|
+
if (!s || !s.trim()) {
|
|
9150
|
+
this.cursor = i + 1;
|
|
9151
|
+
continue;
|
|
9152
|
+
}
|
|
9153
|
+
let rec = null;
|
|
9154
|
+
try {
|
|
9155
|
+
rec = JSON.parse(s);
|
|
9156
|
+
} catch {
|
|
9157
|
+
}
|
|
9158
|
+
scan.onRecord(s, rec);
|
|
9159
|
+
if (rec) {
|
|
8697
9160
|
if (rec.type === "assistant" && rec.message) {
|
|
8698
|
-
|
|
8699
|
-
|
|
8700
|
-
|
|
8701
|
-
}
|
|
8702
|
-
usage.add(rec);
|
|
8703
|
-
if (rec.message.stop_reason && TERMINAL_STOP.has(rec.message.stop_reason)) {
|
|
8704
|
-
stopReason = rec.message.stop_reason;
|
|
9161
|
+
const reason = rec.message.stop_reason;
|
|
9162
|
+
if (typeof reason === "string") {
|
|
9163
|
+
scan.stopReason = TERMINAL_STOP.has(reason) ? reason : null;
|
|
8705
9164
|
}
|
|
9165
|
+
} else if (rec.type === "user") {
|
|
9166
|
+
if (scan.sawUserRecord && isInterruptRecord(rec)) scan.end = "interrupted";
|
|
9167
|
+
scan.sawUserRecord = true;
|
|
9168
|
+
} else if ((scan.sawUserRecord || scan.stopReason) && isTurnDurationRecord(rec)) {
|
|
9169
|
+
scan.end = scan.stopReason ? "stop" : "ended";
|
|
8706
9170
|
}
|
|
8707
9171
|
}
|
|
8708
|
-
this.cursor =
|
|
8709
|
-
if (stopReason) break;
|
|
9172
|
+
this.cursor = i + 1;
|
|
8710
9173
|
}
|
|
9174
|
+
if (!scan.end) this.cursor = lastComplete;
|
|
9175
|
+
scan.lastBeat = Date.now();
|
|
9176
|
+
scan.stopSeenAt = Date.now();
|
|
9177
|
+
return true;
|
|
9178
|
+
}
|
|
9179
|
+
/**
|
|
9180
|
+
* Hand the running turn every record the TUI has already written, now
|
|
9181
|
+
* rather than at the next poll. The TUI writes a reply's records before it
|
|
9182
|
+
* runs the reply's tools, but a proxied tool's MCP call can reach the
|
|
9183
|
+
* plugin inside one poll interval, and the step it ends must not close
|
|
9184
|
+
* before the text that preceded the call was read. No-op between turns.
|
|
9185
|
+
*/
|
|
9186
|
+
flushTranscript() {
|
|
9187
|
+
this.scanTranscript();
|
|
9188
|
+
}
|
|
9189
|
+
/**
|
|
9190
|
+
* Stop the running turn the way the TUI's own Esc does, and keep the
|
|
9191
|
+
* session alive for the next one. Resolves true once the turn ended, false
|
|
9192
|
+
* when it was abandoned after `interruptGraceMs` without the TUI saying so.
|
|
9193
|
+
*/
|
|
9194
|
+
async interrupt(graceMs = this.o.interruptGraceMs) {
|
|
9195
|
+
const turn = this.turn;
|
|
9196
|
+
if (!turn) return true;
|
|
9197
|
+
this.write("\x1B");
|
|
9198
|
+
const ended = await Promise.race([turn.then(() => true), delay(graceMs).then(() => false)]);
|
|
9199
|
+
if (ended) return true;
|
|
9200
|
+
this.write("\x1B");
|
|
9201
|
+
this.abandonTurn = true;
|
|
9202
|
+
await Promise.race([turn, delay(this.o.pollMs * 4)]);
|
|
9203
|
+
return false;
|
|
9204
|
+
}
|
|
9205
|
+
/**
|
|
9206
|
+
* Inject a turn into the live session and return the assistant reply once a
|
|
9207
|
+
* terminal stop_reason is observed in the transcript.
|
|
9208
|
+
*/
|
|
9209
|
+
async ask(prompt, perTurnTimeoutMs) {
|
|
9210
|
+
const timeout = perTurnTimeoutMs ?? this.o.turnTimeoutMs;
|
|
9211
|
+
const t0 = Date.now();
|
|
9212
|
+
const collected2 = [];
|
|
9213
|
+
const usage = new TurnUsageAccumulator();
|
|
9214
|
+
const { stopReason } = await this.runTurnLoop(
|
|
9215
|
+
prompt,
|
|
9216
|
+
(_raw, rec) => {
|
|
9217
|
+
if (rec?.type !== "assistant" || !rec.message) return;
|
|
9218
|
+
for (const b of rec.message.content ?? []) {
|
|
9219
|
+
if (b?.type === "text" && typeof b.text === "string") collected2.push(b.text);
|
|
9220
|
+
}
|
|
9221
|
+
usage.add(rec);
|
|
9222
|
+
},
|
|
9223
|
+
timeout
|
|
9224
|
+
);
|
|
8711
9225
|
if (!stopReason) {
|
|
8712
9226
|
throw new Error(
|
|
8713
9227
|
this.failureMessage(
|
|
8714
|
-
`turn
|
|
9228
|
+
`turn ended without a terminal assistant record after ${Date.now() - t0}ms (collected ${collected2.length} text block(s))`
|
|
8715
9229
|
)
|
|
8716
9230
|
);
|
|
8717
9231
|
}
|
|
@@ -8733,9 +9247,9 @@ var ClaudeSession = class {
|
|
|
8733
9247
|
}
|
|
8734
9248
|
/**
|
|
8735
9249
|
* Like ask(), but instead of collecting the reply text it re-emits each NEW
|
|
8736
|
-
* raw JSONL transcript line via onLine (verbatim) until
|
|
8737
|
-
*
|
|
8738
|
-
*
|
|
9250
|
+
* raw JSONL transcript line via onLine (verbatim) until the turn ends. Used
|
|
9251
|
+
* by the opencode plugin transport shim, which feeds these raw lines into
|
|
9252
|
+
* the existing stream-json line handler unchanged.
|
|
8739
9253
|
*
|
|
8740
9254
|
* `usage` is the turn summed over DISTINCT API calls, which is exactly what
|
|
8741
9255
|
* a headless `result` frame reports, so the shim can synthesize one and
|
|
@@ -8743,53 +9257,24 @@ var ClaudeSession = class {
|
|
|
8743
9257
|
* `turnStats`) behaves as it does on the headless path. `lastCallUsage` is
|
|
8744
9258
|
* the newest real call, returned because a caller with no stream parser
|
|
8745
9259
|
* (`askOnce`) has no other way to get the context side.
|
|
9260
|
+
*
|
|
9261
|
+
* A turn that ends any way other than a terminal stop (an interrupt, a
|
|
9262
|
+
* denied permission dialog, `turn_duration`) resolves with that `end` and a
|
|
9263
|
+
* null `stopReason`; only a timeout throws.
|
|
8746
9264
|
*/
|
|
8747
|
-
async tailTurn(prompt, onLine, perTurnTimeoutMs) {
|
|
8748
|
-
if (this.aborted) throw new Error("aborted");
|
|
8749
|
-
if (!this.proc || this.exited)
|
|
8750
|
-
throw new Error("session not started or already exited");
|
|
9265
|
+
async tailTurn(prompt, onLine, perTurnTimeoutMs, onHeartbeat) {
|
|
8751
9266
|
const timeout = perTurnTimeoutMs ?? this.o.turnTimeoutMs;
|
|
8752
|
-
if (this.o.bracketedPaste) {
|
|
8753
|
-
this.proc.terminal.write("\x1B[200~" + prompt + "\x1B[201~");
|
|
8754
|
-
} else {
|
|
8755
|
-
this.proc.terminal.write(prompt);
|
|
8756
|
-
}
|
|
8757
|
-
await this.submitTurn();
|
|
8758
9267
|
const usage = new TurnUsageAccumulator();
|
|
8759
|
-
|
|
8760
|
-
|
|
8761
|
-
|
|
8762
|
-
|
|
8763
|
-
|
|
8764
|
-
|
|
8765
|
-
|
|
8766
|
-
|
|
8767
|
-
|
|
8768
|
-
|
|
8769
|
-
}
|
|
8770
|
-
continue;
|
|
8771
|
-
}
|
|
8772
|
-
for (let i = this.cursor; i < lastComplete; i++) {
|
|
8773
|
-
const s = lines[i];
|
|
8774
|
-
if (!s || !s.trim()) continue;
|
|
8775
|
-
onLine(s);
|
|
8776
|
-
let rec;
|
|
8777
|
-
try {
|
|
8778
|
-
rec = JSON.parse(s);
|
|
8779
|
-
} catch {
|
|
8780
|
-
continue;
|
|
8781
|
-
}
|
|
8782
|
-
usage.add(rec);
|
|
8783
|
-
if (rec.type === "assistant" && rec.message) {
|
|
8784
|
-
if (rec.message.stop_reason && TERMINAL_STOP.has(rec.message.stop_reason)) {
|
|
8785
|
-
stopReason = rec.message.stop_reason;
|
|
8786
|
-
}
|
|
8787
|
-
}
|
|
8788
|
-
}
|
|
8789
|
-
this.cursor = lastComplete;
|
|
8790
|
-
if (stopReason) break;
|
|
8791
|
-
}
|
|
8792
|
-
if (!stopReason) {
|
|
9268
|
+
const { stopReason, end, denied } = await this.runTurnLoop(
|
|
9269
|
+
prompt,
|
|
9270
|
+
(raw, rec) => {
|
|
9271
|
+
onLine(raw);
|
|
9272
|
+
if (rec) usage.add(rec);
|
|
9273
|
+
},
|
|
9274
|
+
timeout,
|
|
9275
|
+
onHeartbeat
|
|
9276
|
+
);
|
|
9277
|
+
if (!end) {
|
|
8793
9278
|
throw new Error(
|
|
8794
9279
|
this.failureMessage(
|
|
8795
9280
|
`turn timed out after ${timeout}ms (no terminal assistant record)`
|
|
@@ -8798,26 +9283,32 @@ var ClaudeSession = class {
|
|
|
8798
9283
|
}
|
|
8799
9284
|
return {
|
|
8800
9285
|
stopReason,
|
|
9286
|
+
end,
|
|
8801
9287
|
usage: usage.turnTotal,
|
|
8802
9288
|
lastCallUsage: usage.lastCall,
|
|
8803
|
-
callCount: usage.callCount
|
|
9289
|
+
callCount: usage.callCount,
|
|
9290
|
+
denied
|
|
8804
9291
|
};
|
|
8805
9292
|
}
|
|
8806
|
-
|
|
8807
|
-
|
|
8808
|
-
|
|
8809
|
-
|
|
8810
|
-
|
|
8811
|
-
|
|
8812
|
-
try {
|
|
8813
|
-
this.proc.kill();
|
|
8814
|
-
} catch {
|
|
8815
|
-
}
|
|
8816
|
-
try {
|
|
8817
|
-
this.proc.terminal.close();
|
|
8818
|
-
} catch {
|
|
8819
|
-
}
|
|
9293
|
+
killProcess() {
|
|
9294
|
+
const proc = this.proc;
|
|
9295
|
+
if (!proc) return;
|
|
9296
|
+
try {
|
|
9297
|
+
proc.terminal.write("");
|
|
9298
|
+
} catch {
|
|
8820
9299
|
}
|
|
9300
|
+
try {
|
|
9301
|
+
proc.kill();
|
|
9302
|
+
} catch {
|
|
9303
|
+
}
|
|
9304
|
+
try {
|
|
9305
|
+
proc.terminal.close();
|
|
9306
|
+
} catch {
|
|
9307
|
+
}
|
|
9308
|
+
}
|
|
9309
|
+
dispose() {
|
|
9310
|
+
this.clearScreenTimer();
|
|
9311
|
+
this.killProcess();
|
|
8821
9312
|
this.proc = null;
|
|
8822
9313
|
}
|
|
8823
9314
|
};
|
|
@@ -8866,6 +9357,9 @@ function interactiveExtraArgs(opts) {
|
|
|
8866
9357
|
"--strict-mcp-config"
|
|
8867
9358
|
);
|
|
8868
9359
|
}
|
|
9360
|
+
if (opts.disallowedTools && opts.disallowedTools.length > 0) {
|
|
9361
|
+
extraArgs.push("--disallowedTools", ...opts.disallowedTools);
|
|
9362
|
+
}
|
|
8869
9363
|
for (const dir of opts.pluginDirs ?? []) {
|
|
8870
9364
|
extraArgs.push("--plugin-dir", dir);
|
|
8871
9365
|
}
|
|
@@ -8891,9 +9385,106 @@ function interactiveExtraArgs(opts) {
|
|
|
8891
9385
|
}
|
|
8892
9386
|
return extraArgs;
|
|
8893
9387
|
}
|
|
9388
|
+
var OpenToolCalls = class {
|
|
9389
|
+
open = /* @__PURE__ */ new Map();
|
|
9390
|
+
add(rec, now = Date.now()) {
|
|
9391
|
+
const content = rec?.message?.content;
|
|
9392
|
+
if (!Array.isArray(content)) return;
|
|
9393
|
+
for (const block of content) {
|
|
9394
|
+
if (rec.type === "assistant" && block?.type === "tool_use" && typeof block.id === "string") {
|
|
9395
|
+
this.open.set(block.id, { name: String(block.name ?? "tool"), startedAt: now });
|
|
9396
|
+
} else if (rec.type === "user" && block?.type === "tool_result") {
|
|
9397
|
+
this.open.delete(block.tool_use_id);
|
|
9398
|
+
}
|
|
9399
|
+
}
|
|
9400
|
+
}
|
|
9401
|
+
newest() {
|
|
9402
|
+
let newest = null;
|
|
9403
|
+
for (const [id, call] of this.open) newest = { id, ...call };
|
|
9404
|
+
return newest;
|
|
9405
|
+
}
|
|
9406
|
+
clear() {
|
|
9407
|
+
this.open.clear();
|
|
9408
|
+
}
|
|
9409
|
+
};
|
|
9410
|
+
function heartbeatFrame(sessionId, openCall, now = Date.now()) {
|
|
9411
|
+
if (openCall) {
|
|
9412
|
+
return JSON.stringify({
|
|
9413
|
+
type: "tool_progress",
|
|
9414
|
+
tool_use_id: `${openCall.id}-heartbeat`,
|
|
9415
|
+
tool_name: openCall.name,
|
|
9416
|
+
parent_tool_use_id: openCall.id,
|
|
9417
|
+
heartbeat: true,
|
|
9418
|
+
elapsed_time_seconds: Math.round((now - openCall.startedAt) / 1e3),
|
|
9419
|
+
session_id: sessionId
|
|
9420
|
+
});
|
|
9421
|
+
}
|
|
9422
|
+
return JSON.stringify({
|
|
9423
|
+
type: "system",
|
|
9424
|
+
subtype: "status",
|
|
9425
|
+
status: "requesting",
|
|
9426
|
+
session_id: sessionId
|
|
9427
|
+
});
|
|
9428
|
+
}
|
|
9429
|
+
function interactiveResultFrame(opts) {
|
|
9430
|
+
const completed = opts.end === "stop" && !!opts.stopReason;
|
|
9431
|
+
let result;
|
|
9432
|
+
let terminalReason = "completed";
|
|
9433
|
+
if (!completed) {
|
|
9434
|
+
if (opts.end === "failed") {
|
|
9435
|
+
terminalReason = "error_during_execution";
|
|
9436
|
+
result = `Interactive transport failed: ${opts.error ?? "unknown error"}`;
|
|
9437
|
+
} else if (opts.end === "interrupted") {
|
|
9438
|
+
terminalReason = "aborted";
|
|
9439
|
+
result = "Interactive transport: the turn was interrupted.";
|
|
9440
|
+
} else {
|
|
9441
|
+
terminalReason = "error_during_execution";
|
|
9442
|
+
result = "Interactive transport: the turn ended without a terminal stop_reason. Output above may be incomplete.";
|
|
9443
|
+
}
|
|
9444
|
+
}
|
|
9445
|
+
return JSON.stringify({
|
|
9446
|
+
type: "result",
|
|
9447
|
+
subtype: completed ? "success" : "error_during_execution",
|
|
9448
|
+
is_error: !completed,
|
|
9449
|
+
stop_reason: completed ? opts.stopReason : null,
|
|
9450
|
+
terminal_reason: terminalReason,
|
|
9451
|
+
result,
|
|
9452
|
+
session_id: opts.sessionId,
|
|
9453
|
+
usage: opts.usage ?? {},
|
|
9454
|
+
// The TUI has no `can_use_tool` channel, so a permission dialog is the
|
|
9455
|
+
// only way it asks; each one the session denied is reported in the
|
|
9456
|
+
// headless field's place, names and ids only (h #g109).
|
|
9457
|
+
permission_denials: opts.denials ?? [],
|
|
9458
|
+
total_cost_usd: null,
|
|
9459
|
+
duration_ms: 0
|
|
9460
|
+
});
|
|
9461
|
+
}
|
|
9462
|
+
function carriesReplyText(line) {
|
|
9463
|
+
try {
|
|
9464
|
+
const rec = JSON.parse(line);
|
|
9465
|
+
if (rec?.type !== "assistant" || !Array.isArray(rec.message?.content)) return false;
|
|
9466
|
+
return rec.message.content.some(
|
|
9467
|
+
(block) => block?.type === "text" && typeof block.text === "string" && block.text.trim()
|
|
9468
|
+
);
|
|
9469
|
+
} catch {
|
|
9470
|
+
return false;
|
|
9471
|
+
}
|
|
9472
|
+
}
|
|
8894
9473
|
function spawnInteractiveProcess(opts) {
|
|
8895
9474
|
const extraArgs = interactiveExtraArgs(opts);
|
|
9475
|
+
const openCalls = new OpenToolCalls();
|
|
9476
|
+
let denials = [];
|
|
9477
|
+
const lineEmitter = new EventEmitter4();
|
|
9478
|
+
const proc = new EventEmitter4();
|
|
9479
|
+
let ap;
|
|
9480
|
+
let proxyClosed = false;
|
|
9481
|
+
const closeProxyServer = () => {
|
|
9482
|
+
if (proxyClosed || !opts.proxyServer) return;
|
|
9483
|
+
proxyClosed = true;
|
|
9484
|
+
void opts.proxyServer.close();
|
|
9485
|
+
};
|
|
8896
9486
|
const session = new ClaudeSession({
|
|
9487
|
+
...opts.tuning,
|
|
8897
9488
|
cwd: opts.cwd,
|
|
8898
9489
|
cliPath: opts.cliPath,
|
|
8899
9490
|
configDir: opts.configDir,
|
|
@@ -8903,7 +9494,29 @@ function spawnInteractiveProcess(opts) {
|
|
|
8903
9494
|
settingSources: opts.settingSources === void 0 ? null : opts.settingSources,
|
|
8904
9495
|
extraArgs,
|
|
8905
9496
|
ignoreAnthropicApiKey: opts.ignoreAnthropicApiKey,
|
|
8906
|
-
effort: opts.effort ? cliEffortLevel(opts.effort) : void 0
|
|
9497
|
+
effort: opts.effort ? cliEffortLevel(opts.effort) : void 0,
|
|
9498
|
+
resumeSessionId: opts.resumeSessionId,
|
|
9499
|
+
env: opts.env,
|
|
9500
|
+
spawnPty: opts.spawnPty,
|
|
9501
|
+
onScreen: (event) => {
|
|
9502
|
+
if (event.action === "denied") {
|
|
9503
|
+
const call = openCalls.newest();
|
|
9504
|
+
denials.push(call ? { tool_name: call.name, tool_use_id: call.id } : { tool_name: "unknown" });
|
|
9505
|
+
}
|
|
9506
|
+
const data = { sessionId: session.sessionId, screen: event.kind, detail: event.detail };
|
|
9507
|
+
if (event.action === "accepted") log.info("interactive transport accepted a prompt", data);
|
|
9508
|
+
else if (event.action === "fatal") log.error("interactive transport cannot continue", data);
|
|
9509
|
+
else if (event.action === "denied") log.warn("interactive transport denied a permission prompt", data);
|
|
9510
|
+
else log.warn("interactive transport cancelled the usage-limit auto-continue", data);
|
|
9511
|
+
},
|
|
9512
|
+
onExit: (code) => {
|
|
9513
|
+
proc.exitCode = code;
|
|
9514
|
+
proc.signalCode = code === null ? "SIGTERM" : null;
|
|
9515
|
+
closeProxyServer();
|
|
9516
|
+
log.info("interactive claude exited", { sessionId: session.sessionId, code });
|
|
9517
|
+
proc.emit("exit", proc.exitCode, proc.signalCode);
|
|
9518
|
+
proc.emit("close", proc.exitCode, proc.signalCode);
|
|
9519
|
+
}
|
|
8907
9520
|
});
|
|
8908
9521
|
log.info("prepared interactive claude session", {
|
|
8909
9522
|
cwd: opts.cwd,
|
|
@@ -8912,80 +9525,106 @@ function spawnInteractiveProcess(opts) {
|
|
|
8912
9525
|
model: opts.model,
|
|
8913
9526
|
effort: opts.effort,
|
|
8914
9527
|
sessionId: session.sessionId,
|
|
9528
|
+
resumed: !!opts.resumeSessionId,
|
|
8915
9529
|
jsonlPath: session.jsonlPath
|
|
8916
9530
|
});
|
|
8917
|
-
const lineEmitter = new EventEmitter4();
|
|
8918
|
-
const errorHandlers = /* @__PURE__ */ new Set();
|
|
8919
9531
|
let startPromise = null;
|
|
8920
9532
|
const ensureStarted = () => {
|
|
8921
9533
|
if (!startPromise) startPromise = session.start();
|
|
8922
9534
|
return startPromise;
|
|
8923
9535
|
};
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
usage: opts2.usage ?? {},
|
|
8936
|
-
total_cost_usd: null,
|
|
8937
|
-
duration_ms: 0
|
|
8938
|
-
})
|
|
8939
|
-
);
|
|
8940
|
-
};
|
|
8941
|
-
const runTurn = (userMsg) => {
|
|
8942
|
-
void (async () => {
|
|
8943
|
-
try {
|
|
8944
|
-
await ensureStarted();
|
|
8945
|
-
const { stopReason, usage, lastCallUsage, callCount } = await session.tailTurn(userMsg, (raw) => {
|
|
8946
|
-
lineEmitter.emit("line", raw);
|
|
8947
|
-
});
|
|
8948
|
-
log.info("interactive turn usage", {
|
|
8949
|
-
apiCalls: callCount,
|
|
8950
|
-
turnOutputTokens: usage?.output_tokens,
|
|
8951
|
-
turnInputTokens: usage?.input_tokens,
|
|
8952
|
-
turnCacheReadTokens: usage?.cache_read_input_tokens,
|
|
8953
|
-
lastCallInputTokens: lastCallUsage?.input_tokens,
|
|
8954
|
-
lastCallCacheReadTokens: lastCallUsage?.cache_read_input_tokens,
|
|
8955
|
-
lastCallCacheWriteTokens: lastCallUsage?.cache_creation_input_tokens
|
|
8956
|
-
});
|
|
8957
|
-
const timedOut = !stopReason;
|
|
8958
|
-
emitResult({
|
|
8959
|
-
subtype: timedOut ? "error_during_execution" : "success",
|
|
8960
|
-
isError: timedOut,
|
|
8961
|
-
stopReason: timedOut ? null : stopReason,
|
|
8962
|
-
terminalReason: timedOut ? "error_during_execution" : "completed",
|
|
8963
|
-
result: timedOut ? "Interactive transport: the turn ended without a terminal stop_reason (turn timeout or claude exit). Output above may be incomplete." : void 0,
|
|
8964
|
-
usage
|
|
8965
|
-
});
|
|
8966
|
-
} catch (err) {
|
|
8967
|
-
const e = err instanceof Error ? err : new Error(String(err));
|
|
8968
|
-
log.error("interactive turn failed", { error: e.message });
|
|
8969
|
-
emitResult({
|
|
8970
|
-
subtype: "error_during_execution",
|
|
8971
|
-
isError: true,
|
|
8972
|
-
stopReason: null,
|
|
8973
|
-
terminalReason: "error_during_execution",
|
|
8974
|
-
result: `Interactive transport failed: ${e.message}`
|
|
8975
|
-
});
|
|
8976
|
-
if (errorHandlers.size > 0) {
|
|
8977
|
-
for (const h of errorHandlers) h(e);
|
|
8978
|
-
} else {
|
|
8979
|
-
lineEmitter.emit("close");
|
|
8980
|
-
}
|
|
9536
|
+
let writes = 0;
|
|
9537
|
+
let cancelledThrough = 0;
|
|
9538
|
+
let queued = 0;
|
|
9539
|
+
let chain = Promise.resolve();
|
|
9540
|
+
const runTurn = async (userMsg, turnNumber) => {
|
|
9541
|
+
const current = () => turnNumber === writes;
|
|
9542
|
+
const emit2 = (line) => {
|
|
9543
|
+
if (!current()) return;
|
|
9544
|
+
if (lineEmitter.listenerCount("line") === 0) {
|
|
9545
|
+
if (carriesReplyText(line)) bufferUnattendedLine(ap, line);
|
|
9546
|
+
return;
|
|
8981
9547
|
}
|
|
8982
|
-
|
|
9548
|
+
lineEmitter.emit("line", line);
|
|
9549
|
+
};
|
|
9550
|
+
try {
|
|
9551
|
+
await ensureStarted();
|
|
9552
|
+
if (turnNumber <= cancelledThrough) {
|
|
9553
|
+
log.info("interactive turn cancelled before it started", { sessionId: session.sessionId });
|
|
9554
|
+
return;
|
|
9555
|
+
}
|
|
9556
|
+
openCalls.clear();
|
|
9557
|
+
denials = [];
|
|
9558
|
+
const { stopReason, end, usage, lastCallUsage, callCount, denied } = await session.tailTurn(
|
|
9559
|
+
userMsg,
|
|
9560
|
+
(raw) => {
|
|
9561
|
+
try {
|
|
9562
|
+
openCalls.add(JSON.parse(raw));
|
|
9563
|
+
} catch {
|
|
9564
|
+
}
|
|
9565
|
+
emit2(raw);
|
|
9566
|
+
},
|
|
9567
|
+
void 0,
|
|
9568
|
+
// Only to a listening turn: it feeds that turn's watchdog, and the
|
|
9569
|
+
// TUI redraws its spinner the whole time it waits on a proxied call.
|
|
9570
|
+
() => {
|
|
9571
|
+
if (lineEmitter.listenerCount("line") > 0) {
|
|
9572
|
+
emit2(heartbeatFrame(session.sessionId, openCalls.newest()));
|
|
9573
|
+
}
|
|
9574
|
+
}
|
|
9575
|
+
);
|
|
9576
|
+
log.info("interactive turn ended", {
|
|
9577
|
+
end,
|
|
9578
|
+
stopReason,
|
|
9579
|
+
superseded: !current(),
|
|
9580
|
+
apiCalls: callCount,
|
|
9581
|
+
turnOutputTokens: usage?.output_tokens,
|
|
9582
|
+
turnInputTokens: usage?.input_tokens,
|
|
9583
|
+
turnCacheReadTokens: usage?.cache_read_input_tokens,
|
|
9584
|
+
lastCallInputTokens: lastCallUsage?.input_tokens,
|
|
9585
|
+
lastCallCacheReadTokens: lastCallUsage?.cache_read_input_tokens,
|
|
9586
|
+
lastCallCacheWriteTokens: lastCallUsage?.cache_creation_input_tokens,
|
|
9587
|
+
deniedPrompts: denied.length,
|
|
9588
|
+
cancelled: turnNumber <= cancelledThrough
|
|
9589
|
+
});
|
|
9590
|
+
emit2(
|
|
9591
|
+
interactiveResultFrame({
|
|
9592
|
+
sessionId: session.sessionId,
|
|
9593
|
+
end,
|
|
9594
|
+
stopReason,
|
|
9595
|
+
usage,
|
|
9596
|
+
denials
|
|
9597
|
+
})
|
|
9598
|
+
);
|
|
9599
|
+
} catch (err) {
|
|
9600
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
9601
|
+
log.error("interactive turn failed", { error: e.message, superseded: !current() });
|
|
9602
|
+
if (!current()) return;
|
|
9603
|
+
emit2(
|
|
9604
|
+
interactiveResultFrame({
|
|
9605
|
+
sessionId: existsSync4(session.jsonlPath) ? session.sessionId : void 0,
|
|
9606
|
+
end: "failed",
|
|
9607
|
+
stopReason: null,
|
|
9608
|
+
error: e.message
|
|
9609
|
+
})
|
|
9610
|
+
);
|
|
9611
|
+
if (proc.listenerCount("error") > 0) proc.emit("error", e);
|
|
9612
|
+
else lineEmitter.emit("close");
|
|
9613
|
+
}
|
|
8983
9614
|
};
|
|
8984
|
-
const
|
|
9615
|
+
const enqueueTurn = (userMsg) => {
|
|
9616
|
+
const turnNumber = ++writes;
|
|
9617
|
+
queued++;
|
|
9618
|
+
chain = chain.then(() => runTurn(userMsg, turnNumber)).finally(() => {
|
|
9619
|
+
queued--;
|
|
9620
|
+
});
|
|
9621
|
+
};
|
|
9622
|
+
Object.assign(proc, {
|
|
8985
9623
|
stdin: {
|
|
9624
|
+
writable: true,
|
|
8986
9625
|
write(chunk) {
|
|
8987
9626
|
const raw = typeof chunk === "string" && chunk.endsWith("\n") ? chunk.slice(0, -1) : chunk;
|
|
8988
|
-
|
|
9627
|
+
enqueueTurn(decodeUserEnvelope(raw));
|
|
8989
9628
|
return true;
|
|
8990
9629
|
},
|
|
8991
9630
|
end() {
|
|
@@ -8995,47 +9634,61 @@ function spawnInteractiveProcess(opts) {
|
|
|
8995
9634
|
stderr: null,
|
|
8996
9635
|
pid: -1,
|
|
8997
9636
|
killed: false,
|
|
8998
|
-
|
|
8999
|
-
|
|
9000
|
-
return proc;
|
|
9001
|
-
},
|
|
9002
|
-
once() {
|
|
9003
|
-
return proc;
|
|
9004
|
-
},
|
|
9005
|
-
off(event, fn) {
|
|
9006
|
-
if (event === "error") errorHandlers.delete(fn);
|
|
9007
|
-
return proc;
|
|
9008
|
-
},
|
|
9637
|
+
exitCode: null,
|
|
9638
|
+
signalCode: null,
|
|
9009
9639
|
kill() {
|
|
9640
|
+
if (!proc.killed) {
|
|
9641
|
+
proc.killed = true;
|
|
9642
|
+
if (opts.systemPromptFile) {
|
|
9643
|
+
void unlink2(opts.systemPromptFile).catch(() => {
|
|
9644
|
+
});
|
|
9645
|
+
}
|
|
9646
|
+
}
|
|
9647
|
+
closeProxyServer();
|
|
9648
|
+
const started = startPromise !== null;
|
|
9010
9649
|
try {
|
|
9011
9650
|
session.dispose();
|
|
9012
9651
|
} catch {
|
|
9013
9652
|
}
|
|
9014
|
-
if (
|
|
9015
|
-
|
|
9016
|
-
|
|
9653
|
+
if (!started && proc.exitCode === null && proc.signalCode === null) {
|
|
9654
|
+
proc.signalCode = "SIGTERM";
|
|
9655
|
+
proc.emit("exit", null, "SIGTERM");
|
|
9656
|
+
proc.emit("close", null, "SIGTERM");
|
|
9017
9657
|
}
|
|
9018
|
-
proc.killed = true;
|
|
9019
9658
|
return true;
|
|
9020
9659
|
}
|
|
9021
|
-
};
|
|
9022
|
-
|
|
9660
|
+
});
|
|
9661
|
+
ap = {
|
|
9023
9662
|
proc,
|
|
9024
9663
|
lineEmitter,
|
|
9025
|
-
proxyServer: null,
|
|
9664
|
+
proxyServer: opts.proxyServer ?? null,
|
|
9026
9665
|
mcpHash: void 0,
|
|
9027
|
-
|
|
9666
|
+
unattendedLines: [],
|
|
9667
|
+
unattendedDropped: 0,
|
|
9668
|
+
systemPromptFile: opts.systemPromptFile,
|
|
9669
|
+
cliPath: opts.cliPath,
|
|
9670
|
+
interactiveControl: {
|
|
9671
|
+
turnRunning: () => queued > 0,
|
|
9672
|
+
// Stops the running turn and every queued one: they were all written
|
|
9673
|
+
// by the turn being aborted, or by one before it.
|
|
9674
|
+
interrupt: (timeoutMs) => {
|
|
9675
|
+
cancelledThrough = writes;
|
|
9676
|
+
return session.interrupt(timeoutMs);
|
|
9677
|
+
},
|
|
9678
|
+
flushTranscript: () => session.flushTranscript()
|
|
9679
|
+
}
|
|
9028
9680
|
};
|
|
9681
|
+
return ap;
|
|
9029
9682
|
}
|
|
9030
9683
|
|
|
9031
9684
|
// src/prompts.ts
|
|
9032
|
-
import { readFileSync as
|
|
9033
|
-
import { homedir as
|
|
9685
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
9686
|
+
import { homedir as homedir8 } from "os";
|
|
9034
9687
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
9035
|
-
import { dirname as dirname6, join as
|
|
9688
|
+
import { dirname as dirname6, join as join10 } from "path";
|
|
9036
9689
|
function readPromptFileIfPresent(path11) {
|
|
9037
9690
|
try {
|
|
9038
|
-
const content =
|
|
9691
|
+
const content = readFileSync7(path11, "utf8").trim();
|
|
9039
9692
|
return content || void 0;
|
|
9040
9693
|
} catch {
|
|
9041
9694
|
return void 0;
|
|
@@ -9044,7 +9697,7 @@ function readPromptFileIfPresent(path11) {
|
|
|
9044
9697
|
function nearestWorkspaceAgentsPrompt(cwd) {
|
|
9045
9698
|
let dir = cwd;
|
|
9046
9699
|
while (true) {
|
|
9047
|
-
const content = readPromptFileIfPresent(
|
|
9700
|
+
const content = readPromptFileIfPresent(join10(dir, "AGENTS.md"));
|
|
9048
9701
|
if (content) return content;
|
|
9049
9702
|
const parent = dirname6(dir);
|
|
9050
9703
|
if (parent === dir) return void 0;
|
|
@@ -9153,8 +9806,8 @@ ${options.compressionSummary.trim()}`
|
|
|
9153
9806
|
for (const s of extraSystemContent) {
|
|
9154
9807
|
if (s.trim()) parts.push(s.trim());
|
|
9155
9808
|
}
|
|
9156
|
-
const configRoot = process.env.XDG_CONFIG_HOME ??
|
|
9157
|
-
const globalAgents = readPromptFileIfPresent(
|
|
9809
|
+
const configRoot = process.env.XDG_CONFIG_HOME ?? join10(homedir8(), ".config");
|
|
9810
|
+
const globalAgents = readPromptFileIfPresent(join10(configRoot, "opencode", "AGENTS.md"));
|
|
9158
9811
|
const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
|
|
9159
9812
|
const forwarded = extraSystemContent.join("\n\n");
|
|
9160
9813
|
const pushGlobal = !!globalAgents && !forwarded.includes(globalAgents);
|
|
@@ -9165,9 +9818,9 @@ ${options.compressionSummary.trim()}`
|
|
|
9165
9818
|
if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
|
|
9166
9819
|
const content = parts.join("\n\n");
|
|
9167
9820
|
if (!content) return void 0;
|
|
9168
|
-
const path11 =
|
|
9821
|
+
const path11 = join10(pluginTmpDir(), `opencode-cc-sys-${randomUUID5()}.md`);
|
|
9169
9822
|
try {
|
|
9170
|
-
|
|
9823
|
+
writeFileSync5(path11, content, { encoding: "utf8", mode: 384 });
|
|
9171
9824
|
return path11;
|
|
9172
9825
|
} catch (err) {
|
|
9173
9826
|
log.warn("failed to write system prompt file", { error: String(err) });
|
|
@@ -9885,8 +10538,8 @@ function synthesizeTitle(prompt) {
|
|
|
9885
10538
|
"summary",
|
|
9886
10539
|
"project"
|
|
9887
10540
|
]);
|
|
9888
|
-
const
|
|
9889
|
-
const picked = (
|
|
10541
|
+
const words2 = source.split(" ").map((word) => word.trim()).filter(Boolean).filter((word) => !stop.has(word.toLowerCase()));
|
|
10542
|
+
const picked = (words2.length > 0 ? words2 : source.split(" ").filter(Boolean)).slice(0, 6).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
9890
10543
|
return picked || "New Session";
|
|
9891
10544
|
}
|
|
9892
10545
|
|
|
@@ -10350,6 +11003,11 @@ function clearStartWatchdog(state) {
|
|
|
10350
11003
|
function onStartWatchdogFire(state) {
|
|
10351
11004
|
state.startWatchdog = null;
|
|
10352
11005
|
if (state.controllerClosed || state.hasReceivedContent || state.hasReceivedProgress) return;
|
|
11006
|
+
const interactive = state.activeProcess?.interactiveControl;
|
|
11007
|
+
if (interactive) {
|
|
11008
|
+
if (interactive.turnRunning()) armStartWatchdog(state);
|
|
11009
|
+
return;
|
|
11010
|
+
}
|
|
10353
11011
|
if (state.respawnAttempted) {
|
|
10354
11012
|
log.error(
|
|
10355
11013
|
"claude process still silent after respawn; ending turn",
|
|
@@ -10583,6 +11241,8 @@ function drainNow(state) {
|
|
|
10583
11241
|
}
|
|
10584
11242
|
if (state.drainBuffer.length === 0) return;
|
|
10585
11243
|
if (state.controllerClosed) return;
|
|
11244
|
+
state.activeProcess?.interactiveControl?.flushTranscript();
|
|
11245
|
+
if (state.controllerClosed) return;
|
|
10586
11246
|
const batch = state.drainBuffer.splice(0, state.drainBuffer.length);
|
|
10587
11247
|
log.info("draining pending proxy calls into stream finish", {
|
|
10588
11248
|
sessionKey: state.sessionKey,
|
|
@@ -12170,6 +12830,23 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12170
12830
|
}
|
|
12171
12831
|
}
|
|
12172
12832
|
}
|
|
12833
|
+
if (this.config.resumeAfterRestart !== false && includeHistoryContext && !compactionMode && !forkFromClaudeSessionId && failoverAnswer?.kind !== "switch") {
|
|
12834
|
+
const configDir = resolveConfigDir(this.config.configDir);
|
|
12835
|
+
const resumePoint = findResumePoint({
|
|
12836
|
+
sessionKey: sk,
|
|
12837
|
+
prompt: options.prompt,
|
|
12838
|
+
cliPath,
|
|
12839
|
+
transcriptPath: (id) => join11(configDir, "projects", encodeCwd(cwd), `${id}.jsonl`)
|
|
12840
|
+
});
|
|
12841
|
+
if (resumePoint) {
|
|
12842
|
+
setClaudeSessionId(sk, resumePoint.claudeSessionId);
|
|
12843
|
+
includeHistoryContext = false;
|
|
12844
|
+
log.notice("resuming the claude session from before the restart instead of replaying it", {
|
|
12845
|
+
sessionKey: sk,
|
|
12846
|
+
matchedMessages: resumePoint.matched
|
|
12847
|
+
});
|
|
12848
|
+
}
|
|
12849
|
+
}
|
|
12173
12850
|
if (this.config.forkSessions === true && !compactionMode && !useInteractive) {
|
|
12174
12851
|
recordForkFingerprint(sk, options.prompt, cliPath);
|
|
12175
12852
|
}
|
|
@@ -12419,13 +13096,145 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12419
13096
|
state.proxyServer = null;
|
|
12420
13097
|
}
|
|
12421
13098
|
}
|
|
13099
|
+
const resolveProxyWiring = async () => {
|
|
13100
|
+
const discovery = self.effectiveMcpConfig(
|
|
13101
|
+
cwd,
|
|
13102
|
+
void 0,
|
|
13103
|
+
runtimeStatus
|
|
13104
|
+
);
|
|
13105
|
+
const mcpResolution = self.resolvedProxyMcpTools(
|
|
13106
|
+
discovery.allEnabledServerNames,
|
|
13107
|
+
options.tools,
|
|
13108
|
+
new Set((resolvedProxy ?? []).map((def) => def.name))
|
|
13109
|
+
);
|
|
13110
|
+
const proxyMcpTools = mcpResolution?.defs ?? null;
|
|
13111
|
+
const excludeServers = mcpResolution ? mcpResolution.coveredServers : void 0;
|
|
13112
|
+
const taskProxyEnabled = resolvedProxy?.some((t) => t.name === "task") ?? false;
|
|
13113
|
+
const questionProxyEnabled = resolvedProxy?.some((t) => t.name === "question") ?? false;
|
|
13114
|
+
const opencodeToolsRequested = (self.config.proxyOpencodeTools?.length ?? 0) > 0;
|
|
13115
|
+
log.debug("opencode tool forwarding gate", {
|
|
13116
|
+
requested: self.config.proxyOpencodeTools ?? null,
|
|
13117
|
+
opencodeToolsRequested
|
|
13118
|
+
});
|
|
13119
|
+
const liveToolInfo = taskProxyEnabled || questionProxyEnabled || opencodeToolsRequested ? await loadLiveToolInfo() : {
|
|
13120
|
+
resolved: false,
|
|
13121
|
+
taskDescription: void 0,
|
|
13122
|
+
taskParameters: void 0,
|
|
13123
|
+
questionDescription: void 0,
|
|
13124
|
+
hasQuestion: false
|
|
13125
|
+
};
|
|
13126
|
+
let enrichedProxy = resolvedProxy;
|
|
13127
|
+
let backgroundSubagentsSupported = false;
|
|
13128
|
+
if (enrichedProxy && taskProxyEnabled) {
|
|
13129
|
+
enrichedProxy = overlayTaskProxyDescription(
|
|
13130
|
+
enrichedProxy,
|
|
13131
|
+
liveToolInfo.taskDescription
|
|
13132
|
+
);
|
|
13133
|
+
log.info("task proxy description overlay", {
|
|
13134
|
+
applied: Boolean(liveToolInfo.taskDescription),
|
|
13135
|
+
liveDescriptionLength: liveToolInfo.taskDescription?.length ?? 0,
|
|
13136
|
+
listsAgentTypes: Boolean(
|
|
13137
|
+
liveToolInfo.taskDescription?.includes(
|
|
13138
|
+
"Available agent types"
|
|
13139
|
+
)
|
|
13140
|
+
)
|
|
13141
|
+
});
|
|
13142
|
+
const hostDialect = self.config.hostApi === "v2" ? "v2" : "v1";
|
|
13143
|
+
backgroundSubagentsSupported = liveTaskSupportsBackground(
|
|
13144
|
+
liveToolInfo.taskParameters,
|
|
13145
|
+
hostDialect
|
|
13146
|
+
);
|
|
13147
|
+
enrichedProxy = applyBackgroundSubagentSupport(
|
|
13148
|
+
enrichedProxy,
|
|
13149
|
+
backgroundSubagentsSupported,
|
|
13150
|
+
hostDialect
|
|
13151
|
+
);
|
|
13152
|
+
recordBackgroundSubagentGate({
|
|
13153
|
+
supported: backgroundSubagentsSupported,
|
|
13154
|
+
hostApi: hostDialect,
|
|
13155
|
+
registryResolved: liveToolInfo.resolved
|
|
13156
|
+
});
|
|
13157
|
+
log.info("background subagent gate", {
|
|
13158
|
+
supported: backgroundSubagentsSupported,
|
|
13159
|
+
registryResolved: liveToolInfo.resolved,
|
|
13160
|
+
hostApi: hostDialect,
|
|
13161
|
+
note: backgroundSubagentsSupported ? "task accepts `background`; task_status and task_cancel are registered" : "`background` stripped from the task schema; set OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true in opencode's environment to enable it"
|
|
13162
|
+
});
|
|
13163
|
+
}
|
|
13164
|
+
if (enrichedProxy && questionProxyEnabled) {
|
|
13165
|
+
enrichedProxy = overlayQuestionProxyDescription(
|
|
13166
|
+
enrichedProxy,
|
|
13167
|
+
liveToolInfo.hasQuestion ? liveToolInfo.questionDescription : void 0
|
|
13168
|
+
);
|
|
13169
|
+
enrichedProxy = filterQuestionProxyByOpencodeSupport(
|
|
13170
|
+
enrichedProxy,
|
|
13171
|
+
liveToolInfo.hasQuestion
|
|
13172
|
+
);
|
|
13173
|
+
log.info("question proxy version gate", {
|
|
13174
|
+
opencodeHasQuestion: liveToolInfo.hasQuestion,
|
|
13175
|
+
kept: liveToolInfo.hasQuestion
|
|
13176
|
+
});
|
|
13177
|
+
}
|
|
13178
|
+
const opencodeToolDefs = resolveProxyOpencodeToolDefs({
|
|
13179
|
+
requested: self.config.proxyOpencodeTools,
|
|
13180
|
+
items: liveToolInfo.items,
|
|
13181
|
+
modelTools: self.config.hostApi === "v2" ? options.tools ?? [] : void 0,
|
|
13182
|
+
allowCodeExecution: self.config.permissionPreset !== "read-only",
|
|
13183
|
+
taken: new Set(
|
|
13184
|
+
[...enrichedProxy ?? [], ...proxyMcpTools ?? []].map(
|
|
13185
|
+
(t) => t.name
|
|
13186
|
+
)
|
|
13187
|
+
)
|
|
13188
|
+
});
|
|
13189
|
+
if (opencodeToolDefs.length > 0) {
|
|
13190
|
+
log.info("forwarding opencode tools through the proxy", {
|
|
13191
|
+
tools: opencodeToolDefs.map((t) => t.name)
|
|
13192
|
+
});
|
|
13193
|
+
}
|
|
13194
|
+
const combinedList = [
|
|
13195
|
+
...enrichedProxy ?? [],
|
|
13196
|
+
...proxyMcpTools ?? [],
|
|
13197
|
+
...opencodeToolDefs
|
|
13198
|
+
];
|
|
13199
|
+
const combinedProxyTools = combinedList.length > 0 ? combinedList : null;
|
|
13200
|
+
const pluginCompressEnabled = enrichedProxy?.some((t) => t.name === "compress") ?? false;
|
|
13201
|
+
if (!state.proxyServer && combinedProxyTools) {
|
|
13202
|
+
state.proxyServer = await self.ensureProxyServer(
|
|
13203
|
+
combinedProxyTools,
|
|
13204
|
+
sk,
|
|
13205
|
+
pluginCompressEnabled,
|
|
13206
|
+
affinity
|
|
13207
|
+
);
|
|
13208
|
+
}
|
|
13209
|
+
const questionProxyActive = enrichedProxy?.some((t) => t.name === "question") ?? false;
|
|
13210
|
+
const allDisallowed = resolveDisallowedTools({
|
|
13211
|
+
proxyTools: enrichedProxy,
|
|
13212
|
+
extraDisallowedTools: self.config.extraDisallowedTools,
|
|
13213
|
+
disableWebSearch: self.config.webSearch === "disabled"
|
|
13214
|
+
});
|
|
13215
|
+
return {
|
|
13216
|
+
excludeServers,
|
|
13217
|
+
taskProxyEnabled,
|
|
13218
|
+
backgroundSubagentsSupported,
|
|
13219
|
+
questionProxyActive,
|
|
13220
|
+
opencodeToolDefs,
|
|
13221
|
+
pluginCompressEnabled,
|
|
13222
|
+
allDisallowed
|
|
13223
|
+
};
|
|
13224
|
+
};
|
|
12422
13225
|
if (useInteractive && !compactionMode) {
|
|
12423
|
-
const mcp = self.effectiveMcpConfig(cwd, void 0, runtimeStatus);
|
|
12424
13226
|
if (state.activeProcess) {
|
|
12425
13227
|
state.proc = state.activeProcess.proc;
|
|
12426
13228
|
state.lineEmitter = state.activeProcess.lineEmitter;
|
|
12427
13229
|
log.debug("reusing active interactive session", { sk });
|
|
12428
13230
|
} else {
|
|
13231
|
+
const wiring = await resolveProxyWiring();
|
|
13232
|
+
const mcp = self.effectiveMcpConfig(
|
|
13233
|
+
cwd,
|
|
13234
|
+
state.proxyServer?.configPath(),
|
|
13235
|
+
runtimeStatus,
|
|
13236
|
+
wiring.excludeServers
|
|
13237
|
+
);
|
|
12429
13238
|
const allow = [
|
|
12430
13239
|
...mcp.allEnabledServerNames.map((n) => `mcp__${n}__*`),
|
|
12431
13240
|
"mcp__opencode_proxy__*",
|
|
@@ -12439,12 +13248,27 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12439
13248
|
];
|
|
12440
13249
|
const systemPromptFile = self.config.interactiveSystemPrompt === false ? void 0 : buildAppendedSystemPrompt(
|
|
12441
13250
|
cwd,
|
|
12442
|
-
self.config.multiStepContinuation !== false
|
|
13251
|
+
self.config.multiStepContinuation !== false,
|
|
12443
13252
|
// Do not forward opencode's own system prompt into the
|
|
12444
13253
|
// interactive TUI. Live subscription-account testing
|
|
12445
13254
|
// showed that large forwarded payload can trigger Claude
|
|
12446
13255
|
// Code's third-party-app usage gate, while our static
|
|
12447
|
-
// CLI/AGENTS/continuation prompt remains safe.
|
|
13256
|
+
// CLI/AGENTS/continuation prompt remains safe. The
|
|
13257
|
+
// plugin's own short proxy hints are part of that static
|
|
13258
|
+
// prompt: without them the model narrates a subagent
|
|
13259
|
+
// dispatch instead of calling the proxy (h #g74).
|
|
13260
|
+
[
|
|
13261
|
+
...wiring.taskProxyEnabled ? [SUBAGENT_DISPATCH_HINT] : [],
|
|
13262
|
+
...wiring.backgroundSubagentsSupported ? [BACKGROUND_SUBAGENT_HINT] : [],
|
|
13263
|
+
...wiring.questionProxyActive ? [QUESTION_PROXY_HINT] : []
|
|
13264
|
+
],
|
|
13265
|
+
{
|
|
13266
|
+
compressEnabled: wiring.pluginCompressEnabled,
|
|
13267
|
+
opencodeCompressEnabled: wiring.opencodeToolDefs.some(
|
|
13268
|
+
(t) => t.name === "compress"
|
|
13269
|
+
),
|
|
13270
|
+
compressionSummary: getCompressionSummary(sk)
|
|
13271
|
+
}
|
|
12448
13272
|
);
|
|
12449
13273
|
if (self.config.interactiveSystemPrompt === false) {
|
|
12450
13274
|
log.warn(
|
|
@@ -12463,6 +13287,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12463
13287
|
...self.skillBridgeSpawn(failover)
|
|
12464
13288
|
});
|
|
12465
13289
|
if (stoppedBeforeWork()) return;
|
|
13290
|
+
const resumeSessionId = getClaudeSessionId(sk);
|
|
12466
13291
|
const ap = spawnInteractiveProcess({
|
|
12467
13292
|
cwd,
|
|
12468
13293
|
cliPath,
|
|
@@ -12472,10 +13297,24 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12472
13297
|
mcpConfigPaths: mcp.paths,
|
|
12473
13298
|
pluginDirs: skillPluginDirs,
|
|
12474
13299
|
permissionsAllow: allow,
|
|
13300
|
+
disallowedTools: wiring.allDisallowed,
|
|
13301
|
+
proxyServer: state.proxyServer,
|
|
12475
13302
|
systemPromptFile,
|
|
12476
13303
|
ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey,
|
|
12477
|
-
effort: reasoningEffort
|
|
13304
|
+
effort: reasoningEffort,
|
|
13305
|
+
resumeSessionId,
|
|
13306
|
+
// The headless spawn's env, so hygiene, effort and the
|
|
13307
|
+
// agent's prompt cache TTL reach both transports alike.
|
|
13308
|
+
env: claudeSpawnEnv({
|
|
13309
|
+
ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey,
|
|
13310
|
+
effort: reasoningEffort,
|
|
13311
|
+
promptCacheTtl
|
|
13312
|
+
})
|
|
12478
13313
|
});
|
|
13314
|
+
ap.proc.once(
|
|
13315
|
+
"exit",
|
|
13316
|
+
(code) => noteInteractiveProcessExit(sk, ap, code)
|
|
13317
|
+
);
|
|
12479
13318
|
ap.mcpHash = mcp.bridgedHash;
|
|
12480
13319
|
ap.mcpServers = mcp.allEnabledServerNames;
|
|
12481
13320
|
setActiveProcess(sk, ap);
|
|
@@ -12484,6 +13323,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12484
13323
|
state.activeProcess = ap;
|
|
12485
13324
|
log.info("spawned interactive claude session", {
|
|
12486
13325
|
sk,
|
|
13326
|
+
resumed: !!resumeSessionId,
|
|
12487
13327
|
cliPath,
|
|
12488
13328
|
configDir: self.config.configDir,
|
|
12489
13329
|
model: effectiveModelId
|
|
@@ -12505,121 +13345,15 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12505
13345
|
cliVersion
|
|
12506
13346
|
});
|
|
12507
13347
|
} else {
|
|
12508
|
-
const
|
|
12509
|
-
|
|
12510
|
-
|
|
12511
|
-
|
|
12512
|
-
|
|
12513
|
-
|
|
12514
|
-
|
|
12515
|
-
|
|
12516
|
-
|
|
12517
|
-
);
|
|
12518
|
-
const proxyMcpTools = mcpResolution?.defs ?? null;
|
|
12519
|
-
const excludeServers = mcpResolution ? mcpResolution.coveredServers : void 0;
|
|
12520
|
-
const taskProxyEnabled = resolvedProxy?.some((t) => t.name === "task") ?? false;
|
|
12521
|
-
const questionProxyEnabled = resolvedProxy?.some((t) => t.name === "question") ?? false;
|
|
12522
|
-
const opencodeToolsRequested = (self.config.proxyOpencodeTools?.length ?? 0) > 0;
|
|
12523
|
-
log.debug("opencode tool forwarding gate", {
|
|
12524
|
-
requested: self.config.proxyOpencodeTools ?? null,
|
|
12525
|
-
opencodeToolsRequested
|
|
12526
|
-
});
|
|
12527
|
-
const liveToolInfo = taskProxyEnabled || questionProxyEnabled || opencodeToolsRequested ? await loadLiveToolInfo() : {
|
|
12528
|
-
resolved: false,
|
|
12529
|
-
taskDescription: void 0,
|
|
12530
|
-
taskParameters: void 0,
|
|
12531
|
-
questionDescription: void 0,
|
|
12532
|
-
hasQuestion: false
|
|
12533
|
-
};
|
|
12534
|
-
let enrichedProxy = resolvedProxy;
|
|
12535
|
-
let backgroundSubagentsSupported = false;
|
|
12536
|
-
if (enrichedProxy && taskProxyEnabled) {
|
|
12537
|
-
enrichedProxy = overlayTaskProxyDescription(
|
|
12538
|
-
enrichedProxy,
|
|
12539
|
-
liveToolInfo.taskDescription
|
|
12540
|
-
);
|
|
12541
|
-
log.info("task proxy description overlay", {
|
|
12542
|
-
applied: Boolean(liveToolInfo.taskDescription),
|
|
12543
|
-
liveDescriptionLength: liveToolInfo.taskDescription?.length ?? 0,
|
|
12544
|
-
listsAgentTypes: Boolean(
|
|
12545
|
-
liveToolInfo.taskDescription?.includes(
|
|
12546
|
-
"Available agent types"
|
|
12547
|
-
)
|
|
12548
|
-
)
|
|
12549
|
-
});
|
|
12550
|
-
const hostDialect = self.config.hostApi === "v2" ? "v2" : "v1";
|
|
12551
|
-
backgroundSubagentsSupported = liveTaskSupportsBackground(
|
|
12552
|
-
liveToolInfo.taskParameters,
|
|
12553
|
-
hostDialect
|
|
12554
|
-
);
|
|
12555
|
-
enrichedProxy = applyBackgroundSubagentSupport(
|
|
12556
|
-
enrichedProxy,
|
|
12557
|
-
backgroundSubagentsSupported,
|
|
12558
|
-
hostDialect
|
|
12559
|
-
);
|
|
12560
|
-
recordBackgroundSubagentGate({
|
|
12561
|
-
supported: backgroundSubagentsSupported,
|
|
12562
|
-
hostApi: hostDialect,
|
|
12563
|
-
registryResolved: liveToolInfo.resolved
|
|
12564
|
-
});
|
|
12565
|
-
log.info("background subagent gate", {
|
|
12566
|
-
supported: backgroundSubagentsSupported,
|
|
12567
|
-
registryResolved: liveToolInfo.resolved,
|
|
12568
|
-
hostApi: hostDialect,
|
|
12569
|
-
note: backgroundSubagentsSupported ? "task accepts `background`; task_status and task_cancel are registered" : "`background` stripped from the task schema; set OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true in opencode's environment to enable it"
|
|
12570
|
-
});
|
|
12571
|
-
}
|
|
12572
|
-
if (enrichedProxy && questionProxyEnabled) {
|
|
12573
|
-
enrichedProxy = overlayQuestionProxyDescription(
|
|
12574
|
-
enrichedProxy,
|
|
12575
|
-
liveToolInfo.hasQuestion ? liveToolInfo.questionDescription : void 0
|
|
12576
|
-
);
|
|
12577
|
-
enrichedProxy = filterQuestionProxyByOpencodeSupport(
|
|
12578
|
-
enrichedProxy,
|
|
12579
|
-
liveToolInfo.hasQuestion
|
|
12580
|
-
);
|
|
12581
|
-
log.info("question proxy version gate", {
|
|
12582
|
-
opencodeHasQuestion: liveToolInfo.hasQuestion,
|
|
12583
|
-
kept: liveToolInfo.hasQuestion
|
|
12584
|
-
});
|
|
12585
|
-
}
|
|
12586
|
-
const opencodeToolDefs = resolveProxyOpencodeToolDefs({
|
|
12587
|
-
requested: self.config.proxyOpencodeTools,
|
|
12588
|
-
items: liveToolInfo.items,
|
|
12589
|
-
modelTools: self.config.hostApi === "v2" ? options.tools ?? [] : void 0,
|
|
12590
|
-
allowCodeExecution: self.config.permissionPreset !== "read-only",
|
|
12591
|
-
taken: new Set(
|
|
12592
|
-
[...enrichedProxy ?? [], ...proxyMcpTools ?? []].map(
|
|
12593
|
-
(t) => t.name
|
|
12594
|
-
)
|
|
12595
|
-
)
|
|
12596
|
-
});
|
|
12597
|
-
if (opencodeToolDefs.length > 0) {
|
|
12598
|
-
log.info("forwarding opencode tools through the proxy", {
|
|
12599
|
-
tools: opencodeToolDefs.map((t) => t.name)
|
|
12600
|
-
});
|
|
12601
|
-
}
|
|
12602
|
-
const combinedList = [
|
|
12603
|
-
...enrichedProxy ?? [],
|
|
12604
|
-
...proxyMcpTools ?? [],
|
|
12605
|
-
...opencodeToolDefs
|
|
12606
|
-
];
|
|
12607
|
-
const combinedProxyTools = combinedList.length > 0 ? combinedList : null;
|
|
12608
|
-
const pluginCompressEnabled = enrichedProxy?.some((t) => t.name === "compress") ?? false;
|
|
12609
|
-
if (!state.proxyServer && combinedProxyTools) {
|
|
12610
|
-
state.proxyServer = await self.ensureProxyServer(
|
|
12611
|
-
combinedProxyTools,
|
|
12612
|
-
sk,
|
|
12613
|
-
pluginCompressEnabled,
|
|
12614
|
-
affinity
|
|
12615
|
-
);
|
|
12616
|
-
}
|
|
12617
|
-
const questionProxyActive = enrichedProxy?.some((t) => t.name === "question") ?? false;
|
|
12618
|
-
const allDisallowed = resolveDisallowedTools({
|
|
12619
|
-
proxyTools: enrichedProxy,
|
|
12620
|
-
extraDisallowedTools: self.config.extraDisallowedTools,
|
|
12621
|
-
disableWebSearch: self.config.webSearch === "disabled"
|
|
12622
|
-
});
|
|
13348
|
+
const {
|
|
13349
|
+
excludeServers,
|
|
13350
|
+
taskProxyEnabled,
|
|
13351
|
+
backgroundSubagentsSupported,
|
|
13352
|
+
questionProxyActive,
|
|
13353
|
+
opencodeToolDefs,
|
|
13354
|
+
pluginCompressEnabled,
|
|
13355
|
+
allDisallowed
|
|
13356
|
+
} = await resolveProxyWiring();
|
|
12623
13357
|
const mcp = self.effectiveMcpConfig(
|
|
12624
13358
|
cwd,
|
|
12625
13359
|
state.proxyServer?.configPath(),
|
|
@@ -12723,6 +13457,9 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12723
13457
|
state.autoContinueState.startedAt = Date.now();
|
|
12724
13458
|
const completeResult = (msg) => {
|
|
12725
13459
|
if (state.controllerClosed) return;
|
|
13460
|
+
if (self.config.resumeAfterRestart !== false && !compactionMode && msg.is_error !== true && typeof msg.session_id === "string") {
|
|
13461
|
+
recordResumePoint(sk, msg.session_id, options.prompt, cliPath);
|
|
13462
|
+
}
|
|
12726
13463
|
if (deliverPendingCompletions(state)) {
|
|
12727
13464
|
if (state.drainBuffer.length > 0) drainNow(state);
|
|
12728
13465
|
return;
|
|
@@ -13179,15 +13916,15 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13179
13916
|
|
|
13180
13917
|
// src/cleanup-stale.ts
|
|
13181
13918
|
import {
|
|
13182
|
-
existsSync as
|
|
13183
|
-
mkdirSync as
|
|
13184
|
-
readFileSync as
|
|
13919
|
+
existsSync as existsSync5,
|
|
13920
|
+
mkdirSync as mkdirSync5,
|
|
13921
|
+
readFileSync as readFileSync8,
|
|
13185
13922
|
realpathSync as realpathSync4,
|
|
13186
13923
|
rmSync as rmSync3,
|
|
13187
|
-
writeFileSync as
|
|
13924
|
+
writeFileSync as writeFileSync6
|
|
13188
13925
|
} from "fs";
|
|
13189
|
-
import { homedir as
|
|
13190
|
-
import { dirname as dirname7, join as
|
|
13926
|
+
import { homedir as homedir9 } from "os";
|
|
13927
|
+
import { dirname as dirname7, join as join12, resolve as resolve5 } from "path";
|
|
13191
13928
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
13192
13929
|
var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
|
|
13193
13930
|
var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
|
|
@@ -13195,20 +13932,20 @@ var alreadyRan = false;
|
|
|
13195
13932
|
function candidateCacheRoots() {
|
|
13196
13933
|
const xdg = process.env.XDG_CACHE_HOME;
|
|
13197
13934
|
return [
|
|
13198
|
-
xdg ?
|
|
13199
|
-
|
|
13200
|
-
|
|
13935
|
+
xdg ? join12(xdg, "opencode") : null,
|
|
13936
|
+
join12(homedir9(), ".cache", "opencode"),
|
|
13937
|
+
join12(homedir9(), "Library", "Caches", "opencode")
|
|
13201
13938
|
].filter((p) => Boolean(p));
|
|
13202
13939
|
}
|
|
13203
13940
|
function userOpencodeJsonPath() {
|
|
13204
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME ??
|
|
13205
|
-
return
|
|
13941
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME ?? join12(homedir9(), ".config");
|
|
13942
|
+
return join12(xdgConfig, "opencode", "opencode.json");
|
|
13206
13943
|
}
|
|
13207
13944
|
function userIntendsToUseUnscoped() {
|
|
13208
13945
|
const cfg = userOpencodeJsonPath();
|
|
13209
|
-
if (!
|
|
13946
|
+
if (!existsSync5(cfg)) return false;
|
|
13210
13947
|
try {
|
|
13211
|
-
const json = JSON.parse(
|
|
13948
|
+
const json = JSON.parse(readFileSync8(cfg, "utf8"));
|
|
13212
13949
|
const plugins = json.plugin;
|
|
13213
13950
|
if (!Array.isArray(plugins)) return false;
|
|
13214
13951
|
return plugins.some(
|
|
@@ -13229,19 +13966,19 @@ function ourLoadedDir() {
|
|
|
13229
13966
|
function ourVersion(dir) {
|
|
13230
13967
|
if (!dir) return null;
|
|
13231
13968
|
try {
|
|
13232
|
-
const pkg = JSON.parse(
|
|
13969
|
+
const pkg = JSON.parse(readFileSync8(join12(dir, "package.json"), "utf8"));
|
|
13233
13970
|
return typeof pkg.version === "string" && pkg.version ? pkg.version : null;
|
|
13234
13971
|
} catch {
|
|
13235
13972
|
return null;
|
|
13236
13973
|
}
|
|
13237
13974
|
}
|
|
13238
13975
|
function markerPath() {
|
|
13239
|
-
const stateRoot = process.env.XDG_STATE_HOME ??
|
|
13240
|
-
return
|
|
13976
|
+
const stateRoot = process.env.XDG_STATE_HOME ?? join12(homedir9(), ".local", "state");
|
|
13977
|
+
return join12(stateRoot, "opencode-claude-code-plugin", "cleanup-stale.json");
|
|
13241
13978
|
}
|
|
13242
13979
|
function markerVersion() {
|
|
13243
13980
|
try {
|
|
13244
|
-
const marker = JSON.parse(
|
|
13981
|
+
const marker = JSON.parse(readFileSync8(markerPath(), "utf8"));
|
|
13245
13982
|
return typeof marker.version === "string" ? marker.version : null;
|
|
13246
13983
|
} catch {
|
|
13247
13984
|
return null;
|
|
@@ -13250,8 +13987,8 @@ function markerVersion() {
|
|
|
13250
13987
|
function writeMarker(version) {
|
|
13251
13988
|
const file = markerPath();
|
|
13252
13989
|
try {
|
|
13253
|
-
|
|
13254
|
-
|
|
13990
|
+
mkdirSync5(dirname7(file), { recursive: true });
|
|
13991
|
+
writeFileSync6(
|
|
13255
13992
|
file,
|
|
13256
13993
|
JSON.stringify({ version, at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n",
|
|
13257
13994
|
"utf8"
|
|
@@ -13285,20 +14022,20 @@ function cleanupStaleUnscopedInstall() {
|
|
|
13285
14022
|
if (version) writeMarker(version);
|
|
13286
14023
|
}
|
|
13287
14024
|
function cleanupOne(cacheRoot, ourDir) {
|
|
13288
|
-
if (!
|
|
13289
|
-
const stalePath =
|
|
13290
|
-
if (!
|
|
14025
|
+
if (!existsSync5(cacheRoot)) return;
|
|
14026
|
+
const stalePath = join12(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
|
|
14027
|
+
if (!existsSync5(stalePath)) return;
|
|
13291
14028
|
let realStalePath = stalePath;
|
|
13292
14029
|
try {
|
|
13293
14030
|
realStalePath = realpathSync4(stalePath);
|
|
13294
14031
|
} catch {
|
|
13295
14032
|
}
|
|
13296
14033
|
if (ourDir && realStalePath === ourDir) return;
|
|
13297
|
-
const pkgJsonPath =
|
|
13298
|
-
if (!
|
|
14034
|
+
const pkgJsonPath = join12(stalePath, "package.json");
|
|
14035
|
+
if (!existsSync5(pkgJsonPath)) return;
|
|
13299
14036
|
let pkg = {};
|
|
13300
14037
|
try {
|
|
13301
|
-
pkg = JSON.parse(
|
|
14038
|
+
pkg = JSON.parse(readFileSync8(pkgJsonPath, "utf8"));
|
|
13302
14039
|
} catch {
|
|
13303
14040
|
return;
|
|
13304
14041
|
}
|
|
@@ -13314,13 +14051,13 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
13314
14051
|
});
|
|
13315
14052
|
return;
|
|
13316
14053
|
}
|
|
13317
|
-
const cachePkgJson =
|
|
13318
|
-
if (!
|
|
14054
|
+
const cachePkgJson = join12(cacheRoot, "package.json");
|
|
14055
|
+
if (!existsSync5(cachePkgJson)) return;
|
|
13319
14056
|
try {
|
|
13320
|
-
const cfg = JSON.parse(
|
|
14057
|
+
const cfg = JSON.parse(readFileSync8(cachePkgJson, "utf8"));
|
|
13321
14058
|
if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
|
|
13322
14059
|
delete cfg.dependencies[STALE_PACKAGE_NAME];
|
|
13323
|
-
|
|
14060
|
+
writeFileSync6(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
|
|
13324
14061
|
log.info("cleanup-stale: pruned dep from cache package.json");
|
|
13325
14062
|
}
|
|
13326
14063
|
} catch (err) {
|
|
@@ -13981,6 +14718,7 @@ function createClaudeCode(settings = {}) {
|
|
|
13981
14718
|
bridgeSkipNativeSkills: settings.bridgeSkipNativeSkills !== false,
|
|
13982
14719
|
turnStats: settings.turnStats === true,
|
|
13983
14720
|
forkSessions: settings.forkSessions === true,
|
|
14721
|
+
resumeAfterRestart: settings.resumeAfterRestart !== false,
|
|
13984
14722
|
interactive: settings.interactive,
|
|
13985
14723
|
interactiveBypass: settings.interactiveBypass,
|
|
13986
14724
|
interactiveAllowTools: settings.interactiveAllowTools,
|
|
@@ -14066,12 +14804,12 @@ function configModelsForProvider(providerModels, providerID, modelSuffix) {
|
|
|
14066
14804
|
}
|
|
14067
14805
|
return models;
|
|
14068
14806
|
}
|
|
14069
|
-
function
|
|
14807
|
+
function isRecord5(value) {
|
|
14070
14808
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14071
14809
|
}
|
|
14072
14810
|
function completeUserModel(id, entry, providerID) {
|
|
14073
|
-
const record =
|
|
14074
|
-
const runtimeShaped =
|
|
14811
|
+
const record = isRecord5(entry) ? entry : {};
|
|
14812
|
+
const runtimeShaped = isRecord5(record.capabilities) && isRecord5(record.capabilities.input) && isRecord5(record.capabilities.output) && isRecord5(record.cost) && isRecord5(record.cost.cache) && isRecord5(record.api);
|
|
14075
14813
|
if (runtimeShaped) {
|
|
14076
14814
|
return toConfigModel({ ...record, providerID });
|
|
14077
14815
|
}
|
|
@@ -14084,8 +14822,8 @@ function completeUserModel(id, entry, providerID) {
|
|
|
14084
14822
|
const merged = { ...base, ...record, id };
|
|
14085
14823
|
for (const key of ["cost", "limit", "variants"]) {
|
|
14086
14824
|
merged[key] = {
|
|
14087
|
-
...
|
|
14088
|
-
...
|
|
14825
|
+
...isRecord5(base[key]) ? base[key] : {},
|
|
14826
|
+
...isRecord5(record[key]) ? record[key] : {}
|
|
14089
14827
|
};
|
|
14090
14828
|
}
|
|
14091
14829
|
return merged;
|
|
@@ -14182,7 +14920,7 @@ async function buildAgentRegistry(config) {
|
|
|
14182
14920
|
typeof configuredTtl === "string" ? configuredTtl : void 0
|
|
14183
14921
|
);
|
|
14184
14922
|
setProviderFallbackModels(parseFallbackModelList(options?.fallbackModels));
|
|
14185
|
-
const
|
|
14923
|
+
const records2 = await readAgentMarkdownRecords(
|
|
14186
14924
|
agentDirectories(
|
|
14187
14925
|
process.env.HOME ?? process.env.USERPROFILE,
|
|
14188
14926
|
getOpencodeProjectDirectory()
|
|
@@ -14197,18 +14935,18 @@ async function buildAgentRegistry(config) {
|
|
|
14197
14935
|
const declaredChain = parseFallbackModelList(
|
|
14198
14936
|
agent.fallbackModels ?? bag.fallbackModels
|
|
14199
14937
|
);
|
|
14200
|
-
|
|
14201
|
-
mode: pick("mode") ??
|
|
14202
|
-
model: pick("model") ??
|
|
14203
|
-
forceModel: pick("forceModel") ??
|
|
14204
|
-
reasoningEffort: pick("reasoningEffort") ??
|
|
14205
|
-
cacheTtl: pick("cacheTtl") ??
|
|
14206
|
-
fallbackModels: declaredChain.length ? declaredChain :
|
|
14938
|
+
records2[name] = {
|
|
14939
|
+
mode: pick("mode") ?? records2[name]?.mode,
|
|
14940
|
+
model: pick("model") ?? records2[name]?.model,
|
|
14941
|
+
forceModel: pick("forceModel") ?? records2[name]?.forceModel,
|
|
14942
|
+
reasoningEffort: pick("reasoningEffort") ?? records2[name]?.reasoningEffort,
|
|
14943
|
+
cacheTtl: pick("cacheTtl") ?? records2[name]?.cacheTtl,
|
|
14944
|
+
fallbackModels: declaredChain.length ? declaredChain : records2[name]?.fallbackModels
|
|
14207
14945
|
};
|
|
14208
14946
|
}
|
|
14209
|
-
setAgentRegistry(
|
|
14947
|
+
setAgentRegistry(records2);
|
|
14210
14948
|
log.debug("agent registry built", {
|
|
14211
|
-
agents: Object.keys(
|
|
14949
|
+
agents: Object.keys(records2).length,
|
|
14212
14950
|
defaultSubagentModel: getDefaultSubagentModel()
|
|
14213
14951
|
});
|
|
14214
14952
|
}
|