cursor-opencode-provider 0.2.7 → 0.2.9

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.
@@ -1,8 +1,33 @@
1
1
  import { createHash } from "node:crypto";
2
- import { existsSync, mkdirSync } from "node:fs";
2
+ import { mkdirSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
5
6
  import { trace } from "../debug.js";
7
+ /** Host-neutral bridge installed by OCP before an unchanged provider is loaded. */
8
+ export const OPENCODE_PATH_BRIDGE = Symbol.for("opencode.compat.path-bridge");
9
+ function pathBridge() {
10
+ const value = globalThis[OPENCODE_PATH_BRIDGE];
11
+ if (!value || typeof value !== "object")
12
+ return undefined;
13
+ const bridge = value;
14
+ return typeof bridge.projectConfigDirs === "function" && typeof bridge.globalConfigDirs === "function"
15
+ ? bridge
16
+ : undefined;
17
+ }
18
+ export function opencodeProjectConfigDirs(workspaceRoot) {
19
+ return pathBridge()?.projectConfigDirs(path.resolve(workspaceRoot)) ?? [
20
+ path.join(path.resolve(workspaceRoot), ".opencode"),
21
+ ];
22
+ }
23
+ export function opencodeGlobalConfigDirs() {
24
+ return pathBridge()?.globalConfigDirs() ?? [opencodeGlobalConfigDir()];
25
+ }
26
+ export function opencodeConfigFileNames() {
27
+ return pathBridge()?.configFileNames?.length
28
+ ? [...pathBridge().configFileNames]
29
+ : ["opencode.json", "opencode.jsonc"];
30
+ }
6
31
  /** Explicit host cache root (e.g. Effect v2 `Path.cache`, or `createCursor({ cacheDir })`). */
7
32
  let hostCacheDirOverride;
8
33
  function resolveHome(env = process.env) {
@@ -13,11 +38,6 @@ function xdgCacheHome(env = process.env) {
13
38
  return env.XDG_CACHE_HOME;
14
39
  return path.join(resolveHome(env), ".cache");
15
40
  }
16
- function xdgConfigHome(env = process.env) {
17
- if (env.XDG_CONFIG_HOME && env.XDG_CONFIG_HOME.length > 0)
18
- return env.XDG_CONFIG_HOME;
19
- return path.join(resolveHome(env), ".config");
20
- }
21
41
  /**
22
42
  * Pin the process-wide cache root. Highest precedence for {@link opencodeGlobalCacheDir}.
23
43
  * Use for host-injected `Path.cache` or an explicit `createCursor({ cacheDir })`.
@@ -30,23 +50,35 @@ export function getHostCacheDirOverride() {
30
50
  }
31
51
  /**
32
52
  * Resolve the host cache directory without an override.
33
- * Mirrors OCP HostProfile drafts: MiMo (`mimocode` / `$MIMOCODE_HOME`), Kilo (`kilo`),
34
- * then OpenCode (`opencode`). Prefer fork config dirs when several exist.
53
+ *
54
+ * Explicit host environment wins. Otherwise, an installed provider inherits
55
+ * the host-named cache containing its module. A source checkout or otherwise
56
+ * unidentifiable install defaults to OpenCode. Merely having another host's
57
+ * config directory installed is not evidence that it owns this process.
35
58
  */
36
- export function resolveHostCacheDir(env = process.env) {
59
+ export function resolveHostCacheDir(env = process.env, moduleUrl = import.meta.url) {
37
60
  const mimoHome = env.MIMOCODE_HOME;
38
61
  if (mimoHome && mimoHome.length > 0) {
39
62
  return path.join(mimoHome, "cache");
40
63
  }
41
- const configHome = xdgConfigHome(env);
42
64
  const cacheHome = xdgCacheHome(env);
43
65
  const kiloConfig = env.KILO_CONFIG_DIR;
44
- // Prefer more specific fork dirs before OpenCode (same order as OCP detect).
45
- if ((kiloConfig && kiloConfig.length > 0) || existsSync(path.join(configHome, "kilo"))) {
66
+ if (kiloConfig && kiloConfig.length > 0) {
46
67
  return path.join(cacheHome, "kilo");
47
68
  }
48
- if (existsSync(path.join(configHome, "mimocode"))) {
49
- return path.join(cacheHome, "mimocode");
69
+ let modulePath;
70
+ try {
71
+ modulePath = moduleUrl.startsWith("file:") ? fileURLToPath(moduleUrl) : path.resolve(moduleUrl);
72
+ }
73
+ catch {
74
+ modulePath = undefined;
75
+ }
76
+ if (modulePath) {
77
+ for (const host of ["mimocode", "kilo", "opencode"]) {
78
+ const root = path.resolve(cacheHome, host);
79
+ if (modulePath === root || modulePath.startsWith(`${root}${path.sep}`))
80
+ return root;
81
+ }
50
82
  }
51
83
  return path.join(cacheHome, "opencode");
52
84
  }
@@ -54,12 +86,18 @@ export function resolveHostCacheDir(env = process.env) {
54
86
  * Best-effort: if `@opencode-compat/profile` is installed, adopt `detect().profile.paths.cacheDir`
55
87
  * when the host is supported. No-op when OCP is absent or detection fails.
56
88
  */
57
- export async function adoptCompatHostCacheDir() {
89
+ export async function adoptCompatHostCacheDir(detector) {
90
+ if (hostCacheDirOverride)
91
+ return hostCacheDirOverride;
58
92
  try {
59
- const { detect } = await import("@opencode-compat/profile");
93
+ const detect = detector ?? (await import("@opencode-compat/profile")).detect;
60
94
  const result = detect();
61
95
  if (!result.supported || result.id === "unknown")
62
96
  return undefined;
97
+ if (!result.source || !["env", "binary", "package"].includes(result.source)) {
98
+ trace(`host-cache: ignored weak OCP detect host=${result.id} source=${result.source ?? "unknown"}`);
99
+ return undefined;
100
+ }
63
101
  const cacheDir = result.profile.paths.cacheDir;
64
102
  if (!cacheDir || cacheDir.length === 0)
65
103
  return undefined;
@@ -80,8 +118,8 @@ export function opencodeGlobalConfigDir() {
80
118
  *
81
119
  * Precedence:
82
120
  * 1. {@link setHostCacheDirOverride} / `createCursor({ cacheDir })` (host `Path.cache`)
83
- * 2. OCP `detect()` when {@link adoptCompatHostCacheDir} ran successfully
84
- * 3. Local host heuristic ({@link resolveHostCacheDir})
121
+ * 2. Strong OCP `detect()` identity when {@link adoptCompatHostCacheDir} ran successfully
122
+ * 3. Explicit host environment / provider install path ({@link resolveHostCacheDir})
85
123
  */
86
124
  export function opencodeGlobalCacheDir() {
87
125
  if (hostCacheDirOverride)
@@ -1,6 +1,6 @@
1
1
  import { readdir, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { opencodeGlobalConfigDir } from "./paths.js";
3
+ import { opencodeGlobalConfigDirs, opencodeProjectConfigDirs } from "./paths.js";
4
4
  async function listLocalPlugins(dir) {
5
5
  try {
6
6
  await stat(dir);
@@ -34,17 +34,21 @@ export async function collectPlugins(workspaceRoot, config) {
34
34
  seen.add(id);
35
35
  out.push({ id, source: "npm" });
36
36
  }
37
- for (const p of await listLocalPlugins(path.join(workspaceRoot, ".opencode", "plugins"))) {
38
- if (seen.has(p.id))
39
- continue;
40
- seen.add(p.id);
41
- out.push(p);
42
- }
43
- for (const p of await listLocalPlugins(path.join(opencodeGlobalConfigDir(), "plugins"))) {
44
- if (seen.has(p.id))
45
- continue;
46
- seen.add(p.id);
47
- out.push(p);
37
+ for (const configDir of opencodeProjectConfigDirs(workspaceRoot)) {
38
+ for (const p of await listLocalPlugins(path.join(configDir, "plugins"))) {
39
+ if (seen.has(p.id))
40
+ continue;
41
+ seen.add(p.id);
42
+ out.push(p);
43
+ }
44
+ }
45
+ for (const configDir of opencodeGlobalConfigDirs()) {
46
+ for (const p of await listLocalPlugins(path.join(configDir, "plugins"))) {
47
+ if (seen.has(p.id))
48
+ continue;
49
+ seen.add(p.id);
50
+ out.push(p);
51
+ }
48
52
  }
49
53
  return out;
50
54
  }
@@ -1,7 +1,7 @@
1
1
  import { readFile, readdir, stat } from "node:fs/promises";
2
2
  import { homedir } from "node:os";
3
3
  import path from "node:path";
4
- import { opencodeGlobalConfigDir, resolveHomeRelative } from "./paths.js";
4
+ import { opencodeConfigFileNames, opencodeGlobalConfigDirs, opencodeProjectConfigDirs, resolveHomeRelative } from "./paths.js";
5
5
  async function exists(file) {
6
6
  try {
7
7
  await stat(file);
@@ -12,7 +12,7 @@ async function exists(file) {
12
12
  }
13
13
  }
14
14
  async function readJsonConfig(dir) {
15
- for (const name of ["opencode.json", "opencode.jsonc"]) {
15
+ for (const name of opencodeConfigFileNames()) {
16
16
  const file = path.join(dir, name);
17
17
  if (!(await exists(file)))
18
18
  continue;
@@ -151,29 +151,30 @@ export async function fetchRemoteInstruction(url, timeoutMs = 5000) {
151
151
  clearTimeout(timer);
152
152
  }
153
153
  }
154
- export async function loadMergedConfig(workspaceRoot) {
155
- const globalConfig = await readJsonConfig(opencodeGlobalConfigDir());
156
- if (isProjectConfigDisabled()) {
157
- return {
158
- ...globalConfig,
159
- instructions: [...(globalConfig.instructions ?? [])],
160
- plugin: [...(globalConfig.plugin ?? [])],
161
- plugins: [...(globalConfig.plugins ?? [])],
162
- mcp: { ...(globalConfig.mcp ?? {}) },
163
- permission: globalConfig.permission,
164
- };
165
- }
166
- const projectConfig = await readJsonConfig(workspaceRoot);
154
+ function mergeConfig(base, overlay) {
167
155
  return {
168
- ...globalConfig,
169
- ...projectConfig,
170
- instructions: [...(globalConfig.instructions ?? []), ...(projectConfig.instructions ?? [])],
171
- plugin: [...new Set([...(globalConfig.plugin ?? []), ...(projectConfig.plugin ?? [])])],
172
- plugins: [...new Set([...(globalConfig.plugins ?? []), ...(projectConfig.plugins ?? [])])],
173
- mcp: { ...(globalConfig.mcp ?? {}), ...(projectConfig.mcp ?? {}) },
174
- permission: projectConfig.permission ?? globalConfig.permission,
156
+ ...base,
157
+ ...overlay,
158
+ instructions: [...(base.instructions ?? []), ...(overlay.instructions ?? [])],
159
+ plugin: [...new Set([...(base.plugin ?? []), ...(overlay.plugin ?? [])])],
160
+ plugins: [...new Set([...(base.plugins ?? []), ...(overlay.plugins ?? [])])],
161
+ mcp: { ...(base.mcp ?? {}), ...(overlay.mcp ?? {}) },
162
+ permission: overlay.permission ?? base.permission,
175
163
  };
176
164
  }
165
+ export async function loadMergedConfig(workspaceRoot) {
166
+ const globalConfig = await readJsonConfig(opencodeGlobalConfigDirs()[0] ?? "");
167
+ if (isProjectConfigDisabled())
168
+ return mergeConfig({}, globalConfig);
169
+ // The bridge supplies native project config roots for an unchanged plugin:
170
+ // .opencode on OpenCode, .mimocode on MiMo, and .kilo/.kilocode on Kilo.
171
+ // Later roots have higher precedence, matching the host's native ordering.
172
+ let projectConfig = await readJsonConfig(workspaceRoot);
173
+ for (const configDir of opencodeProjectConfigDirs(workspaceRoot)) {
174
+ projectConfig = mergeConfig(projectConfig, await readJsonConfig(configDir));
175
+ }
176
+ return mergeConfig(globalConfig, projectConfig);
177
+ }
177
178
  /**
178
179
  * Collect OpenCode instruction files.
179
180
  */
@@ -205,7 +206,9 @@ export async function collectRules(workspaceRoot) {
205
206
  }
206
207
  }
207
208
  }
208
- await add(path.join(opencodeGlobalConfigDir(), "AGENTS.md"));
209
+ for (const globalDir of opencodeGlobalConfigDirs()) {
210
+ await add(path.join(globalDir, "AGENTS.md"));
211
+ }
209
212
  await add(path.join(homedir(), ".claude", "CLAUDE.md"));
210
213
  for (const raw of config.instructions ?? []) {
211
214
  if (raw.startsWith("http://") || raw.startsWith("https://")) {
@@ -1,7 +1,7 @@
1
1
  import { readdir, readFile, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { homedir } from "node:os";
4
- import { opencodeGlobalConfigDir } from "./paths.js";
4
+ import { opencodeGlobalConfigDirs, opencodeProjectConfigDirs } from "./paths.js";
5
5
  async function exists(p) {
6
6
  try {
7
7
  await stat(p);
@@ -94,8 +94,13 @@ async function walkAncestorsFor(dir, rel, stop, out) {
94
94
  export async function collectSkills(workspaceRoot, worktree) {
95
95
  const out = new Map();
96
96
  const home = homedir();
97
- await walkAncestorsFor(workspaceRoot, path.join(".opencode", "skills"), worktree, out);
98
- await scanSkillsRoot(path.join(opencodeGlobalConfigDir(), "skills"), out);
97
+ for (const projectDir of opencodeProjectConfigDirs(workspaceRoot)) {
98
+ const relative = path.relative(workspaceRoot, projectDir);
99
+ await walkAncestorsFor(workspaceRoot, path.join(relative, "skills"), worktree, out);
100
+ }
101
+ for (const globalDir of opencodeGlobalConfigDirs()) {
102
+ await scanSkillsRoot(path.join(globalDir, "skills"), out);
103
+ }
99
104
  await walkAncestorsFor(workspaceRoot, path.join(".claude", "skills"), worktree, out);
100
105
  await scanSkillsRoot(path.join(home, ".claude", "skills"), out);
101
106
  await walkAncestorsFor(workspaceRoot, path.join(".agents", "skills"), worktree, out);
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Bound a complete async operation, not only fetch(). Response body readers can
3
+ * ignore abort signals, so Promise.race remains the authoritative deadline.
4
+ *
5
+ * Rejects with `timeoutError()` before aborting so callers observe the domain
6
+ * timeout error rather than a generic AbortError.
7
+ */
8
+ export declare function withAbortDeadline<T>(timeoutMs: number, timeoutError: () => Error, run: (signal: AbortSignal) => Promise<T>): Promise<T>;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Bound a complete async operation, not only fetch(). Response body readers can
3
+ * ignore abort signals, so Promise.race remains the authoritative deadline.
4
+ *
5
+ * Rejects with `timeoutError()` before aborting so callers observe the domain
6
+ * timeout error rather than a generic AbortError.
7
+ */
8
+ export async function withAbortDeadline(timeoutMs, timeoutError, run) {
9
+ const controller = new AbortController();
10
+ let timer;
11
+ const deadline = new Promise((_, reject) => {
12
+ timer = setTimeout(() => {
13
+ reject(timeoutError());
14
+ controller.abort();
15
+ }, timeoutMs);
16
+ timer.unref?.();
17
+ });
18
+ try {
19
+ return await Promise.race([run(controller.signal), deadline]);
20
+ }
21
+ finally {
22
+ if (timer)
23
+ clearTimeout(timer);
24
+ }
25
+ }