@khalilgharbaoui/opencode-claude-code-plugin 0.2.0 → 0.2.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.d.ts CHANGED
@@ -90,12 +90,6 @@ type OpenCodeHooks = {
90
90
  id: string;
91
91
  models?: (provider: OpenCodeProvider) => Promise<Record<string, OpenCodeModel>>;
92
92
  };
93
- /**
94
- * Called for every bus event opencode publishes. We use this to react to
95
- * `global.disposed` (fired when opencode invalidates its config — e.g.
96
- * after a UI MCP toggle or `updateGlobal`) and evict cached claude
97
- * subprocesses so the next turn picks up the fresh config.
98
- */
99
93
  event?: (input: {
100
94
  event: OpenCodeEvent;
101
95
  }) => Promise<void>;
package/dist/index.js CHANGED
@@ -742,15 +742,6 @@ function deleteActiveProcess(key) {
742
742
  activeProcesses.delete(key);
743
743
  }
744
744
  }
745
- function evictAllSessions(reason) {
746
- const count = activeProcesses.size;
747
- if (count === 0) return 0;
748
- log.info("evicting all claude processes", { reason, count });
749
- for (const key of Array.from(activeProcesses.keys())) {
750
- deleteActiveProcess(key);
751
- }
752
- return count;
753
- }
754
745
  function getClaudeSessionId(key) {
755
746
  return claudeSessions.get(key);
756
747
  }
@@ -1045,12 +1036,12 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
1045
1036
  hasInput: input != null
1046
1037
  });
1047
1038
  const result = await new Promise(
1048
- (resolve2, reject) => {
1039
+ (resolve3, reject) => {
1049
1040
  const entry = {
1050
1041
  id: callId,
1051
1042
  toolName,
1052
1043
  input,
1053
- resolve: resolve2,
1044
+ resolve: resolve3,
1054
1045
  reject
1055
1046
  };
1056
1047
  pending.set(callId, entry);
@@ -1107,11 +1098,11 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
1107
1098
  }
1108
1099
  }
1109
1100
  });
1110
- await new Promise((resolve2, reject) => {
1101
+ await new Promise((resolve3, reject) => {
1111
1102
  server2.once("error", reject);
1112
1103
  server2.listen(0, "127.0.0.1", () => {
1113
1104
  server2.off("error", reject);
1114
- resolve2();
1105
+ resolve3();
1115
1106
  });
1116
1107
  });
1117
1108
  const addr = server2.address();
@@ -1158,8 +1149,8 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
1158
1149
  entry.reject(new Error("proxy MCP server closed"));
1159
1150
  }
1160
1151
  pending.clear();
1161
- await new Promise((resolve2) => {
1162
- server2.close(() => resolve2());
1152
+ await new Promise((resolve3) => {
1153
+ server2.close(() => resolve3());
1163
1154
  });
1164
1155
  }
1165
1156
  };
@@ -1183,10 +1174,10 @@ function disallowedToolFlags(tools) {
1183
1174
  return out;
1184
1175
  }
1185
1176
  function readBody(req) {
1186
- return new Promise((resolve2, reject) => {
1177
+ return new Promise((resolve3, reject) => {
1187
1178
  const chunks = [];
1188
1179
  req.on("data", (chunk) => chunks.push(chunk));
1189
- req.on("end", () => resolve2(Buffer.concat(chunks).toString("utf8")));
1180
+ req.on("end", () => resolve3(Buffer.concat(chunks).toString("utf8")));
1190
1181
  req.on("error", reject);
1191
1182
  });
1192
1183
  }
@@ -1684,7 +1675,7 @@ var ClaudeCodeLanguageModel = class {
1684
1675
  let thinkingText = "";
1685
1676
  let resultMeta = {};
1686
1677
  const toolCalls = [];
1687
- const result = await new Promise((resolve2, reject) => {
1678
+ const result = await new Promise((resolve3, reject) => {
1688
1679
  rl.on("line", (line) => {
1689
1680
  if (!line.trim()) return;
1690
1681
  try {
@@ -1775,7 +1766,7 @@ ${plan}
1775
1766
  durationMs: msg.duration_ms,
1776
1767
  usage: msg.usage
1777
1768
  };
1778
- resolve2({
1769
+ resolve3({
1779
1770
  ...resultMeta,
1780
1771
  text: responseText,
1781
1772
  thinking: thinkingText,
@@ -1786,7 +1777,7 @@ ${plan}
1786
1777
  }
1787
1778
  });
1788
1779
  rl.on("close", () => {
1789
- resolve2({
1780
+ resolve3({
1790
1781
  ...resultMeta,
1791
1782
  text: responseText,
1792
1783
  thinking: thinkingText,
@@ -2901,6 +2892,117 @@ function titleizeAccount(account) {
2901
2892
  return normalizeAccountName(account).split("-").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
2902
2893
  }
2903
2894
 
2895
+ // src/cleanup-stale.ts
2896
+ import {
2897
+ existsSync as existsSync2,
2898
+ readFileSync as readFileSync2,
2899
+ realpathSync,
2900
+ rmSync,
2901
+ writeFileSync as writeFileSync3
2902
+ } from "fs";
2903
+ import { homedir as homedir2 } from "os";
2904
+ import { join as join3, resolve as resolve2 } from "path";
2905
+ import { fileURLToPath } from "url";
2906
+ var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
2907
+ var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
2908
+ var alreadyRan = false;
2909
+ function candidateCacheRoots() {
2910
+ const xdg = process.env.XDG_CACHE_HOME;
2911
+ return [
2912
+ xdg ? join3(xdg, "opencode") : null,
2913
+ join3(homedir2(), ".cache", "opencode"),
2914
+ join3(homedir2(), "Library", "Caches", "opencode")
2915
+ ].filter((p) => Boolean(p));
2916
+ }
2917
+ function userOpencodeJsonPath() {
2918
+ const xdgConfig = process.env.XDG_CONFIG_HOME ?? join3(homedir2(), ".config");
2919
+ return join3(xdgConfig, "opencode", "opencode.json");
2920
+ }
2921
+ function userIntendsToUseUnscoped() {
2922
+ const cfg = userOpencodeJsonPath();
2923
+ if (!existsSync2(cfg)) return false;
2924
+ try {
2925
+ const json = JSON.parse(readFileSync2(cfg, "utf8"));
2926
+ const plugins = json.plugin;
2927
+ if (!Array.isArray(plugins)) return false;
2928
+ return plugins.some(
2929
+ (entry) => typeof entry === "string" && /^opencode-claude-code-plugin(@[^/]+)?$/.test(entry)
2930
+ );
2931
+ } catch {
2932
+ return false;
2933
+ }
2934
+ }
2935
+ function ourLoadedDir() {
2936
+ try {
2937
+ const filePath = fileURLToPath(import.meta.url);
2938
+ return realpathSync(resolve2(filePath, "..", ".."));
2939
+ } catch {
2940
+ return null;
2941
+ }
2942
+ }
2943
+ function cleanupStaleUnscopedInstall() {
2944
+ if (alreadyRan) return;
2945
+ alreadyRan = true;
2946
+ if (process.env.OPENCODE_CLAUDE_CODE_PLUGIN_NO_CLEANUP === "1") return;
2947
+ if (userIntendsToUseUnscoped()) return;
2948
+ const ourDir = ourLoadedDir();
2949
+ for (const cacheRoot of candidateCacheRoots()) {
2950
+ try {
2951
+ cleanupOne(cacheRoot, ourDir);
2952
+ } catch (err) {
2953
+ log.warn("cleanup-stale: error processing cache root", {
2954
+ cacheRoot,
2955
+ error: String(err)
2956
+ });
2957
+ }
2958
+ }
2959
+ }
2960
+ function cleanupOne(cacheRoot, ourDir) {
2961
+ if (!existsSync2(cacheRoot)) return;
2962
+ const stalePath = join3(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
2963
+ if (!existsSync2(stalePath)) return;
2964
+ let realStalePath = stalePath;
2965
+ try {
2966
+ realStalePath = realpathSync(stalePath);
2967
+ } catch {
2968
+ }
2969
+ if (ourDir && realStalePath === ourDir) return;
2970
+ const pkgJsonPath = join3(stalePath, "package.json");
2971
+ if (!existsSync2(pkgJsonPath)) return;
2972
+ let pkg = {};
2973
+ try {
2974
+ pkg = JSON.parse(readFileSync2(pkgJsonPath, "utf8"));
2975
+ } catch {
2976
+ return;
2977
+ }
2978
+ if (pkg.name !== STALE_PACKAGE_NAME) return;
2979
+ if (!pkg.description?.includes(SUSPECT_DESCRIPTION_TOKEN)) return;
2980
+ log.info("cleanup-stale: removing unscoped install", { stalePath });
2981
+ try {
2982
+ rmSync(stalePath, { recursive: true, force: true });
2983
+ } catch (err) {
2984
+ log.warn("cleanup-stale: rmSync failed", {
2985
+ stalePath,
2986
+ error: String(err)
2987
+ });
2988
+ return;
2989
+ }
2990
+ const cachePkgJson = join3(cacheRoot, "package.json");
2991
+ if (!existsSync2(cachePkgJson)) return;
2992
+ try {
2993
+ const cfg = JSON.parse(readFileSync2(cachePkgJson, "utf8"));
2994
+ if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
2995
+ delete cfg.dependencies[STALE_PACKAGE_NAME];
2996
+ writeFileSync3(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
2997
+ log.info("cleanup-stale: pruned dep from cache package.json");
2998
+ }
2999
+ } catch (err) {
3000
+ log.warn("cleanup-stale: cache package.json update failed", {
3001
+ error: String(err)
3002
+ });
3003
+ }
3004
+ }
3005
+
2904
3006
  // src/index.ts
2905
3007
  function createClaudeCode(settings = {}) {
2906
3008
  const cliPath = settings.cliPath ?? process.env.CLAUDE_CLI_PATH ?? "claude";
@@ -3080,18 +3182,8 @@ async function expandAccountProviders(config) {
3080
3182
  }
3081
3183
  return expandedCount > 0;
3082
3184
  }
3083
- function readEventType(ev) {
3084
- if (!ev || typeof ev !== "object") return void 0;
3085
- const e = ev;
3086
- if (typeof e.type === "string") return e.type;
3087
- const payload = e.payload;
3088
- if (payload && typeof payload === "object") {
3089
- const t = payload.type;
3090
- if (typeof t === "string") return t;
3091
- }
3092
- return void 0;
3093
- }
3094
3185
  var server = async (input) => {
3186
+ cleanupStaleUnscopedInstall();
3095
3187
  if (input && typeof input === "object" && "client" in input) {
3096
3188
  setOpencodeClient(input.client);
3097
3189
  }
@@ -3106,11 +3198,10 @@ var server = async (input) => {
3106
3198
  ...await providerConfig(existing)
3107
3199
  };
3108
3200
  },
3109
- event: async ({ event }) => {
3110
- if (readEventType(event) === "global.disposed") {
3111
- evictAllSessions("global.disposed");
3112
- }
3113
- },
3201
+ // No `event` hook: MCP config drift is detected at turn start by the
3202
+ // hot-reload check in `claude-code-language-model.ts`, which respawns
3203
+ // claude safely between turns. Eviction on `global.disposed` would kill
3204
+ // an in-flight stream and abort the user's current turn.
3114
3205
  provider: {
3115
3206
  id: PROVIDER_ID2,
3116
3207
  models: async (provider) => defaultModelsForProvider(provider.models)