@khalilgharbaoui/opencode-claude-code-plugin 0.2.0 → 0.2.2

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
@@ -15,6 +15,9 @@ var log = {
15
15
  info(msg, data) {
16
16
  if (DEBUG) console.error(fmt("INFO", msg, data));
17
17
  },
18
+ notice(msg, data) {
19
+ console.error(fmt("NOTICE", msg, data));
20
+ },
18
21
  warn(msg, data) {
19
22
  if (DEBUG) console.error(fmt("WARN", msg, data));
20
23
  },
@@ -711,6 +714,7 @@ async function getRuntimeMcpStatus() {
711
714
  import { spawn } from "child_process";
712
715
  import { createInterface } from "readline";
713
716
  import { EventEmitter } from "events";
717
+ import { unlink } from "fs/promises";
714
718
  var activeProcesses = /* @__PURE__ */ new Map();
715
719
  var claudeSessions = /* @__PURE__ */ new Map();
716
720
  var MAX_ACTIVE_PROCESSES = 16;
@@ -742,15 +746,6 @@ function deleteActiveProcess(key) {
742
746
  activeProcesses.delete(key);
743
747
  }
744
748
  }
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
749
  function getClaudeSessionId(key) {
755
750
  return claudeSessions.get(key);
756
751
  }
@@ -760,7 +755,7 @@ function setClaudeSessionId(key, sessionId) {
760
755
  function deleteClaudeSessionId(key) {
761
756
  claudeSessions.delete(key);
762
757
  }
763
- function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcpHash) {
758
+ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcpHash, systemPromptFile) {
764
759
  evictIfNeeded();
765
760
  log.info("spawning new claude process", { cliPath, cliArgs, cwd, sessionKey: sessionKey2 });
766
761
  const proc = spawn(cliPath, cliArgs, {
@@ -781,7 +776,8 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
781
776
  proc,
782
777
  lineEmitter,
783
778
  proxyServer: proxyServer ?? null,
784
- mcpHash
779
+ mcpHash,
780
+ systemPromptFile
785
781
  };
786
782
  activeProcesses.set(sessionKey2, ap);
787
783
  proc.on("error", (err) => {
@@ -790,6 +786,10 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
790
786
  proc.on("exit", (code, signal) => {
791
787
  log.info("claude process exited", { code, signal, sessionKey: sessionKey2 });
792
788
  void proxyServer?.close();
789
+ if (systemPromptFile) {
790
+ void unlink(systemPromptFile).catch(() => {
791
+ });
792
+ }
793
793
  activeProcesses.delete(sessionKey2);
794
794
  if (code !== 0 && code !== null) {
795
795
  log.info("process exited with error, clearing session", {
@@ -821,7 +821,8 @@ function buildCliArgs(opts) {
821
821
  permissionMode,
822
822
  mcpConfig,
823
823
  strictMcpConfig,
824
- disallowedTools
824
+ disallowedTools,
825
+ appendSystemPromptFile
825
826
  } = opts;
826
827
  const args = [
827
828
  "--output-format",
@@ -855,6 +856,9 @@ function buildCliArgs(opts) {
855
856
  if (disallowedTools && disallowedTools.length > 0) {
856
857
  args.push("--disallowedTools", ...disallowedTools);
857
858
  }
859
+ if (appendSystemPromptFile) {
860
+ args.push("--append-system-prompt-file", appendSystemPromptFile);
861
+ }
858
862
  if (skipPermissions) {
859
863
  args.push("--dangerously-skip-permissions");
860
864
  }
@@ -1045,12 +1049,12 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
1045
1049
  hasInput: input != null
1046
1050
  });
1047
1051
  const result = await new Promise(
1048
- (resolve2, reject) => {
1052
+ (resolve3, reject) => {
1049
1053
  const entry = {
1050
1054
  id: callId,
1051
1055
  toolName,
1052
1056
  input,
1053
- resolve: resolve2,
1057
+ resolve: resolve3,
1054
1058
  reject
1055
1059
  };
1056
1060
  pending.set(callId, entry);
@@ -1107,11 +1111,11 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
1107
1111
  }
1108
1112
  }
1109
1113
  });
1110
- await new Promise((resolve2, reject) => {
1114
+ await new Promise((resolve3, reject) => {
1111
1115
  server2.once("error", reject);
1112
1116
  server2.listen(0, "127.0.0.1", () => {
1113
1117
  server2.off("error", reject);
1114
- resolve2();
1118
+ resolve3();
1115
1119
  });
1116
1120
  });
1117
1121
  const addr = server2.address();
@@ -1158,8 +1162,8 @@ async function createProxyMcpServer(tools = DEFAULT_PROXY_TOOLS) {
1158
1162
  entry.reject(new Error("proxy MCP server closed"));
1159
1163
  }
1160
1164
  pending.clear();
1161
- await new Promise((resolve2) => {
1162
- server2.close(() => resolve2());
1165
+ await new Promise((resolve3) => {
1166
+ server2.close(() => resolve3());
1163
1167
  });
1164
1168
  }
1165
1169
  };
@@ -1183,10 +1187,10 @@ function disallowedToolFlags(tools) {
1183
1187
  return out;
1184
1188
  }
1185
1189
  function readBody(req) {
1186
- return new Promise((resolve2, reject) => {
1190
+ return new Promise((resolve3, reject) => {
1187
1191
  const chunks = [];
1188
1192
  req.on("data", (chunk) => chunks.push(chunk));
1189
- req.on("end", () => resolve2(Buffer.concat(chunks).toString("utf8")));
1193
+ req.on("end", () => resolve3(Buffer.concat(chunks).toString("utf8")));
1190
1194
  req.on("error", reject);
1191
1195
  });
1192
1196
  }
@@ -1252,6 +1256,47 @@ function resolvePendingProxyCall(sessionKey2, result) {
1252
1256
  }
1253
1257
 
1254
1258
  // src/claude-code-language-model.ts
1259
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "fs";
1260
+ import { unlink as unlink2 } from "fs/promises";
1261
+ import { homedir as homedir2, tmpdir as tmpdir3 } from "os";
1262
+ import { randomUUID as randomUUID2 } from "crypto";
1263
+ import { dirname as dirname2, join as join3 } from "path";
1264
+ function readPromptFileIfPresent(path4) {
1265
+ try {
1266
+ const content = readFileSync2(path4, "utf8").trim();
1267
+ return content || void 0;
1268
+ } catch {
1269
+ return void 0;
1270
+ }
1271
+ }
1272
+ function nearestWorkspaceAgentsPrompt(cwd) {
1273
+ let dir = cwd;
1274
+ while (true) {
1275
+ const content = readPromptFileIfPresent(join3(dir, "AGENTS.md"));
1276
+ if (content) return content;
1277
+ const parent = dirname2(dir);
1278
+ if (parent === dir) return void 0;
1279
+ dir = parent;
1280
+ }
1281
+ }
1282
+ function buildAppendedSystemPrompt(cwd) {
1283
+ const parts = [];
1284
+ const configRoot = process.env.XDG_CONFIG_HOME ?? join3(homedir2(), ".config");
1285
+ const globalAgents = readPromptFileIfPresent(join3(configRoot, "opencode", "AGENTS.md"));
1286
+ const workspaceAgents = nearestWorkspaceAgentsPrompt(cwd);
1287
+ if (globalAgents) parts.push(globalAgents);
1288
+ if (workspaceAgents && workspaceAgents !== globalAgents) parts.push(workspaceAgents);
1289
+ const content = parts.join("\n\n");
1290
+ if (!content) return void 0;
1291
+ const path4 = join3(tmpdir3(), `opencode-cc-sys-${randomUUID2()}.md`);
1292
+ try {
1293
+ writeFileSync3(path4, content, "utf8");
1294
+ return path4;
1295
+ } catch (err) {
1296
+ log.warn("failed to write system prompt file", { error: String(err) });
1297
+ return void 0;
1298
+ }
1299
+ }
1255
1300
  var ClaudeCodeLanguageModel = class {
1256
1301
  specificationVersion = "v3";
1257
1302
  modelId;
@@ -1655,6 +1700,7 @@ var ClaudeCodeLanguageModel = class {
1655
1700
  reasoningEffort
1656
1701
  );
1657
1702
  const runtimeStatus = await getRuntimeMcpStatus();
1703
+ const systemPromptFile = buildAppendedSystemPrompt(cwd);
1658
1704
  const cliArgs = buildCliArgs({
1659
1705
  sessionKey: sk,
1660
1706
  skipPermissions: this.config.skipPermissions !== false,
@@ -1663,7 +1709,8 @@ var ClaudeCodeLanguageModel = class {
1663
1709
  permissionMode: this.config.permissionMode,
1664
1710
  mcpConfig: this.effectiveMcpConfig(cwd, void 0, runtimeStatus).paths,
1665
1711
  strictMcpConfig: this.config.strictMcpConfig,
1666
- disallowedTools: this.config.webSearch === "disabled" ? ["WebSearch"] : void 0
1712
+ disallowedTools: this.config.webSearch === "disabled" ? ["WebSearch"] : void 0,
1713
+ appendSystemPromptFile: systemPromptFile
1667
1714
  });
1668
1715
  log.info("doGenerate starting", {
1669
1716
  cwd,
@@ -1679,12 +1726,18 @@ var ClaudeCodeLanguageModel = class {
1679
1726
  env: { ...process.env, TERM: "xterm-256color" },
1680
1727
  shell: process.platform === "win32"
1681
1728
  });
1729
+ if (systemPromptFile) {
1730
+ proc.on("exit", () => {
1731
+ void unlink2(systemPromptFile).catch(() => {
1732
+ });
1733
+ });
1734
+ }
1682
1735
  const rl = createInterface2({ input: proc.stdout });
1683
1736
  let responseText = "";
1684
1737
  let thinkingText = "";
1685
1738
  let resultMeta = {};
1686
1739
  const toolCalls = [];
1687
- const result = await new Promise((resolve2, reject) => {
1740
+ const result = await new Promise((resolve3, reject) => {
1688
1741
  rl.on("line", (line) => {
1689
1742
  if (!line.trim()) return;
1690
1743
  try {
@@ -1775,7 +1828,7 @@ ${plan}
1775
1828
  durationMs: msg.duration_ms,
1776
1829
  usage: msg.usage
1777
1830
  };
1778
- resolve2({
1831
+ resolve3({
1779
1832
  ...resultMeta,
1780
1833
  text: responseText,
1781
1834
  thinking: thinkingText,
@@ -1786,7 +1839,7 @@ ${plan}
1786
1839
  }
1787
1840
  });
1788
1841
  rl.on("close", () => {
1789
- resolve2({
1842
+ resolve3({
1790
1843
  ...resultMeta,
1791
1844
  text: responseText,
1792
1845
  thinking: thinkingText,
@@ -1977,6 +2030,7 @@ ${plan}
1977
2030
  proxyServer?.configPath(),
1978
2031
  runtimeStatus
1979
2032
  );
2033
+ const systemPromptFile = activeProcess ? void 0 : buildAppendedSystemPrompt(cwd);
1980
2034
  const cliArgs = buildCliArgs({
1981
2035
  sessionKey: sk,
1982
2036
  skipPermissions,
@@ -1984,7 +2038,8 @@ ${plan}
1984
2038
  permissionMode: self.config.permissionMode,
1985
2039
  mcpConfig: mcp.paths,
1986
2040
  strictMcpConfig: self.config.strictMcpConfig,
1987
- disallowedTools: allDisallowed.length > 0 ? allDisallowed : void 0
2041
+ disallowedTools: allDisallowed.length > 0 ? allDisallowed : void 0,
2042
+ appendSystemPromptFile: systemPromptFile
1988
2043
  });
1989
2044
  if (activeProcess) {
1990
2045
  proc = activeProcess.proc;
@@ -1997,7 +2052,8 @@ ${plan}
1997
2052
  cwd,
1998
2053
  sk,
1999
2054
  proxyServer,
2000
- mcp.bridgedHash
2055
+ mcp.bridgedHash,
2056
+ systemPromptFile
2001
2057
  );
2002
2058
  proc = ap.proc;
2003
2059
  lineEmitter = ap.lineEmitter;
@@ -2901,6 +2957,117 @@ function titleizeAccount(account) {
2901
2957
  return normalizeAccountName(account).split("-").filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ");
2902
2958
  }
2903
2959
 
2960
+ // src/cleanup-stale.ts
2961
+ import {
2962
+ existsSync as existsSync2,
2963
+ readFileSync as readFileSync3,
2964
+ realpathSync,
2965
+ rmSync,
2966
+ writeFileSync as writeFileSync4
2967
+ } from "fs";
2968
+ import { homedir as homedir3 } from "os";
2969
+ import { join as join4, resolve as resolve2 } from "path";
2970
+ import { fileURLToPath } from "url";
2971
+ var STALE_PACKAGE_NAME = "opencode-claude-code-plugin";
2972
+ var SUSPECT_DESCRIPTION_TOKEN = "Claude Code";
2973
+ var alreadyRan = false;
2974
+ function candidateCacheRoots() {
2975
+ const xdg = process.env.XDG_CACHE_HOME;
2976
+ return [
2977
+ xdg ? join4(xdg, "opencode") : null,
2978
+ join4(homedir3(), ".cache", "opencode"),
2979
+ join4(homedir3(), "Library", "Caches", "opencode")
2980
+ ].filter((p) => Boolean(p));
2981
+ }
2982
+ function userOpencodeJsonPath() {
2983
+ const xdgConfig = process.env.XDG_CONFIG_HOME ?? join4(homedir3(), ".config");
2984
+ return join4(xdgConfig, "opencode", "opencode.json");
2985
+ }
2986
+ function userIntendsToUseUnscoped() {
2987
+ const cfg = userOpencodeJsonPath();
2988
+ if (!existsSync2(cfg)) return false;
2989
+ try {
2990
+ const json = JSON.parse(readFileSync3(cfg, "utf8"));
2991
+ const plugins = json.plugin;
2992
+ if (!Array.isArray(plugins)) return false;
2993
+ return plugins.some(
2994
+ (entry) => typeof entry === "string" && /^opencode-claude-code-plugin(@[^/]+)?$/.test(entry)
2995
+ );
2996
+ } catch {
2997
+ return false;
2998
+ }
2999
+ }
3000
+ function ourLoadedDir() {
3001
+ try {
3002
+ const filePath = fileURLToPath(import.meta.url);
3003
+ return realpathSync(resolve2(filePath, "..", ".."));
3004
+ } catch {
3005
+ return null;
3006
+ }
3007
+ }
3008
+ function cleanupStaleUnscopedInstall() {
3009
+ if (alreadyRan) return;
3010
+ alreadyRan = true;
3011
+ if (process.env.OPENCODE_CLAUDE_CODE_PLUGIN_NO_CLEANUP === "1") return;
3012
+ if (userIntendsToUseUnscoped()) return;
3013
+ const ourDir = ourLoadedDir();
3014
+ for (const cacheRoot of candidateCacheRoots()) {
3015
+ try {
3016
+ cleanupOne(cacheRoot, ourDir);
3017
+ } catch (err) {
3018
+ log.warn("cleanup-stale: error processing cache root", {
3019
+ cacheRoot,
3020
+ error: String(err)
3021
+ });
3022
+ }
3023
+ }
3024
+ }
3025
+ function cleanupOne(cacheRoot, ourDir) {
3026
+ if (!existsSync2(cacheRoot)) return;
3027
+ const stalePath = join4(cacheRoot, "node_modules", STALE_PACKAGE_NAME);
3028
+ if (!existsSync2(stalePath)) return;
3029
+ let realStalePath = stalePath;
3030
+ try {
3031
+ realStalePath = realpathSync(stalePath);
3032
+ } catch {
3033
+ }
3034
+ if (ourDir && realStalePath === ourDir) return;
3035
+ const pkgJsonPath = join4(stalePath, "package.json");
3036
+ if (!existsSync2(pkgJsonPath)) return;
3037
+ let pkg = {};
3038
+ try {
3039
+ pkg = JSON.parse(readFileSync3(pkgJsonPath, "utf8"));
3040
+ } catch {
3041
+ return;
3042
+ }
3043
+ if (pkg.name !== STALE_PACKAGE_NAME) return;
3044
+ if (!pkg.description?.includes(SUSPECT_DESCRIPTION_TOKEN)) return;
3045
+ log.info("cleanup-stale: removing unscoped install", { stalePath });
3046
+ try {
3047
+ rmSync(stalePath, { recursive: true, force: true });
3048
+ } catch (err) {
3049
+ log.warn("cleanup-stale: rmSync failed", {
3050
+ stalePath,
3051
+ error: String(err)
3052
+ });
3053
+ return;
3054
+ }
3055
+ const cachePkgJson = join4(cacheRoot, "package.json");
3056
+ if (!existsSync2(cachePkgJson)) return;
3057
+ try {
3058
+ const cfg = JSON.parse(readFileSync3(cachePkgJson, "utf8"));
3059
+ if (cfg?.dependencies?.[STALE_PACKAGE_NAME]) {
3060
+ delete cfg.dependencies[STALE_PACKAGE_NAME];
3061
+ writeFileSync4(cachePkgJson, JSON.stringify(cfg, null, 2) + "\n");
3062
+ log.info("cleanup-stale: pruned dep from cache package.json");
3063
+ }
3064
+ } catch (err) {
3065
+ log.warn("cleanup-stale: cache package.json update failed", {
3066
+ error: String(err)
3067
+ });
3068
+ }
3069
+ }
3070
+
2904
3071
  // src/index.ts
2905
3072
  function createClaudeCode(settings = {}) {
2906
3073
  const cliPath = settings.cliPath ?? process.env.CLAUDE_CLI_PATH ?? "claude";
@@ -3080,18 +3247,8 @@ async function expandAccountProviders(config) {
3080
3247
  }
3081
3248
  return expandedCount > 0;
3082
3249
  }
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
3250
  var server = async (input) => {
3251
+ cleanupStaleUnscopedInstall();
3095
3252
  if (input && typeof input === "object" && "client" in input) {
3096
3253
  setOpencodeClient(input.client);
3097
3254
  }
@@ -3099,18 +3256,25 @@ var server = async (input) => {
3099
3256
  config: async (config) => {
3100
3257
  config.provider ??= {};
3101
3258
  const expanded = await expandAccountProviders(config);
3102
- if (expanded) return;
3259
+ if (expanded) {
3260
+ const registered = Object.entries(config.provider).filter(([id]) => id === PROVIDER_ID2 || id.startsWith(`${PROVIDER_ID2}-`)).map(([id, p]) => ({ id, name: p?.name ?? id }));
3261
+ log.notice("registered claude-code providers", { providers: registered });
3262
+ return;
3263
+ }
3103
3264
  const existing = config.provider[PROVIDER_ID2];
3104
3265
  config.provider[PROVIDER_ID2] = {
3105
3266
  ...existing,
3106
3267
  ...await providerConfig(existing)
3107
3268
  };
3269
+ log.notice("registered claude-code provider", {
3270
+ id: PROVIDER_ID2,
3271
+ name: config.provider[PROVIDER_ID2]?.name ?? PROVIDER_ID2
3272
+ });
3108
3273
  },
3109
- event: async ({ event }) => {
3110
- if (readEventType(event) === "global.disposed") {
3111
- evictAllSessions("global.disposed");
3112
- }
3113
- },
3274
+ // No `event` hook: MCP config drift is detected at turn start by the
3275
+ // hot-reload check in `claude-code-language-model.ts`, which respawns
3276
+ // claude safely between turns. Eviction on `global.disposed` would kill
3277
+ // an in-flight stream and abort the user's current turn.
3114
3278
  provider: {
3115
3279
  id: PROVIDER_ID2,
3116
3280
  models: async (provider) => defaultModelsForProvider(provider.models)