@khalilgharbaoui/opencode-claude-code-plugin 0.10.0 → 0.11.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/README.md CHANGED
@@ -531,6 +531,47 @@ Boolean env vars accept `1/true/on/yes` for on and `0/false/no/off` for
531
531
  off; empty / unset falls through to config. Invalid `level` values fall
532
532
  through to config.
533
533
 
534
+ ### Startup diagnostics
535
+
536
+ Once per process, right after the provider(s) register, the plugin logs a
537
+ single `NOTICE: claude-code plugin ready` line summarizing everything worth
538
+ knowing before you start debugging anything else:
539
+
540
+ ```bash
541
+ OPENCODE_CLAUDE_CODE_LOG_FILE=1 opencode
542
+ grep "plugin ready" ~/.local/share/opencode-claude-code/plugin.log
543
+ ```
544
+
545
+ ```json
546
+ {
547
+ "plugin": "0.11.1",
548
+ "opencode": "1.18.5",
549
+ "cwd": { "resolved": "/Users/you/code/app", "source": "process" },
550
+ "providers": ["claude-code-default", "claude-code-work"],
551
+ "accounts": ["default", "work"],
552
+ "proxyTools": ["Bash", "Edit", "Write", "WebFetch", "Task"],
553
+ "mcpServers": ["github", "slack"],
554
+ "interactiveTransport": false,
555
+ "anthropicApiKeyInEnv": false,
556
+ "claudeCli": { "path": "claude", "version": "2.1.211 (Claude Code)" }
557
+ }
558
+ ```
559
+
560
+ Reading it:
561
+
562
+ - **`cwd.source`** is which rule picked the working directory Claude will be
563
+ spawned in — `configured` (you pinned `options.cwd`), `process` (normal),
564
+ `captured` (`process.cwd()` was unusable and opencode's project directory
565
+ rescued it, the macOS GUI-launch case), or `unresolved` (neither worked).
566
+ - **`claudeCli.version`** reading `not detected` means the `claude` binary at
567
+ that path didn't answer `--version`, which also disables version-gated
568
+ flags like `--thinking-display`.
569
+ - **`mcpServers`** is the on-disk merge, before opencode's runtime toggles
570
+ are applied (those aren't settled yet at startup).
571
+ - **`opencode`** is read from the running opencode binary (`--version`), since
572
+ opencode still does not hand its version to plugins. It reads `unknown` when
573
+ opencode is run from source rather than as the packaged binary.
574
+
534
575
  ### Default behavior (no config, no env)
535
576
 
536
577
  Nothing persists; only WARN and ERROR bubble in the TUI. The plugin
package/dist/index.d.ts CHANGED
@@ -467,6 +467,11 @@ interface ClaudeStreamMessage {
467
467
  index?: number;
468
468
  }
469
469
 
470
+ interface DiagnosticsProviderEntry {
471
+ name?: string;
472
+ options?: Record<string, unknown>;
473
+ }
474
+
470
475
  declare class ClaudeCodeLanguageModel implements LanguageModelV3 {
471
476
  readonly specificationVersion = "v3";
472
477
  readonly modelId: string;
@@ -598,9 +603,15 @@ declare function createClaudeCode(settings?: ClaudeCodeProviderSettings): Claude
598
603
  * so the config-path provider loader parses them correctly.
599
604
  */
600
605
  declare function configModelsForProvider(providerModels: OpenCodeProvider["models"], providerID: string, modelSuffix?: string): Record<string, Record<string, unknown>>;
606
+ /**
607
+ * Narrow opencode's full provider map down to the ones this plugin owns
608
+ * (`claude-code` plus every `claude-code-<account>` expansion) so startup
609
+ * diagnostics never report another provider's options.
610
+ */
611
+ declare function claudeCodeProviders(providers: Record<string, DiagnosticsProviderEntry> | undefined): Record<string, DiagnosticsProviderEntry>;
601
612
  declare const _default: {
602
613
  id: string;
603
614
  server: OpenCodePlugin;
604
615
  };
605
616
 
606
- export { type ClaudeCodeConfig, ClaudeCodeLanguageModel, type ClaudeCodeProvider, type ClaudeCodeProviderSettings, type ClaudeStreamMessage, type OpenCodeHooks, type OpenCodeModel, type OpenCodePlugin, bridgeOpencodeMcp, configModelsForProvider, createClaudeCode, _default as default, defaultModels };
617
+ export { type ClaudeCodeConfig, ClaudeCodeLanguageModel, type ClaudeCodeProvider, type ClaudeCodeProviderSettings, type ClaudeStreamMessage, type OpenCodeHooks, type OpenCodeModel, type OpenCodePlugin, bridgeOpencodeMcp, claudeCodeProviders, configModelsForProvider, createClaudeCode, _default as default, defaultModels };
package/dist/index.js CHANGED
@@ -974,6 +974,31 @@ function mergeMcp(target, source) {
974
974
  return out;
975
975
  }
976
976
  function bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers) {
977
+ const {
978
+ servers: merged,
979
+ enabledServerNames: allEnabledServerNames,
980
+ hash
981
+ } = mergeOpencodeMcp(cwd, runtimeStatus);
982
+ const servers = {};
983
+ const bridgedServerNames = [];
984
+ for (const [name, spec] of Object.entries(merged)) {
985
+ if (!spec || typeof spec !== "object") continue;
986
+ if (excludeServers?.has(name)) continue;
987
+ const translated = translateServer(name, spec);
988
+ if (translated) {
989
+ servers[name] = translated;
990
+ bridgedServerNames.push(name);
991
+ }
992
+ }
993
+ return finishBridge({
994
+ servers,
995
+ bridgedServerNames,
996
+ allEnabledServerNames,
997
+ hash,
998
+ excludeServers
999
+ });
1000
+ }
1001
+ function mergeOpencodeMcp(cwd, runtimeStatus) {
977
1002
  const worktree = detectWorktree(cwd);
978
1003
  let merged = {};
979
1004
  merged = mergeMcp(merged, extractMcpBlock(loadGlobalConfig()));
@@ -1012,26 +1037,19 @@ function bridgeOpencodeMcp(cwd, runtimeStatus, excludeServers) {
1012
1037
  merged[name] = { ...base, enabled: status === "connected" };
1013
1038
  }
1014
1039
  }
1015
- const allEnabledServerNames = [];
1040
+ const enabledServerNames = [];
1016
1041
  for (const [name, spec] of Object.entries(merged)) {
1017
1042
  if (!spec || typeof spec !== "object") continue;
1018
1043
  const enabled = spec.enabled;
1019
1044
  if (enabled === false) continue;
1020
- allEnabledServerNames.push(name);
1021
- }
1022
- const servers = {};
1023
- const bridgedServerNames = [];
1024
- for (const [name, spec] of Object.entries(merged)) {
1025
- if (!spec || typeof spec !== "object") continue;
1026
- if (excludeServers?.has(name)) continue;
1027
- const translated = translateServer(name, spec);
1028
- if (translated) {
1029
- servers[name] = translated;
1030
- bridgedServerNames.push(name);
1031
- }
1045
+ enabledServerNames.push(name);
1032
1046
  }
1033
1047
  const mergedBody = JSON.stringify({ mcpServers: merged }, null, 2);
1034
1048
  const hash = crypto.createHash("sha256").update(mergedBody).digest("hex").slice(0, 12);
1049
+ return { servers: merged, enabledServerNames, hash };
1050
+ }
1051
+ function finishBridge(input) {
1052
+ const { servers, bridgedServerNames, allEnabledServerNames, hash, excludeServers } = input;
1035
1053
  if (Object.keys(servers).length === 0) {
1036
1054
  const allEnabledServersExcluded = excludeServers && allEnabledServerNames.length > 0 && allEnabledServerNames.every((name) => excludeServers.has(name));
1037
1055
  if (!allEnabledServersExcluded) return null;
@@ -1082,6 +1100,9 @@ var opencodeProjectDirectory;
1082
1100
  function setOpencodeProjectDirectory(dir) {
1083
1101
  opencodeProjectDirectory = dir;
1084
1102
  }
1103
+ function getOpencodeProjectDirectory() {
1104
+ return opencodeProjectDirectory;
1105
+ }
1085
1106
  function isUsableDirectory(d) {
1086
1107
  return typeof d === "string" && d.length > 1 && d !== "/";
1087
1108
  }
@@ -2832,9 +2853,9 @@ function makeAutoContinueMessage() {
2832
2853
  }
2833
2854
  });
2834
2855
  }
2835
- function readPromptFileIfPresent(path6) {
2856
+ function readPromptFileIfPresent(path7) {
2836
2857
  try {
2837
- const content = readFileSync3(path6, "utf8").trim();
2858
+ const content = readFileSync3(path7, "utf8").trim();
2838
2859
  return content || void 0;
2839
2860
  } catch {
2840
2861
  return void 0;
@@ -2903,10 +2924,10 @@ function buildAppendedSystemPrompt(cwd, includeMultiStepHint = true, extraSystem
2903
2924
  if (includeMultiStepHint) parts.push(MULTI_STEP_TASK_HINT);
2904
2925
  const content = parts.join("\n\n");
2905
2926
  if (!content) return void 0;
2906
- const path6 = join6(tmpdir2(), `opencode-cc-sys-${randomUUID3()}.md`);
2927
+ const path7 = join6(tmpdir2(), `opencode-cc-sys-${randomUUID3()}.md`);
2907
2928
  try {
2908
- writeFileSync3(path6, content, "utf8");
2909
- return path6;
2929
+ writeFileSync3(path7, content, "utf8");
2930
+ return path7;
2910
2931
  } catch (err) {
2911
2932
  log.warn("failed to write system prompt file", { error: String(err) });
2912
2933
  return void 0;
@@ -5515,6 +5536,130 @@ function cleanupOne(cacheRoot, ourDir) {
5515
5536
  }
5516
5537
  }
5517
5538
 
5539
+ // src/startup-diagnostics.ts
5540
+ import { execFile as execFile2 } from "child_process";
5541
+ import * as fs5 from "fs";
5542
+ import * as path6 from "path";
5543
+ import { promisify as promisify2 } from "util";
5544
+ import { fileURLToPath as fileURLToPath2 } from "url";
5545
+ var cachedPluginVersion;
5546
+ function pluginVersion() {
5547
+ if (cachedPluginVersion) return cachedPluginVersion;
5548
+ try {
5549
+ const here = path6.dirname(fileURLToPath2(import.meta.url));
5550
+ const raw = fs5.readFileSync(path6.join(here, "..", "package.json"), "utf8");
5551
+ const version = JSON.parse(raw).version;
5552
+ cachedPluginVersion = typeof version === "string" ? version : "unknown";
5553
+ } catch {
5554
+ cachedPluginVersion = "unknown";
5555
+ }
5556
+ return cachedPluginVersion;
5557
+ }
5558
+ function pickOpencodeVersion(input) {
5559
+ if (!input || typeof input !== "object") return void 0;
5560
+ const app = input.app;
5561
+ if (app && typeof app === "object") {
5562
+ const version = app.version;
5563
+ if (typeof version === "string" && version.length > 0) return version;
5564
+ }
5565
+ const direct = input.version;
5566
+ if (typeof direct === "string" && direct.length > 0) return direct;
5567
+ return void 0;
5568
+ }
5569
+ var execFileAsync2 = promisify2(execFile2);
5570
+ var opencodeVersionProbe;
5571
+ function detectOpencodeVersion(execPath = process.execPath) {
5572
+ if (opencodeVersionProbe) return opencodeVersionProbe;
5573
+ opencodeVersionProbe = (async () => {
5574
+ if (!path6.basename(execPath).toLowerCase().includes("opencode")) {
5575
+ log.debug("skipping opencode version probe: execPath is not opencode", { execPath });
5576
+ return void 0;
5577
+ }
5578
+ try {
5579
+ const { stdout } = await execFileAsync2(execPath, ["--version"], { timeout: 5e3 });
5580
+ const match = /\d+\.\d+\.\d+\S*/.exec(stdout.trim());
5581
+ return match ? match[0] : void 0;
5582
+ } catch (err) {
5583
+ log.debug("opencode version probe failed", {
5584
+ execPath,
5585
+ error: err instanceof Error ? err.message : String(err)
5586
+ });
5587
+ return void 0;
5588
+ }
5589
+ })();
5590
+ return opencodeVersionProbe;
5591
+ }
5592
+ function describeSpawnCwd(configured, live = process.cwd(), captured = getOpencodeProjectDirectory()) {
5593
+ if (typeof configured === "string" && configured.length > 0) {
5594
+ return { resolved: configured, source: "configured" };
5595
+ }
5596
+ if (isUsableDirectory(live)) return { resolved: live, source: "process" };
5597
+ if (isUsableDirectory(captured)) return { resolved: captured, source: "captured" };
5598
+ return { resolved: live, source: "unresolved" };
5599
+ }
5600
+ function stringList(value) {
5601
+ if (!Array.isArray(value)) return [];
5602
+ return value.filter((entry) => typeof entry === "string");
5603
+ }
5604
+ function firstOption(providers, key) {
5605
+ for (const entry of Object.values(providers)) {
5606
+ const value = entry?.options?.[key];
5607
+ if (value !== void 0) return value;
5608
+ }
5609
+ return void 0;
5610
+ }
5611
+ function collectStartupDiagnostics(providers, opencodeVersion) {
5612
+ const accounts = [];
5613
+ for (const entry of Object.values(providers)) {
5614
+ const account = entry?.options?.account;
5615
+ if (typeof account === "string" && account.length > 0) accounts.push(account);
5616
+ }
5617
+ const cwd = describeSpawnCwd(firstOption(providers, "cwd"));
5618
+ let mcpServers = [];
5619
+ try {
5620
+ mcpServers = mergeOpencodeMcp(cwd.resolved).enabledServerNames;
5621
+ } catch (err) {
5622
+ log.debug("startup diagnostics could not read MCP config", {
5623
+ error: err instanceof Error ? err.message : String(err)
5624
+ });
5625
+ }
5626
+ return {
5627
+ plugin: pluginVersion(),
5628
+ opencode: opencodeVersion ?? process.env.OPENCODE_VERSION ?? "unknown",
5629
+ claudeCliPath: String(firstOption(providers, "cliPath") ?? "claude"),
5630
+ cwd,
5631
+ providers: Object.keys(providers),
5632
+ accounts,
5633
+ proxyTools: stringList(firstOption(providers, "proxyTools")),
5634
+ mcpServers,
5635
+ interactiveTransport: firstOption(providers, "interactive") === true || process.env.CLAUDE_CODE_INTERACTIVE_TRANSPORT === "1",
5636
+ anthropicApiKeyInEnv: Boolean(
5637
+ process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN
5638
+ )
5639
+ };
5640
+ }
5641
+ var logged = false;
5642
+ function logStartupDiagnostics(providers, opencodeVersion) {
5643
+ if (logged) return;
5644
+ logged = true;
5645
+ void (async () => {
5646
+ try {
5647
+ const version = opencodeVersion ?? process.env.OPENCODE_VERSION ?? await detectOpencodeVersion();
5648
+ const { claudeCliPath, ...rest } = collectStartupDiagnostics(providers, version);
5649
+ const cli = await detectCliVersion(claudeCliPath);
5650
+ const diagnostics = {
5651
+ ...rest,
5652
+ claudeCli: { path: claudeCliPath, version: cli?.raw ?? "not detected" }
5653
+ };
5654
+ log.notice("claude-code plugin ready", { ...diagnostics });
5655
+ } catch (err) {
5656
+ log.debug("startup diagnostics failed", {
5657
+ error: err instanceof Error ? err.message : String(err)
5658
+ });
5659
+ }
5660
+ })();
5661
+ }
5662
+
5518
5663
  // src/index.ts
5519
5664
  function pickOpencodeDirectory(input) {
5520
5665
  if (!input || typeof input !== "object") return void 0;
@@ -5690,6 +5835,13 @@ async function providerConfig(existing, providerID = PROVIDER_ID2, optionDefault
5690
5835
  // opencode's config-path loader parses (and merges user variants).
5691
5836
  };
5692
5837
  }
5838
+ function claudeCodeProviders(providers) {
5839
+ const out = {};
5840
+ for (const [id, entry] of Object.entries(providers ?? {})) {
5841
+ if (id === PROVIDER_ID2 || id.startsWith(`${PROVIDER_ID2}-`)) out[id] = entry;
5842
+ }
5843
+ return out;
5844
+ }
5693
5845
  async function expandAccountProviders(config) {
5694
5846
  const seed = config.provider?.[PROVIDER_ID2];
5695
5847
  const accounts = resolveAccounts(seed?.options?.accounts);
@@ -5735,6 +5887,7 @@ async function expandAccountProviders(config) {
5735
5887
  }
5736
5888
  var server = async (input) => {
5737
5889
  cleanupStaleUnscopedInstall();
5890
+ const opencodeVersion = pickOpencodeVersion(input);
5738
5891
  if (input && typeof input === "object" && "client" in input) {
5739
5892
  setOpencodeClient(input.client);
5740
5893
  }
@@ -5744,12 +5897,10 @@ var server = async (input) => {
5744
5897
  config.provider ??= {};
5745
5898
  const expanded = await expandAccountProviders(config);
5746
5899
  if (expanded) {
5747
- const registered2 = Object.entries(config.provider).filter(([id]) => id === PROVIDER_ID2 || id.startsWith(`${PROVIDER_ID2}-`)).map(([id, p]) => ({
5748
- id,
5749
- name: p?.name ?? id,
5750
- cwd: p?.options?.cwd
5751
- }));
5752
- log.notice("registered claude-code providers", { providers: registered2 });
5900
+ logStartupDiagnostics(
5901
+ claudeCodeProviders(config.provider),
5902
+ opencodeVersion
5903
+ );
5753
5904
  return;
5754
5905
  }
5755
5906
  const existing = config.provider[PROVIDER_ID2];
@@ -5761,11 +5912,10 @@ var server = async (input) => {
5761
5912
  PROVIDER_ID2
5762
5913
  )
5763
5914
  };
5764
- log.notice("registered claude-code provider", {
5765
- id: PROVIDER_ID2,
5766
- name: config.provider[PROVIDER_ID2]?.name ?? PROVIDER_ID2,
5767
- cwd: config.provider[PROVIDER_ID2]?.options?.cwd
5768
- });
5915
+ logStartupDiagnostics(
5916
+ claudeCodeProviders(config.provider),
5917
+ opencodeVersion
5918
+ );
5769
5919
  },
5770
5920
  // No `event` hook: MCP config drift is detected at turn start by the
5771
5921
  // hot-reload check in `claude-code-language-model.ts`, which respawns
@@ -5810,6 +5960,7 @@ var index_default = {
5810
5960
  export {
5811
5961
  ClaudeCodeLanguageModel,
5812
5962
  bridgeOpencodeMcp,
5963
+ claudeCodeProviders,
5813
5964
  configModelsForProvider,
5814
5965
  createClaudeCode,
5815
5966
  index_default as default,