@khalilgharbaoui/opencode-claude-code-plugin 0.34.1 → 0.35.1

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/dist/index.js CHANGED
@@ -1,5 +1,14 @@
1
- // src/claude-code-language-model.ts
2
- import { generateId as generateId3 } from "@ai-sdk/provider-utils";
1
+ // src/ids.ts
2
+ import { randomInt } from "crypto";
3
+ var ID_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
4
+ var ID_LENGTH = 16;
5
+ function generateId() {
6
+ let id = "";
7
+ for (let index = 0; index < ID_LENGTH; index++) {
8
+ id += ID_ALPHABET[randomInt(ID_ALPHABET.length)];
9
+ }
10
+ return id;
11
+ }
3
12
 
4
13
  // src/host-tools.ts
5
14
  function defined(input) {
@@ -197,6 +206,9 @@ function configureLogger(input) {
197
206
  function resolvedLogFile() {
198
207
  return join(activeConfig.dir ?? DEFAULT_DIR, "plugin.log");
199
208
  }
209
+ function describeLogFile() {
210
+ return { path: resolvedLogFile(), enabled: activeConfig.file && !fileLoggingDisabled };
211
+ }
200
212
  function rotateIfNeeded(logFile) {
201
213
  try {
202
214
  const stat = statSync(logFile);
@@ -830,6 +842,135 @@ function reportUnrecognizedModel(stderr) {
830
842
  warnedUnrecognizedModels.add(model);
831
843
  log.warn(message, { model, ...floor ? { cliFloor: floor } : {} });
832
844
  }
845
+ var HOOK_SUBTYPES = {
846
+ hook_started: "started",
847
+ hook_progress: "progress",
848
+ hook_response: "response"
849
+ };
850
+ function parseHookEvent(msg) {
851
+ if (msg.type !== "system") return null;
852
+ const phase = msg.subtype ? HOOK_SUBTYPES[msg.subtype] : void 0;
853
+ if (!phase) return null;
854
+ const raw = msg;
855
+ const hookName = str(raw.hook_name);
856
+ const hookEvent = str(raw.hook_event);
857
+ if (!hookName || !hookEvent) return null;
858
+ return {
859
+ phase,
860
+ hookId: str(raw.hook_id) ?? "unknown",
861
+ hookName,
862
+ hookEvent,
863
+ exitCode: num(raw.exit_code),
864
+ outcome: str(raw.outcome),
865
+ stderr: str(raw.stderr)
866
+ };
867
+ }
868
+ function isHookFailure(hook) {
869
+ if (hook.phase !== "response") return false;
870
+ if (hook.outcome === "error") return true;
871
+ if (hook.outcome === "cancelled" || hook.outcome === "success") return false;
872
+ return hook.exitCode !== void 0 && hook.exitCode !== 0;
873
+ }
874
+ var HOOK_STDERR_CAP = 200;
875
+ function describeHookFailure(hook) {
876
+ const how = hook.exitCode !== void 0 ? `exited ${hook.exitCode}` : `reported \`${hook.outcome ?? "error"}\``;
877
+ const tail = hook.stderr?.trim();
878
+ const detail = tail ? ` It wrote to stderr: ${tail.length > HOOK_STDERR_CAP ? `${tail.slice(0, HOOK_STDERR_CAP)}...` : tail}` : " It wrote nothing to stderr.";
879
+ 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}`;
880
+ }
881
+ var warnedHookFailures = /* @__PURE__ */ new Set();
882
+ var lastHookFailures = /* @__PURE__ */ new Map();
883
+ function snapshotHookFailures() {
884
+ return [...lastHookFailures.values()];
885
+ }
886
+ function reportHookEvent(msg) {
887
+ const hook = parseHookEvent(msg);
888
+ if (!hook) return;
889
+ const data = {
890
+ hook: hook.hookName,
891
+ event: hook.hookEvent,
892
+ ...hook.exitCode === void 0 ? {} : { exitCode: hook.exitCode },
893
+ ...hook.outcome ? { outcome: hook.outcome } : {}
894
+ };
895
+ if (!isHookFailure(hook)) {
896
+ log.debug(`claude code hook ${hook.phase}`, data);
897
+ return;
898
+ }
899
+ const failure = {
900
+ hookName: hook.hookName,
901
+ hookEvent: hook.hookEvent,
902
+ exitCode: hook.exitCode,
903
+ outcome: hook.outcome,
904
+ stderr: hook.stderr?.trim().slice(0, HOOK_STDERR_CAP) ?? ""
905
+ };
906
+ lastHookFailures.set(`${hook.hookEvent}:${hook.hookName}`, failure);
907
+ const key = `${hook.hookEvent}:${hook.hookName}:${hook.outcome ?? "?"}:${hook.exitCode ?? "?"}`;
908
+ const message = describeHookFailure(hook);
909
+ if (warnedHookFailures.has(key)) {
910
+ log.debug(message, data);
911
+ return;
912
+ }
913
+ warnedHookFailures.add(key);
914
+ log.warn(message, data);
915
+ }
916
+ function parseToolProgress(msg) {
917
+ if (msg.type !== "tool_progress") return null;
918
+ const raw = msg;
919
+ const frameToolUseId = str(raw.tool_use_id);
920
+ const toolName = str(raw.tool_name);
921
+ const elapsedSeconds = num(raw.elapsed_time_seconds);
922
+ if (!frameToolUseId || !toolName || elapsedSeconds === void 0) return null;
923
+ const retry = isRecord(raw.subagent_retry) ? raw.subagent_retry : void 0;
924
+ return {
925
+ toolUseId: str(raw.parent_tool_use_id) ?? frameToolUseId,
926
+ frameToolUseId,
927
+ toolName,
928
+ elapsedSeconds,
929
+ heartbeat: raw.heartbeat === true,
930
+ taskId: str(raw.task_id),
931
+ subagentType: str(raw.subagent_type),
932
+ subagentRetry: retry ? {
933
+ agentId: str(retry.agent_id) ?? "unknown",
934
+ attempt: num(retry.attempt) ?? 0,
935
+ maxRetries: num(retry.max_retries) ?? 0,
936
+ errorStatus: num(retry.error_status),
937
+ errorCategory: str(retry.error_category) ?? "unknown"
938
+ } : void 0
939
+ };
940
+ }
941
+ function describeToolProgress(progress) {
942
+ if (progress.subagentRetry) {
943
+ const retry = progress.subagentRetry;
944
+ 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.`;
945
+ }
946
+ return `claude code tool still running: ${progress.toolName} at ${progress.elapsedSeconds}s`;
947
+ }
948
+ var reportedSubagentRetries = /* @__PURE__ */ new Set();
949
+ function reportToolProgress(msg) {
950
+ const progress = parseToolProgress(msg);
951
+ if (!progress) return;
952
+ const data = {
953
+ tool: progress.toolName,
954
+ toolUseId: progress.toolUseId,
955
+ elapsedSeconds: progress.elapsedSeconds,
956
+ ...progress.taskId ? { taskId: progress.taskId } : {},
957
+ ...progress.subagentType ? { subagentType: progress.subagentType } : {}
958
+ };
959
+ const message = describeToolProgress(progress);
960
+ if (!progress.subagentRetry) {
961
+ log.info(message, data);
962
+ return;
963
+ }
964
+ const retry = progress.subagentRetry;
965
+ const key = `${retry.agentId}:${retry.errorCategory}`;
966
+ const retryData = { ...data, ...retry };
967
+ if (reportedSubagentRetries.has(key)) {
968
+ log.debug(message, retryData);
969
+ return;
970
+ }
971
+ reportedSubagentRetries.add(key);
972
+ log.notice(message, retryData);
973
+ }
833
974
 
834
975
  // src/plan-mode-question.ts
835
976
  var QUESTION_TOOL_NAME = "question";
@@ -4996,6 +5137,625 @@ async function handleBtwCommand(client, input, options = {}) {
4996
5137
  }
4997
5138
  }
4998
5139
 
5140
+ // src/diagnostic-bundle.ts
5141
+ import { createHash as createHash3, randomBytes as randomBytes2 } from "crypto";
5142
+ import { openSync, readSync, closeSync, fstatSync, realpathSync } from "fs";
5143
+ import { homedir as homedir4 } from "os";
5144
+
5145
+ // src/log-messages.ts
5146
+ var PLUGIN_LOG_MESSAGES = /* @__PURE__ */ new Set([
5147
+ "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.",
5148
+ "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.",
5149
+ "Code Mode execute not forwarded: no model-visible input schema",
5150
+ "Code Mode execute not forwarded: read-only permission preset",
5151
+ "MCP tool not routed through the proxy: another proxy tool already holds that name, and it keeps it",
5152
+ "account failover declined; ending the turn",
5153
+ "agent effort override refused: unknown level",
5154
+ "agent model override refused: unknown model",
5155
+ "agent prompt cache ttl refused: unknown value",
5156
+ "auto-continuation stopped",
5157
+ "auto-continuing incomplete claude result",
5158
+ "btw: a turn is running but no claude process appeared for it",
5159
+ "btw: early aside failed; the message will ask again",
5160
+ "cannot interrupt this transport; waiting for the turn to end",
5161
+ "claude --version output unparseable",
5162
+ "claude already registers a different skill under this name; not bridging ours",
5163
+ "claude cli < 2.1.142 detected; Opus 4.7 thinking summaries unavailable. Run `npm i -g @anthropic-ai/claude-code` to upgrade.",
5164
+ "claude cli does not support --plugin-dir; opencode skills will not be bridged. Run `npm i -g @anthropic-ai/claude-code` to upgrade.",
5165
+ "claude code compacted its own context",
5166
+ "claude code reset its conversation",
5167
+ "claude finished the turn without a reply",
5168
+ "claude kept missing the probe deadline; keeping the conservative answer for this process",
5169
+ "claude process belongs to another account; starting fresh",
5170
+ "claude process closed without a result",
5171
+ "claude process did not exit; starting a fresh session",
5172
+ "claude process error",
5173
+ "claude process stdin error",
5174
+ "claude process still silent after respawn; ending turn",
5175
+ "claude session ID error, clearing session",
5176
+ "claude-code doctor failed to build its report",
5177
+ "claude-code plugin ready",
5178
+ "cleanup-stale: cache package.json update failed",
5179
+ "cleanup-stale: could not record the sweep marker",
5180
+ "cleanup-stale: error processing cache root",
5181
+ "cleanup-stale: rmSync failed",
5182
+ "delivering proxy results after interrupted continuation",
5183
+ "draining proxy calls queued between turns",
5184
+ "empty user content; sending sentinel to satisfy CLI",
5185
+ "every claude process is mid-turn; skipping LRU eviction",
5186
+ "failed to abort opencode session",
5187
+ "failed to build the agent registry for opencode 2",
5188
+ "failed to close the proxy server of an aborted turn",
5189
+ "failed to detect claude cli version",
5190
+ "failed to expand account provider",
5191
+ "failed to fetch opencode MCP runtime status",
5192
+ "failed to fetch opencode session directory",
5193
+ "failed to fetch opencode session messages",
5194
+ "failed to fetch opencode session parent",
5195
+ "failed to fetch opencode tool list",
5196
+ "failed to parse opencode config",
5197
+ "failed to prepare the failover account runtime; staying put",
5198
+ "failed to probe claude cli flag support",
5199
+ "failed to re-send envelope after respawn",
5200
+ "failed to set up doStream",
5201
+ "failed to stage opencode skill plugin dir",
5202
+ "failed to symlink shared capabilities; continuing anyway",
5203
+ "failed to write bridged MCP config",
5204
+ "failed to write control response",
5205
+ "failed to write interrupt control request",
5206
+ "failed to write system prompt file",
5207
+ "fallback model refused: unknown model",
5208
+ "falling back to the next model in the chain",
5209
+ "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.",
5210
+ "file part without data, skipping",
5211
+ "ignoring a failover reset time that is not in the future",
5212
+ "ignoring unknown proxyOpencodeTools entries",
5213
+ "ignoring unknown proxyTools entries",
5214
+ "interactive permissionMode bypassPermissions ignored: Claude Code prompts for confirmation in the TUI",
5215
+ "interactive system prompt disabled; opencode agent prompts will not be appended",
5216
+ "interactive transport dropped non-text content blocks",
5217
+ "interactive turn failed",
5218
+ "interactiveBypass ignored: Claude Code prompts for bypassPermissions confirmation in the interactive TUI",
5219
+ "malformed data URI, skipping file part",
5220
+ "no active process to respawn (start watchdog); ending turn",
5221
+ "no proxyTools entry was recognised; nothing will be proxied this turn",
5222
+ "no stdout after envelope write; respawning claude process to resume conversation",
5223
+ "opencode 2 event subscription ended; deleted sessions keep their claude process until idle eviction",
5224
+ "pending proxy call arrived after stream close; rejecting",
5225
+ '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.',
5226
+ `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.`,
5227
+ '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.',
5228
+ "previous turn did not stop in time; this turn may see stale output",
5229
+ "previous turn still in flight; interrupting it",
5230
+ "process error",
5231
+ "provider model entry is partial; completing it from the pass-through template",
5232
+ "proxy call past its deadline, but opencode is still serving it; waiting",
5233
+ "proxy call still waiting, deadline approaching",
5234
+ "proxy call still waiting, no deadline",
5235
+ "proxy-mcp client closed a tool call before its result",
5236
+ "proxy-mcp dropping result for a closed tool call",
5237
+ "proxy-mcp interceptor failed",
5238
+ "proxy-mcp rejected a request",
5239
+ "proxy-mcp tool call timed out",
5240
+ "proxyOpencodeTools entry dropped: a proxy tool already holds that name, and it keeps it",
5241
+ "proxyOpencodeTools is set but opencode's tool registry did not answer; forwarding nothing this spawn",
5242
+ "refusing the pid-named scratch directory",
5243
+ "rejected pending proxy call",
5244
+ "remote URL images are not supported by Claude CLI, skipping",
5245
+ "replaying stdout the child emitted between turns",
5246
+ "result fallback timer fired \u2014 closing stream without result event",
5247
+ "shared Claude capability already exists; leaving untouched",
5248
+ "skipping MCP server with unknown type",
5249
+ "skipping local MCP server with no command",
5250
+ "skipping remote MCP server with no url",
5251
+ "stale scratch directory sweep failed",
5252
+ "task_batch resolving with child results missing",
5253
+ "timed out pending proxy call",
5254
+ "turn abort listener threw",
5255
+ "two different skills share a name; bridging the one the nearest root claimed",
5256
+ "unknown permissionPreset; no preset applied and every permission setting is left exactly as configured",
5257
+ "unsupported file part data type",
5258
+ "unsupported media type for Claude image block, skipping"
5259
+ ]);
5260
+
5261
+ // src/diagnostic-bundle.ts
5262
+ var BUNDLE_ARGUMENTS = /* @__PURE__ */ new Set(["bundle", "issue", "report", "paste"]);
5263
+ function wantsDiagnosticBundle(argument) {
5264
+ return BUNDLE_ARGUMENTS.has(argument.trim().toLowerCase());
5265
+ }
5266
+ var BUNDLE_LEVELS = /* @__PURE__ */ new Set(["NOTICE", "WARN", "ERROR"]);
5267
+ var BUNDLE_MAX_LINES = 120;
5268
+ var BUNDLE_MAX_BYTES = 24e3;
5269
+ var BUNDLE_TAIL_BYTES = 512 * 1024;
5270
+ var BUNDLE_MAX_VALUE_CHARS = 200;
5271
+ var BUNDLE_MAX_ENUM_CHARS = 40;
5272
+ var BUNDLE_MAX_VERSION_CHARS = 60;
5273
+ var BUNDLE_MAX_NAME_CHARS = 64;
5274
+ var BUNDLE_MAX_ARRAY_ITEMS = 20;
5275
+ var BUNDLE_DATA_ALLOWLIST = {
5276
+ // booleans: every one of these is a decision the plugin made
5277
+ applied: "flag",
5278
+ attached: "flag",
5279
+ bridgeSkipNativeSkills: "flag",
5280
+ channelClosed: "flag",
5281
+ compaction: "flag",
5282
+ compactionMode: "flag",
5283
+ emitted: "flag",
5284
+ enabled: "flag",
5285
+ executed: "flag",
5286
+ isUsingOverage: "flag",
5287
+ hadProxyActivity: "flag",
5288
+ hadReasoning: "flag",
5289
+ hadStreamThinking: "flag",
5290
+ hadToolActivity: "flag",
5291
+ hasActiveProcess: "flag",
5292
+ hasInput: "flag",
5293
+ hasText: "flag",
5294
+ inFlight: "flag",
5295
+ anthropicApiKeyInEnv: "flag",
5296
+ includeHistoryContext: "flag",
5297
+ interactive: "flag",
5298
+ interactiveTransport: "flag",
5299
+ isError: "flag",
5300
+ planModeQuestion: "flag",
5301
+ kept: "flag",
5302
+ listsAgentTypes: "flag",
5303
+ opencodeHasQuestion: "flag",
5304
+ registryResolved: "flag",
5305
+ sse: "flag",
5306
+ strictMcpConfig: "flag",
5307
+ supported: "flag",
5308
+ turnStats: "flag",
5309
+ // counts and sizes: a length is a shape, never content
5310
+ attempts: "count",
5311
+ cacheReadTokens: "count",
5312
+ cacheWriteTokens: "count",
5313
+ code: "count",
5314
+ count: "count",
5315
+ dropped: "count",
5316
+ exitCode: "count",
5317
+ historyLength: "count",
5318
+ inputTokens: "count",
5319
+ lastTextLength: "count",
5320
+ lines: "count",
5321
+ liveDescriptionLength: "count",
5322
+ numTurns: "count",
5323
+ outputTokens: "count",
5324
+ permissionDenials: "count",
5325
+ pid: "count",
5326
+ port: "count",
5327
+ postTokens: "count",
5328
+ preTokens: "count",
5329
+ removed: "count",
5330
+ skills: "count",
5331
+ stderrBytes: "count",
5332
+ textLength: "count",
5333
+ tools: "count",
5334
+ total: "count",
5335
+ ageMs: "duration",
5336
+ delayMs: "duration",
5337
+ deadlineMs: "duration",
5338
+ durationApiMs: "duration",
5339
+ durationMs: "duration",
5340
+ elapsedMs: "duration",
5341
+ remainingMs: "duration",
5342
+ timeoutMs: "duration",
5343
+ waitedMs: "duration",
5344
+ costUsd: "money",
5345
+ // enums: a fixed vocabulary this package or the CLI defines
5346
+ apiKeySource: "enum",
5347
+ delivery: "enum",
5348
+ effort: "enum",
5349
+ finishReason: "enum",
5350
+ hash: "enum",
5351
+ hostApi: "enum",
5352
+ kind: "enum",
5353
+ level: "enum",
5354
+ mode: "enum",
5355
+ overageDisabledReason: "enum",
5356
+ overageStatus: "enum",
5357
+ permissionMode: "enum",
5358
+ preset: "enum",
5359
+ rateLimitType: "enum",
5360
+ reason: "enum",
5361
+ reasoningEffort: "enum",
5362
+ scope: "enum",
5363
+ signal: "enum",
5364
+ source: "enum",
5365
+ state: "enum",
5366
+ status: "enum",
5367
+ stopReason: "enum",
5368
+ subtype: "enum",
5369
+ transport: "enum",
5370
+ trigger: "enum",
5371
+ type: "enum",
5372
+ verdict: "enum",
5373
+ // names: identifiers the operator configured or Claude Code published. The
5374
+ // doctor table already prints the same class of value (providers, accounts,
5375
+ // proxyTools, MCP servers), so a bundle is no more revealing than the report
5376
+ // it is attached to.
5377
+ accounts: "name",
5378
+ agent: "name",
5379
+ bundled: "name",
5380
+ excluded: "name",
5381
+ excludeServers: "name",
5382
+ from: "name",
5383
+ joined: "name",
5384
+ left: "name",
5385
+ mappedName: "name",
5386
+ model: "name",
5387
+ name: "name",
5388
+ names: "name",
5389
+ opencodeAgent: "name",
5390
+ provider: "name",
5391
+ providerOptionsKeys: "name",
5392
+ providers: "name",
5393
+ proxyTools: "name",
5394
+ server: "name",
5395
+ servers: "name",
5396
+ skipped: "name",
5397
+ toolName: "name",
5398
+ toolNames: "name",
5399
+ cliFloor: "version",
5400
+ cliVersion: "version",
5401
+ opencode: "version",
5402
+ plugin: "version",
5403
+ version: "version",
5404
+ // ids: hashed, never printed. Correlation survives, identity does not.
5405
+ callId: "id",
5406
+ claudeSessionId: "id",
5407
+ id: "id",
5408
+ messageId: "id",
5409
+ newConversationId: "id",
5410
+ previousSessionId: "id",
5411
+ sessionId: "id",
5412
+ sessionID: "id",
5413
+ sessionKey: "id",
5414
+ toolCallId: "id",
5415
+ toolCallIds: "id",
5416
+ toolUseId: "id",
5417
+ // paths: home becomes `~`. The doctor table prints cwd and the CLI path
5418
+ // already, so withholding them here would buy nothing and cost the one
5419
+ // field that explains most "it picked the wrong directory" reports.
5420
+ claudeLoads: "path",
5421
+ cliPath: "path",
5422
+ configDir: "path",
5423
+ cwd: "path",
5424
+ dir: "path",
5425
+ notBridged: "path",
5426
+ path: "path",
5427
+ pluginDir: "path",
5428
+ resolved: "path",
5429
+ target: "path",
5430
+ url: "url",
5431
+ cliArgs: "argv",
5432
+ // The hook and tool_progress reporters (#g185), added when the two lanes
5433
+ // merged: their message text is built at runtime and redacted, so these
5434
+ // fields are what keeps a failed-hook WARN readable in a bundle. `stderr`
5435
+ // is deliberately absent, because a hook can print anything to it.
5436
+ hook: "name",
5437
+ hookName: "name",
5438
+ event: "enum",
5439
+ hookEvent: "enum",
5440
+ outcome: "enum",
5441
+ tool: "name",
5442
+ subagentType: "name",
5443
+ errorCategory: "enum",
5444
+ errorStatus: "count",
5445
+ elapsedSeconds: "count",
5446
+ attempt: "count",
5447
+ maxRetries: "count",
5448
+ taskId: "id",
5449
+ agentId: "id"
5450
+ };
5451
+ function shape(value) {
5452
+ if (typeof value === "string") return `[redacted, ${value.length} chars]`;
5453
+ let size;
5454
+ try {
5455
+ size = JSON.stringify(value)?.length ?? 0;
5456
+ } catch {
5457
+ size = 0;
5458
+ }
5459
+ return `[redacted, ${size} chars]`;
5460
+ }
5461
+ function createIdHasher(salt = randomBytes2(16).toString("hex")) {
5462
+ return (raw) => createHash3("sha256").update(salt).update("\0").update(raw).digest("hex").slice(0, 8);
5463
+ }
5464
+ function createRedactionContext(options = {}) {
5465
+ const home = options.home ?? homedir4();
5466
+ const homes = /* @__PURE__ */ new Set();
5467
+ if (home) homes.add(home);
5468
+ if (home) {
5469
+ try {
5470
+ homes.add(realpathSync.native(home));
5471
+ } catch {
5472
+ }
5473
+ }
5474
+ return {
5475
+ homes: [...homes].filter(Boolean).sort((a, b) => b.length - a.length),
5476
+ hash: createIdHasher(options.salt)
5477
+ };
5478
+ }
5479
+ function redactHome(text, context) {
5480
+ let out = text;
5481
+ for (const home of context.homes) out = out.split(home).join("~");
5482
+ return out;
5483
+ }
5484
+ var OPENCODE_SESSION_ID = /\bses_[A-Za-z0-9]{6,}/g;
5485
+ 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;
5486
+ function redactSessionIds(text, context) {
5487
+ return text.replace(OPENCODE_SESSION_ID, (raw) => `ses_${context.hash(raw)}`).replace(UUID, (raw) => `uuid_${context.hash(raw)}`);
5488
+ }
5489
+ function redactForPaste(text, context) {
5490
+ return redactSessionIds(redactHome(text, context), context);
5491
+ }
5492
+ var SAFE_NAME = /^[A-Za-z0-9][\w.:@+/-]*$/;
5493
+ var SAFE_ENUM = /^[A-Za-z0-9][\w.:@+/-]*$/;
5494
+ var SAFE_VERSION = /^[v0-9][\w.()+ -]*$/;
5495
+ var VERSION_PLACEHOLDERS = /* @__PURE__ */ new Set(["unknown", "none", "not detected", "unset"]);
5496
+ var SAFE_PATH = /^(?:[/~.]|[A-Za-z]:[\\/])/;
5497
+ var CLI_FLAG = /^-{1,2}[A-Za-z][\w-]*$/;
5498
+ var SUSPICIOUS = [
5499
+ /sk-[A-Za-z0-9]/,
5500
+ // Anthropic and OpenAI style API keys
5501
+ /\b(?:gh[pousr]_|xox[baprs]-|github_pat_)/,
5502
+ // GitHub and Slack tokens
5503
+ /[0-9a-f]{32,}/i,
5504
+ // hex secrets; the proxy authToken is 64 of these
5505
+ /[A-Za-z0-9+/]{40,}={0,2}/,
5506
+ // base64 blobs
5507
+ /BEGIN [A-Z ]*PRIVATE KEY/,
5508
+ /^bearer\b/i,
5509
+ /=/
5510
+ // any assignment: ANTHROPIC_API_KEY=…, token=…
5511
+ ];
5512
+ function looksSecret(value) {
5513
+ return SUSPICIOUS.some((pattern) => pattern.test(value));
5514
+ }
5515
+ function sanitizeScalar(kind, value, context) {
5516
+ if (value === null) return null;
5517
+ switch (kind) {
5518
+ case "flag":
5519
+ return typeof value === "boolean" ? value : shape(value);
5520
+ case "count":
5521
+ case "duration":
5522
+ return typeof value === "number" && Number.isFinite(value) ? value : shape(value);
5523
+ case "money":
5524
+ return typeof value === "number" && Number.isFinite(value) ? Math.round(value * 1e4) / 1e4 : shape(value);
5525
+ case "enum": {
5526
+ if (typeof value === "boolean" || typeof value === "number") return value;
5527
+ if (typeof value !== "string") return shape(value);
5528
+ return value.length <= BUNDLE_MAX_ENUM_CHARS && SAFE_ENUM.test(value) && !looksSecret(value) ? value : shape(value);
5529
+ }
5530
+ case "name": {
5531
+ if (typeof value !== "string") return shape(value);
5532
+ return value.length <= BUNDLE_MAX_NAME_CHARS && SAFE_NAME.test(value) && !looksSecret(value) ? value : shape(value);
5533
+ }
5534
+ case "version": {
5535
+ if (typeof value !== "string") return shape(value);
5536
+ if (VERSION_PLACEHOLDERS.has(value)) return value;
5537
+ return value.length <= BUNDLE_MAX_VERSION_CHARS && SAFE_VERSION.test(value) && !looksSecret(value) ? value : shape(value);
5538
+ }
5539
+ case "id":
5540
+ return typeof value === "string" ? context.hash(value) : shape(value);
5541
+ case "path": {
5542
+ if (typeof value !== "string") return shape(value);
5543
+ if (!SAFE_PATH.test(value)) return sanitizeScalar("name", value, context);
5544
+ if (looksSecret(value)) return shape(value);
5545
+ const rewritten = redactHome(value, context);
5546
+ return rewritten.length <= BUNDLE_MAX_VALUE_CHARS && !/\s{2,}|[\n\r]/.test(rewritten) ? rewritten : shape(value);
5547
+ }
5548
+ case "url":
5549
+ return typeof value === "string" ? sanitizeUrl(value) : shape(value);
5550
+ case "argv":
5551
+ return shape(value);
5552
+ }
5553
+ }
5554
+ function sanitizeUrl(raw) {
5555
+ let parsed;
5556
+ try {
5557
+ parsed = new URL(raw);
5558
+ } catch {
5559
+ return shape(raw);
5560
+ }
5561
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return shape(raw);
5562
+ if (looksSecret(parsed.pathname)) return shape(raw);
5563
+ const credentials = parsed.username || parsed.password ? "[redacted]@" : "";
5564
+ const query = parsed.search ? `?[redacted, ${parsed.search.length - 1} chars]` : "";
5565
+ return `${parsed.protocol}//${credentials}${parsed.host}${parsed.pathname}${query}`;
5566
+ }
5567
+ function sanitizeArgv(value) {
5568
+ if (!Array.isArray(value)) return shape(value);
5569
+ const out = [];
5570
+ for (const item of value.slice(0, BUNDLE_MAX_ARRAY_ITEMS * 2)) {
5571
+ if (typeof item !== "string") {
5572
+ out.push(shape(item));
5573
+ continue;
5574
+ }
5575
+ if (!item.startsWith("-")) {
5576
+ out.push(`[redacted, ${item.length} chars]`);
5577
+ continue;
5578
+ }
5579
+ const equals = item.indexOf("=");
5580
+ if (equals === -1) {
5581
+ out.push(CLI_FLAG.test(item) ? item : shape(item));
5582
+ continue;
5583
+ }
5584
+ const flag = item.slice(0, equals);
5585
+ out.push(
5586
+ `${CLI_FLAG.test(flag) ? flag : "[redacted]"}=[redacted, ${item.length - equals - 1} chars]`
5587
+ );
5588
+ }
5589
+ if (value.length > out.length) out.push(`[+${value.length - out.length} more]`);
5590
+ return out;
5591
+ }
5592
+ var BUNDLE_MAX_DEPTH = 4;
5593
+ function kindFor(key) {
5594
+ return Object.prototype.hasOwnProperty.call(BUNDLE_DATA_ALLOWLIST, key) ? BUNDLE_DATA_ALLOWLIST[key] : void 0;
5595
+ }
5596
+ function sanitizeValue(key, value, context, depth = 0) {
5597
+ if (kindFor(key) === "argv") return sanitizeArgv(value);
5598
+ if (depth > BUNDLE_MAX_DEPTH) return shape(value);
5599
+ if (Array.isArray(value)) {
5600
+ const out = value.slice(0, BUNDLE_MAX_ARRAY_ITEMS).map((item) => sanitizeValue(key, item, context, depth + 1));
5601
+ if (value.length > out.length) out.push(`[+${value.length - out.length} more]`);
5602
+ return out;
5603
+ }
5604
+ if (value !== null && typeof value === "object") {
5605
+ return sanitizeLogData(value, context, depth + 1);
5606
+ }
5607
+ const kind = kindFor(key);
5608
+ return kind === void 0 ? shape(value) : sanitizeScalar(kind, value, context);
5609
+ }
5610
+ function sanitizeLogData(data, context, depth = 0) {
5611
+ const out = {};
5612
+ for (const [key, value] of Object.entries(data)) {
5613
+ const known = kindFor(key) !== void 0;
5614
+ const safeKey = known || key.length <= BUNDLE_MAX_NAME_CHARS && SAFE_NAME.test(key) && !looksSecret(key) ? key : `[redacted key, ${key.length} chars]`;
5615
+ out[safeKey] = sanitizeValue(key, value, context, depth);
5616
+ }
5617
+ return out;
5618
+ }
5619
+ var LOG_LINE = /^\[([^\]]+)\] \[opencode-claude-code\] (DEBUG|INFO|NOTICE|WARN|ERROR): ([\s\S]*)$/;
5620
+ function parseLogLine(raw) {
5621
+ const match = LOG_LINE.exec(raw);
5622
+ if (!match) return null;
5623
+ const rest = match[3];
5624
+ for (let index = rest.indexOf("{"); index !== -1; index = rest.indexOf("{", index + 1)) {
5625
+ let parsed;
5626
+ try {
5627
+ parsed = JSON.parse(rest.slice(index));
5628
+ } catch {
5629
+ continue;
5630
+ }
5631
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
5632
+ return {
5633
+ timestamp: match[1],
5634
+ level: match[2],
5635
+ message: rest.slice(0, index).trimEnd(),
5636
+ data: parsed
5637
+ };
5638
+ }
5639
+ }
5640
+ return { timestamp: match[1], level: match[2], message: rest, data: null };
5641
+ }
5642
+ function redactLogLine(raw, context) {
5643
+ const parsed = parseLogLine(raw);
5644
+ if (!parsed || !BUNDLE_LEVELS.has(parsed.level)) return null;
5645
+ const message = PLUGIN_LOG_MESSAGES.has(parsed.message) ? parsed.message : `[redacted message, ${parsed.message.length} chars]`;
5646
+ const head = `[${parsed.timestamp}] ${parsed.level}: ${message}`;
5647
+ if (!parsed.data || Object.keys(parsed.data).length === 0) return head;
5648
+ return `${head} ${JSON.stringify(sanitizeLogData(parsed.data, context))}`;
5649
+ }
5650
+ function readLogTail(path10, maxBytes = BUNDLE_TAIL_BYTES) {
5651
+ const fd = openSync(path10, "r");
5652
+ try {
5653
+ const size = fstatSync(fd).size;
5654
+ const length = Math.min(size, maxBytes);
5655
+ const buffer = Buffer.allocUnsafe(length);
5656
+ readSync(fd, buffer, 0, length, size - length);
5657
+ const text = buffer.toString("utf8");
5658
+ if (length < size) {
5659
+ const newline = text.indexOf("\n");
5660
+ return newline === -1 ? "" : text.slice(newline + 1);
5661
+ }
5662
+ return text;
5663
+ } finally {
5664
+ closeSync(fd);
5665
+ }
5666
+ }
5667
+ var BUNDLE_HEADER = "**Redacted log bundle**";
5668
+ var BUNDLE_PREAMBLE = [
5669
+ "Safe to paste into a GitHub issue. Built by an allowlist, not a filter: a line",
5670
+ "keeps its timestamp, its level, its message **only** when that message is a",
5671
+ "constant in this package's own source, and only those data fields whose values",
5672
+ "are non-secret by type (versions, counts, flags, enums, durations, exit codes,",
5673
+ "model and tool and server names). Everything else, including every unknown",
5674
+ "field, is replaced by `[redacted, N chars]`. Session ids are a per-bundle hash,",
5675
+ "so lines correlate here and nowhere else, and your home directory is `~`.",
5676
+ "",
5677
+ "Never included: prompt or reply text, system prompts, tool inputs or outputs,",
5678
+ "file contents, environment values, bearer tokens, API keys, `Authorization`",
5679
+ "headers, MCP server env or headers, URL credentials or query strings, or the",
5680
+ "raw spawn argv.",
5681
+ "",
5682
+ "Kept on purpose, so read it before pasting: folder paths below your home (your",
5683
+ "project and config folder names) and your `accounts` names, which a maintainer",
5684
+ "needs to read a cwd or an account problem."
5685
+ ].join("\n");
5686
+ var BUNDLE_LOGGING_OFF = [
5687
+ "File logging is **off**, so there is nothing to bundle. The report above still",
5688
+ "answers versions, config shape and live processes.",
5689
+ "",
5690
+ "Turn it on for one run and reproduce the problem:",
5691
+ "",
5692
+ "```sh",
5693
+ "OPENCODE_CLAUDE_CODE_LOG_FILE=1 opencode",
5694
+ "```",
5695
+ "",
5696
+ "or set it permanently in `opencode.json` under",
5697
+ "`provider.claude-code.options.logging`:",
5698
+ "",
5699
+ "```json",
5700
+ '{ "logging": { "file": true } }',
5701
+ "```",
5702
+ "",
5703
+ "Then run `/claude-code-doctor bundle` again. Logging is read once at process",
5704
+ "start, so a running window has to be relaunched."
5705
+ ].join("\n");
5706
+ function buildLogBundleSection(input) {
5707
+ const lines = [BUNDLE_HEADER, "", BUNDLE_PREAMBLE, ""];
5708
+ if (!input.fileLogging) {
5709
+ lines.push(BUNDLE_LOGGING_OFF);
5710
+ return lines.join("\n");
5711
+ }
5712
+ let tail;
5713
+ try {
5714
+ tail = (input.readTailImpl ?? readLogTail)(input.logPath, BUNDLE_TAIL_BYTES);
5715
+ } catch (error) {
5716
+ const reason = error instanceof Error ? error.message : String(error);
5717
+ lines.push(
5718
+ `File logging is on, but the log could not be read: ${redactHome(reason, input.context)}`
5719
+ );
5720
+ return lines.join("\n");
5721
+ }
5722
+ const raw = tail.split("\n");
5723
+ let scanned = 0;
5724
+ const kept = [];
5725
+ for (let index = raw.length - 1; index >= 0; index--) {
5726
+ const line = raw[index];
5727
+ if (!line) continue;
5728
+ if (parseLogLine(line)) scanned++;
5729
+ const redacted = redactLogLine(line, input.context);
5730
+ if (redacted === null) continue;
5731
+ if (kept.length >= BUNDLE_MAX_LINES) break;
5732
+ if (kept.reduce((total, entry) => total + entry.length + 1, 0) + redacted.length > BUNDLE_MAX_BYTES) {
5733
+ break;
5734
+ }
5735
+ kept.push(redacted);
5736
+ }
5737
+ kept.reverse();
5738
+ const dropped = Math.max(0, scanned - kept.length);
5739
+ lines.push(
5740
+ `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).`
5741
+ );
5742
+ if (dropped > 0) {
5743
+ lines.push("");
5744
+ lines.push(
5745
+ `${dropped} earlier or lower-level line${dropped === 1 ? " was" : "s were"} not included.`
5746
+ );
5747
+ }
5748
+ lines.push("");
5749
+ lines.push("```text");
5750
+ if (kept.length === 0) {
5751
+ lines.push("No NOTICE, WARN or ERROR line in the retained log.");
5752
+ } else {
5753
+ lines.push(...kept);
5754
+ }
5755
+ lines.push("```");
5756
+ return lines.join("\n");
5757
+ }
5758
+
4999
5759
  // src/plan-usage.ts
5000
5760
  import { execFile as execFile2 } from "child_process";
5001
5761
  var PLAN_USAGE_ARGUMENTS = /* @__PURE__ */ new Set(["usage", "cost", "stats", "limits"]);
@@ -5219,7 +5979,7 @@ function logStartupDiagnostics(providers, opencodeVersion) {
5219
5979
 
5220
5980
  // src/doctor.ts
5221
5981
  var DOCTOR_COMMAND = "claude-code-doctor";
5222
- var DOCTOR_COMMAND_DESCRIPTION = "Report what the Claude Code plugin sees: versions, cwd, live processes, pending proxy calls. Add `usage` for plan windows";
5982
+ 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";
5223
5983
  var DOCTOR_MARKER = "\u258C **claude-code doctor**";
5224
5984
  function isRecord3(value) {
5225
5985
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -5278,6 +6038,9 @@ function describePermissionPresets(rows) {
5278
6038
  return `${row.provider}: ${row.preset}${suffix}`;
5279
6039
  }).join(", ");
5280
6040
  }
6041
+ function tableCell(text) {
6042
+ return text.replace(/\r?\n/g, " ").replace(/\|/g, "\\|");
6043
+ }
5281
6044
  function formatDoctorReport(report) {
5282
6045
  const lines = [];
5283
6046
  lines.push(DOCTOR_MARKER);
@@ -5356,7 +6119,7 @@ function formatDoctorReport(report) {
5356
6119
  lines.push("| server | category | Claude Code said |");
5357
6120
  lines.push("|---|---|---|");
5358
6121
  for (const error of report.mcpServerErrors) {
5359
- lines.push(`| ${error.name} | \`${error.type}\` | ${error.message || "no detail"} |`);
6122
+ lines.push(`| ${error.name} | \`${error.type}\` | ${tableCell(error.message) || "no detail"} |`);
5360
6123
  }
5361
6124
  lines.push("");
5362
6125
  lines.push("A skipped server is missing from the model's tools with no other sign of it.");
@@ -5369,10 +6132,26 @@ function formatDoctorReport(report) {
5369
6132
  lines.push("|---|---|---|---|");
5370
6133
  for (const failure of report.pluginLoadFailures) {
5371
6134
  lines.push(
5372
- `| ${failure.plugin} | ${failure.kind} | \`${failure.type}\` | ${failure.message || "no detail"} |`
6135
+ `| ${failure.plugin} | ${failure.kind} | \`${failure.type}\` | ${tableCell(failure.message) || "no detail"} |`
5373
6136
  );
5374
6137
  }
5375
6138
  }
6139
+ if (report.hookFailures.length > 0) {
6140
+ lines.push("");
6141
+ lines.push("**Hooks Claude Code ran that failed**");
6142
+ lines.push("");
6143
+ lines.push("| hook | event | exit | outcome | its stderr |");
6144
+ lines.push("|---|---|---|---|---|");
6145
+ for (const failure of report.hookFailures) {
6146
+ lines.push(
6147
+ `| ${failure.hookName} | ${failure.hookEvent} | ${failure.exitCode ?? "n/a"} | ${failure.outcome ?? "unknown"} | ${tableCell(failure.stderr) || "nothing"} |`
6148
+ );
6149
+ }
6150
+ lines.push("");
6151
+ lines.push(
6152
+ "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."
6153
+ );
6154
+ }
5376
6155
  lines.push("");
5377
6156
  lines.push("**Background subagents**");
5378
6157
  lines.push("");
@@ -5529,6 +6308,7 @@ async function gatherDoctorReport(options) {
5529
6308
  proxyServers,
5530
6309
  mcpServerErrors: snapshotMcpServerErrors(),
5531
6310
  pluginLoadFailures: snapshotPluginLoadFailures(),
6311
+ hookFailures: snapshotHookFailures(),
5532
6312
  planUsage,
5533
6313
  backgroundSubagents: {
5534
6314
  gate: snapshotBackgroundSubagentGate(),
@@ -5536,6 +6316,42 @@ async function gatherDoctorReport(options) {
5536
6316
  }
5537
6317
  };
5538
6318
  }
6319
+ function decorateDoctorReport(report, options) {
6320
+ if (!wantsDiagnosticBundle(options.argument ?? "")) return report;
6321
+ const context = options.redactionContextImpl ?? createRedactionContext();
6322
+ const logFile = (options.logFileImpl ?? describeLogFile)();
6323
+ const section = buildLogBundleSection({
6324
+ logPath: logFile.path,
6325
+ fileLogging: logFile.enabled,
6326
+ context,
6327
+ readTailImpl: options.readLogTailImpl
6328
+ });
6329
+ return redactForPaste(`${report}
6330
+
6331
+ ${section}`, context);
6332
+ }
6333
+ function withholdFreeText(report) {
6334
+ const withhold = (text) => text ? `[redacted, ${text.length} chars]` : text;
6335
+ return {
6336
+ ...report,
6337
+ mcpServerErrors: report.mcpServerErrors.map((error) => ({
6338
+ ...error,
6339
+ message: withhold(error.message)
6340
+ })),
6341
+ pluginLoadFailures: report.pluginLoadFailures.map((failure) => ({
6342
+ ...failure,
6343
+ message: withhold(failure.message)
6344
+ })),
6345
+ hookFailures: report.hookFailures.map((failure) => ({
6346
+ ...failure,
6347
+ stderr: withhold(failure.stderr)
6348
+ }))
6349
+ };
6350
+ }
6351
+ function renderDoctorReport(report, options) {
6352
+ const bundle = wantsDiagnosticBundle(options.argument ?? "");
6353
+ return decorateDoctorReport(formatDoctorReport(bundle ? withholdFreeText(report) : report), options);
6354
+ }
5539
6355
  async function buildDoctorReport(options) {
5540
6356
  try {
5541
6357
  const report = await gatherDoctorReport(options);
@@ -5548,10 +6364,11 @@ async function buildDoctorReport(options) {
5548
6364
  proxyServers: report.proxyServers.map((server2) => server2.auth.status),
5549
6365
  mcpServerErrors: report.mcpServerErrors.length,
5550
6366
  pluginLoadFailures: report.pluginLoadFailures.length,
6367
+ hookFailures: report.hookFailures.length,
5551
6368
  planUsage: report.planUsage.status,
5552
6369
  backgroundSubagents: report.backgroundSubagents.gate?.supported ?? "not read"
5553
6370
  });
5554
- return formatDoctorReport(report);
6371
+ return renderDoctorReport(report, options);
5555
6372
  } catch (error) {
5556
6373
  const message = error instanceof Error ? error.message : String(error);
5557
6374
  log.warn("claude-code doctor failed to build its report", { error: message });
@@ -7673,7 +8490,7 @@ function spawnInteractiveProcess(opts) {
7673
8490
 
7674
8491
  // src/prompts.ts
7675
8492
  import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
7676
- import { homedir as homedir6 } from "os";
8493
+ import { homedir as homedir7 } from "os";
7677
8494
  import { randomUUID as randomUUID5 } from "crypto";
7678
8495
  import { dirname as dirname5, join as join8 } from "path";
7679
8496
  function readPromptFileIfPresent(path10) {
@@ -7796,7 +8613,7 @@ ${options.compressionSummary.trim()}`
7796
8613
  for (const s of extraSystemContent) {
7797
8614
  if (s.trim()) parts.push(s.trim());
7798
8615
  }
7799
- const configRoot = process.env.XDG_CONFIG_HOME ?? join8(homedir6(), ".config");
8616
+ const configRoot = process.env.XDG_CONFIG_HOME ?? join8(homedir7(), ".config");
7800
8617
  const globalAgents = readPromptFileIfPresent(join8(configRoot, "opencode", "AGENTS.md"));
7801
8618
  const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
7802
8619
  const forwarded = extraSystemContent.join("\n\n");
@@ -8574,7 +9391,6 @@ function toFinishReason(reason = "stop") {
8574
9391
  }
8575
9392
 
8576
9393
  // src/turn-state.ts
8577
- import { generateId } from "@ai-sdk/provider-utils";
8578
9394
  function resolveResultFallbackMs() {
8579
9395
  const env = process.env.CLAUDE_CODE_RESULT_FALLBACK_MS;
8580
9396
  const parsed = env ? Number.parseInt(env, 10) : NaN;
@@ -8728,9 +9544,6 @@ function watchTurnAbort(signal) {
8728
9544
  };
8729
9545
  }
8730
9546
 
8731
- // src/stream-parser.ts
8732
- import { generateId as generateId2 } from "@ai-sdk/provider-utils";
8733
-
8734
9547
  // src/tool-mapping.ts
8735
9548
  function isWebSearchTool(name) {
8736
9549
  return name === "WebSearch" || name === "web_search";
@@ -9343,6 +10156,13 @@ function createLineHandler(state, ctx) {
9343
10156
  state.endTextBlock();
9344
10157
  }
9345
10158
  }
10159
+ if (msg.type === "system") {
10160
+ reportHookEvent(msg);
10161
+ }
10162
+ if (msg.type === "tool_progress") {
10163
+ reportToolProgress(msg);
10164
+ return;
10165
+ }
9346
10166
  if (msg.type === "conversation_reset") {
9347
10167
  const note = reportConversationReset(msg);
9348
10168
  if (note) {
@@ -9377,7 +10197,7 @@ function createLineHandler(state, ctx) {
9377
10197
  const idx = msg.index;
9378
10198
  if (block2.type === "thinking") {
9379
10199
  noteReasoning(state);
9380
- const reasoningId = generateId2();
10200
+ const reasoningId = generateId();
9381
10201
  state.reasoningIds.set(idx, reasoningId);
9382
10202
  }
9383
10203
  if (block2.type === "text") {
@@ -9623,7 +10443,7 @@ ${plan}
9623
10443
  if (block2.thinking && block2.thinking.length > 0) {
9624
10444
  noteReasoning(state);
9625
10445
  state.hadThinkingTextFromStream = true;
9626
- const thinkingId = generateId2();
10446
+ const thinkingId = generateId();
9627
10447
  state.controller.enqueue({
9628
10448
  type: "reasoning-start",
9629
10449
  id: thinkingId
@@ -9673,7 +10493,7 @@ ${plan}
9673
10493
  }
9674
10494
  if (block2.type === "thinking" && block2.thinking) {
9675
10495
  noteReasoning(state);
9676
- const thinkingId = generateId2();
10496
+ const thinkingId = generateId();
9677
10497
  state.controller.enqueue({
9678
10498
  type: "reasoning-start",
9679
10499
  id: thinkingId
@@ -10255,7 +11075,7 @@ var ClaudeCodeLanguageModel = class {
10255
11075
  usage,
10256
11076
  request: result.request,
10257
11077
  response: {
10258
- id: typeof sessionId === "string" && sessionId ? sessionId : generateId3(),
11078
+ id: typeof sessionId === "string" && sessionId ? sessionId : generateId(),
10259
11079
  timestamp: /* @__PURE__ */ new Date(),
10260
11080
  modelId: this.modelId
10261
11081
  },
@@ -10327,7 +11147,7 @@ var ClaudeCodeLanguageModel = class {
10327
11147
  const buffered = [];
10328
11148
  let flushed = false;
10329
11149
  const emitNote = (note) => {
10330
- const id = generateId3();
11150
+ const id = generateId();
10331
11151
  controller.enqueue({ type: "text-start", id });
10332
11152
  controller.enqueue({ type: "text-delta", id, delta: note });
10333
11153
  controller.enqueue({ type: "text-end", id });
@@ -10479,7 +11299,7 @@ var ClaudeCodeLanguageModel = class {
10479
11299
  controller.enqueue({ type: "stream-start", warnings });
10480
11300
  try {
10481
11301
  const text = await buildDoctorReport(doctorOptions);
10482
- const id = generateId3();
11302
+ const id = generateId();
10483
11303
  controller.enqueue({ type: "text-start", id });
10484
11304
  controller.enqueue({ type: "text-delta", id, delta: text });
10485
11305
  controller.enqueue({ type: "text-end", id });
@@ -10527,7 +11347,7 @@ var ClaudeCodeLanguageModel = class {
10527
11347
  controller.enqueue({ type: "stream-start", warnings });
10528
11348
  try {
10529
11349
  const answer = await answerAside();
10530
- const id = generateId3();
11350
+ const id = generateId();
10531
11351
  controller.enqueue({ type: "text-start", id });
10532
11352
  controller.enqueue({ type: "text-delta", id, delta: answer.response });
10533
11353
  controller.enqueue({ type: "text-end", id });
@@ -10553,7 +11373,7 @@ var ClaudeCodeLanguageModel = class {
10553
11373
  providerOptionsKeys: options.providerOptions ? Object.keys(options.providerOptions) : []
10554
11374
  });
10555
11375
  const text = this.synthesizeTitle(options.prompt);
10556
- const textId = generateId3();
11376
+ const textId = generateId();
10557
11377
  const stream2 = new ReadableStream({
10558
11378
  start(controller) {
10559
11379
  controller.enqueue({ type: "stream-start", warnings });
@@ -10622,7 +11442,7 @@ var ClaudeCodeLanguageModel = class {
10622
11442
  const stream2 = new ReadableStream({
10623
11443
  start(controller) {
10624
11444
  controller.enqueue({ type: "stream-start", warnings });
10625
- const id = generateId3();
11445
+ const id = generateId();
10626
11446
  controller.enqueue({ type: "text-start", id });
10627
11447
  controller.enqueue({ type: "text-delta", id, delta: note });
10628
11448
  controller.enqueue({ type: "text-end", id });
@@ -11663,11 +12483,11 @@ import {
11663
12483
  existsSync as existsSync3,
11664
12484
  mkdirSync as mkdirSync4,
11665
12485
  readFileSync as readFileSync6,
11666
- realpathSync as realpathSync3,
12486
+ realpathSync as realpathSync4,
11667
12487
  rmSync as rmSync3,
11668
12488
  writeFileSync as writeFileSync5
11669
12489
  } from "fs";
11670
- import { homedir as homedir7 } from "os";
12490
+ import { homedir as homedir8 } from "os";
11671
12491
  import { dirname as dirname6, join as join9, resolve as resolve5 } from "path";
11672
12492
  import { fileURLToPath as fileURLToPath3 } from "url";
11673
12493
  var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
@@ -11677,12 +12497,12 @@ function candidateCacheRoots() {
11677
12497
  const xdg = process.env.XDG_CACHE_HOME;
11678
12498
  return [
11679
12499
  xdg ? join9(xdg, "opencode") : null,
11680
- join9(homedir7(), ".cache", "opencode"),
11681
- join9(homedir7(), "Library", "Caches", "opencode")
12500
+ join9(homedir8(), ".cache", "opencode"),
12501
+ join9(homedir8(), "Library", "Caches", "opencode")
11682
12502
  ].filter((p) => Boolean(p));
11683
12503
  }
11684
12504
  function userOpencodeJsonPath() {
11685
- const xdgConfig = process.env.XDG_CONFIG_HOME ?? join9(homedir7(), ".config");
12505
+ const xdgConfig = process.env.XDG_CONFIG_HOME ?? join9(homedir8(), ".config");
11686
12506
  return join9(xdgConfig, "opencode", "opencode.json");
11687
12507
  }
11688
12508
  function userIntendsToUseUnscoped() {
@@ -11702,7 +12522,7 @@ function userIntendsToUseUnscoped() {
11702
12522
  function ourLoadedDir() {
11703
12523
  try {
11704
12524
  const filePath = fileURLToPath3(import.meta.url);
11705
- return realpathSync3(resolve5(filePath, "..", ".."));
12525
+ return realpathSync4(resolve5(filePath, "..", ".."));
11706
12526
  } catch {
11707
12527
  return null;
11708
12528
  }
@@ -11717,7 +12537,7 @@ function ourVersion(dir) {
11717
12537
  }
11718
12538
  }
11719
12539
  function markerPath() {
11720
- const stateRoot = process.env.XDG_STATE_HOME ?? join9(homedir7(), ".local", "state");
12540
+ const stateRoot = process.env.XDG_STATE_HOME ?? join9(homedir8(), ".local", "state");
11721
12541
  return join9(stateRoot, "opencode-claude-code-plugin", "cleanup-stale.json");
11722
12542
  }
11723
12543
  function markerVersion() {
@@ -11771,7 +12591,7 @@ function cleanupOne(cacheRoot, ourDir) {
11771
12591
  if (!existsSync3(stalePath)) return;
11772
12592
  let realStalePath = stalePath;
11773
12593
  try {
11774
- realStalePath = realpathSync3(stalePath);
12594
+ realStalePath = realpathSync4(stalePath);
11775
12595
  } catch {
11776
12596
  }
11777
12597
  if (ourDir && realStalePath === ourDir) return;