@khalilgharbaoui/opencode-claude-code-plugin 0.38.0 → 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/index.d.ts +18 -0
- package/dist/index.js +908 -294
- 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,15 @@ 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 (code !== 0 && code !== null) {
|
|
4891
|
+
claudeSessions.delete(sessionKey2);
|
|
4892
|
+
forgetResumePoint(sessionKey2);
|
|
4893
|
+
}
|
|
4894
|
+
}
|
|
4764
4895
|
function scheduleIdleProcessEviction(key, timeoutMs) {
|
|
4765
4896
|
cancelIdleProcessEviction(key);
|
|
4766
4897
|
if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_IDLE_TIMEOUT_MS) {
|
|
@@ -4906,6 +5037,7 @@ function deleteClaudeSessionId(key) {
|
|
|
4906
5037
|
if (claudeSessionId) clearLedger(claudeSessionId);
|
|
4907
5038
|
claudeSessions.delete(key);
|
|
4908
5039
|
forgetForkFingerprint(key);
|
|
5040
|
+
forgetResumePoint(key);
|
|
4909
5041
|
}
|
|
4910
5042
|
function effortSessionKey(baseKey, effort) {
|
|
4911
5043
|
return effort ? `${baseKey}::effort=${effort}` : baseKey;
|
|
@@ -5018,6 +5150,7 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
|
|
|
5018
5150
|
sessionKey: sessionKey2
|
|
5019
5151
|
});
|
|
5020
5152
|
claudeSessions.delete(sessionKey2);
|
|
5153
|
+
forgetResumePoint(sessionKey2);
|
|
5021
5154
|
}
|
|
5022
5155
|
});
|
|
5023
5156
|
proc.stderr?.on("data", (data) => {
|
|
@@ -5032,6 +5165,7 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
|
|
|
5032
5165
|
error: stderr.slice(0, 200)
|
|
5033
5166
|
});
|
|
5034
5167
|
claudeSessions.delete(sessionKey2);
|
|
5168
|
+
forgetResumePoint(sessionKey2);
|
|
5035
5169
|
} else {
|
|
5036
5170
|
log.debug("ignoring session ID error from stale claude process", {
|
|
5037
5171
|
sessionKey: sessionKey2
|
|
@@ -5510,7 +5644,7 @@ async function handleBtwCommand(client, input, options = {}) {
|
|
|
5510
5644
|
// src/diagnostic-bundle.ts
|
|
5511
5645
|
import { createHash as createHash4, randomBytes as randomBytes2 } from "crypto";
|
|
5512
5646
|
import { openSync, readSync, closeSync, fstatSync, realpathSync } from "fs";
|
|
5513
|
-
import { homedir as
|
|
5647
|
+
import { homedir as homedir5 } from "os";
|
|
5514
5648
|
|
|
5515
5649
|
// src/log-messages.ts
|
|
5516
5650
|
var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
@@ -5586,6 +5720,9 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
5586
5720
|
"ignoring unknown proxyTools entries",
|
|
5587
5721
|
"interactive permissionMode bypassPermissions ignored: Claude Code prompts for confirmation in the TUI",
|
|
5588
5722
|
"interactive system prompt disabled; opencode agent prompts will not be appended",
|
|
5723
|
+
"interactive transport cancelled the usage-limit auto-continue",
|
|
5724
|
+
"interactive transport cannot continue",
|
|
5725
|
+
"interactive transport denied a permission prompt",
|
|
5589
5726
|
"interactive transport dropped non-text content blocks",
|
|
5590
5727
|
"interactive turn failed",
|
|
5591
5728
|
"interactiveBypass ignored: Claude Code prompts for bypassPermissions confirmation in the interactive TUI",
|
|
@@ -5617,6 +5754,7 @@ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
|
5617
5754
|
"remote URL images are not supported by Claude CLI, skipping",
|
|
5618
5755
|
"replaying stdout the child emitted between turns",
|
|
5619
5756
|
"result fallback timer fired \u2014 closing stream without result event",
|
|
5757
|
+
"resuming the claude session from before the restart instead of replaying it",
|
|
5620
5758
|
"shared Claude capability already exists; leaving untouched",
|
|
5621
5759
|
"skipping MCP server with unknown type",
|
|
5622
5760
|
"skipping local MCP server with no command",
|
|
@@ -5840,7 +5978,7 @@ function createIdHasher(salt = randomBytes2(16).toString("hex")) {
|
|
|
5840
5978
|
return (raw) => createHash4("sha256").update(salt).update("\0").update(raw).digest("hex").slice(0, 8);
|
|
5841
5979
|
}
|
|
5842
5980
|
function createRedactionContext(options = {}) {
|
|
5843
|
-
const home = options.home ??
|
|
5981
|
+
const home = options.home ?? homedir5();
|
|
5844
5982
|
const homes = /* @__PURE__ */ new Set();
|
|
5845
5983
|
if (home) homes.add(home);
|
|
5846
5984
|
if (home) {
|
|
@@ -6514,7 +6652,7 @@ function staleBuildWatch() {
|
|
|
6514
6652
|
var DOCTOR_COMMAND = "claude-code-doctor";
|
|
6515
6653
|
var DOCTOR_COMMAND_DESCRIPTION = "Report what the Claude Code plugin sees: versions, cwd, live processes, pending proxy calls. Add `usage` for plan windows, or `bundle` for a redacted log you can paste into an issue";
|
|
6516
6654
|
var DOCTOR_MARKER = "\u258C **claude-code doctor**";
|
|
6517
|
-
function
|
|
6655
|
+
function isRecord4(value) {
|
|
6518
6656
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
6519
6657
|
}
|
|
6520
6658
|
var SYSTEM_REMINDER_BLOCK2 = /<system-reminder>[\s\S]*?<\/system-reminder>/g;
|
|
@@ -6525,7 +6663,7 @@ function parseDoctorCommandContent(content) {
|
|
|
6525
6663
|
} else if (Array.isArray(content)) {
|
|
6526
6664
|
const parts = [];
|
|
6527
6665
|
for (const part of content) {
|
|
6528
|
-
if (!
|
|
6666
|
+
if (!isRecord4(part) || part.type !== "text" || typeof part.text !== "string") return null;
|
|
6529
6667
|
parts.push(part.text);
|
|
6530
6668
|
}
|
|
6531
6669
|
text = parts.join("\n");
|
|
@@ -7266,8 +7404,8 @@ var registry = {};
|
|
|
7266
7404
|
var defaultSubagentModel;
|
|
7267
7405
|
var defaultSubagentCacheTtl;
|
|
7268
7406
|
var providerFallbackModels = [];
|
|
7269
|
-
function setAgentRegistry(
|
|
7270
|
-
registry =
|
|
7407
|
+
function setAgentRegistry(records2) {
|
|
7408
|
+
registry = records2;
|
|
7271
7409
|
}
|
|
7272
7410
|
function getAgentRegistry() {
|
|
7273
7411
|
return registry;
|
|
@@ -7407,7 +7545,7 @@ function parseFallbackModelList(value) {
|
|
|
7407
7545
|
return value.trim().replace(/^\[|\]$/g, "").split(/[,\s]+/).map((entry) => entry.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
|
|
7408
7546
|
}
|
|
7409
7547
|
async function readAgentMarkdownRecords(directories) {
|
|
7410
|
-
const
|
|
7548
|
+
const records2 = {};
|
|
7411
7549
|
for (const directory of directories) {
|
|
7412
7550
|
let entries;
|
|
7413
7551
|
try {
|
|
@@ -7418,10 +7556,10 @@ async function readAgentMarkdownRecords(directories) {
|
|
|
7418
7556
|
for (const entry of entries) {
|
|
7419
7557
|
if (!entry.endsWith(".md")) continue;
|
|
7420
7558
|
const name = entry.slice(0, -3);
|
|
7421
|
-
if (
|
|
7559
|
+
if (records2[name]) continue;
|
|
7422
7560
|
try {
|
|
7423
7561
|
const text = await readFile(path7.join(directory, entry), "utf8");
|
|
7424
|
-
|
|
7562
|
+
records2[name] = parseAgentFrontmatter(text);
|
|
7425
7563
|
} catch (err) {
|
|
7426
7564
|
log.debug("failed to read agent markdown", {
|
|
7427
7565
|
file: path7.join(directory, entry),
|
|
@@ -7430,7 +7568,7 @@ async function readAgentMarkdownRecords(directories) {
|
|
|
7430
7568
|
}
|
|
7431
7569
|
}
|
|
7432
7570
|
}
|
|
7433
|
-
return
|
|
7571
|
+
return records2;
|
|
7434
7572
|
}
|
|
7435
7573
|
function agentDirectories(home, projectDirectory) {
|
|
7436
7574
|
const directories = [];
|
|
@@ -8369,6 +8507,7 @@ async function resolveSkillPluginDirs(opts) {
|
|
|
8369
8507
|
|
|
8370
8508
|
// src/claude-session-wrapper.ts
|
|
8371
8509
|
import { EventEmitter as EventEmitter4 } from "events";
|
|
8510
|
+
import { existsSync as existsSync4 } from "fs";
|
|
8372
8511
|
import { unlink as unlink2 } from "fs/promises";
|
|
8373
8512
|
|
|
8374
8513
|
// src/claude-session-bun.ts
|
|
@@ -8402,10 +8541,99 @@ function encodeCwd(cwd) {
|
|
|
8402
8541
|
}
|
|
8403
8542
|
return real.replace(/[^a-zA-Z0-9]/g, "-");
|
|
8404
8543
|
}
|
|
8544
|
+
var bunPtySpawner = (argv, opts) => {
|
|
8545
|
+
const [command, ...args] = argv;
|
|
8546
|
+
return Bun.spawn([resolveClaude(command ?? "claude"), ...args], {
|
|
8547
|
+
cwd: opts.cwd,
|
|
8548
|
+
env: opts.env,
|
|
8549
|
+
terminal: {
|
|
8550
|
+
cols: opts.cols,
|
|
8551
|
+
rows: opts.rows,
|
|
8552
|
+
data: (_term, data) => opts.onData(Buffer.from(data).toString("utf8"))
|
|
8553
|
+
}
|
|
8554
|
+
});
|
|
8555
|
+
};
|
|
8556
|
+
function words(text) {
|
|
8557
|
+
return text.split(/\s+/).map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\s*");
|
|
8558
|
+
}
|
|
8559
|
+
var SCREEN_PATTERNS = [
|
|
8560
|
+
{
|
|
8561
|
+
kind: "login",
|
|
8562
|
+
pattern: new RegExp(
|
|
8563
|
+
[
|
|
8564
|
+
words("Select login method"),
|
|
8565
|
+
words("Please run /login"),
|
|
8566
|
+
words("Invalid API key"),
|
|
8567
|
+
words("OAuth token has expired"),
|
|
8568
|
+
words("Not logged in")
|
|
8569
|
+
].join("|"),
|
|
8570
|
+
"i"
|
|
8571
|
+
)
|
|
8572
|
+
},
|
|
8573
|
+
{
|
|
8574
|
+
kind: "onboarding",
|
|
8575
|
+
pattern: new RegExp(
|
|
8576
|
+
[words("Choose the text style that looks best"), words("Let's get started.")].join("|"),
|
|
8577
|
+
"i"
|
|
8578
|
+
)
|
|
8579
|
+
},
|
|
8580
|
+
{
|
|
8581
|
+
kind: "trust",
|
|
8582
|
+
pattern: new RegExp(
|
|
8583
|
+
[
|
|
8584
|
+
words("Do you trust the files in this folder?"),
|
|
8585
|
+
words("Is this a project you created or one you trust?"),
|
|
8586
|
+
words("Yes, I trust this folder")
|
|
8587
|
+
].join("|"),
|
|
8588
|
+
"i"
|
|
8589
|
+
)
|
|
8590
|
+
},
|
|
8591
|
+
{
|
|
8592
|
+
// The question alone is not enough: Claude can write "Do you want to
|
|
8593
|
+
// proceed?" in a reply. The dialog always numbers its options.
|
|
8594
|
+
kind: "permission",
|
|
8595
|
+
pattern: new RegExp(
|
|
8596
|
+
`${words("Do you want to")}\\s*(?:proceed|make\\s*this\\s*edit|create|overwrite|allow|run)[^?]{0,160}\\?[\\s\\S]{0,600}?1\\.\\s*Yes`,
|
|
8597
|
+
"i"
|
|
8598
|
+
)
|
|
8599
|
+
},
|
|
8600
|
+
{
|
|
8601
|
+
kind: "auto-continue",
|
|
8602
|
+
pattern: new RegExp(
|
|
8603
|
+
`${words("continuing automatically at")}[^\xB7\\n]{1,40}\xB7\\s*${words("esc to cancel")}`,
|
|
8604
|
+
"i"
|
|
8605
|
+
)
|
|
8606
|
+
}
|
|
8607
|
+
];
|
|
8608
|
+
function stripTerminal(raw) {
|
|
8609
|
+
return raw.replace(
|
|
8610
|
+
/\x1B\[(\d*)C/g,
|
|
8611
|
+
(_match, count) => " ".repeat(Math.min(Number(count) || 1, 200))
|
|
8612
|
+
).replace(/\x1B\[[0-9;?]*[ABDEFGHJKfd]/g, "\n").replace(/\x1B\][^\x07\x1B]*(?:\x07|\x1B\\)/g, "").replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, "").replace(/[\x00-\x08\x0B-\x1F\x7F]/g, "").replace(/[ \t]+/g, " ");
|
|
8613
|
+
}
|
|
8614
|
+
function highlightedChoice(text) {
|
|
8615
|
+
let last = null;
|
|
8616
|
+
for (const match of text.matchAll(/❯\s*(?:\d+\s*\.\s*)?(Yes|No)\b/gi)) {
|
|
8617
|
+
last = match[1].toLowerCase();
|
|
8618
|
+
}
|
|
8619
|
+
return last === "yes" || last === "no" ? last : null;
|
|
8620
|
+
}
|
|
8621
|
+
function classifyScreen(text) {
|
|
8622
|
+
for (const { kind, pattern } of SCREEN_PATTERNS) {
|
|
8623
|
+
const match = pattern.exec(text);
|
|
8624
|
+
if (match) {
|
|
8625
|
+
return { kind, detail: match[0].replace(/\s+/g, " ").trim().slice(0, 200) };
|
|
8626
|
+
}
|
|
8627
|
+
}
|
|
8628
|
+
return null;
|
|
8629
|
+
}
|
|
8630
|
+
function configDirEnv(configDir) {
|
|
8631
|
+
return configDir ? { CLAUDE_CONFIG_DIR: configDir } : {};
|
|
8632
|
+
}
|
|
8405
8633
|
function interactiveSpawnEnv(opts) {
|
|
8406
8634
|
return {
|
|
8407
8635
|
...process.env,
|
|
8408
|
-
|
|
8636
|
+
...configDirEnv(opts.configDir),
|
|
8409
8637
|
TERM: "xterm-256color",
|
|
8410
8638
|
// Pin the binary so a mid-session autoupdate cannot invalidate the
|
|
8411
8639
|
// detected version the flag gates read, and skip non-essential traffic.
|
|
@@ -8480,6 +8708,21 @@ var TurnUsageAccumulator = class {
|
|
|
8480
8708
|
};
|
|
8481
8709
|
var TERMINAL_STOP = /* @__PURE__ */ new Set(["end_turn", "stop_sequence", "max_tokens"]);
|
|
8482
8710
|
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
8711
|
+
var RAW_KEEP_CHARS = 64 * 1024;
|
|
8712
|
+
var SCREEN_WINDOW_CHARS = 16 * 1024;
|
|
8713
|
+
var INTERRUPT_MARKER = "[Request interrupted by user";
|
|
8714
|
+
function isInterruptRecord(rec) {
|
|
8715
|
+
if (!rec || rec.type !== "user" || !rec.message) return false;
|
|
8716
|
+
const content = rec.message.content;
|
|
8717
|
+
if (typeof content === "string") return content.startsWith(INTERRUPT_MARKER);
|
|
8718
|
+
if (!Array.isArray(content)) return false;
|
|
8719
|
+
return content.some(
|
|
8720
|
+
(block) => block?.type === "text" && typeof block.text === "string" && block.text.startsWith(INTERRUPT_MARKER)
|
|
8721
|
+
);
|
|
8722
|
+
}
|
|
8723
|
+
function isTurnDurationRecord(rec) {
|
|
8724
|
+
return rec?.type === "system" && rec.subtype === "turn_duration";
|
|
8725
|
+
}
|
|
8483
8726
|
function resolveConfigDir(configDir) {
|
|
8484
8727
|
const value = configDir ?? process.env.CLAUDE_CONFIG_DIR;
|
|
8485
8728
|
if (!value) return path9.join(os4.homedir(), ".claude");
|
|
@@ -8489,11 +8732,21 @@ function resolveConfigDir(configDir) {
|
|
|
8489
8732
|
}
|
|
8490
8733
|
return path9.resolve(value);
|
|
8491
8734
|
}
|
|
8735
|
+
function fatalScreenMessage(kind, configDir, cwd) {
|
|
8736
|
+
if (kind === "trust") {
|
|
8737
|
+
return `Claude Code's folder trust dialog for ${cwd} could not be answered. Run \`claude\` in that folder once by hand and trust it, then retry.`;
|
|
8738
|
+
}
|
|
8739
|
+
if (kind === "login") {
|
|
8740
|
+
return `Claude Code is not logged in for ${configDir}. Run \`claude\` there once by hand and log in (or \`claude auth login\`), then retry.`;
|
|
8741
|
+
}
|
|
8742
|
+
return `Claude Code has not finished its first-run setup for ${configDir}. Run \`claude\` there once by hand, then retry.`;
|
|
8743
|
+
}
|
|
8492
8744
|
var ClaudeSession = class {
|
|
8493
8745
|
sessionId;
|
|
8494
8746
|
cwd;
|
|
8495
8747
|
configDir;
|
|
8496
8748
|
jsonlPath;
|
|
8749
|
+
/** The newest terminal output, ANSI included, for diagnostics. */
|
|
8497
8750
|
raw = "";
|
|
8498
8751
|
proc = null;
|
|
8499
8752
|
cursor = 0;
|
|
@@ -8503,12 +8756,36 @@ var ClaudeSession = class {
|
|
|
8503
8756
|
exitCode = null;
|
|
8504
8757
|
aborted = false;
|
|
8505
8758
|
signal;
|
|
8759
|
+
resumeSessionId;
|
|
8760
|
+
env;
|
|
8761
|
+
onScreen;
|
|
8762
|
+
onExit;
|
|
8763
|
+
spawnPty;
|
|
8764
|
+
/** The configured config dir, which alone is exported to the child. */
|
|
8765
|
+
explicitConfigDir;
|
|
8766
|
+
/** Output since the screen was last acted on, read by `checkScreen`. */
|
|
8767
|
+
screen = "";
|
|
8768
|
+
screenTimer = null;
|
|
8769
|
+
fatal = null;
|
|
8770
|
+
/** The folder trust dialog is up and not answered yet. */
|
|
8771
|
+
trustPending = false;
|
|
8772
|
+
trustMoves = 0;
|
|
8773
|
+
trustMovedAt = 0;
|
|
8774
|
+
turn = null;
|
|
8775
|
+
turnDenied = [];
|
|
8776
|
+
abandonTurn = false;
|
|
8506
8777
|
o;
|
|
8507
8778
|
constructor(opts = {}) {
|
|
8508
8779
|
this.cwd = path9.resolve(opts.cwd ?? process.cwd());
|
|
8509
8780
|
this.configDir = resolveConfigDir(opts.configDir);
|
|
8781
|
+
this.explicitConfigDir = opts.configDir ? this.configDir : void 0;
|
|
8510
8782
|
this.signal = opts.signal;
|
|
8511
|
-
this.
|
|
8783
|
+
this.resumeSessionId = opts.resumeSessionId;
|
|
8784
|
+
this.env = opts.env;
|
|
8785
|
+
this.onScreen = opts.onScreen;
|
|
8786
|
+
this.onExit = opts.onExit;
|
|
8787
|
+
this.spawnPty = opts.spawnPty ?? bunPtySpawner;
|
|
8788
|
+
this.sessionId = opts.resumeSessionId ?? randomUUID4();
|
|
8512
8789
|
this.jsonlPath = path9.join(
|
|
8513
8790
|
this.configDir,
|
|
8514
8791
|
"projects",
|
|
@@ -8538,9 +8815,47 @@ var ClaudeSession = class {
|
|
|
8538
8815
|
submitMinMs: opts.submitMinMs ?? 200,
|
|
8539
8816
|
submitConfirmMs: opts.submitConfirmMs ?? 1500,
|
|
8540
8817
|
submitMaxRetries: opts.submitMaxRetries ?? 8,
|
|
8541
|
-
debug: opts.debug ?? false
|
|
8818
|
+
debug: opts.debug ?? false,
|
|
8819
|
+
// The headless CLI's `tool_progress` heartbeat runs every 30 s, and the
|
|
8820
|
+
// wire-inactivity watchdog it keeps quiet fires at 60 s.
|
|
8821
|
+
heartbeatMs: opts.heartbeatMs ?? 3e4,
|
|
8822
|
+
interruptGraceMs: opts.interruptGraceMs ?? 5e3,
|
|
8823
|
+
// Shorter than `bootQuietMs`, so a trust dialog is answered before boot
|
|
8824
|
+
// could declare the TUI ready and paste a prompt into it.
|
|
8825
|
+
permissionQuietMs: opts.permissionQuietMs ?? 1e3,
|
|
8826
|
+
stopSettleMs: opts.stopSettleMs ?? 750
|
|
8542
8827
|
};
|
|
8543
8828
|
}
|
|
8829
|
+
/** The CLI arguments this session spawns with. */
|
|
8830
|
+
spawnArgs() {
|
|
8831
|
+
const args = this.resumeSessionId ? ["--resume", this.resumeSessionId] : ["--session-id", this.sessionId];
|
|
8832
|
+
if (this.o.model) args.push("--model", this.o.model);
|
|
8833
|
+
if (this.o.settingSources !== null && this.o.settingSources !== void 0) {
|
|
8834
|
+
args.push("--setting-sources", this.o.settingSources);
|
|
8835
|
+
}
|
|
8836
|
+
if (this.o.extraArgs && this.o.extraArgs.length) args.push(...this.o.extraArgs);
|
|
8837
|
+
return args;
|
|
8838
|
+
}
|
|
8839
|
+
/** The child environment: the caller's (or the default one) plus `TERM`,
|
|
8840
|
+
* and `CLAUDE_CONFIG_DIR` only for a configured one (`configDirEnv`). */
|
|
8841
|
+
spawnEnv() {
|
|
8842
|
+
if (!this.env) {
|
|
8843
|
+
return interactiveSpawnEnv({
|
|
8844
|
+
configDir: this.explicitConfigDir,
|
|
8845
|
+
ignoreAnthropicApiKey: this.o.ignoreAnthropicApiKey,
|
|
8846
|
+
effort: this.o.effort
|
|
8847
|
+
});
|
|
8848
|
+
}
|
|
8849
|
+
return { ...this.env, ...configDirEnv(this.explicitConfigDir), TERM: "xterm-256color" };
|
|
8850
|
+
}
|
|
8851
|
+
/** True while a turn is between its paste and its end. */
|
|
8852
|
+
get turnRunning() {
|
|
8853
|
+
return this.turn !== null;
|
|
8854
|
+
}
|
|
8855
|
+
/** True once the child is gone (exited, killed or never started). */
|
|
8856
|
+
get hasExited() {
|
|
8857
|
+
return this.exited;
|
|
8858
|
+
}
|
|
8544
8859
|
async start() {
|
|
8545
8860
|
if (this.signal?.aborted) throw new Error("aborted before start");
|
|
8546
8861
|
this.signal?.addEventListener(
|
|
@@ -8551,61 +8866,147 @@ var ClaudeSession = class {
|
|
|
8551
8866
|
},
|
|
8552
8867
|
{ once: true }
|
|
8553
8868
|
);
|
|
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);
|
|
8869
|
+
const claude = this.o.cliPath ?? "claude";
|
|
8870
|
+
const args = this.spawnArgs();
|
|
8561
8871
|
if (this.o.debug)
|
|
8562
8872
|
process.stderr.write(`[session] spawn: ${claude} ${args.join(" ")}
|
|
8563
8873
|
`);
|
|
8564
8874
|
this.lastDataAt = Date.now();
|
|
8565
|
-
|
|
8875
|
+
const proc = this.spawnPty([claude, ...args], {
|
|
8566
8876
|
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
|
-
}
|
|
8877
|
+
env: this.spawnEnv(),
|
|
8878
|
+
cols: this.o.cols,
|
|
8879
|
+
rows: this.o.rows,
|
|
8880
|
+
onData: (chunk) => this.onData(chunk)
|
|
8584
8881
|
});
|
|
8585
|
-
this.proc
|
|
8586
|
-
|
|
8587
|
-
this.exited
|
|
8588
|
-
this.
|
|
8589
|
-
}).catch(() => {
|
|
8882
|
+
this.proc = proc;
|
|
8883
|
+
const markExited = (code) => {
|
|
8884
|
+
if (this.exited) return;
|
|
8885
|
+
this.exitCode = code;
|
|
8590
8886
|
this.exited = true;
|
|
8591
8887
|
this.proc = null;
|
|
8592
|
-
|
|
8888
|
+
this.clearScreenTimer();
|
|
8889
|
+
this.onExit?.(code);
|
|
8890
|
+
};
|
|
8891
|
+
proc.exited.then((code) => markExited(typeof code === "number" ? code : null)).catch(() => markExited(null));
|
|
8593
8892
|
await this.waitForBoot();
|
|
8594
8893
|
this.cursor = this.lineCount();
|
|
8595
8894
|
}
|
|
8895
|
+
onData(chunk) {
|
|
8896
|
+
this.lastDataAt = Date.now();
|
|
8897
|
+
this.raw = (this.raw + chunk).slice(-RAW_KEEP_CHARS);
|
|
8898
|
+
this.screen = (this.screen + chunk).slice(-SCREEN_WINDOW_CHARS);
|
|
8899
|
+
if (this.o.debug) process.stdout.write(chunk);
|
|
8900
|
+
this.clearScreenTimer();
|
|
8901
|
+
this.screenTimer = setTimeout(() => {
|
|
8902
|
+
this.screenTimer = null;
|
|
8903
|
+
this.checkScreen();
|
|
8904
|
+
}, this.o.permissionQuietMs);
|
|
8905
|
+
}
|
|
8906
|
+
clearScreenTimer() {
|
|
8907
|
+
if (this.screenTimer) {
|
|
8908
|
+
clearTimeout(this.screenTimer);
|
|
8909
|
+
this.screenTimer = null;
|
|
8910
|
+
}
|
|
8911
|
+
}
|
|
8912
|
+
/**
|
|
8913
|
+
* Answer the screen the TUI is blocked on, if it is one of the few this
|
|
8914
|
+
* session recognises. Returns what it did, or null. Each screen is acted on
|
|
8915
|
+
* once: the window is cleared afterwards, so the next check only reads what
|
|
8916
|
+
* the TUI drew since.
|
|
8917
|
+
*/
|
|
8918
|
+
checkScreen() {
|
|
8919
|
+
if (!this.proc || this.exited) return null;
|
|
8920
|
+
const text = stripTerminal(this.screen);
|
|
8921
|
+
const state = this.trustPending ? { kind: "trust", detail: "folder trust dialog" } : classifyScreen(text);
|
|
8922
|
+
if (!state) return null;
|
|
8923
|
+
let action;
|
|
8924
|
+
switch (state.kind) {
|
|
8925
|
+
case "trust": {
|
|
8926
|
+
this.trustPending = true;
|
|
8927
|
+
const choice = highlightedChoice(text);
|
|
8928
|
+
if (choice === "yes") {
|
|
8929
|
+
this.write("\r");
|
|
8930
|
+
this.trustPending = false;
|
|
8931
|
+
action = "accepted";
|
|
8932
|
+
break;
|
|
8933
|
+
}
|
|
8934
|
+
if (choice === null) return null;
|
|
8935
|
+
if (this.trustMovedAt && this.lastDataAt <= this.trustMovedAt) return null;
|
|
8936
|
+
if (this.trustMoves >= 4) {
|
|
8937
|
+
action = "fatal";
|
|
8938
|
+
break;
|
|
8939
|
+
}
|
|
8940
|
+
this.trustMoves++;
|
|
8941
|
+
this.trustMovedAt = Date.now();
|
|
8942
|
+
this.write("\x1B[B");
|
|
8943
|
+
return null;
|
|
8944
|
+
}
|
|
8945
|
+
case "permission":
|
|
8946
|
+
if (!this.turn) return null;
|
|
8947
|
+
this.write("\x1B");
|
|
8948
|
+
this.turnDenied.push(state);
|
|
8949
|
+
action = "denied";
|
|
8950
|
+
break;
|
|
8951
|
+
case "auto-continue":
|
|
8952
|
+
this.write("\x1B");
|
|
8953
|
+
action = "cancelled";
|
|
8954
|
+
break;
|
|
8955
|
+
case "login":
|
|
8956
|
+
case "onboarding":
|
|
8957
|
+
action = "fatal";
|
|
8958
|
+
break;
|
|
8959
|
+
}
|
|
8960
|
+
this.screen = "";
|
|
8961
|
+
const event = { ...state, action };
|
|
8962
|
+
if (action === "fatal") {
|
|
8963
|
+
this.fatal = event;
|
|
8964
|
+
this.killProcess();
|
|
8965
|
+
}
|
|
8966
|
+
try {
|
|
8967
|
+
this.onScreen?.(event);
|
|
8968
|
+
} catch {
|
|
8969
|
+
}
|
|
8970
|
+
return event;
|
|
8971
|
+
}
|
|
8972
|
+
write(data) {
|
|
8973
|
+
try {
|
|
8974
|
+
this.proc?.terminal.write(data);
|
|
8975
|
+
} catch {
|
|
8976
|
+
}
|
|
8977
|
+
}
|
|
8978
|
+
fatalError() {
|
|
8979
|
+
if (!this.fatal) return null;
|
|
8980
|
+
return new Error(
|
|
8981
|
+
this.failureMessage(fatalScreenMessage(this.fatal.kind, this.configDir, this.cwd), true)
|
|
8982
|
+
);
|
|
8983
|
+
}
|
|
8596
8984
|
/** Wait until the TUI has been quiet for bootQuietMs (Ink ready), bounded by
|
|
8597
|
-
* bootMinMs..bootMaxMs.
|
|
8985
|
+
* bootMinMs..bootMaxMs. A screen still waiting to be answered when the TUI
|
|
8986
|
+
* goes quiet is answered first, and the wait goes on. */
|
|
8598
8987
|
async waitForBoot() {
|
|
8599
8988
|
const start = Date.now();
|
|
8600
8989
|
while (Date.now() - start < this.o.bootMaxMs) {
|
|
8601
8990
|
await delay(150);
|
|
8602
8991
|
if (this.aborted) throw new Error("aborted during boot");
|
|
8992
|
+
const fatal = this.fatalError();
|
|
8993
|
+
if (fatal) throw fatal;
|
|
8603
8994
|
if (this.exited) {
|
|
8604
8995
|
throw new Error(this.failureMessage("claude exited during boot", true));
|
|
8605
8996
|
}
|
|
8606
8997
|
const elapsed = Date.now() - start;
|
|
8607
8998
|
const sinceData = Date.now() - this.lastDataAt;
|
|
8608
|
-
if (elapsed >= this.o.bootMinMs && sinceData >= this.o.bootQuietMs)
|
|
8999
|
+
if (elapsed >= this.o.bootMinMs && sinceData >= this.o.bootQuietMs) {
|
|
9000
|
+
const acted = this.checkScreen();
|
|
9001
|
+
if (!acted && !this.trustPending) return;
|
|
9002
|
+
const fatalNow = this.fatalError();
|
|
9003
|
+
if (fatalNow) throw fatalNow;
|
|
9004
|
+
}
|
|
9005
|
+
}
|
|
9006
|
+
if (this.trustPending) {
|
|
9007
|
+
this.fatal = { kind: "trust", detail: "folder trust dialog", action: "fatal" };
|
|
9008
|
+
this.killProcess();
|
|
9009
|
+
throw this.fatalError();
|
|
8609
9010
|
}
|
|
8610
9011
|
}
|
|
8611
9012
|
/** Submit the freshly-injected prompt and confirm the turn was actually
|
|
@@ -8643,7 +9044,7 @@ var ClaudeSession = class {
|
|
|
8643
9044
|
return lines.length > 0 ? lines.length - 1 : 0;
|
|
8644
9045
|
}
|
|
8645
9046
|
rawTail(max = 600) {
|
|
8646
|
-
const clean = this.raw
|
|
9047
|
+
const clean = stripTerminal(this.raw).replace(/\s+/g, " ").trim();
|
|
8647
9048
|
return clean.length > max ? clean.slice(-max) : clean;
|
|
8648
9049
|
}
|
|
8649
9050
|
failureMessage(reason, includeRaw = false) {
|
|
@@ -8657,61 +9058,133 @@ var ClaudeSession = class {
|
|
|
8657
9058
|
return parts.join("; ");
|
|
8658
9059
|
}
|
|
8659
9060
|
/**
|
|
8660
|
-
*
|
|
8661
|
-
*
|
|
9061
|
+
* Paste a turn into the live session and follow the transcript until the
|
|
9062
|
+
* turn ends, handing every new record to `onRecord` (raw line and parsed
|
|
9063
|
+
* record, the latter null when the line is not JSON). One turn at a time:
|
|
9064
|
+
* a second call while one runs is refused, because both would read the
|
|
9065
|
+
* same cursor.
|
|
8662
9066
|
*/
|
|
8663
|
-
async
|
|
9067
|
+
async runTurnLoop(prompt, onRecord, timeout, onHeartbeat) {
|
|
8664
9068
|
if (this.aborted) throw new Error("aborted");
|
|
9069
|
+
const fatal = this.fatalError();
|
|
9070
|
+
if (fatal) throw fatal;
|
|
8665
9071
|
if (!this.proc || this.exited)
|
|
8666
9072
|
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;
|
|
9073
|
+
if (this.turn) throw new Error("a turn is already running in this session");
|
|
9074
|
+
let settle;
|
|
9075
|
+
this.turn = new Promise((resolve6) => settle = resolve6);
|
|
9076
|
+
this.turnDenied = [];
|
|
9077
|
+
this.abandonTurn = false;
|
|
9078
|
+
this.cursor = Math.max(this.cursor, this.lineCount());
|
|
9079
|
+
this.screen = "";
|
|
9080
|
+
try {
|
|
9081
|
+
if (this.o.bracketedPaste) {
|
|
9082
|
+
this.proc.terminal.write("\x1B[200~" + prompt + "\x1B[201~");
|
|
9083
|
+
} else {
|
|
9084
|
+
this.proc.terminal.write(prompt);
|
|
8687
9085
|
}
|
|
8688
|
-
|
|
8689
|
-
|
|
8690
|
-
|
|
8691
|
-
|
|
8692
|
-
|
|
8693
|
-
|
|
8694
|
-
|
|
9086
|
+
await this.submitTurn();
|
|
9087
|
+
let stopReason = null;
|
|
9088
|
+
let stopSeenAt = 0;
|
|
9089
|
+
let end = null;
|
|
9090
|
+
let sawUserRecord = false;
|
|
9091
|
+
let lastBeat = Date.now();
|
|
9092
|
+
const deadline = Date.now() + timeout;
|
|
9093
|
+
while (Date.now() < deadline) {
|
|
9094
|
+
await delay(this.o.pollMs);
|
|
9095
|
+
if (this.aborted) throw new Error("aborted mid-turn");
|
|
9096
|
+
const fatalNow = this.fatalError();
|
|
9097
|
+
if (fatalNow) throw fatalNow;
|
|
9098
|
+
const lines = this.readRawLines();
|
|
9099
|
+
const lastComplete = lines.length - 1;
|
|
9100
|
+
if (lastComplete > this.cursor) {
|
|
9101
|
+
for (let i = this.cursor; i < lastComplete && !end; i++) {
|
|
9102
|
+
const s = lines[i];
|
|
9103
|
+
if (!s || !s.trim()) continue;
|
|
9104
|
+
let rec = null;
|
|
9105
|
+
try {
|
|
9106
|
+
rec = JSON.parse(s);
|
|
9107
|
+
} catch {
|
|
9108
|
+
}
|
|
9109
|
+
onRecord(s, rec);
|
|
9110
|
+
if (!rec) continue;
|
|
9111
|
+
if (rec.type === "assistant" && rec.message) {
|
|
9112
|
+
const reason = rec.message.stop_reason;
|
|
9113
|
+
if (typeof reason === "string") {
|
|
9114
|
+
stopReason = TERMINAL_STOP.has(reason) ? reason : null;
|
|
9115
|
+
}
|
|
9116
|
+
} else if (rec.type === "user") {
|
|
9117
|
+
if (sawUserRecord && isInterruptRecord(rec)) end = "interrupted";
|
|
9118
|
+
sawUserRecord = true;
|
|
9119
|
+
} else if ((sawUserRecord || stopReason) && isTurnDurationRecord(rec)) {
|
|
9120
|
+
end = stopReason ? "stop" : "ended";
|
|
9121
|
+
}
|
|
9122
|
+
this.cursor = i + 1;
|
|
9123
|
+
}
|
|
9124
|
+
if (!end) this.cursor = lastComplete;
|
|
9125
|
+
lastBeat = Date.now();
|
|
9126
|
+
stopSeenAt = Date.now();
|
|
9127
|
+
if (end) return { stopReason: end === "stop" ? stopReason : null, end, denied: this.turnDenied };
|
|
8695
9128
|
continue;
|
|
8696
9129
|
}
|
|
8697
|
-
if (
|
|
8698
|
-
|
|
8699
|
-
|
|
8700
|
-
|
|
8701
|
-
|
|
8702
|
-
|
|
8703
|
-
|
|
8704
|
-
|
|
8705
|
-
|
|
9130
|
+
if (stopReason && Date.now() - stopSeenAt >= this.o.stopSettleMs) {
|
|
9131
|
+
return { stopReason, end: "stop", denied: this.turnDenied };
|
|
9132
|
+
}
|
|
9133
|
+
if (this.exited) throw new Error(this.failureMessage("claude exited mid-turn", true));
|
|
9134
|
+
if (this.abandonTurn) return { stopReason: null, end: "interrupted", denied: this.turnDenied };
|
|
9135
|
+
const now = Date.now();
|
|
9136
|
+
if (onHeartbeat && now - lastBeat >= this.o.heartbeatMs) {
|
|
9137
|
+
lastBeat = now;
|
|
9138
|
+
if (now - this.lastDataAt < this.o.heartbeatMs) onHeartbeat();
|
|
8706
9139
|
}
|
|
8707
9140
|
}
|
|
8708
|
-
this.
|
|
8709
|
-
|
|
9141
|
+
return { stopReason: null, end: null, denied: this.turnDenied };
|
|
9142
|
+
} finally {
|
|
9143
|
+
this.turn = null;
|
|
9144
|
+
this.abandonTurn = false;
|
|
9145
|
+
settle();
|
|
8710
9146
|
}
|
|
9147
|
+
}
|
|
9148
|
+
/**
|
|
9149
|
+
* Stop the running turn the way the TUI's own Esc does, and keep the
|
|
9150
|
+
* session alive for the next one. Resolves true once the turn ended, false
|
|
9151
|
+
* when it was abandoned after `interruptGraceMs` without the TUI saying so.
|
|
9152
|
+
*/
|
|
9153
|
+
async interrupt(graceMs = this.o.interruptGraceMs) {
|
|
9154
|
+
const turn = this.turn;
|
|
9155
|
+
if (!turn) return true;
|
|
9156
|
+
this.write("\x1B");
|
|
9157
|
+
const ended = await Promise.race([turn.then(() => true), delay(graceMs).then(() => false)]);
|
|
9158
|
+
if (ended) return true;
|
|
9159
|
+
this.write("\x1B");
|
|
9160
|
+
this.abandonTurn = true;
|
|
9161
|
+
await Promise.race([turn, delay(this.o.pollMs * 4)]);
|
|
9162
|
+
return false;
|
|
9163
|
+
}
|
|
9164
|
+
/**
|
|
9165
|
+
* Inject a turn into the live session and return the assistant reply once a
|
|
9166
|
+
* terminal stop_reason is observed in the transcript.
|
|
9167
|
+
*/
|
|
9168
|
+
async ask(prompt, perTurnTimeoutMs) {
|
|
9169
|
+
const timeout = perTurnTimeoutMs ?? this.o.turnTimeoutMs;
|
|
9170
|
+
const t0 = Date.now();
|
|
9171
|
+
const collected2 = [];
|
|
9172
|
+
const usage = new TurnUsageAccumulator();
|
|
9173
|
+
const { stopReason } = await this.runTurnLoop(
|
|
9174
|
+
prompt,
|
|
9175
|
+
(_raw, rec) => {
|
|
9176
|
+
if (rec?.type !== "assistant" || !rec.message) return;
|
|
9177
|
+
for (const b of rec.message.content ?? []) {
|
|
9178
|
+
if (b?.type === "text" && typeof b.text === "string") collected2.push(b.text);
|
|
9179
|
+
}
|
|
9180
|
+
usage.add(rec);
|
|
9181
|
+
},
|
|
9182
|
+
timeout
|
|
9183
|
+
);
|
|
8711
9184
|
if (!stopReason) {
|
|
8712
9185
|
throw new Error(
|
|
8713
9186
|
this.failureMessage(
|
|
8714
|
-
`turn
|
|
9187
|
+
`turn ended without a terminal assistant record after ${Date.now() - t0}ms (collected ${collected2.length} text block(s))`
|
|
8715
9188
|
)
|
|
8716
9189
|
);
|
|
8717
9190
|
}
|
|
@@ -8733,9 +9206,9 @@ var ClaudeSession = class {
|
|
|
8733
9206
|
}
|
|
8734
9207
|
/**
|
|
8735
9208
|
* 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
|
-
*
|
|
9209
|
+
* raw JSONL transcript line via onLine (verbatim) until the turn ends. Used
|
|
9210
|
+
* by the opencode plugin transport shim, which feeds these raw lines into
|
|
9211
|
+
* the existing stream-json line handler unchanged.
|
|
8739
9212
|
*
|
|
8740
9213
|
* `usage` is the turn summed over DISTINCT API calls, which is exactly what
|
|
8741
9214
|
* a headless `result` frame reports, so the shim can synthesize one and
|
|
@@ -8743,53 +9216,24 @@ var ClaudeSession = class {
|
|
|
8743
9216
|
* `turnStats`) behaves as it does on the headless path. `lastCallUsage` is
|
|
8744
9217
|
* the newest real call, returned because a caller with no stream parser
|
|
8745
9218
|
* (`askOnce`) has no other way to get the context side.
|
|
9219
|
+
*
|
|
9220
|
+
* A turn that ends any way other than a terminal stop (an interrupt, a
|
|
9221
|
+
* denied permission dialog, `turn_duration`) resolves with that `end` and a
|
|
9222
|
+
* null `stopReason`; only a timeout throws.
|
|
8746
9223
|
*/
|
|
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");
|
|
9224
|
+
async tailTurn(prompt, onLine, perTurnTimeoutMs, onHeartbeat) {
|
|
8751
9225
|
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
9226
|
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) {
|
|
9227
|
+
const { stopReason, end, denied } = await this.runTurnLoop(
|
|
9228
|
+
prompt,
|
|
9229
|
+
(raw, rec) => {
|
|
9230
|
+
onLine(raw);
|
|
9231
|
+
if (rec) usage.add(rec);
|
|
9232
|
+
},
|
|
9233
|
+
timeout,
|
|
9234
|
+
onHeartbeat
|
|
9235
|
+
);
|
|
9236
|
+
if (!end) {
|
|
8793
9237
|
throw new Error(
|
|
8794
9238
|
this.failureMessage(
|
|
8795
9239
|
`turn timed out after ${timeout}ms (no terminal assistant record)`
|
|
@@ -8798,26 +9242,32 @@ var ClaudeSession = class {
|
|
|
8798
9242
|
}
|
|
8799
9243
|
return {
|
|
8800
9244
|
stopReason,
|
|
9245
|
+
end,
|
|
8801
9246
|
usage: usage.turnTotal,
|
|
8802
9247
|
lastCallUsage: usage.lastCall,
|
|
8803
|
-
callCount: usage.callCount
|
|
9248
|
+
callCount: usage.callCount,
|
|
9249
|
+
denied
|
|
8804
9250
|
};
|
|
8805
9251
|
}
|
|
8806
|
-
|
|
8807
|
-
|
|
8808
|
-
|
|
8809
|
-
|
|
8810
|
-
|
|
8811
|
-
|
|
8812
|
-
|
|
8813
|
-
|
|
8814
|
-
|
|
8815
|
-
|
|
8816
|
-
try {
|
|
8817
|
-
this.proc.terminal.close();
|
|
8818
|
-
} catch {
|
|
8819
|
-
}
|
|
9252
|
+
killProcess() {
|
|
9253
|
+
const proc = this.proc;
|
|
9254
|
+
if (!proc) return;
|
|
9255
|
+
try {
|
|
9256
|
+
proc.terminal.write("");
|
|
9257
|
+
} catch {
|
|
9258
|
+
}
|
|
9259
|
+
try {
|
|
9260
|
+
proc.kill();
|
|
9261
|
+
} catch {
|
|
8820
9262
|
}
|
|
9263
|
+
try {
|
|
9264
|
+
proc.terminal.close();
|
|
9265
|
+
} catch {
|
|
9266
|
+
}
|
|
9267
|
+
}
|
|
9268
|
+
dispose() {
|
|
9269
|
+
this.clearScreenTimer();
|
|
9270
|
+
this.killProcess();
|
|
8821
9271
|
this.proc = null;
|
|
8822
9272
|
}
|
|
8823
9273
|
};
|
|
@@ -8891,9 +9341,88 @@ function interactiveExtraArgs(opts) {
|
|
|
8891
9341
|
}
|
|
8892
9342
|
return extraArgs;
|
|
8893
9343
|
}
|
|
9344
|
+
var OpenToolCalls = class {
|
|
9345
|
+
open = /* @__PURE__ */ new Map();
|
|
9346
|
+
add(rec, now = Date.now()) {
|
|
9347
|
+
const content = rec?.message?.content;
|
|
9348
|
+
if (!Array.isArray(content)) return;
|
|
9349
|
+
for (const block of content) {
|
|
9350
|
+
if (rec.type === "assistant" && block?.type === "tool_use" && typeof block.id === "string") {
|
|
9351
|
+
this.open.set(block.id, { name: String(block.name ?? "tool"), startedAt: now });
|
|
9352
|
+
} else if (rec.type === "user" && block?.type === "tool_result") {
|
|
9353
|
+
this.open.delete(block.tool_use_id);
|
|
9354
|
+
}
|
|
9355
|
+
}
|
|
9356
|
+
}
|
|
9357
|
+
newest() {
|
|
9358
|
+
let newest = null;
|
|
9359
|
+
for (const [id, call] of this.open) newest = { id, ...call };
|
|
9360
|
+
return newest;
|
|
9361
|
+
}
|
|
9362
|
+
clear() {
|
|
9363
|
+
this.open.clear();
|
|
9364
|
+
}
|
|
9365
|
+
};
|
|
9366
|
+
function heartbeatFrame(sessionId, openCall, now = Date.now()) {
|
|
9367
|
+
if (openCall) {
|
|
9368
|
+
return JSON.stringify({
|
|
9369
|
+
type: "tool_progress",
|
|
9370
|
+
tool_use_id: `${openCall.id}-heartbeat`,
|
|
9371
|
+
tool_name: openCall.name,
|
|
9372
|
+
parent_tool_use_id: openCall.id,
|
|
9373
|
+
heartbeat: true,
|
|
9374
|
+
elapsed_time_seconds: Math.round((now - openCall.startedAt) / 1e3),
|
|
9375
|
+
session_id: sessionId
|
|
9376
|
+
});
|
|
9377
|
+
}
|
|
9378
|
+
return JSON.stringify({
|
|
9379
|
+
type: "system",
|
|
9380
|
+
subtype: "status",
|
|
9381
|
+
status: "requesting",
|
|
9382
|
+
session_id: sessionId
|
|
9383
|
+
});
|
|
9384
|
+
}
|
|
9385
|
+
function interactiveResultFrame(opts) {
|
|
9386
|
+
const completed = opts.end === "stop" && !!opts.stopReason;
|
|
9387
|
+
let result;
|
|
9388
|
+
let terminalReason = "completed";
|
|
9389
|
+
if (!completed) {
|
|
9390
|
+
if (opts.end === "failed") {
|
|
9391
|
+
terminalReason = "error_during_execution";
|
|
9392
|
+
result = `Interactive transport failed: ${opts.error ?? "unknown error"}`;
|
|
9393
|
+
} else if (opts.end === "interrupted") {
|
|
9394
|
+
terminalReason = "aborted";
|
|
9395
|
+
result = "Interactive transport: the turn was interrupted.";
|
|
9396
|
+
} else {
|
|
9397
|
+
terminalReason = "error_during_execution";
|
|
9398
|
+
result = "Interactive transport: the turn ended without a terminal stop_reason. Output above may be incomplete.";
|
|
9399
|
+
}
|
|
9400
|
+
}
|
|
9401
|
+
return JSON.stringify({
|
|
9402
|
+
type: "result",
|
|
9403
|
+
subtype: completed ? "success" : "error_during_execution",
|
|
9404
|
+
is_error: !completed,
|
|
9405
|
+
stop_reason: completed ? opts.stopReason : null,
|
|
9406
|
+
terminal_reason: terminalReason,
|
|
9407
|
+
result,
|
|
9408
|
+
session_id: opts.sessionId,
|
|
9409
|
+
usage: opts.usage ?? {},
|
|
9410
|
+
// The TUI has no `can_use_tool` channel, so a permission dialog is the
|
|
9411
|
+
// only way it asks; each one the session denied is reported in the
|
|
9412
|
+
// headless field's place, names and ids only (h #g109).
|
|
9413
|
+
permission_denials: opts.denials ?? [],
|
|
9414
|
+
total_cost_usd: null,
|
|
9415
|
+
duration_ms: 0
|
|
9416
|
+
});
|
|
9417
|
+
}
|
|
8894
9418
|
function spawnInteractiveProcess(opts) {
|
|
8895
9419
|
const extraArgs = interactiveExtraArgs(opts);
|
|
9420
|
+
const openCalls = new OpenToolCalls();
|
|
9421
|
+
let denials = [];
|
|
9422
|
+
const lineEmitter = new EventEmitter4();
|
|
9423
|
+
const proc = new EventEmitter4();
|
|
8896
9424
|
const session = new ClaudeSession({
|
|
9425
|
+
...opts.tuning,
|
|
8897
9426
|
cwd: opts.cwd,
|
|
8898
9427
|
cliPath: opts.cliPath,
|
|
8899
9428
|
configDir: opts.configDir,
|
|
@@ -8903,7 +9432,28 @@ function spawnInteractiveProcess(opts) {
|
|
|
8903
9432
|
settingSources: opts.settingSources === void 0 ? null : opts.settingSources,
|
|
8904
9433
|
extraArgs,
|
|
8905
9434
|
ignoreAnthropicApiKey: opts.ignoreAnthropicApiKey,
|
|
8906
|
-
effort: opts.effort ? cliEffortLevel(opts.effort) : void 0
|
|
9435
|
+
effort: opts.effort ? cliEffortLevel(opts.effort) : void 0,
|
|
9436
|
+
resumeSessionId: opts.resumeSessionId,
|
|
9437
|
+
env: opts.env,
|
|
9438
|
+
spawnPty: opts.spawnPty,
|
|
9439
|
+
onScreen: (event) => {
|
|
9440
|
+
if (event.action === "denied") {
|
|
9441
|
+
const call = openCalls.newest();
|
|
9442
|
+
denials.push(call ? { tool_name: call.name, tool_use_id: call.id } : { tool_name: "unknown" });
|
|
9443
|
+
}
|
|
9444
|
+
const data = { sessionId: session.sessionId, screen: event.kind, detail: event.detail };
|
|
9445
|
+
if (event.action === "accepted") log.info("interactive transport accepted a prompt", data);
|
|
9446
|
+
else if (event.action === "fatal") log.error("interactive transport cannot continue", data);
|
|
9447
|
+
else if (event.action === "denied") log.warn("interactive transport denied a permission prompt", data);
|
|
9448
|
+
else log.warn("interactive transport cancelled the usage-limit auto-continue", data);
|
|
9449
|
+
},
|
|
9450
|
+
onExit: (code) => {
|
|
9451
|
+
proc.exitCode = code;
|
|
9452
|
+
proc.signalCode = code === null ? "SIGTERM" : null;
|
|
9453
|
+
log.info("interactive claude exited", { sessionId: session.sessionId, code });
|
|
9454
|
+
proc.emit("exit", proc.exitCode, proc.signalCode);
|
|
9455
|
+
proc.emit("close", proc.exitCode, proc.signalCode);
|
|
9456
|
+
}
|
|
8907
9457
|
});
|
|
8908
9458
|
log.info("prepared interactive claude session", {
|
|
8909
9459
|
cwd: opts.cwd,
|
|
@@ -8912,80 +9462,95 @@ function spawnInteractiveProcess(opts) {
|
|
|
8912
9462
|
model: opts.model,
|
|
8913
9463
|
effort: opts.effort,
|
|
8914
9464
|
sessionId: session.sessionId,
|
|
9465
|
+
resumed: !!opts.resumeSessionId,
|
|
8915
9466
|
jsonlPath: session.jsonlPath
|
|
8916
9467
|
});
|
|
8917
|
-
const lineEmitter = new EventEmitter4();
|
|
8918
|
-
const errorHandlers = /* @__PURE__ */ new Set();
|
|
8919
9468
|
let startPromise = null;
|
|
8920
9469
|
const ensureStarted = () => {
|
|
8921
9470
|
if (!startPromise) startPromise = session.start();
|
|
8922
9471
|
return startPromise;
|
|
8923
9472
|
};
|
|
8924
|
-
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
|
|
8937
|
-
|
|
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
|
-
}
|
|
9473
|
+
let writes = 0;
|
|
9474
|
+
let cancelledThrough = 0;
|
|
9475
|
+
let queued = 0;
|
|
9476
|
+
let chain = Promise.resolve();
|
|
9477
|
+
const runTurn = async (userMsg, turnNumber) => {
|
|
9478
|
+
const current = () => turnNumber === writes;
|
|
9479
|
+
const emit2 = (line) => {
|
|
9480
|
+
if (current()) lineEmitter.emit("line", line);
|
|
9481
|
+
};
|
|
9482
|
+
try {
|
|
9483
|
+
await ensureStarted();
|
|
9484
|
+
if (turnNumber <= cancelledThrough) {
|
|
9485
|
+
log.info("interactive turn cancelled before it started", { sessionId: session.sessionId });
|
|
9486
|
+
return;
|
|
8981
9487
|
}
|
|
8982
|
-
|
|
9488
|
+
openCalls.clear();
|
|
9489
|
+
denials = [];
|
|
9490
|
+
const { stopReason, end, usage, lastCallUsage, callCount, denied } = await session.tailTurn(
|
|
9491
|
+
userMsg,
|
|
9492
|
+
(raw) => {
|
|
9493
|
+
try {
|
|
9494
|
+
openCalls.add(JSON.parse(raw));
|
|
9495
|
+
} catch {
|
|
9496
|
+
}
|
|
9497
|
+
emit2(raw);
|
|
9498
|
+
},
|
|
9499
|
+
void 0,
|
|
9500
|
+
() => emit2(heartbeatFrame(session.sessionId, openCalls.newest()))
|
|
9501
|
+
);
|
|
9502
|
+
log.info("interactive turn ended", {
|
|
9503
|
+
end,
|
|
9504
|
+
stopReason,
|
|
9505
|
+
superseded: !current(),
|
|
9506
|
+
apiCalls: callCount,
|
|
9507
|
+
turnOutputTokens: usage?.output_tokens,
|
|
9508
|
+
turnInputTokens: usage?.input_tokens,
|
|
9509
|
+
turnCacheReadTokens: usage?.cache_read_input_tokens,
|
|
9510
|
+
lastCallInputTokens: lastCallUsage?.input_tokens,
|
|
9511
|
+
lastCallCacheReadTokens: lastCallUsage?.cache_read_input_tokens,
|
|
9512
|
+
lastCallCacheWriteTokens: lastCallUsage?.cache_creation_input_tokens,
|
|
9513
|
+
deniedPrompts: denied.length,
|
|
9514
|
+
cancelled: turnNumber <= cancelledThrough
|
|
9515
|
+
});
|
|
9516
|
+
emit2(
|
|
9517
|
+
interactiveResultFrame({
|
|
9518
|
+
sessionId: session.sessionId,
|
|
9519
|
+
end,
|
|
9520
|
+
stopReason,
|
|
9521
|
+
usage,
|
|
9522
|
+
denials
|
|
9523
|
+
})
|
|
9524
|
+
);
|
|
9525
|
+
} catch (err) {
|
|
9526
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
9527
|
+
log.error("interactive turn failed", { error: e.message, superseded: !current() });
|
|
9528
|
+
if (!current()) return;
|
|
9529
|
+
emit2(
|
|
9530
|
+
interactiveResultFrame({
|
|
9531
|
+
sessionId: existsSync4(session.jsonlPath) ? session.sessionId : void 0,
|
|
9532
|
+
end: "failed",
|
|
9533
|
+
stopReason: null,
|
|
9534
|
+
error: e.message
|
|
9535
|
+
})
|
|
9536
|
+
);
|
|
9537
|
+
if (proc.listenerCount("error") > 0) proc.emit("error", e);
|
|
9538
|
+
else lineEmitter.emit("close");
|
|
9539
|
+
}
|
|
9540
|
+
};
|
|
9541
|
+
const enqueueTurn = (userMsg) => {
|
|
9542
|
+
const turnNumber = ++writes;
|
|
9543
|
+
queued++;
|
|
9544
|
+
chain = chain.then(() => runTurn(userMsg, turnNumber)).finally(() => {
|
|
9545
|
+
queued--;
|
|
9546
|
+
});
|
|
8983
9547
|
};
|
|
8984
|
-
|
|
9548
|
+
Object.assign(proc, {
|
|
8985
9549
|
stdin: {
|
|
9550
|
+
writable: true,
|
|
8986
9551
|
write(chunk) {
|
|
8987
9552
|
const raw = typeof chunk === "string" && chunk.endsWith("\n") ? chunk.slice(0, -1) : chunk;
|
|
8988
|
-
|
|
9553
|
+
enqueueTurn(decodeUserEnvelope(raw));
|
|
8989
9554
|
return true;
|
|
8990
9555
|
},
|
|
8991
9556
|
end() {
|
|
@@ -8995,47 +9560,56 @@ function spawnInteractiveProcess(opts) {
|
|
|
8995
9560
|
stderr: null,
|
|
8996
9561
|
pid: -1,
|
|
8997
9562
|
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
|
-
},
|
|
9563
|
+
exitCode: null,
|
|
9564
|
+
signalCode: null,
|
|
9009
9565
|
kill() {
|
|
9566
|
+
if (!proc.killed) {
|
|
9567
|
+
proc.killed = true;
|
|
9568
|
+
if (opts.systemPromptFile) {
|
|
9569
|
+
void unlink2(opts.systemPromptFile).catch(() => {
|
|
9570
|
+
});
|
|
9571
|
+
}
|
|
9572
|
+
}
|
|
9573
|
+
const started = startPromise !== null;
|
|
9010
9574
|
try {
|
|
9011
9575
|
session.dispose();
|
|
9012
9576
|
} catch {
|
|
9013
9577
|
}
|
|
9014
|
-
if (
|
|
9015
|
-
|
|
9016
|
-
|
|
9578
|
+
if (!started && proc.exitCode === null && proc.signalCode === null) {
|
|
9579
|
+
proc.signalCode = "SIGTERM";
|
|
9580
|
+
proc.emit("exit", null, "SIGTERM");
|
|
9581
|
+
proc.emit("close", null, "SIGTERM");
|
|
9017
9582
|
}
|
|
9018
|
-
proc.killed = true;
|
|
9019
9583
|
return true;
|
|
9020
9584
|
}
|
|
9021
|
-
};
|
|
9585
|
+
});
|
|
9022
9586
|
return {
|
|
9023
9587
|
proc,
|
|
9024
9588
|
lineEmitter,
|
|
9025
9589
|
proxyServer: null,
|
|
9026
9590
|
mcpHash: void 0,
|
|
9027
|
-
systemPromptFile: opts.systemPromptFile
|
|
9591
|
+
systemPromptFile: opts.systemPromptFile,
|
|
9592
|
+
cliPath: opts.cliPath,
|
|
9593
|
+
interactiveControl: {
|
|
9594
|
+
turnRunning: () => queued > 0,
|
|
9595
|
+
// Stops the running turn and every queued one: they were all written
|
|
9596
|
+
// by the turn being aborted, or by one before it.
|
|
9597
|
+
interrupt: (timeoutMs) => {
|
|
9598
|
+
cancelledThrough = writes;
|
|
9599
|
+
return session.interrupt(timeoutMs);
|
|
9600
|
+
}
|
|
9601
|
+
}
|
|
9028
9602
|
};
|
|
9029
9603
|
}
|
|
9030
9604
|
|
|
9031
9605
|
// src/prompts.ts
|
|
9032
|
-
import { readFileSync as
|
|
9033
|
-
import { homedir as
|
|
9606
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
|
|
9607
|
+
import { homedir as homedir8 } from "os";
|
|
9034
9608
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
9035
|
-
import { dirname as dirname6, join as
|
|
9609
|
+
import { dirname as dirname6, join as join10 } from "path";
|
|
9036
9610
|
function readPromptFileIfPresent(path11) {
|
|
9037
9611
|
try {
|
|
9038
|
-
const content =
|
|
9612
|
+
const content = readFileSync7(path11, "utf8").trim();
|
|
9039
9613
|
return content || void 0;
|
|
9040
9614
|
} catch {
|
|
9041
9615
|
return void 0;
|
|
@@ -9044,7 +9618,7 @@ function readPromptFileIfPresent(path11) {
|
|
|
9044
9618
|
function nearestWorkspaceAgentsPrompt(cwd) {
|
|
9045
9619
|
let dir = cwd;
|
|
9046
9620
|
while (true) {
|
|
9047
|
-
const content = readPromptFileIfPresent(
|
|
9621
|
+
const content = readPromptFileIfPresent(join10(dir, "AGENTS.md"));
|
|
9048
9622
|
if (content) return content;
|
|
9049
9623
|
const parent = dirname6(dir);
|
|
9050
9624
|
if (parent === dir) return void 0;
|
|
@@ -9153,8 +9727,8 @@ ${options.compressionSummary.trim()}`
|
|
|
9153
9727
|
for (const s of extraSystemContent) {
|
|
9154
9728
|
if (s.trim()) parts.push(s.trim());
|
|
9155
9729
|
}
|
|
9156
|
-
const configRoot = process.env.XDG_CONFIG_HOME ??
|
|
9157
|
-
const globalAgents = readPromptFileIfPresent(
|
|
9730
|
+
const configRoot = process.env.XDG_CONFIG_HOME ?? join10(homedir8(), ".config");
|
|
9731
|
+
const globalAgents = readPromptFileIfPresent(join10(configRoot, "opencode", "AGENTS.md"));
|
|
9158
9732
|
const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
|
|
9159
9733
|
const forwarded = extraSystemContent.join("\n\n");
|
|
9160
9734
|
const pushGlobal = !!globalAgents && !forwarded.includes(globalAgents);
|
|
@@ -9165,9 +9739,9 @@ ${options.compressionSummary.trim()}`
|
|
|
9165
9739
|
if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
|
|
9166
9740
|
const content = parts.join("\n\n");
|
|
9167
9741
|
if (!content) return void 0;
|
|
9168
|
-
const path11 =
|
|
9742
|
+
const path11 = join10(pluginTmpDir(), `opencode-cc-sys-${randomUUID5()}.md`);
|
|
9169
9743
|
try {
|
|
9170
|
-
|
|
9744
|
+
writeFileSync5(path11, content, { encoding: "utf8", mode: 384 });
|
|
9171
9745
|
return path11;
|
|
9172
9746
|
} catch (err) {
|
|
9173
9747
|
log.warn("failed to write system prompt file", { error: String(err) });
|
|
@@ -9885,8 +10459,8 @@ function synthesizeTitle(prompt) {
|
|
|
9885
10459
|
"summary",
|
|
9886
10460
|
"project"
|
|
9887
10461
|
]);
|
|
9888
|
-
const
|
|
9889
|
-
const picked = (
|
|
10462
|
+
const words2 = source.split(" ").map((word) => word.trim()).filter(Boolean).filter((word) => !stop.has(word.toLowerCase()));
|
|
10463
|
+
const picked = (words2.length > 0 ? words2 : source.split(" ").filter(Boolean)).slice(0, 6).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
9890
10464
|
return picked || "New Session";
|
|
9891
10465
|
}
|
|
9892
10466
|
|
|
@@ -10350,6 +10924,11 @@ function clearStartWatchdog(state) {
|
|
|
10350
10924
|
function onStartWatchdogFire(state) {
|
|
10351
10925
|
state.startWatchdog = null;
|
|
10352
10926
|
if (state.controllerClosed || state.hasReceivedContent || state.hasReceivedProgress) return;
|
|
10927
|
+
const interactive = state.activeProcess?.interactiveControl;
|
|
10928
|
+
if (interactive) {
|
|
10929
|
+
if (interactive.turnRunning()) armStartWatchdog(state);
|
|
10930
|
+
return;
|
|
10931
|
+
}
|
|
10353
10932
|
if (state.respawnAttempted) {
|
|
10354
10933
|
log.error(
|
|
10355
10934
|
"claude process still silent after respawn; ending turn",
|
|
@@ -12170,6 +12749,23 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12170
12749
|
}
|
|
12171
12750
|
}
|
|
12172
12751
|
}
|
|
12752
|
+
if (this.config.resumeAfterRestart !== false && includeHistoryContext && !compactionMode && !forkFromClaudeSessionId && failoverAnswer?.kind !== "switch") {
|
|
12753
|
+
const configDir = resolveConfigDir(this.config.configDir);
|
|
12754
|
+
const resumePoint = findResumePoint({
|
|
12755
|
+
sessionKey: sk,
|
|
12756
|
+
prompt: options.prompt,
|
|
12757
|
+
cliPath,
|
|
12758
|
+
transcriptPath: (id) => join11(configDir, "projects", encodeCwd(cwd), `${id}.jsonl`)
|
|
12759
|
+
});
|
|
12760
|
+
if (resumePoint) {
|
|
12761
|
+
setClaudeSessionId(sk, resumePoint.claudeSessionId);
|
|
12762
|
+
includeHistoryContext = false;
|
|
12763
|
+
log.notice("resuming the claude session from before the restart instead of replaying it", {
|
|
12764
|
+
sessionKey: sk,
|
|
12765
|
+
matchedMessages: resumePoint.matched
|
|
12766
|
+
});
|
|
12767
|
+
}
|
|
12768
|
+
}
|
|
12173
12769
|
if (this.config.forkSessions === true && !compactionMode && !useInteractive) {
|
|
12174
12770
|
recordForkFingerprint(sk, options.prompt, cliPath);
|
|
12175
12771
|
}
|
|
@@ -12463,6 +13059,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12463
13059
|
...self.skillBridgeSpawn(failover)
|
|
12464
13060
|
});
|
|
12465
13061
|
if (stoppedBeforeWork()) return;
|
|
13062
|
+
const resumeSessionId = getClaudeSessionId(sk);
|
|
12466
13063
|
const ap = spawnInteractiveProcess({
|
|
12467
13064
|
cwd,
|
|
12468
13065
|
cliPath,
|
|
@@ -12474,8 +13071,20 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12474
13071
|
permissionsAllow: allow,
|
|
12475
13072
|
systemPromptFile,
|
|
12476
13073
|
ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey,
|
|
12477
|
-
effort: reasoningEffort
|
|
13074
|
+
effort: reasoningEffort,
|
|
13075
|
+
resumeSessionId,
|
|
13076
|
+
// The headless spawn's env, so hygiene, effort and the
|
|
13077
|
+
// agent's prompt cache TTL reach both transports alike.
|
|
13078
|
+
env: claudeSpawnEnv({
|
|
13079
|
+
ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey,
|
|
13080
|
+
effort: reasoningEffort,
|
|
13081
|
+
promptCacheTtl
|
|
13082
|
+
})
|
|
12478
13083
|
});
|
|
13084
|
+
ap.proc.once(
|
|
13085
|
+
"exit",
|
|
13086
|
+
(code) => noteInteractiveProcessExit(sk, ap, code)
|
|
13087
|
+
);
|
|
12479
13088
|
ap.mcpHash = mcp.bridgedHash;
|
|
12480
13089
|
ap.mcpServers = mcp.allEnabledServerNames;
|
|
12481
13090
|
setActiveProcess(sk, ap);
|
|
@@ -12484,6 +13093,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12484
13093
|
state.activeProcess = ap;
|
|
12485
13094
|
log.info("spawned interactive claude session", {
|
|
12486
13095
|
sk,
|
|
13096
|
+
resumed: !!resumeSessionId,
|
|
12487
13097
|
cliPath,
|
|
12488
13098
|
configDir: self.config.configDir,
|
|
12489
13099
|
model: effectiveModelId
|
|
@@ -12723,6 +13333,9 @@ var ClaudeCodeLanguageModel = class {
|
|
|
12723
13333
|
state.autoContinueState.startedAt = Date.now();
|
|
12724
13334
|
const completeResult = (msg) => {
|
|
12725
13335
|
if (state.controllerClosed) return;
|
|
13336
|
+
if (self.config.resumeAfterRestart !== false && !compactionMode && msg.is_error !== true && typeof msg.session_id === "string") {
|
|
13337
|
+
recordResumePoint(sk, msg.session_id, options.prompt, cliPath);
|
|
13338
|
+
}
|
|
12726
13339
|
if (deliverPendingCompletions(state)) {
|
|
12727
13340
|
if (state.drainBuffer.length > 0) drainNow(state);
|
|
12728
13341
|
return;
|
|
@@ -13179,15 +13792,15 @@ var ClaudeCodeLanguageModel = class {
|
|
|
13179
13792
|
|
|
13180
13793
|
// src/cleanup-stale.ts
|
|
13181
13794
|
import {
|
|
13182
|
-
existsSync as
|
|
13183
|
-
mkdirSync as
|
|
13184
|
-
readFileSync as
|
|
13795
|
+
existsSync as existsSync5,
|
|
13796
|
+
mkdirSync as mkdirSync5,
|
|
13797
|
+
readFileSync as readFileSync8,
|
|
13185
13798
|
realpathSync as realpathSync4,
|
|
13186
13799
|
rmSync as rmSync3,
|
|
13187
|
-
writeFileSync as
|
|
13800
|
+
writeFileSync as writeFileSync6
|
|
13188
13801
|
} from "fs";
|
|
13189
|
-
import { homedir as
|
|
13190
|
-
import { dirname as dirname7, join as
|
|
13802
|
+
import { homedir as homedir9 } from "os";
|
|
13803
|
+
import { dirname as dirname7, join as join12, resolve as resolve5 } from "path";
|
|
13191
13804
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
13192
13805
|
var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
|
|
13193
13806
|
var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
|
|
@@ -13195,20 +13808,20 @@ var alreadyRan = false;
|
|
|
13195
13808
|
function candidateCacheRoots() {
|
|
13196
13809
|
const xdg = process.env.XDG_CACHE_HOME;
|
|
13197
13810
|
return [
|
|
13198
|
-
xdg ?
|
|
13199
|
-
|
|
13200
|
-
|
|
13811
|
+
xdg ? join12(xdg, "opencode") : null,
|
|
13812
|
+
join12(homedir9(), ".cache", "opencode"),
|
|
13813
|
+
join12(homedir9(), "Library", "Caches", "opencode")
|
|
13201
13814
|
].filter((p) => Boolean(p));
|
|
13202
13815
|
}
|
|
13203
13816
|
function userOpencodeJsonPath() {
|
|
13204
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME ??
|
|
13205
|
-
return
|
|
13817
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME ?? join12(homedir9(), ".config");
|
|
13818
|
+
return join12(xdgConfig, "opencode", "opencode.json");
|
|
13206
13819
|
}
|
|
13207
13820
|
function userIntendsToUseUnscoped() {
|
|
13208
13821
|
const cfg = userOpencodeJsonPath();
|
|
13209
|
-
if (!
|
|
13822
|
+
if (!existsSync5(cfg)) return false;
|
|
13210
13823
|
try {
|
|
13211
|
-
const json = JSON.parse(
|
|
13824
|
+
const json = JSON.parse(readFileSync8(cfg, "utf8"));
|
|
13212
13825
|
const plugins = json.plugin;
|
|
13213
13826
|
if (!Array.isArray(plugins)) return false;
|
|
13214
13827
|
return plugins.some(
|
|
@@ -13229,19 +13842,19 @@ function ourLoadedDir() {
|
|
|
13229
13842
|
function ourVersion(dir) {
|
|
13230
13843
|
if (!dir) return null;
|
|
13231
13844
|
try {
|
|
13232
|
-
const pkg = JSON.parse(
|
|
13845
|
+
const pkg = JSON.parse(readFileSync8(join12(dir, "package.json"), "utf8"));
|
|
13233
13846
|
return typeof pkg.version === "string" && pkg.version ? pkg.version : null;
|
|
13234
13847
|
} catch {
|
|
13235
13848
|
return null;
|
|
13236
13849
|
}
|
|
13237
13850
|
}
|
|
13238
13851
|
function markerPath() {
|
|
13239
|
-
const stateRoot = process.env.XDG_STATE_HOME ??
|
|
13240
|
-
return
|
|
13852
|
+
const stateRoot = process.env.XDG_STATE_HOME ?? join12(homedir9(), ".local", "state");
|
|
13853
|
+
return join12(stateRoot, "opencode-claude-code-plugin", "cleanup-stale.json");
|
|
13241
13854
|
}
|
|
13242
13855
|
function markerVersion() {
|
|
13243
13856
|
try {
|
|
13244
|
-
const marker = JSON.parse(
|
|
13857
|
+
const marker = JSON.parse(readFileSync8(markerPath(), "utf8"));
|
|
13245
13858
|
return typeof marker.version === "string" ? marker.version : null;
|
|
13246
13859
|
} catch {
|
|
13247
13860
|
return null;
|
|
@@ -13250,8 +13863,8 @@ function markerVersion() {
|
|
|
13250
13863
|
function writeMarker(version) {
|
|
13251
13864
|
const file = markerPath();
|
|
13252
13865
|
try {
|
|
13253
|
-
|
|
13254
|
-
|
|
13866
|
+
mkdirSync5(dirname7(file), { recursive: true });
|
|
13867
|
+
writeFileSync6(
|
|
13255
13868
|
file,
|
|
13256
13869
|
JSON.stringify({ version, at: (/* @__PURE__ */ new Date()).toISOString() }, null, 2) + "\n",
|
|
13257
13870
|
"utf8"
|
|
@@ -13285,20 +13898,20 @@ function cleanupStaleUnscopedInstall() {
|
|
|
13285
13898
|
if (version) writeMarker(version);
|
|
13286
13899
|
}
|
|
13287
13900
|
function cleanupOne(cacheRoot, ourDir) {
|
|
13288
|
-
if (!
|
|
13289
|
-
const stalePath =
|
|
13290
|
-
if (!
|
|
13901
|
+
if (!existsSync5(cacheRoot)) return;
|
|
13902
|
+
const stalePath = join12(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
|
|
13903
|
+
if (!existsSync5(stalePath)) return;
|
|
13291
13904
|
let realStalePath = stalePath;
|
|
13292
13905
|
try {
|
|
13293
13906
|
realStalePath = realpathSync4(stalePath);
|
|
13294
13907
|
} catch {
|
|
13295
13908
|
}
|
|
13296
13909
|
if (ourDir && realStalePath === ourDir) return;
|
|
13297
|
-
const pkgJsonPath =
|
|
13298
|
-
if (!
|
|
13910
|
+
const pkgJsonPath = join12(stalePath, "package.json");
|
|
13911
|
+
if (!existsSync5(pkgJsonPath)) return;
|
|
13299
13912
|
let pkg = {};
|
|
13300
13913
|
try {
|
|
13301
|
-
pkg = JSON.parse(
|
|
13914
|
+
pkg = JSON.parse(readFileSync8(pkgJsonPath, "utf8"));
|
|
13302
13915
|
} catch {
|
|
13303
13916
|
return;
|
|
13304
13917
|
}
|
|
@@ -13314,13 +13927,13 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
13314
13927
|
});
|
|
13315
13928
|
return;
|
|
13316
13929
|
}
|
|
13317
|
-
const cachePkgJson =
|
|
13318
|
-
if (!
|
|
13930
|
+
const cachePkgJson = join12(cacheRoot, "package.json");
|
|
13931
|
+
if (!existsSync5(cachePkgJson)) return;
|
|
13319
13932
|
try {
|
|
13320
|
-
const cfg = JSON.parse(
|
|
13933
|
+
const cfg = JSON.parse(readFileSync8(cachePkgJson, "utf8"));
|
|
13321
13934
|
if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
|
|
13322
13935
|
delete cfg.dependencies[STALE_PACKAGE_NAME];
|
|
13323
|
-
|
|
13936
|
+
writeFileSync6(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
|
|
13324
13937
|
log.info("cleanup-stale: pruned dep from cache package.json");
|
|
13325
13938
|
}
|
|
13326
13939
|
} catch (err) {
|
|
@@ -13981,6 +14594,7 @@ function createClaudeCode(settings = {}) {
|
|
|
13981
14594
|
bridgeSkipNativeSkills: settings.bridgeSkipNativeSkills !== false,
|
|
13982
14595
|
turnStats: settings.turnStats === true,
|
|
13983
14596
|
forkSessions: settings.forkSessions === true,
|
|
14597
|
+
resumeAfterRestart: settings.resumeAfterRestart !== false,
|
|
13984
14598
|
interactive: settings.interactive,
|
|
13985
14599
|
interactiveBypass: settings.interactiveBypass,
|
|
13986
14600
|
interactiveAllowTools: settings.interactiveAllowTools,
|
|
@@ -14066,12 +14680,12 @@ function configModelsForProvider(providerModels, providerID, modelSuffix) {
|
|
|
14066
14680
|
}
|
|
14067
14681
|
return models;
|
|
14068
14682
|
}
|
|
14069
|
-
function
|
|
14683
|
+
function isRecord5(value) {
|
|
14070
14684
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14071
14685
|
}
|
|
14072
14686
|
function completeUserModel(id, entry, providerID) {
|
|
14073
|
-
const record =
|
|
14074
|
-
const runtimeShaped =
|
|
14687
|
+
const record = isRecord5(entry) ? entry : {};
|
|
14688
|
+
const runtimeShaped = isRecord5(record.capabilities) && isRecord5(record.capabilities.input) && isRecord5(record.capabilities.output) && isRecord5(record.cost) && isRecord5(record.cost.cache) && isRecord5(record.api);
|
|
14075
14689
|
if (runtimeShaped) {
|
|
14076
14690
|
return toConfigModel({ ...record, providerID });
|
|
14077
14691
|
}
|
|
@@ -14084,8 +14698,8 @@ function completeUserModel(id, entry, providerID) {
|
|
|
14084
14698
|
const merged = { ...base, ...record, id };
|
|
14085
14699
|
for (const key of ["cost", "limit", "variants"]) {
|
|
14086
14700
|
merged[key] = {
|
|
14087
|
-
...
|
|
14088
|
-
...
|
|
14701
|
+
...isRecord5(base[key]) ? base[key] : {},
|
|
14702
|
+
...isRecord5(record[key]) ? record[key] : {}
|
|
14089
14703
|
};
|
|
14090
14704
|
}
|
|
14091
14705
|
return merged;
|
|
@@ -14182,7 +14796,7 @@ async function buildAgentRegistry(config) {
|
|
|
14182
14796
|
typeof configuredTtl === "string" ? configuredTtl : void 0
|
|
14183
14797
|
);
|
|
14184
14798
|
setProviderFallbackModels(parseFallbackModelList(options?.fallbackModels));
|
|
14185
|
-
const
|
|
14799
|
+
const records2 = await readAgentMarkdownRecords(
|
|
14186
14800
|
agentDirectories(
|
|
14187
14801
|
process.env.HOME ?? process.env.USERPROFILE,
|
|
14188
14802
|
getOpencodeProjectDirectory()
|
|
@@ -14197,18 +14811,18 @@ async function buildAgentRegistry(config) {
|
|
|
14197
14811
|
const declaredChain = parseFallbackModelList(
|
|
14198
14812
|
agent.fallbackModels ?? bag.fallbackModels
|
|
14199
14813
|
);
|
|
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 :
|
|
14814
|
+
records2[name] = {
|
|
14815
|
+
mode: pick("mode") ?? records2[name]?.mode,
|
|
14816
|
+
model: pick("model") ?? records2[name]?.model,
|
|
14817
|
+
forceModel: pick("forceModel") ?? records2[name]?.forceModel,
|
|
14818
|
+
reasoningEffort: pick("reasoningEffort") ?? records2[name]?.reasoningEffort,
|
|
14819
|
+
cacheTtl: pick("cacheTtl") ?? records2[name]?.cacheTtl,
|
|
14820
|
+
fallbackModels: declaredChain.length ? declaredChain : records2[name]?.fallbackModels
|
|
14207
14821
|
};
|
|
14208
14822
|
}
|
|
14209
|
-
setAgentRegistry(
|
|
14823
|
+
setAgentRegistry(records2);
|
|
14210
14824
|
log.debug("agent registry built", {
|
|
14211
|
-
agents: Object.keys(
|
|
14825
|
+
agents: Object.keys(records2).length,
|
|
14212
14826
|
defaultSubagentModel: getDefaultSubagentModel()
|
|
14213
14827
|
});
|
|
14214
14828
|
}
|