@khalilgharbaoui/opencode-claude-code-plugin 0.34.0 → 0.35.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 +36 -3
- package/dist/index.js +1023 -84
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/skills/claude-code-plugin/SKILL.md +46 -1
package/dist/index.js
CHANGED
|
@@ -197,6 +197,9 @@ function configureLogger(input) {
|
|
|
197
197
|
function resolvedLogFile() {
|
|
198
198
|
return join(activeConfig.dir ?? DEFAULT_DIR, "plugin.log");
|
|
199
199
|
}
|
|
200
|
+
function describeLogFile() {
|
|
201
|
+
return { path: resolvedLogFile(), enabled: activeConfig.file && !fileLoggingDisabled };
|
|
202
|
+
}
|
|
200
203
|
function rotateIfNeeded(logFile) {
|
|
201
204
|
try {
|
|
202
205
|
const stat = statSync(logFile);
|
|
@@ -830,6 +833,135 @@ function reportUnrecognizedModel(stderr) {
|
|
|
830
833
|
warnedUnrecognizedModels.add(model);
|
|
831
834
|
log.warn(message, { model, ...floor ? { cliFloor: floor } : {} });
|
|
832
835
|
}
|
|
836
|
+
var HOOK_SUBTYPES = {
|
|
837
|
+
hook_started: "started",
|
|
838
|
+
hook_progress: "progress",
|
|
839
|
+
hook_response: "response"
|
|
840
|
+
};
|
|
841
|
+
function parseHookEvent(msg) {
|
|
842
|
+
if (msg.type !== "system") return null;
|
|
843
|
+
const phase = msg.subtype ? HOOK_SUBTYPES[msg.subtype] : void 0;
|
|
844
|
+
if (!phase) return null;
|
|
845
|
+
const raw = msg;
|
|
846
|
+
const hookName = str(raw.hook_name);
|
|
847
|
+
const hookEvent = str(raw.hook_event);
|
|
848
|
+
if (!hookName || !hookEvent) return null;
|
|
849
|
+
return {
|
|
850
|
+
phase,
|
|
851
|
+
hookId: str(raw.hook_id) ?? "unknown",
|
|
852
|
+
hookName,
|
|
853
|
+
hookEvent,
|
|
854
|
+
exitCode: num(raw.exit_code),
|
|
855
|
+
outcome: str(raw.outcome),
|
|
856
|
+
stderr: str(raw.stderr)
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
function isHookFailure(hook) {
|
|
860
|
+
if (hook.phase !== "response") return false;
|
|
861
|
+
if (hook.outcome === "error") return true;
|
|
862
|
+
if (hook.outcome === "cancelled" || hook.outcome === "success") return false;
|
|
863
|
+
return hook.exitCode !== void 0 && hook.exitCode !== 0;
|
|
864
|
+
}
|
|
865
|
+
var HOOK_STDERR_CAP = 200;
|
|
866
|
+
function describeHookFailure(hook) {
|
|
867
|
+
const how = hook.exitCode !== void 0 ? `exited ${hook.exitCode}` : `reported \`${hook.outcome ?? "error"}\``;
|
|
868
|
+
const tail = hook.stderr?.trim();
|
|
869
|
+
const detail = tail ? ` It wrote to stderr: ${tail.length > HOOK_STDERR_CAP ? `${tail.slice(0, HOOK_STDERR_CAP)}...` : tail}` : " It wrote nothing to stderr.";
|
|
870
|
+
return `Claude Code's "${hook.hookName}" hook (${hook.hookEvent}) ${how}, so whatever it contributes to the session is missing and every turn on this process runs without it. Fix or remove the hook in your Claude Code settings.${detail}`;
|
|
871
|
+
}
|
|
872
|
+
var warnedHookFailures = /* @__PURE__ */ new Set();
|
|
873
|
+
var lastHookFailures = /* @__PURE__ */ new Map();
|
|
874
|
+
function snapshotHookFailures() {
|
|
875
|
+
return [...lastHookFailures.values()];
|
|
876
|
+
}
|
|
877
|
+
function reportHookEvent(msg) {
|
|
878
|
+
const hook = parseHookEvent(msg);
|
|
879
|
+
if (!hook) return;
|
|
880
|
+
const data = {
|
|
881
|
+
hook: hook.hookName,
|
|
882
|
+
event: hook.hookEvent,
|
|
883
|
+
...hook.exitCode === void 0 ? {} : { exitCode: hook.exitCode },
|
|
884
|
+
...hook.outcome ? { outcome: hook.outcome } : {}
|
|
885
|
+
};
|
|
886
|
+
if (!isHookFailure(hook)) {
|
|
887
|
+
log.debug(`claude code hook ${hook.phase}`, data);
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
const failure = {
|
|
891
|
+
hookName: hook.hookName,
|
|
892
|
+
hookEvent: hook.hookEvent,
|
|
893
|
+
exitCode: hook.exitCode,
|
|
894
|
+
outcome: hook.outcome,
|
|
895
|
+
stderr: hook.stderr?.trim().slice(0, HOOK_STDERR_CAP) ?? ""
|
|
896
|
+
};
|
|
897
|
+
lastHookFailures.set(`${hook.hookEvent}:${hook.hookName}`, failure);
|
|
898
|
+
const key = `${hook.hookEvent}:${hook.hookName}:${hook.outcome ?? "?"}:${hook.exitCode ?? "?"}`;
|
|
899
|
+
const message = describeHookFailure(hook);
|
|
900
|
+
if (warnedHookFailures.has(key)) {
|
|
901
|
+
log.debug(message, data);
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
warnedHookFailures.add(key);
|
|
905
|
+
log.warn(message, data);
|
|
906
|
+
}
|
|
907
|
+
function parseToolProgress(msg) {
|
|
908
|
+
if (msg.type !== "tool_progress") return null;
|
|
909
|
+
const raw = msg;
|
|
910
|
+
const frameToolUseId = str(raw.tool_use_id);
|
|
911
|
+
const toolName = str(raw.tool_name);
|
|
912
|
+
const elapsedSeconds = num(raw.elapsed_time_seconds);
|
|
913
|
+
if (!frameToolUseId || !toolName || elapsedSeconds === void 0) return null;
|
|
914
|
+
const retry = isRecord(raw.subagent_retry) ? raw.subagent_retry : void 0;
|
|
915
|
+
return {
|
|
916
|
+
toolUseId: str(raw.parent_tool_use_id) ?? frameToolUseId,
|
|
917
|
+
frameToolUseId,
|
|
918
|
+
toolName,
|
|
919
|
+
elapsedSeconds,
|
|
920
|
+
heartbeat: raw.heartbeat === true,
|
|
921
|
+
taskId: str(raw.task_id),
|
|
922
|
+
subagentType: str(raw.subagent_type),
|
|
923
|
+
subagentRetry: retry ? {
|
|
924
|
+
agentId: str(retry.agent_id) ?? "unknown",
|
|
925
|
+
attempt: num(retry.attempt) ?? 0,
|
|
926
|
+
maxRetries: num(retry.max_retries) ?? 0,
|
|
927
|
+
errorStatus: num(retry.error_status),
|
|
928
|
+
errorCategory: str(retry.error_category) ?? "unknown"
|
|
929
|
+
} : void 0
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
function describeToolProgress(progress) {
|
|
933
|
+
if (progress.subagentRetry) {
|
|
934
|
+
const retry = progress.subagentRetry;
|
|
935
|
+
return `Claude Code is retrying a subagent's API call (${retry.errorCategory}${retry.errorStatus === void 0 ? "" : ` ${retry.errorStatus}`}), attempt ${retry.attempt} of ${retry.maxRetries}. The turn is still running.`;
|
|
936
|
+
}
|
|
937
|
+
return `claude code tool still running: ${progress.toolName} at ${progress.elapsedSeconds}s`;
|
|
938
|
+
}
|
|
939
|
+
var reportedSubagentRetries = /* @__PURE__ */ new Set();
|
|
940
|
+
function reportToolProgress(msg) {
|
|
941
|
+
const progress = parseToolProgress(msg);
|
|
942
|
+
if (!progress) return;
|
|
943
|
+
const data = {
|
|
944
|
+
tool: progress.toolName,
|
|
945
|
+
toolUseId: progress.toolUseId,
|
|
946
|
+
elapsedSeconds: progress.elapsedSeconds,
|
|
947
|
+
...progress.taskId ? { taskId: progress.taskId } : {},
|
|
948
|
+
...progress.subagentType ? { subagentType: progress.subagentType } : {}
|
|
949
|
+
};
|
|
950
|
+
const message = describeToolProgress(progress);
|
|
951
|
+
if (!progress.subagentRetry) {
|
|
952
|
+
log.info(message, data);
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
const retry = progress.subagentRetry;
|
|
956
|
+
const key = `${retry.agentId}:${retry.errorCategory}`;
|
|
957
|
+
const retryData = { ...data, ...retry };
|
|
958
|
+
if (reportedSubagentRetries.has(key)) {
|
|
959
|
+
log.debug(message, retryData);
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
reportedSubagentRetries.add(key);
|
|
963
|
+
log.notice(message, retryData);
|
|
964
|
+
}
|
|
833
965
|
|
|
834
966
|
// src/plan-mode-question.ts
|
|
835
967
|
var QUESTION_TOOL_NAME = "question";
|
|
@@ -3504,16 +3636,18 @@ async function getRuntimeMcpStatus(options = {}) {
|
|
|
3504
3636
|
const current = await fetchRuntimeMcpStatus();
|
|
3505
3637
|
const budgetMs = options.waitForPendingMs ?? 0;
|
|
3506
3638
|
if (!current || budgetMs <= 0) return current;
|
|
3507
|
-
return waitForPendingMcpServers(current, budgetMs);
|
|
3639
|
+
return waitForPendingMcpServers(current, budgetMs, options.signal);
|
|
3508
3640
|
}
|
|
3509
|
-
async function waitForPendingMcpServers(first, budgetMs) {
|
|
3641
|
+
async function waitForPendingMcpServers(first, budgetMs, signal) {
|
|
3510
3642
|
let current = first;
|
|
3511
3643
|
const waitedFor = pendingMcpServers(current);
|
|
3512
3644
|
if (waitedFor.length === 0) return current;
|
|
3513
3645
|
const started = Date.now();
|
|
3514
3646
|
for (; ; ) {
|
|
3515
3647
|
if (Date.now() - started >= budgetMs) break;
|
|
3648
|
+
if (signal?.aborted) break;
|
|
3516
3649
|
await new Promise((resolve6) => setTimeout(resolve6, MCP_CONNECT_POLL_MS));
|
|
3650
|
+
if (signal?.aborted) break;
|
|
3517
3651
|
const next = await fetchRuntimeMcpStatus();
|
|
3518
3652
|
if (!next) break;
|
|
3519
3653
|
current = next;
|
|
@@ -3524,7 +3658,8 @@ async function waitForPendingMcpServers(first, budgetMs) {
|
|
|
3524
3658
|
waitedFor,
|
|
3525
3659
|
stillPending,
|
|
3526
3660
|
budgetMs,
|
|
3527
|
-
elapsedMs: Date.now() - started
|
|
3661
|
+
elapsedMs: Date.now() - started,
|
|
3662
|
+
aborted: signal?.aborted === true
|
|
3528
3663
|
});
|
|
3529
3664
|
return current;
|
|
3530
3665
|
}
|
|
@@ -4993,6 +5128,621 @@ async function handleBtwCommand(client, input, options = {}) {
|
|
|
4993
5128
|
}
|
|
4994
5129
|
}
|
|
4995
5130
|
|
|
5131
|
+
// src/diagnostic-bundle.ts
|
|
5132
|
+
import { createHash as createHash3, randomBytes as randomBytes2 } from "crypto";
|
|
5133
|
+
import { openSync, readSync, closeSync, fstatSync, realpathSync } from "fs";
|
|
5134
|
+
import { homedir as homedir4 } from "os";
|
|
5135
|
+
|
|
5136
|
+
// src/log-messages.ts
|
|
5137
|
+
var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
|
|
5138
|
+
"ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN detected; claude may bill as pay-as-you-go API usage instead of your subscription. Set provider option `ignoreAnthropicApiKey: true` to force subscription auth.",
|
|
5139
|
+
"ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN detected; stripping it from claude spawns (ignoreAnthropicApiKey) so requests use your subscription auth, not pay-as-you-go API billing.",
|
|
5140
|
+
"Code Mode execute not forwarded: no model-visible input schema",
|
|
5141
|
+
"Code Mode execute not forwarded: read-only permission preset",
|
|
5142
|
+
"MCP tool not routed through the proxy: another proxy tool already holds that name, and it keeps it",
|
|
5143
|
+
"account failover declined; ending the turn",
|
|
5144
|
+
"agent effort override refused: unknown level",
|
|
5145
|
+
"agent model override refused: unknown model",
|
|
5146
|
+
"agent prompt cache ttl refused: unknown value",
|
|
5147
|
+
"auto-continuation stopped",
|
|
5148
|
+
"auto-continuing incomplete claude result",
|
|
5149
|
+
"btw: a turn is running but no claude process appeared for it",
|
|
5150
|
+
"btw: early aside failed; the message will ask again",
|
|
5151
|
+
"cannot interrupt this transport; waiting for the turn to end",
|
|
5152
|
+
"claude --version output unparseable",
|
|
5153
|
+
"claude already registers a different skill under this name; not bridging ours",
|
|
5154
|
+
"claude cli < 2.1.142 detected; Opus 4.7 thinking summaries unavailable. Run `npm i -g @anthropic-ai/claude-code` to upgrade.",
|
|
5155
|
+
"claude cli does not support --plugin-dir; opencode skills will not be bridged. Run `npm i -g @anthropic-ai/claude-code` to upgrade.",
|
|
5156
|
+
"claude code compacted its own context",
|
|
5157
|
+
"claude code reset its conversation",
|
|
5158
|
+
"claude finished the turn without a reply",
|
|
5159
|
+
"claude kept missing the probe deadline; keeping the conservative answer for this process",
|
|
5160
|
+
"claude process belongs to another account; starting fresh",
|
|
5161
|
+
"claude process closed without a result",
|
|
5162
|
+
"claude process did not exit; starting a fresh session",
|
|
5163
|
+
"claude process error",
|
|
5164
|
+
"claude process stdin error",
|
|
5165
|
+
"claude process still silent after respawn; ending turn",
|
|
5166
|
+
"claude session ID error, clearing session",
|
|
5167
|
+
"claude-code doctor failed to build its report",
|
|
5168
|
+
"claude-code plugin ready",
|
|
5169
|
+
"cleanup-stale: cache package.json update failed",
|
|
5170
|
+
"cleanup-stale: could not record the sweep marker",
|
|
5171
|
+
"cleanup-stale: error processing cache root",
|
|
5172
|
+
"cleanup-stale: rmSync failed",
|
|
5173
|
+
"delivering proxy results after interrupted continuation",
|
|
5174
|
+
"draining proxy calls queued between turns",
|
|
5175
|
+
"empty user content; sending sentinel to satisfy CLI",
|
|
5176
|
+
"every claude process is mid-turn; skipping LRU eviction",
|
|
5177
|
+
"failed to abort opencode session",
|
|
5178
|
+
"failed to build the agent registry for opencode 2",
|
|
5179
|
+
"failed to close the proxy server of an aborted turn",
|
|
5180
|
+
"failed to detect claude cli version",
|
|
5181
|
+
"failed to expand account provider",
|
|
5182
|
+
"failed to fetch opencode MCP runtime status",
|
|
5183
|
+
"failed to fetch opencode session directory",
|
|
5184
|
+
"failed to fetch opencode session messages",
|
|
5185
|
+
"failed to fetch opencode session parent",
|
|
5186
|
+
"failed to fetch opencode tool list",
|
|
5187
|
+
"failed to parse opencode config",
|
|
5188
|
+
"failed to prepare the failover account runtime; staying put",
|
|
5189
|
+
"failed to probe claude cli flag support",
|
|
5190
|
+
"failed to re-send envelope after respawn",
|
|
5191
|
+
"failed to set up doStream",
|
|
5192
|
+
"failed to stage opencode skill plugin dir",
|
|
5193
|
+
"failed to symlink shared capabilities; continuing anyway",
|
|
5194
|
+
"failed to write bridged MCP config",
|
|
5195
|
+
"failed to write control response",
|
|
5196
|
+
"failed to write interrupt control request",
|
|
5197
|
+
"failed to write system prompt file",
|
|
5198
|
+
"fallback model refused: unknown model",
|
|
5199
|
+
"falling back to the next model in the chain",
|
|
5200
|
+
"fast mode is in cooldown after a rate limit; this turn runs at standard speed and is billed at standard Opus rates, not the fast price shown in the model picker.",
|
|
5201
|
+
"file part without data, skipping",
|
|
5202
|
+
"ignoring a failover reset time that is not in the future",
|
|
5203
|
+
"ignoring unknown proxyOpencodeTools entries",
|
|
5204
|
+
"ignoring unknown proxyTools entries",
|
|
5205
|
+
"interactive permissionMode bypassPermissions ignored: Claude Code prompts for confirmation in the TUI",
|
|
5206
|
+
"interactive system prompt disabled; opencode agent prompts will not be appended",
|
|
5207
|
+
"interactive transport dropped non-text content blocks",
|
|
5208
|
+
"interactive turn failed",
|
|
5209
|
+
"interactiveBypass ignored: Claude Code prompts for bypassPermissions confirmation in the interactive TUI",
|
|
5210
|
+
"malformed data URI, skipping file part",
|
|
5211
|
+
"no active process to respawn (start watchdog); ending turn",
|
|
5212
|
+
"no proxyTools entry was recognised; nothing will be proxied this turn",
|
|
5213
|
+
"no stdout after envelope write; respawning claude process to resume conversation",
|
|
5214
|
+
"opencode 2 event subscription ended; deleted sessions keep their claude process until idle eviction",
|
|
5215
|
+
"pending proxy call arrived after stream close; rejecting",
|
|
5216
|
+
'permissionMode "plan" is enforced: claude cannot edit files or run commands, and --dangerously-skip-permissions is deliberately not passed so it stays that way. Headless Claude Code is not offered an ExitPlanMode tool, so nothing releases plan mode mid-session; approving a plan in chat does not unlock writes. Leaving plan mode means changing the config and restarting opencode.',
|
|
5217
|
+
`permissionPreset "read-only": this claude CLI has no --permission-prompts (2.1.263+), so permission requests are denied by the plugin's own can_use_tool handler instead of by the CLI. Same outcome, one layer fewer.`,
|
|
5218
|
+
'permissionPreset "read-only": this claude CLI has no --restricted (2.1.258+), so file reads are not confined to the working directory. The mutating tools are still refused via --disallowedTools and every permission request is denied.',
|
|
5219
|
+
"previous turn did not stop in time; this turn may see stale output",
|
|
5220
|
+
"previous turn still in flight; interrupting it",
|
|
5221
|
+
"process error",
|
|
5222
|
+
"provider model entry is partial; completing it from the pass-through template",
|
|
5223
|
+
"proxy call past its deadline, but opencode is still serving it; waiting",
|
|
5224
|
+
"proxy call still waiting, deadline approaching",
|
|
5225
|
+
"proxy call still waiting, no deadline",
|
|
5226
|
+
"proxy-mcp client closed a tool call before its result",
|
|
5227
|
+
"proxy-mcp dropping result for a closed tool call",
|
|
5228
|
+
"proxy-mcp interceptor failed",
|
|
5229
|
+
"proxy-mcp rejected a request",
|
|
5230
|
+
"proxy-mcp tool call timed out",
|
|
5231
|
+
"proxyOpencodeTools entry dropped: a proxy tool already holds that name, and it keeps it",
|
|
5232
|
+
"proxyOpencodeTools is set but opencode's tool registry did not answer; forwarding nothing this spawn",
|
|
5233
|
+
"refusing the pid-named scratch directory",
|
|
5234
|
+
"rejected pending proxy call",
|
|
5235
|
+
"remote URL images are not supported by Claude CLI, skipping",
|
|
5236
|
+
"replaying stdout the child emitted between turns",
|
|
5237
|
+
"result fallback timer fired \u2014 closing stream without result event",
|
|
5238
|
+
"shared Claude capability already exists; leaving untouched",
|
|
5239
|
+
"skipping MCP server with unknown type",
|
|
5240
|
+
"skipping local MCP server with no command",
|
|
5241
|
+
"skipping remote MCP server with no url",
|
|
5242
|
+
"stale scratch directory sweep failed",
|
|
5243
|
+
"task_batch resolving with child results missing",
|
|
5244
|
+
"timed out pending proxy call",
|
|
5245
|
+
"turn abort listener threw",
|
|
5246
|
+
"two different skills share a name; bridging the one the nearest root claimed",
|
|
5247
|
+
"unknown permissionPreset; no preset applied and every permission setting is left exactly as configured",
|
|
5248
|
+
"unsupported file part data type",
|
|
5249
|
+
"unsupported media type for Claude image block, skipping"
|
|
5250
|
+
]);
|
|
5251
|
+
|
|
5252
|
+
// src/diagnostic-bundle.ts
|
|
5253
|
+
var BUNDLE_ARGUMENTS = /* @__PURE__ */ new Set(["bundle", "issue", "report", "paste"]);
|
|
5254
|
+
function wantsDiagnosticBundle(argument) {
|
|
5255
|
+
return BUNDLE_ARGUMENTS.has(argument.trim().toLowerCase());
|
|
5256
|
+
}
|
|
5257
|
+
var BUNDLE_LEVELS = /* @__PURE__ */ new Set(["NOTICE", "WARN", "ERROR"]);
|
|
5258
|
+
var BUNDLE_MAX_LINES = 120;
|
|
5259
|
+
var BUNDLE_MAX_BYTES = 24e3;
|
|
5260
|
+
var BUNDLE_TAIL_BYTES = 512 * 1024;
|
|
5261
|
+
var BUNDLE_MAX_VALUE_CHARS = 200;
|
|
5262
|
+
var BUNDLE_MAX_ENUM_CHARS = 40;
|
|
5263
|
+
var BUNDLE_MAX_VERSION_CHARS = 60;
|
|
5264
|
+
var BUNDLE_MAX_NAME_CHARS = 64;
|
|
5265
|
+
var BUNDLE_MAX_ARRAY_ITEMS = 20;
|
|
5266
|
+
var BUNDLE_DATA_ALLOWLIST = {
|
|
5267
|
+
// booleans: every one of these is a decision the plugin made
|
|
5268
|
+
applied: "flag",
|
|
5269
|
+
attached: "flag",
|
|
5270
|
+
bridgeSkipNativeSkills: "flag",
|
|
5271
|
+
channelClosed: "flag",
|
|
5272
|
+
compaction: "flag",
|
|
5273
|
+
compactionMode: "flag",
|
|
5274
|
+
emitted: "flag",
|
|
5275
|
+
enabled: "flag",
|
|
5276
|
+
executed: "flag",
|
|
5277
|
+
isUsingOverage: "flag",
|
|
5278
|
+
hadProxyActivity: "flag",
|
|
5279
|
+
hadReasoning: "flag",
|
|
5280
|
+
hadStreamThinking: "flag",
|
|
5281
|
+
hadToolActivity: "flag",
|
|
5282
|
+
hasActiveProcess: "flag",
|
|
5283
|
+
hasInput: "flag",
|
|
5284
|
+
hasText: "flag",
|
|
5285
|
+
inFlight: "flag",
|
|
5286
|
+
anthropicApiKeyInEnv: "flag",
|
|
5287
|
+
includeHistoryContext: "flag",
|
|
5288
|
+
interactive: "flag",
|
|
5289
|
+
interactiveTransport: "flag",
|
|
5290
|
+
isError: "flag",
|
|
5291
|
+
planModeQuestion: "flag",
|
|
5292
|
+
kept: "flag",
|
|
5293
|
+
listsAgentTypes: "flag",
|
|
5294
|
+
opencodeHasQuestion: "flag",
|
|
5295
|
+
registryResolved: "flag",
|
|
5296
|
+
sse: "flag",
|
|
5297
|
+
strictMcpConfig: "flag",
|
|
5298
|
+
supported: "flag",
|
|
5299
|
+
turnStats: "flag",
|
|
5300
|
+
// counts and sizes: a length is a shape, never content
|
|
5301
|
+
attempts: "count",
|
|
5302
|
+
cacheReadTokens: "count",
|
|
5303
|
+
cacheWriteTokens: "count",
|
|
5304
|
+
code: "count",
|
|
5305
|
+
count: "count",
|
|
5306
|
+
dropped: "count",
|
|
5307
|
+
exitCode: "count",
|
|
5308
|
+
historyLength: "count",
|
|
5309
|
+
inputTokens: "count",
|
|
5310
|
+
lastTextLength: "count",
|
|
5311
|
+
lines: "count",
|
|
5312
|
+
liveDescriptionLength: "count",
|
|
5313
|
+
numTurns: "count",
|
|
5314
|
+
outputTokens: "count",
|
|
5315
|
+
permissionDenials: "count",
|
|
5316
|
+
pid: "count",
|
|
5317
|
+
port: "count",
|
|
5318
|
+
postTokens: "count",
|
|
5319
|
+
preTokens: "count",
|
|
5320
|
+
removed: "count",
|
|
5321
|
+
skills: "count",
|
|
5322
|
+
stderrBytes: "count",
|
|
5323
|
+
textLength: "count",
|
|
5324
|
+
tools: "count",
|
|
5325
|
+
total: "count",
|
|
5326
|
+
ageMs: "duration",
|
|
5327
|
+
delayMs: "duration",
|
|
5328
|
+
deadlineMs: "duration",
|
|
5329
|
+
durationApiMs: "duration",
|
|
5330
|
+
durationMs: "duration",
|
|
5331
|
+
elapsedMs: "duration",
|
|
5332
|
+
remainingMs: "duration",
|
|
5333
|
+
timeoutMs: "duration",
|
|
5334
|
+
waitedMs: "duration",
|
|
5335
|
+
costUsd: "money",
|
|
5336
|
+
// enums: a fixed vocabulary this package or the CLI defines
|
|
5337
|
+
apiKeySource: "enum",
|
|
5338
|
+
delivery: "enum",
|
|
5339
|
+
effort: "enum",
|
|
5340
|
+
finishReason: "enum",
|
|
5341
|
+
hash: "enum",
|
|
5342
|
+
hostApi: "enum",
|
|
5343
|
+
kind: "enum",
|
|
5344
|
+
level: "enum",
|
|
5345
|
+
mode: "enum",
|
|
5346
|
+
overageDisabledReason: "enum",
|
|
5347
|
+
overageStatus: "enum",
|
|
5348
|
+
permissionMode: "enum",
|
|
5349
|
+
preset: "enum",
|
|
5350
|
+
rateLimitType: "enum",
|
|
5351
|
+
reason: "enum",
|
|
5352
|
+
reasoningEffort: "enum",
|
|
5353
|
+
scope: "enum",
|
|
5354
|
+
signal: "enum",
|
|
5355
|
+
source: "enum",
|
|
5356
|
+
state: "enum",
|
|
5357
|
+
status: "enum",
|
|
5358
|
+
stopReason: "enum",
|
|
5359
|
+
subtype: "enum",
|
|
5360
|
+
transport: "enum",
|
|
5361
|
+
trigger: "enum",
|
|
5362
|
+
type: "enum",
|
|
5363
|
+
verdict: "enum",
|
|
5364
|
+
// names: identifiers the operator configured or Claude Code published. The
|
|
5365
|
+
// doctor table already prints the same class of value (providers, accounts,
|
|
5366
|
+
// proxyTools, MCP servers), so a bundle is no more revealing than the report
|
|
5367
|
+
// it is attached to.
|
|
5368
|
+
accounts: "name",
|
|
5369
|
+
agent: "name",
|
|
5370
|
+
bundled: "name",
|
|
5371
|
+
excluded: "name",
|
|
5372
|
+
excludeServers: "name",
|
|
5373
|
+
from: "name",
|
|
5374
|
+
joined: "name",
|
|
5375
|
+
left: "name",
|
|
5376
|
+
mappedName: "name",
|
|
5377
|
+
model: "name",
|
|
5378
|
+
name: "name",
|
|
5379
|
+
names: "name",
|
|
5380
|
+
opencodeAgent: "name",
|
|
5381
|
+
provider: "name",
|
|
5382
|
+
providerOptionsKeys: "name",
|
|
5383
|
+
providers: "name",
|
|
5384
|
+
proxyTools: "name",
|
|
5385
|
+
server: "name",
|
|
5386
|
+
servers: "name",
|
|
5387
|
+
skipped: "name",
|
|
5388
|
+
toolName: "name",
|
|
5389
|
+
toolNames: "name",
|
|
5390
|
+
cliFloor: "version",
|
|
5391
|
+
cliVersion: "version",
|
|
5392
|
+
opencode: "version",
|
|
5393
|
+
plugin: "version",
|
|
5394
|
+
version: "version",
|
|
5395
|
+
// ids: hashed, never printed. Correlation survives, identity does not.
|
|
5396
|
+
callId: "id",
|
|
5397
|
+
claudeSessionId: "id",
|
|
5398
|
+
id: "id",
|
|
5399
|
+
messageId: "id",
|
|
5400
|
+
newConversationId: "id",
|
|
5401
|
+
previousSessionId: "id",
|
|
5402
|
+
sessionId: "id",
|
|
5403
|
+
sessionID: "id",
|
|
5404
|
+
sessionKey: "id",
|
|
5405
|
+
toolCallId: "id",
|
|
5406
|
+
toolCallIds: "id",
|
|
5407
|
+
toolUseId: "id",
|
|
5408
|
+
// paths: home becomes `~`. The doctor table prints cwd and the CLI path
|
|
5409
|
+
// already, so withholding them here would buy nothing and cost the one
|
|
5410
|
+
// field that explains most "it picked the wrong directory" reports.
|
|
5411
|
+
claudeLoads: "path",
|
|
5412
|
+
cliPath: "path",
|
|
5413
|
+
configDir: "path",
|
|
5414
|
+
cwd: "path",
|
|
5415
|
+
dir: "path",
|
|
5416
|
+
notBridged: "path",
|
|
5417
|
+
path: "path",
|
|
5418
|
+
pluginDir: "path",
|
|
5419
|
+
resolved: "path",
|
|
5420
|
+
target: "path",
|
|
5421
|
+
url: "url",
|
|
5422
|
+
cliArgs: "argv",
|
|
5423
|
+
// The hook and tool_progress reporters (#g185), added when the two lanes
|
|
5424
|
+
// merged: their message text is built at runtime and redacted, so these
|
|
5425
|
+
// fields are what keeps a failed-hook WARN readable in a bundle. `stderr`
|
|
5426
|
+
// is deliberately absent, because a hook can print anything to it.
|
|
5427
|
+
hook: "name",
|
|
5428
|
+
hookName: "name",
|
|
5429
|
+
event: "enum",
|
|
5430
|
+
hookEvent: "enum",
|
|
5431
|
+
outcome: "enum",
|
|
5432
|
+
tool: "name",
|
|
5433
|
+
subagentType: "name",
|
|
5434
|
+
errorCategory: "enum",
|
|
5435
|
+
errorStatus: "count",
|
|
5436
|
+
elapsedSeconds: "count",
|
|
5437
|
+
attempt: "count",
|
|
5438
|
+
maxRetries: "count",
|
|
5439
|
+
taskId: "id",
|
|
5440
|
+
agentId: "id"
|
|
5441
|
+
};
|
|
5442
|
+
function shape(value) {
|
|
5443
|
+
if (typeof value === "string") return `[redacted, ${value.length} chars]`;
|
|
5444
|
+
let size;
|
|
5445
|
+
try {
|
|
5446
|
+
size = JSON.stringify(value)?.length ?? 0;
|
|
5447
|
+
} catch {
|
|
5448
|
+
size = 0;
|
|
5449
|
+
}
|
|
5450
|
+
return `[redacted, ${size} chars]`;
|
|
5451
|
+
}
|
|
5452
|
+
function createIdHasher(salt = randomBytes2(16).toString("hex")) {
|
|
5453
|
+
return (raw) => createHash3("sha256").update(salt).update("\0").update(raw).digest("hex").slice(0, 8);
|
|
5454
|
+
}
|
|
5455
|
+
function createRedactionContext(options = {}) {
|
|
5456
|
+
const home = options.home ?? homedir4();
|
|
5457
|
+
const homes = /* @__PURE__ */ new Set();
|
|
5458
|
+
if (home) homes.add(home);
|
|
5459
|
+
if (home) {
|
|
5460
|
+
try {
|
|
5461
|
+
homes.add(realpathSync.native(home));
|
|
5462
|
+
} catch {
|
|
5463
|
+
}
|
|
5464
|
+
}
|
|
5465
|
+
return {
|
|
5466
|
+
homes: [...homes].filter(Boolean).sort((a, b) => b.length - a.length),
|
|
5467
|
+
hash: createIdHasher(options.salt)
|
|
5468
|
+
};
|
|
5469
|
+
}
|
|
5470
|
+
function redactHome(text, context) {
|
|
5471
|
+
let out = text;
|
|
5472
|
+
for (const home of context.homes) out = out.split(home).join("~");
|
|
5473
|
+
return out;
|
|
5474
|
+
}
|
|
5475
|
+
var OPENCODE_SESSION_ID = /\bses_[A-Za-z0-9]{6,}/g;
|
|
5476
|
+
var UUID = /\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b/g;
|
|
5477
|
+
function redactSessionIds(text, context) {
|
|
5478
|
+
return text.replace(OPENCODE_SESSION_ID, (raw) => `ses_${context.hash(raw)}`).replace(UUID, (raw) => `uuid_${context.hash(raw)}`);
|
|
5479
|
+
}
|
|
5480
|
+
function redactForPaste(text, context) {
|
|
5481
|
+
return redactSessionIds(redactHome(text, context), context);
|
|
5482
|
+
}
|
|
5483
|
+
var SAFE_NAME = /^[A-Za-z0-9][\w.:@+/-]*$/;
|
|
5484
|
+
var SAFE_ENUM = /^[A-Za-z0-9][\w.:@+/-]*$/;
|
|
5485
|
+
var SAFE_VERSION = /^[v0-9][\w.()+ -]*$/;
|
|
5486
|
+
var VERSION_PLACEHOLDERS = /* @__PURE__ */ new Set(["unknown", "none", "not detected", "unset"]);
|
|
5487
|
+
var SAFE_PATH = /^(?:[/~.]|[A-Za-z]:[\\/])/;
|
|
5488
|
+
var CLI_FLAG = /^-{1,2}[A-Za-z][\w-]*$/;
|
|
5489
|
+
var SUSPICIOUS = [
|
|
5490
|
+
/sk-[A-Za-z0-9]/,
|
|
5491
|
+
// Anthropic and OpenAI style API keys
|
|
5492
|
+
/\b(?:gh[pousr]_|xox[baprs]-|github_pat_)/,
|
|
5493
|
+
// GitHub and Slack tokens
|
|
5494
|
+
/[0-9a-f]{32,}/i,
|
|
5495
|
+
// hex secrets; the proxy authToken is 64 of these
|
|
5496
|
+
/[A-Za-z0-9+/]{40,}={0,2}/,
|
|
5497
|
+
// base64 blobs
|
|
5498
|
+
/BEGIN [A-Z ]*PRIVATE KEY/,
|
|
5499
|
+
/^bearer\b/i,
|
|
5500
|
+
/=/
|
|
5501
|
+
// any assignment: ANTHROPIC_API_KEY=…, token=…
|
|
5502
|
+
];
|
|
5503
|
+
function looksSecret(value) {
|
|
5504
|
+
return SUSPICIOUS.some((pattern) => pattern.test(value));
|
|
5505
|
+
}
|
|
5506
|
+
function sanitizeScalar(kind, value, context) {
|
|
5507
|
+
if (value === null) return null;
|
|
5508
|
+
switch (kind) {
|
|
5509
|
+
case "flag":
|
|
5510
|
+
return typeof value === "boolean" ? value : shape(value);
|
|
5511
|
+
case "count":
|
|
5512
|
+
case "duration":
|
|
5513
|
+
return typeof value === "number" && Number.isFinite(value) ? value : shape(value);
|
|
5514
|
+
case "money":
|
|
5515
|
+
return typeof value === "number" && Number.isFinite(value) ? Math.round(value * 1e4) / 1e4 : shape(value);
|
|
5516
|
+
case "enum": {
|
|
5517
|
+
if (typeof value === "boolean" || typeof value === "number") return value;
|
|
5518
|
+
if (typeof value !== "string") return shape(value);
|
|
5519
|
+
return value.length <= BUNDLE_MAX_ENUM_CHARS && SAFE_ENUM.test(value) && !looksSecret(value) ? value : shape(value);
|
|
5520
|
+
}
|
|
5521
|
+
case "name": {
|
|
5522
|
+
if (typeof value !== "string") return shape(value);
|
|
5523
|
+
return value.length <= BUNDLE_MAX_NAME_CHARS && SAFE_NAME.test(value) && !looksSecret(value) ? value : shape(value);
|
|
5524
|
+
}
|
|
5525
|
+
case "version": {
|
|
5526
|
+
if (typeof value !== "string") return shape(value);
|
|
5527
|
+
if (VERSION_PLACEHOLDERS.has(value)) return value;
|
|
5528
|
+
return value.length <= BUNDLE_MAX_VERSION_CHARS && SAFE_VERSION.test(value) && !looksSecret(value) ? value : shape(value);
|
|
5529
|
+
}
|
|
5530
|
+
case "id":
|
|
5531
|
+
return typeof value === "string" ? context.hash(value) : shape(value);
|
|
5532
|
+
case "path": {
|
|
5533
|
+
if (typeof value !== "string") return shape(value);
|
|
5534
|
+
if (!SAFE_PATH.test(value)) return sanitizeScalar("name", value, context);
|
|
5535
|
+
if (looksSecret(value)) return shape(value);
|
|
5536
|
+
const rewritten = redactHome(value, context);
|
|
5537
|
+
return rewritten.length <= BUNDLE_MAX_VALUE_CHARS && !/\s{2,}|[\n\r]/.test(rewritten) ? rewritten : shape(value);
|
|
5538
|
+
}
|
|
5539
|
+
case "url":
|
|
5540
|
+
return typeof value === "string" ? sanitizeUrl(value) : shape(value);
|
|
5541
|
+
case "argv":
|
|
5542
|
+
return shape(value);
|
|
5543
|
+
}
|
|
5544
|
+
}
|
|
5545
|
+
function sanitizeUrl(raw) {
|
|
5546
|
+
let parsed;
|
|
5547
|
+
try {
|
|
5548
|
+
parsed = new URL(raw);
|
|
5549
|
+
} catch {
|
|
5550
|
+
return shape(raw);
|
|
5551
|
+
}
|
|
5552
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return shape(raw);
|
|
5553
|
+
if (looksSecret(parsed.pathname)) return shape(raw);
|
|
5554
|
+
const credentials = parsed.username || parsed.password ? "[redacted]@" : "";
|
|
5555
|
+
const query = parsed.search ? `?[redacted, ${parsed.search.length - 1} chars]` : "";
|
|
5556
|
+
return `${parsed.protocol}//${credentials}${parsed.host}${parsed.pathname}${query}`;
|
|
5557
|
+
}
|
|
5558
|
+
function sanitizeArgv(value) {
|
|
5559
|
+
if (!Array.isArray(value)) return shape(value);
|
|
5560
|
+
const out = [];
|
|
5561
|
+
for (const item of value.slice(0, BUNDLE_MAX_ARRAY_ITEMS * 2)) {
|
|
5562
|
+
if (typeof item !== "string") {
|
|
5563
|
+
out.push(shape(item));
|
|
5564
|
+
continue;
|
|
5565
|
+
}
|
|
5566
|
+
if (!item.startsWith("-")) {
|
|
5567
|
+
out.push(`[redacted, ${item.length} chars]`);
|
|
5568
|
+
continue;
|
|
5569
|
+
}
|
|
5570
|
+
const equals = item.indexOf("=");
|
|
5571
|
+
if (equals === -1) {
|
|
5572
|
+
out.push(CLI_FLAG.test(item) ? item : shape(item));
|
|
5573
|
+
continue;
|
|
5574
|
+
}
|
|
5575
|
+
const flag = item.slice(0, equals);
|
|
5576
|
+
out.push(
|
|
5577
|
+
`${CLI_FLAG.test(flag) ? flag : "[redacted]"}=[redacted, ${item.length - equals - 1} chars]`
|
|
5578
|
+
);
|
|
5579
|
+
}
|
|
5580
|
+
if (value.length > out.length) out.push(`[+${value.length - out.length} more]`);
|
|
5581
|
+
return out;
|
|
5582
|
+
}
|
|
5583
|
+
var BUNDLE_MAX_DEPTH = 4;
|
|
5584
|
+
function kindFor(key) {
|
|
5585
|
+
return Object.prototype.hasOwnProperty.call(BUNDLE_DATA_ALLOWLIST, key) ? BUNDLE_DATA_ALLOWLIST[key] : void 0;
|
|
5586
|
+
}
|
|
5587
|
+
function sanitizeValue(key, value, context, depth = 0) {
|
|
5588
|
+
if (kindFor(key) === "argv") return sanitizeArgv(value);
|
|
5589
|
+
if (depth > BUNDLE_MAX_DEPTH) return shape(value);
|
|
5590
|
+
if (Array.isArray(value)) {
|
|
5591
|
+
const out = value.slice(0, BUNDLE_MAX_ARRAY_ITEMS).map((item) => sanitizeValue(key, item, context, depth + 1));
|
|
5592
|
+
if (value.length > out.length) out.push(`[+${value.length - out.length} more]`);
|
|
5593
|
+
return out;
|
|
5594
|
+
}
|
|
5595
|
+
if (value !== null && typeof value === "object") {
|
|
5596
|
+
return sanitizeLogData(value, context, depth + 1);
|
|
5597
|
+
}
|
|
5598
|
+
const kind = kindFor(key);
|
|
5599
|
+
return kind === void 0 ? shape(value) : sanitizeScalar(kind, value, context);
|
|
5600
|
+
}
|
|
5601
|
+
function sanitizeLogData(data, context, depth = 0) {
|
|
5602
|
+
const out = {};
|
|
5603
|
+
for (const [key, value] of Object.entries(data)) {
|
|
5604
|
+
const known = kindFor(key) !== void 0;
|
|
5605
|
+
const safeKey = known || key.length <= BUNDLE_MAX_NAME_CHARS && SAFE_NAME.test(key) && !looksSecret(key) ? key : `[redacted key, ${key.length} chars]`;
|
|
5606
|
+
out[safeKey] = sanitizeValue(key, value, context, depth);
|
|
5607
|
+
}
|
|
5608
|
+
return out;
|
|
5609
|
+
}
|
|
5610
|
+
var LOG_LINE = /^\[([^\]]+)\] \[opencode-claude-code\] (DEBUG|INFO|NOTICE|WARN|ERROR): ([\s\S]*)$/;
|
|
5611
|
+
function parseLogLine(raw) {
|
|
5612
|
+
const match = LOG_LINE.exec(raw);
|
|
5613
|
+
if (!match) return null;
|
|
5614
|
+
const rest = match[3];
|
|
5615
|
+
for (let index = rest.indexOf("{"); index !== -1; index = rest.indexOf("{", index + 1)) {
|
|
5616
|
+
let parsed;
|
|
5617
|
+
try {
|
|
5618
|
+
parsed = JSON.parse(rest.slice(index));
|
|
5619
|
+
} catch {
|
|
5620
|
+
continue;
|
|
5621
|
+
}
|
|
5622
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
5623
|
+
return {
|
|
5624
|
+
timestamp: match[1],
|
|
5625
|
+
level: match[2],
|
|
5626
|
+
message: rest.slice(0, index).trimEnd(),
|
|
5627
|
+
data: parsed
|
|
5628
|
+
};
|
|
5629
|
+
}
|
|
5630
|
+
}
|
|
5631
|
+
return { timestamp: match[1], level: match[2], message: rest, data: null };
|
|
5632
|
+
}
|
|
5633
|
+
function redactLogLine(raw, context) {
|
|
5634
|
+
const parsed = parseLogLine(raw);
|
|
5635
|
+
if (!parsed || !BUNDLE_LEVELS.has(parsed.level)) return null;
|
|
5636
|
+
const message = PLUGIN_LOG_MESSAGES.has(parsed.message) ? parsed.message : `[redacted message, ${parsed.message.length} chars]`;
|
|
5637
|
+
const head = `[${parsed.timestamp}] ${parsed.level}: ${message}`;
|
|
5638
|
+
if (!parsed.data || Object.keys(parsed.data).length === 0) return head;
|
|
5639
|
+
return `${head} ${JSON.stringify(sanitizeLogData(parsed.data, context))}`;
|
|
5640
|
+
}
|
|
5641
|
+
function readLogTail(path10, maxBytes = BUNDLE_TAIL_BYTES) {
|
|
5642
|
+
const fd = openSync(path10, "r");
|
|
5643
|
+
try {
|
|
5644
|
+
const size = fstatSync(fd).size;
|
|
5645
|
+
const length = Math.min(size, maxBytes);
|
|
5646
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
5647
|
+
readSync(fd, buffer, 0, length, size - length);
|
|
5648
|
+
const text = buffer.toString("utf8");
|
|
5649
|
+
if (length < size) {
|
|
5650
|
+
const newline = text.indexOf("\n");
|
|
5651
|
+
return newline === -1 ? "" : text.slice(newline + 1);
|
|
5652
|
+
}
|
|
5653
|
+
return text;
|
|
5654
|
+
} finally {
|
|
5655
|
+
closeSync(fd);
|
|
5656
|
+
}
|
|
5657
|
+
}
|
|
5658
|
+
var BUNDLE_HEADER = "**Redacted log bundle**";
|
|
5659
|
+
var BUNDLE_PREAMBLE = [
|
|
5660
|
+
"Safe to paste into a GitHub issue. Built by an allowlist, not a filter: a line",
|
|
5661
|
+
"keeps its timestamp, its level, its message **only** when that message is a",
|
|
5662
|
+
"constant in this package's own source, and only those data fields whose values",
|
|
5663
|
+
"are non-secret by type (versions, counts, flags, enums, durations, exit codes,",
|
|
5664
|
+
"model and tool and server names). Everything else, including every unknown",
|
|
5665
|
+
"field, is replaced by `[redacted, N chars]`. Session ids are a per-bundle hash,",
|
|
5666
|
+
"so lines correlate here and nowhere else, and your home directory is `~`.",
|
|
5667
|
+
"",
|
|
5668
|
+
"Never included: prompt or reply text, system prompts, tool inputs or outputs,",
|
|
5669
|
+
"file contents, environment values, bearer tokens, API keys, `Authorization`",
|
|
5670
|
+
"headers, MCP server env or headers, URL credentials or query strings, or the",
|
|
5671
|
+
"raw spawn argv."
|
|
5672
|
+
].join("\n");
|
|
5673
|
+
var BUNDLE_LOGGING_OFF = [
|
|
5674
|
+
"File logging is **off**, so there is nothing to bundle. The report above still",
|
|
5675
|
+
"answers versions, config shape and live processes.",
|
|
5676
|
+
"",
|
|
5677
|
+
"Turn it on for one run and reproduce the problem:",
|
|
5678
|
+
"",
|
|
5679
|
+
"```sh",
|
|
5680
|
+
"OPENCODE_CLAUDE_CODE_LOG_FILE=1 opencode",
|
|
5681
|
+
"```",
|
|
5682
|
+
"",
|
|
5683
|
+
"or set it permanently in `opencode.json` under",
|
|
5684
|
+
"`provider.claude-code.options.logging`:",
|
|
5685
|
+
"",
|
|
5686
|
+
"```json",
|
|
5687
|
+
'{ "logging": { "file": true } }',
|
|
5688
|
+
"```",
|
|
5689
|
+
"",
|
|
5690
|
+
"Then run `/claude-code-doctor bundle` again. Logging is read once at process",
|
|
5691
|
+
"start, so a running window has to be relaunched."
|
|
5692
|
+
].join("\n");
|
|
5693
|
+
function buildLogBundleSection(input) {
|
|
5694
|
+
const lines = [BUNDLE_HEADER, "", BUNDLE_PREAMBLE, ""];
|
|
5695
|
+
if (!input.fileLogging) {
|
|
5696
|
+
lines.push(BUNDLE_LOGGING_OFF);
|
|
5697
|
+
return lines.join("\n");
|
|
5698
|
+
}
|
|
5699
|
+
let tail;
|
|
5700
|
+
try {
|
|
5701
|
+
tail = (input.readTailImpl ?? readLogTail)(input.logPath, BUNDLE_TAIL_BYTES);
|
|
5702
|
+
} catch (error) {
|
|
5703
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
5704
|
+
lines.push(
|
|
5705
|
+
`File logging is on, but the log could not be read: ${redactHome(reason, input.context)}`
|
|
5706
|
+
);
|
|
5707
|
+
return lines.join("\n");
|
|
5708
|
+
}
|
|
5709
|
+
const raw = tail.split("\n");
|
|
5710
|
+
let scanned = 0;
|
|
5711
|
+
const kept = [];
|
|
5712
|
+
for (let index = raw.length - 1; index >= 0; index--) {
|
|
5713
|
+
const line = raw[index];
|
|
5714
|
+
if (!line) continue;
|
|
5715
|
+
if (parseLogLine(line)) scanned++;
|
|
5716
|
+
const redacted = redactLogLine(line, input.context);
|
|
5717
|
+
if (redacted === null) continue;
|
|
5718
|
+
if (kept.length >= BUNDLE_MAX_LINES) break;
|
|
5719
|
+
if (kept.reduce((total, entry) => total + entry.length + 1, 0) + redacted.length > BUNDLE_MAX_BYTES) {
|
|
5720
|
+
break;
|
|
5721
|
+
}
|
|
5722
|
+
kept.push(redacted);
|
|
5723
|
+
}
|
|
5724
|
+
kept.reverse();
|
|
5725
|
+
const dropped = Math.max(0, scanned - kept.length);
|
|
5726
|
+
lines.push(
|
|
5727
|
+
`Log: \`${redactHome(input.logPath, input.context)}\`. ${kept.length} NOTICE/WARN/ERROR line${kept.length === 1 ? "" : "s"} kept of ${scanned} scanned (caps: ${BUNDLE_MAX_LINES} lines, ${BUNDLE_MAX_BYTES} bytes, last ${BUNDLE_TAIL_BYTES} bytes of the file).`
|
|
5728
|
+
);
|
|
5729
|
+
if (dropped > 0) {
|
|
5730
|
+
lines.push("");
|
|
5731
|
+
lines.push(
|
|
5732
|
+
`${dropped} earlier or lower-level line${dropped === 1 ? " was" : "s were"} not included.`
|
|
5733
|
+
);
|
|
5734
|
+
}
|
|
5735
|
+
lines.push("");
|
|
5736
|
+
lines.push("```text");
|
|
5737
|
+
if (kept.length === 0) {
|
|
5738
|
+
lines.push("No NOTICE, WARN or ERROR line in the retained log.");
|
|
5739
|
+
} else {
|
|
5740
|
+
lines.push(...kept);
|
|
5741
|
+
}
|
|
5742
|
+
lines.push("```");
|
|
5743
|
+
return lines.join("\n");
|
|
5744
|
+
}
|
|
5745
|
+
|
|
4996
5746
|
// src/plan-usage.ts
|
|
4997
5747
|
import { execFile as execFile2 } from "child_process";
|
|
4998
5748
|
var PLAN_USAGE_ARGUMENTS = /* @__PURE__ */ new Set(["usage", "cost", "stats", "limits"]);
|
|
@@ -5216,7 +5966,7 @@ function logStartupDiagnostics(providers, opencodeVersion) {
|
|
|
5216
5966
|
|
|
5217
5967
|
// src/doctor.ts
|
|
5218
5968
|
var DOCTOR_COMMAND = "claude-code-doctor";
|
|
5219
|
-
var DOCTOR_COMMAND_DESCRIPTION = "Report what the Claude Code plugin sees: versions, cwd, live processes, pending proxy calls. Add `usage` for plan windows";
|
|
5969
|
+
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";
|
|
5220
5970
|
var DOCTOR_MARKER = "\u258C **claude-code doctor**";
|
|
5221
5971
|
function isRecord3(value) {
|
|
5222
5972
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -5275,6 +6025,9 @@ function describePermissionPresets(rows) {
|
|
|
5275
6025
|
return `${row.provider}: ${row.preset}${suffix}`;
|
|
5276
6026
|
}).join(", ");
|
|
5277
6027
|
}
|
|
6028
|
+
function tableCell(text) {
|
|
6029
|
+
return text.replace(/\r?\n/g, " ").replace(/\|/g, "\\|");
|
|
6030
|
+
}
|
|
5278
6031
|
function formatDoctorReport(report) {
|
|
5279
6032
|
const lines = [];
|
|
5280
6033
|
lines.push(DOCTOR_MARKER);
|
|
@@ -5353,7 +6106,7 @@ function formatDoctorReport(report) {
|
|
|
5353
6106
|
lines.push("| server | category | Claude Code said |");
|
|
5354
6107
|
lines.push("|---|---|---|");
|
|
5355
6108
|
for (const error of report.mcpServerErrors) {
|
|
5356
|
-
lines.push(`| ${error.name} | \`${error.type}\` | ${error.message || "no detail"} |`);
|
|
6109
|
+
lines.push(`| ${error.name} | \`${error.type}\` | ${tableCell(error.message) || "no detail"} |`);
|
|
5357
6110
|
}
|
|
5358
6111
|
lines.push("");
|
|
5359
6112
|
lines.push("A skipped server is missing from the model's tools with no other sign of it.");
|
|
@@ -5366,9 +6119,25 @@ function formatDoctorReport(report) {
|
|
|
5366
6119
|
lines.push("|---|---|---|---|");
|
|
5367
6120
|
for (const failure of report.pluginLoadFailures) {
|
|
5368
6121
|
lines.push(
|
|
5369
|
-
`| ${failure.plugin} | ${failure.kind} | \`${failure.type}\` | ${failure.message || "no detail"} |`
|
|
6122
|
+
`| ${failure.plugin} | ${failure.kind} | \`${failure.type}\` | ${tableCell(failure.message) || "no detail"} |`
|
|
6123
|
+
);
|
|
6124
|
+
}
|
|
6125
|
+
}
|
|
6126
|
+
if (report.hookFailures.length > 0) {
|
|
6127
|
+
lines.push("");
|
|
6128
|
+
lines.push("**Hooks Claude Code ran that failed**");
|
|
6129
|
+
lines.push("");
|
|
6130
|
+
lines.push("| hook | event | exit | outcome | its stderr |");
|
|
6131
|
+
lines.push("|---|---|---|---|---|");
|
|
6132
|
+
for (const failure of report.hookFailures) {
|
|
6133
|
+
lines.push(
|
|
6134
|
+
`| ${failure.hookName} | ${failure.hookEvent} | ${failure.exitCode ?? "n/a"} | ${failure.outcome ?? "unknown"} | ${tableCell(failure.stderr) || "nothing"} |`
|
|
5370
6135
|
);
|
|
5371
6136
|
}
|
|
6137
|
+
lines.push("");
|
|
6138
|
+
lines.push(
|
|
6139
|
+
"These are your own Claude Code hooks, not opencode's. A failed hook's contribution to the session is missing and the turn succeeds anyway. Only the hook's stderr is shown: its stdout is spliced into the model's context and has no business in a bug report."
|
|
6140
|
+
);
|
|
5372
6141
|
}
|
|
5373
6142
|
lines.push("");
|
|
5374
6143
|
lines.push("**Background subagents**");
|
|
@@ -5526,6 +6295,7 @@ async function gatherDoctorReport(options) {
|
|
|
5526
6295
|
proxyServers,
|
|
5527
6296
|
mcpServerErrors: snapshotMcpServerErrors(),
|
|
5528
6297
|
pluginLoadFailures: snapshotPluginLoadFailures(),
|
|
6298
|
+
hookFailures: snapshotHookFailures(),
|
|
5529
6299
|
planUsage,
|
|
5530
6300
|
backgroundSubagents: {
|
|
5531
6301
|
gate: snapshotBackgroundSubagentGate(),
|
|
@@ -5533,6 +6303,42 @@ async function gatherDoctorReport(options) {
|
|
|
5533
6303
|
}
|
|
5534
6304
|
};
|
|
5535
6305
|
}
|
|
6306
|
+
function decorateDoctorReport(report, options) {
|
|
6307
|
+
if (!wantsDiagnosticBundle(options.argument ?? "")) return report;
|
|
6308
|
+
const context = options.redactionContextImpl ?? createRedactionContext();
|
|
6309
|
+
const logFile = (options.logFileImpl ?? describeLogFile)();
|
|
6310
|
+
const section = buildLogBundleSection({
|
|
6311
|
+
logPath: logFile.path,
|
|
6312
|
+
fileLogging: logFile.enabled,
|
|
6313
|
+
context,
|
|
6314
|
+
readTailImpl: options.readLogTailImpl
|
|
6315
|
+
});
|
|
6316
|
+
return redactForPaste(`${report}
|
|
6317
|
+
|
|
6318
|
+
${section}`, context);
|
|
6319
|
+
}
|
|
6320
|
+
function withholdFreeText(report) {
|
|
6321
|
+
const withhold = (text) => text ? `[redacted, ${text.length} chars]` : text;
|
|
6322
|
+
return {
|
|
6323
|
+
...report,
|
|
6324
|
+
mcpServerErrors: report.mcpServerErrors.map((error) => ({
|
|
6325
|
+
...error,
|
|
6326
|
+
message: withhold(error.message)
|
|
6327
|
+
})),
|
|
6328
|
+
pluginLoadFailures: report.pluginLoadFailures.map((failure) => ({
|
|
6329
|
+
...failure,
|
|
6330
|
+
message: withhold(failure.message)
|
|
6331
|
+
})),
|
|
6332
|
+
hookFailures: report.hookFailures.map((failure) => ({
|
|
6333
|
+
...failure,
|
|
6334
|
+
stderr: withhold(failure.stderr)
|
|
6335
|
+
}))
|
|
6336
|
+
};
|
|
6337
|
+
}
|
|
6338
|
+
function renderDoctorReport(report, options) {
|
|
6339
|
+
const bundle = wantsDiagnosticBundle(options.argument ?? "");
|
|
6340
|
+
return decorateDoctorReport(formatDoctorReport(bundle ? withholdFreeText(report) : report), options);
|
|
6341
|
+
}
|
|
5536
6342
|
async function buildDoctorReport(options) {
|
|
5537
6343
|
try {
|
|
5538
6344
|
const report = await gatherDoctorReport(options);
|
|
@@ -5545,10 +6351,11 @@ async function buildDoctorReport(options) {
|
|
|
5545
6351
|
proxyServers: report.proxyServers.map((server2) => server2.auth.status),
|
|
5546
6352
|
mcpServerErrors: report.mcpServerErrors.length,
|
|
5547
6353
|
pluginLoadFailures: report.pluginLoadFailures.length,
|
|
6354
|
+
hookFailures: report.hookFailures.length,
|
|
5548
6355
|
planUsage: report.planUsage.status,
|
|
5549
6356
|
backgroundSubagents: report.backgroundSubagents.gate?.supported ?? "not read"
|
|
5550
6357
|
});
|
|
5551
|
-
return
|
|
6358
|
+
return renderDoctorReport(report, options);
|
|
5552
6359
|
} catch (error) {
|
|
5553
6360
|
const message = error instanceof Error ? error.message : String(error);
|
|
5554
6361
|
log.warn("claude-code doctor failed to build its report", { error: message });
|
|
@@ -7670,7 +8477,7 @@ function spawnInteractiveProcess(opts) {
|
|
|
7670
8477
|
|
|
7671
8478
|
// src/prompts.ts
|
|
7672
8479
|
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
7673
|
-
import { homedir as
|
|
8480
|
+
import { homedir as homedir7 } from "os";
|
|
7674
8481
|
import { randomUUID as randomUUID5 } from "crypto";
|
|
7675
8482
|
import { dirname as dirname5, join as join8 } from "path";
|
|
7676
8483
|
function readPromptFileIfPresent(path10) {
|
|
@@ -7793,7 +8600,7 @@ ${options.compressionSummary.trim()}`
|
|
|
7793
8600
|
for (const s of extraSystemContent) {
|
|
7794
8601
|
if (s.trim()) parts.push(s.trim());
|
|
7795
8602
|
}
|
|
7796
|
-
const configRoot = process.env.XDG_CONFIG_HOME ?? join8(
|
|
8603
|
+
const configRoot = process.env.XDG_CONFIG_HOME ?? join8(homedir7(), ".config");
|
|
7797
8604
|
const globalAgents = readPromptFileIfPresent(join8(configRoot, "opencode", "AGENTS.md"));
|
|
7798
8605
|
const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
|
|
7799
8606
|
const forwarded = extraSystemContent.join("\n\n");
|
|
@@ -8621,6 +9428,8 @@ function createTurnState(init) {
|
|
|
8621
9428
|
reasoningIds: /* @__PURE__ */ new Map(),
|
|
8622
9429
|
reasoningStarted: /* @__PURE__ */ new Map(),
|
|
8623
9430
|
hadThinkingTextFromStream: false,
|
|
9431
|
+
cliAskedForWork: false,
|
|
9432
|
+
streamStarted: false,
|
|
8624
9433
|
turnCompleted: false,
|
|
8625
9434
|
controllerClosed: false,
|
|
8626
9435
|
cleanedUp: false,
|
|
@@ -8670,6 +9479,59 @@ function createTurnState(init) {
|
|
|
8670
9479
|
return state;
|
|
8671
9480
|
}
|
|
8672
9481
|
|
|
9482
|
+
// src/turn-abort.ts
|
|
9483
|
+
function watchTurnAbort(signal) {
|
|
9484
|
+
let aborted = signal?.aborted === true;
|
|
9485
|
+
let listeners = [];
|
|
9486
|
+
let settle;
|
|
9487
|
+
const whenAborted = new Promise((resolve6) => {
|
|
9488
|
+
settle = resolve6;
|
|
9489
|
+
});
|
|
9490
|
+
const fire = () => {
|
|
9491
|
+
if (aborted && listeners === null) return;
|
|
9492
|
+
aborted = true;
|
|
9493
|
+
const pending = listeners ?? [];
|
|
9494
|
+
listeners = null;
|
|
9495
|
+
settle();
|
|
9496
|
+
for (const fn of pending) {
|
|
9497
|
+
try {
|
|
9498
|
+
fn();
|
|
9499
|
+
} catch (error) {
|
|
9500
|
+
log.warn("turn abort listener threw", {
|
|
9501
|
+
error: error instanceof Error ? error.message : String(error)
|
|
9502
|
+
});
|
|
9503
|
+
}
|
|
9504
|
+
}
|
|
9505
|
+
};
|
|
9506
|
+
const onSignalAbort = () => fire();
|
|
9507
|
+
if (aborted) {
|
|
9508
|
+
listeners = null;
|
|
9509
|
+
settle();
|
|
9510
|
+
} else {
|
|
9511
|
+
signal?.addEventListener("abort", onSignalAbort, { once: true });
|
|
9512
|
+
}
|
|
9513
|
+
return {
|
|
9514
|
+
get aborted() {
|
|
9515
|
+
return aborted;
|
|
9516
|
+
},
|
|
9517
|
+
get reason() {
|
|
9518
|
+
return signal?.reason;
|
|
9519
|
+
},
|
|
9520
|
+
whenAborted,
|
|
9521
|
+
onAbort(fn) {
|
|
9522
|
+
if (aborted) {
|
|
9523
|
+
fn();
|
|
9524
|
+
return;
|
|
9525
|
+
}
|
|
9526
|
+
listeners?.push(fn);
|
|
9527
|
+
},
|
|
9528
|
+
dispose() {
|
|
9529
|
+
listeners = null;
|
|
9530
|
+
signal?.removeEventListener("abort", onSignalAbort);
|
|
9531
|
+
}
|
|
9532
|
+
};
|
|
9533
|
+
}
|
|
9534
|
+
|
|
8673
9535
|
// src/stream-parser.ts
|
|
8674
9536
|
import { generateId as generateId2 } from "@ai-sdk/provider-utils";
|
|
8675
9537
|
|
|
@@ -9285,6 +10147,13 @@ function createLineHandler(state, ctx) {
|
|
|
9285
10147
|
state.endTextBlock();
|
|
9286
10148
|
}
|
|
9287
10149
|
}
|
|
10150
|
+
if (msg.type === "system") {
|
|
10151
|
+
reportHookEvent(msg);
|
|
10152
|
+
}
|
|
10153
|
+
if (msg.type === "tool_progress") {
|
|
10154
|
+
reportToolProgress(msg);
|
|
10155
|
+
return;
|
|
10156
|
+
}
|
|
9288
10157
|
if (msg.type === "conversation_reset") {
|
|
9289
10158
|
const note = reportConversationReset(msg);
|
|
9290
10159
|
if (note) {
|
|
@@ -10344,6 +11213,21 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10344
11213
|
}
|
|
10345
11214
|
async doStreamForHost(options, attempt, mode = "stream") {
|
|
10346
11215
|
const warnings = [];
|
|
11216
|
+
const turnAbort = watchTurnAbort(options.abortSignal);
|
|
11217
|
+
const abortedBeforeWork = (stage) => {
|
|
11218
|
+
log.info("abort before the turn asked claude for work; nothing spawned", {
|
|
11219
|
+
stage,
|
|
11220
|
+
reason: describeAbortReason(turnAbort.reason)
|
|
11221
|
+
});
|
|
11222
|
+
turnAbort.dispose();
|
|
11223
|
+
const aborted = new ReadableStream({
|
|
11224
|
+
start(controller) {
|
|
11225
|
+
controller.enqueue({ type: "stream-start", warnings });
|
|
11226
|
+
controller.close();
|
|
11227
|
+
}
|
|
11228
|
+
});
|
|
11229
|
+
return { stream: aborted, request: { body: { text: "" } } };
|
|
11230
|
+
};
|
|
10347
11231
|
const skipPermissions = this.config.skipPermissions !== false;
|
|
10348
11232
|
const scope = this.requestScope(options);
|
|
10349
11233
|
const affinity = this.sessionAffinity(options);
|
|
@@ -10388,6 +11272,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10388
11272
|
modelId: effectiveModelId
|
|
10389
11273
|
});
|
|
10390
11274
|
let cliPath = failover.cliPath;
|
|
11275
|
+
if (turnAbort.aborted) return abortedBeforeWork("resolving the spawn cwd and account");
|
|
10391
11276
|
const asideTransportRef = { cliPath, interactive: !!useInteractive };
|
|
10392
11277
|
const doctor = !compactionMode && scope !== "no-tools" ? parseDoctorCommand(options.prompt) : null;
|
|
10393
11278
|
if (doctor) {
|
|
@@ -10639,7 +11524,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10639
11524
|
const hasMatchedPendingResults = previousPendingProxyMatches.some(
|
|
10640
11525
|
(m) => m.result !== null
|
|
10641
11526
|
);
|
|
10642
|
-
const
|
|
11527
|
+
const prologueWork = Promise.all([
|
|
10643
11528
|
compactionMode ? Promise.resolve(void 0) : getRuntimeMcpStatus({
|
|
10644
11529
|
// Give a server opencode is still starting a bounded moment to
|
|
10645
11530
|
// reach a decision before the spawn is planned without it. Only
|
|
@@ -10647,10 +11532,20 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10647
11532
|
// call exactly as before.
|
|
10648
11533
|
waitForPendingMs: resolveMcpConnectWaitMs(
|
|
10649
11534
|
this.config.mcpConnectWaitMs
|
|
10650
|
-
)
|
|
11535
|
+
),
|
|
11536
|
+
signal: options.abortSignal
|
|
10651
11537
|
}),
|
|
10652
11538
|
detectCliVersion(cliPath)
|
|
10653
11539
|
]);
|
|
11540
|
+
prologueWork.catch(() => void 0);
|
|
11541
|
+
const prologue = await Promise.race([
|
|
11542
|
+
prologueWork.then((value) => ({ value })),
|
|
11543
|
+
turnAbort.whenAborted.then(() => void 0)
|
|
11544
|
+
]);
|
|
11545
|
+
if (!prologue) {
|
|
11546
|
+
return abortedBeforeWork("waiting for opencode's MCP status and the CLI version");
|
|
11547
|
+
}
|
|
11548
|
+
const [runtimeStatus, cliVersion] = prologue.value;
|
|
10654
11549
|
const failoverAccounts = failoverCandidates(
|
|
10655
11550
|
this.config.failoverAccounts,
|
|
10656
11551
|
sourceAccount
|
|
@@ -10681,6 +11576,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10681
11576
|
opencodeAgent: this.getOpencodeAgent(options),
|
|
10682
11577
|
providerOptionsKeys: options.providerOptions ? Object.keys(options.providerOptions) : []
|
|
10683
11578
|
});
|
|
11579
|
+
if (turnAbort.aborted) return abortedBeforeWork("planning the spawn");
|
|
10684
11580
|
const stream = new ReadableStream({
|
|
10685
11581
|
start(controller) {
|
|
10686
11582
|
if (compactionMode) {
|
|
@@ -10710,7 +11606,109 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10710
11606
|
});
|
|
10711
11607
|
state.activeProcess = getActiveProcess(sk);
|
|
10712
11608
|
state.proxyServer = state.activeProcess?.proxyServer ?? null;
|
|
11609
|
+
const discardUnattachedProxyServer = () => {
|
|
11610
|
+
const own = state.proxyServer;
|
|
11611
|
+
if (!own || own === state.activeProcess?.proxyServer) return;
|
|
11612
|
+
state.proxyServer = null;
|
|
11613
|
+
void own.close().catch((error) => {
|
|
11614
|
+
log.warn("failed to close the proxy server of an aborted turn", {
|
|
11615
|
+
sessionKey: sk,
|
|
11616
|
+
error: error instanceof Error ? error.message : String(error)
|
|
11617
|
+
});
|
|
11618
|
+
});
|
|
11619
|
+
};
|
|
11620
|
+
const releaseAbandonedProxyCalls = (reason) => {
|
|
11621
|
+
if (state.drainBuffer.length === 0 && getPendingProxyCalls(sk).length === 0) return;
|
|
11622
|
+
rejectAllPendingProxyCallsForSession(sk, new Error(reason));
|
|
11623
|
+
state.drainBuffer.length = 0;
|
|
11624
|
+
};
|
|
11625
|
+
turnAbort.onAbort(() => {
|
|
11626
|
+
state.autoContinueState.aborted = true;
|
|
11627
|
+
if (!state.cliAskedForWork) {
|
|
11628
|
+
if (state.controllerClosed) return;
|
|
11629
|
+
state.controllerClosed = true;
|
|
11630
|
+
log.info("abort while the turn was still being prepared; nothing was sent to claude", {
|
|
11631
|
+
sessionKey: sk,
|
|
11632
|
+
// A process here was spawned by this turn or reused from an
|
|
11633
|
+
// earlier one; either way nothing was asked of it.
|
|
11634
|
+
attachedProcess: !!state.activeProcess,
|
|
11635
|
+
reason: describeAbortReason(turnAbort.reason)
|
|
11636
|
+
});
|
|
11637
|
+
discardUnattachedProxyServer();
|
|
11638
|
+
state.cleanupTurn?.();
|
|
11639
|
+
if (!state.streamStarted) {
|
|
11640
|
+
state.streamStarted = true;
|
|
11641
|
+
controller.enqueue({ type: "stream-start", warnings });
|
|
11642
|
+
}
|
|
11643
|
+
try {
|
|
11644
|
+
controller.close();
|
|
11645
|
+
} catch {
|
|
11646
|
+
}
|
|
11647
|
+
return;
|
|
11648
|
+
}
|
|
11649
|
+
if (state.turnCompleted || state.controllerClosed) {
|
|
11650
|
+
const stoppedProcess = state.activeProcess;
|
|
11651
|
+
if (stoppedProcess && stoppedProcess.lineEmitter.listenerCount("line") === 0 && getPendingProxyCalls(sk).length > 0) {
|
|
11652
|
+
const reason = describeAbortReason(options.abortSignal?.reason);
|
|
11653
|
+
void settleSessionRunState(affinity).then((stopped) => {
|
|
11654
|
+
const stillParked = stoppedProcess.lineEmitter.listenerCount("line") === 0 && getPendingProxyCalls(sk).length > 0;
|
|
11655
|
+
if (stopped === "busy" || !stillParked) {
|
|
11656
|
+
log.debug("abort at a tool boundary while opencode is still running the turn; keeping pending calls", {
|
|
11657
|
+
sk,
|
|
11658
|
+
session: stopped,
|
|
11659
|
+
stillParked,
|
|
11660
|
+
reason
|
|
11661
|
+
});
|
|
11662
|
+
return;
|
|
11663
|
+
}
|
|
11664
|
+
log.info("abort between proxy tool boundaries; releasing pending calls", { sk, reason });
|
|
11665
|
+
void interruptTurn(stoppedProcess).then((idle) => {
|
|
11666
|
+
log.info("interrupt sent for aborted turn", { sk, idle });
|
|
11667
|
+
});
|
|
11668
|
+
releaseAbandonedProxyCalls(
|
|
11669
|
+
"Provider stream was aborted while opencode was running its proxy tool calls"
|
|
11670
|
+
);
|
|
11671
|
+
});
|
|
11672
|
+
}
|
|
11673
|
+
return;
|
|
11674
|
+
}
|
|
11675
|
+
if (state.activeProcess) {
|
|
11676
|
+
void interruptTurn(state.activeProcess).then((idle) => {
|
|
11677
|
+
log.info("interrupt sent for aborted turn", { sk, idle });
|
|
11678
|
+
});
|
|
11679
|
+
}
|
|
11680
|
+
if (!state.hasReceivedContent) {
|
|
11681
|
+
log.info(
|
|
11682
|
+
"abort signal received before content, closing stream immediately",
|
|
11683
|
+
{ cwd }
|
|
11684
|
+
);
|
|
11685
|
+
releaseAbandonedProxyCalls(
|
|
11686
|
+
"Provider stream was aborted before pending proxy calls were emitted"
|
|
11687
|
+
);
|
|
11688
|
+
state.controllerClosed = true;
|
|
11689
|
+
state.cleanupTurn?.();
|
|
11690
|
+
try {
|
|
11691
|
+
controller.close();
|
|
11692
|
+
} catch {
|
|
11693
|
+
}
|
|
11694
|
+
return;
|
|
11695
|
+
}
|
|
11696
|
+
log.info(
|
|
11697
|
+
"abort signal received mid-turn, starting grace period",
|
|
11698
|
+
{ cwd }
|
|
11699
|
+
);
|
|
11700
|
+
releaseAbandonedProxyCalls(
|
|
11701
|
+
"Provider stream was aborted while proxy tool calls were pending"
|
|
11702
|
+
);
|
|
11703
|
+
startResultFallback(state, 5e3);
|
|
11704
|
+
});
|
|
11705
|
+
const stoppedBeforeWork = () => {
|
|
11706
|
+
if (!state.controllerClosed || state.cliAskedForWork) return false;
|
|
11707
|
+
discardUnattachedProxyServer();
|
|
11708
|
+
return true;
|
|
11709
|
+
};
|
|
10713
11710
|
const setup = async () => {
|
|
11711
|
+
if (stoppedBeforeWork()) return;
|
|
10714
11712
|
if (!compactionMode && state.activeProcess && self.config.hotReloadMcp !== false && self.config.bridgeOpencodeMcp !== false) {
|
|
10715
11713
|
const probe = self.effectiveMcpConfig(cwd, void 0, runtimeStatus);
|
|
10716
11714
|
const previousHash = state.activeProcess.mcpHash ?? null;
|
|
@@ -10783,6 +11781,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10783
11781
|
enabled: self.config.bridgeOpencodeSkills === true,
|
|
10784
11782
|
...self.skillBridgeSpawn(failover)
|
|
10785
11783
|
});
|
|
11784
|
+
if (stoppedBeforeWork()) return;
|
|
10786
11785
|
const ap = spawnInteractiveProcess({
|
|
10787
11786
|
cwd,
|
|
10788
11787
|
cliPath,
|
|
@@ -10989,6 +11988,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
10989
11988
|
spawnMcpHash = mcp.bridgedHash;
|
|
10990
11989
|
spawnMcpServers = mcp.allEnabledServerNames;
|
|
10991
11990
|
}
|
|
11991
|
+
if (stoppedBeforeWork()) return;
|
|
10992
11992
|
if (state.activeProcess && !compactionMode) {
|
|
10993
11993
|
state.proc = state.activeProcess.proc;
|
|
10994
11994
|
state.lineEmitter = state.activeProcess.lineEmitter;
|
|
@@ -11019,6 +12019,8 @@ var ClaudeCodeLanguageModel = class {
|
|
|
11019
12019
|
log.warn("previous turn did not stop in time; this turn may see stale output", { sk });
|
|
11020
12020
|
}
|
|
11021
12021
|
}
|
|
12022
|
+
if (stoppedBeforeWork()) return;
|
|
12023
|
+
state.streamStarted = true;
|
|
11022
12024
|
controller.enqueue({ type: "stream-start", warnings });
|
|
11023
12025
|
if (failoverNote) {
|
|
11024
12026
|
controller.enqueue({
|
|
@@ -11377,72 +12379,8 @@ var ClaudeCodeLanguageModel = class {
|
|
|
11377
12379
|
state.drainTimer = setTimeout(() => drainNow(state), DRAIN_QUIET_MS);
|
|
11378
12380
|
});
|
|
11379
12381
|
state.proc.on("error", procErrorHandler);
|
|
11380
|
-
if (options.abortSignal) {
|
|
11381
|
-
const releaseAbandonedProxyCalls = (reason) => {
|
|
11382
|
-
if (state.drainBuffer.length === 0 && getPendingProxyCalls(sk).length === 0) return;
|
|
11383
|
-
rejectAllPendingProxyCallsForSession(sk, new Error(reason));
|
|
11384
|
-
state.drainBuffer.length = 0;
|
|
11385
|
-
};
|
|
11386
|
-
options.abortSignal.addEventListener("abort", () => {
|
|
11387
|
-
state.autoContinueState.aborted = true;
|
|
11388
|
-
if (state.turnCompleted || state.controllerClosed) {
|
|
11389
|
-
const stoppedProcess = state.activeProcess;
|
|
11390
|
-
if (stoppedProcess && stoppedProcess.lineEmitter.listenerCount("line") === 0 && getPendingProxyCalls(sk).length > 0) {
|
|
11391
|
-
const reason = describeAbortReason(options.abortSignal?.reason);
|
|
11392
|
-
void settleSessionRunState(affinity).then((stopped) => {
|
|
11393
|
-
const stillParked = stoppedProcess.lineEmitter.listenerCount("line") === 0 && getPendingProxyCalls(sk).length > 0;
|
|
11394
|
-
if (stopped === "busy" || !stillParked) {
|
|
11395
|
-
log.debug("abort at a tool boundary while opencode is still running the turn; keeping pending calls", {
|
|
11396
|
-
sk,
|
|
11397
|
-
session: stopped,
|
|
11398
|
-
stillParked,
|
|
11399
|
-
reason
|
|
11400
|
-
});
|
|
11401
|
-
return;
|
|
11402
|
-
}
|
|
11403
|
-
log.info("abort between proxy tool boundaries; releasing pending calls", { sk, reason });
|
|
11404
|
-
void interruptTurn(stoppedProcess).then((idle) => {
|
|
11405
|
-
log.info("interrupt sent for aborted turn", { sk, idle });
|
|
11406
|
-
});
|
|
11407
|
-
releaseAbandonedProxyCalls(
|
|
11408
|
-
"Provider stream was aborted while opencode was running its proxy tool calls"
|
|
11409
|
-
);
|
|
11410
|
-
});
|
|
11411
|
-
}
|
|
11412
|
-
return;
|
|
11413
|
-
}
|
|
11414
|
-
if (state.activeProcess) {
|
|
11415
|
-
void interruptTurn(state.activeProcess).then((idle) => {
|
|
11416
|
-
log.info("interrupt sent for aborted turn", { sk, idle });
|
|
11417
|
-
});
|
|
11418
|
-
}
|
|
11419
|
-
if (!state.hasReceivedContent) {
|
|
11420
|
-
log.info(
|
|
11421
|
-
"abort signal received before content, closing stream immediately",
|
|
11422
|
-
{ cwd }
|
|
11423
|
-
);
|
|
11424
|
-
releaseAbandonedProxyCalls(
|
|
11425
|
-
"Provider stream was aborted before pending proxy calls were emitted"
|
|
11426
|
-
);
|
|
11427
|
-
state.controllerClosed = true;
|
|
11428
|
-
cleanupTurn();
|
|
11429
|
-
try {
|
|
11430
|
-
controller.close();
|
|
11431
|
-
} catch {
|
|
11432
|
-
}
|
|
11433
|
-
return;
|
|
11434
|
-
}
|
|
11435
|
-
log.info(
|
|
11436
|
-
"abort signal received mid-turn, starting grace period",
|
|
11437
|
-
{ cwd }
|
|
11438
|
-
);
|
|
11439
|
-
releaseAbandonedProxyCalls(
|
|
11440
|
-
"Provider stream was aborted while proxy tool calls were pending"
|
|
11441
|
-
);
|
|
11442
|
-
startResultFallback(state, 5e3);
|
|
11443
|
-
});
|
|
11444
|
-
}
|
|
11445
12382
|
if (hasMatchedPendingResults) {
|
|
12383
|
+
state.cliAskedForWork = true;
|
|
11446
12384
|
for (const { call, result } of previousPendingProxyMatches) {
|
|
11447
12385
|
if (result) {
|
|
11448
12386
|
const channelClosed = isPendingProxyCallChannelClosed(call);
|
|
@@ -11500,6 +12438,7 @@ var ClaudeCodeLanguageModel = class {
|
|
|
11500
12438
|
);
|
|
11501
12439
|
}
|
|
11502
12440
|
}
|
|
12441
|
+
state.cliAskedForWork = true;
|
|
11503
12442
|
if (state.activeProcess) noteTurnStarted(state.activeProcess);
|
|
11504
12443
|
state.proc.stdin?.write(userMsg + "\n");
|
|
11505
12444
|
log.debug("sent user message", { textLength: userMsg.length });
|
|
@@ -11535,11 +12474,11 @@ import {
|
|
|
11535
12474
|
existsSync as existsSync3,
|
|
11536
12475
|
mkdirSync as mkdirSync4,
|
|
11537
12476
|
readFileSync as readFileSync6,
|
|
11538
|
-
realpathSync as
|
|
12477
|
+
realpathSync as realpathSync4,
|
|
11539
12478
|
rmSync as rmSync3,
|
|
11540
12479
|
writeFileSync as writeFileSync5
|
|
11541
12480
|
} from "fs";
|
|
11542
|
-
import { homedir as
|
|
12481
|
+
import { homedir as homedir8 } from "os";
|
|
11543
12482
|
import { dirname as dirname6, join as join9, resolve as resolve5 } from "path";
|
|
11544
12483
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
11545
12484
|
var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
|
|
@@ -11549,12 +12488,12 @@ function candidateCacheRoots() {
|
|
|
11549
12488
|
const xdg = process.env.XDG_CACHE_HOME;
|
|
11550
12489
|
return [
|
|
11551
12490
|
xdg ? join9(xdg, "opencode") : null,
|
|
11552
|
-
join9(
|
|
11553
|
-
join9(
|
|
12491
|
+
join9(homedir8(), ".cache", "opencode"),
|
|
12492
|
+
join9(homedir8(), "Library", "Caches", "opencode")
|
|
11554
12493
|
].filter((p) => Boolean(p));
|
|
11555
12494
|
}
|
|
11556
12495
|
function userOpencodeJsonPath() {
|
|
11557
|
-
const xdgConfig = process.env.XDG_CONFIG_HOME ?? join9(
|
|
12496
|
+
const xdgConfig = process.env.XDG_CONFIG_HOME ?? join9(homedir8(), ".config");
|
|
11558
12497
|
return join9(xdgConfig, "opencode", "opencode.json");
|
|
11559
12498
|
}
|
|
11560
12499
|
function userIntendsToUseUnscoped() {
|
|
@@ -11574,7 +12513,7 @@ function userIntendsToUseUnscoped() {
|
|
|
11574
12513
|
function ourLoadedDir() {
|
|
11575
12514
|
try {
|
|
11576
12515
|
const filePath = fileURLToPath3(import.meta.url);
|
|
11577
|
-
return
|
|
12516
|
+
return realpathSync4(resolve5(filePath, "..", ".."));
|
|
11578
12517
|
} catch {
|
|
11579
12518
|
return null;
|
|
11580
12519
|
}
|
|
@@ -11589,7 +12528,7 @@ function ourVersion(dir) {
|
|
|
11589
12528
|
}
|
|
11590
12529
|
}
|
|
11591
12530
|
function markerPath() {
|
|
11592
|
-
const stateRoot = process.env.XDG_STATE_HOME ?? join9(
|
|
12531
|
+
const stateRoot = process.env.XDG_STATE_HOME ?? join9(homedir8(), ".local", "state");
|
|
11593
12532
|
return join9(stateRoot, "opencode-claude-code-plugin", "cleanup-stale.json");
|
|
11594
12533
|
}
|
|
11595
12534
|
function markerVersion() {
|
|
@@ -11643,7 +12582,7 @@ function cleanupOne(cacheRoot, ourDir) {
|
|
|
11643
12582
|
if (!existsSync3(stalePath)) return;
|
|
11644
12583
|
let realStalePath = stalePath;
|
|
11645
12584
|
try {
|
|
11646
|
-
realStalePath =
|
|
12585
|
+
realStalePath = realpathSync4(stalePath);
|
|
11647
12586
|
} catch {
|
|
11648
12587
|
}
|
|
11649
12588
|
if (ourDir && realStalePath === ourDir) return;
|