cursor-opencode-provider 0.4.3 → 0.6.0

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.
Files changed (71) hide show
  1. package/README.md +33 -23
  2. package/dist/context/agents.d.ts +1 -1
  3. package/dist/context/agents.js +1 -1
  4. package/dist/context/auth-store.js +2 -2
  5. package/dist/context/build.d.ts +11 -1
  6. package/dist/context/build.js +76 -25
  7. package/dist/context/frozen.d.ts +29 -0
  8. package/dist/context/frozen.js +168 -0
  9. package/dist/context/index.d.ts +3 -2
  10. package/dist/context/index.js +3 -2
  11. package/dist/context/paths.d.ts +34 -53
  12. package/dist/context/paths.js +54 -118
  13. package/dist/context/rules.d.ts +1 -0
  14. package/dist/context/rules.js +2 -2
  15. package/dist/image-save-tool.d.ts +19 -0
  16. package/dist/image-save-tool.js +25 -0
  17. package/dist/image-save.d.ts +58 -0
  18. package/dist/image-save.js +189 -0
  19. package/dist/image-staging.d.ts +59 -0
  20. package/dist/image-staging.js +91 -0
  21. package/dist/index.d.ts +1 -1
  22. package/dist/language-model.d.ts +13 -11
  23. package/dist/language-model.js +957 -150
  24. package/dist/plan-execution-kickoff.d.ts +55 -0
  25. package/dist/plan-execution-kickoff.js +162 -0
  26. package/dist/plugin-opencode2.js +52 -3
  27. package/dist/plugin-v2.d.ts +11 -9
  28. package/dist/plugin-v2.js +6 -13
  29. package/dist/plugin.d.ts +9 -0
  30. package/dist/plugin.js +61 -5
  31. package/dist/pricing-data.d.ts +16 -4
  32. package/dist/pricing-data.js +16 -4
  33. package/dist/protocol/ask-question.d.ts +133 -0
  34. package/dist/protocol/ask-question.js +320 -0
  35. package/dist/protocol/blob-reachability.d.ts +15 -0
  36. package/dist/protocol/blob-reachability.js +140 -0
  37. package/dist/protocol/blob-store.d.ts +37 -0
  38. package/dist/protocol/blob-store.js +115 -0
  39. package/dist/protocol/conversation-bind.d.ts +13 -0
  40. package/dist/protocol/conversation-bind.js +27 -1
  41. package/dist/protocol/conversation-persistence.d.ts +32 -0
  42. package/dist/protocol/conversation-persistence.js +504 -0
  43. package/dist/protocol/conversation-state.d.ts +16 -0
  44. package/dist/protocol/conversation-state.js +62 -0
  45. package/dist/protocol/create-plan.d.ts +141 -0
  46. package/dist/protocol/create-plan.js +373 -0
  47. package/dist/protocol/generate-image.d.ts +54 -0
  48. package/dist/protocol/generate-image.js +101 -0
  49. package/dist/protocol/interactions.d.ts +110 -8
  50. package/dist/protocol/interactions.js +295 -35
  51. package/dist/protocol/kv.d.ts +1 -0
  52. package/dist/protocol/kv.js +2 -0
  53. package/dist/protocol/messages.d.ts +2 -0
  54. package/dist/protocol/messages.js +148 -8
  55. package/dist/protocol/request.d.ts +2 -0
  56. package/dist/protocol/request.js +2 -0
  57. package/dist/protocol/switch-mode.d.ts +178 -0
  58. package/dist/protocol/switch-mode.js +417 -0
  59. package/dist/protocol/token-details.d.ts +33 -0
  60. package/dist/protocol/token-details.js +155 -0
  61. package/dist/protocol/tool-call-bridge.js +10 -5
  62. package/dist/protocol/tools.d.ts +44 -11
  63. package/dist/protocol/tools.js +171 -41
  64. package/dist/session.d.ts +39 -11
  65. package/dist/shared.d.ts +3 -0
  66. package/dist/shared.js +3 -0
  67. package/dist/usage.d.ts +42 -37
  68. package/dist/usage.js +201 -70
  69. package/dist/web-search-tool.d.ts +22 -26
  70. package/dist/web-search-tool.js +25 -15
  71. package/package.json +7 -4
@@ -0,0 +1,168 @@
1
+ import { buildDynamicRequestContext, buildRequestContext, materializeRequestContext, requestContextBase, } from "./build.js";
2
+ import { trace } from "../debug.js";
3
+ import { encodeMessage } from "../protocol/messages.js";
4
+ /**
5
+ * Freeze the expensive RequestContext base for the life of a conversation_id.
6
+ *
7
+ * Cursor's current exec-daemon cache treats RequestContext as a baked stable
8
+ * base plus live plugin/tool overlays. Rebuilding volatile git/layout data on
9
+ * every Run shifts the prompt prefix and tanks prompt-cache hits.
10
+ *
11
+ * Skills, subagents, plugin metadata, and tool/MCP capabilities are rebuilt on
12
+ * each Run and overlaid on that base. If their encoded bytes did not change,
13
+ * the exact prior materialized object is reused. This matches Cursor's newer
14
+ * baked-context + live-overlay design without retaining stale capabilities.
15
+ */
16
+ const byConversationId = new Map();
17
+ const materializedByConversationId = new Map();
18
+ const buildsByConversationId = new Map();
19
+ export const MAX_FROZEN_REQUEST_CONTEXTS = 256;
20
+ function freezeSnapshot(value, seen = new Set()) {
21
+ if (!value || typeof value !== "object")
22
+ return value;
23
+ const object = value;
24
+ if (seen.has(object) || ArrayBuffer.isView(object))
25
+ return value;
26
+ seen.add(object);
27
+ for (const child of Object.values(object))
28
+ freezeSnapshot(child, seen);
29
+ return Object.freeze(value);
30
+ }
31
+ function remember(conversationId, context) {
32
+ // Map insertion order is the LRU order. Refresh existing entries on use.
33
+ byConversationId.delete(conversationId);
34
+ // Clone + recursively freeze so session.requestContext cannot mutate the
35
+ // byte-stable snapshot retained for later Runs in the same conversation.
36
+ byConversationId.set(conversationId, freezeSnapshot(structuredClone(context)));
37
+ materializedByConversationId.delete(conversationId);
38
+ while (byConversationId.size > MAX_FROZEN_REQUEST_CONTEXTS) {
39
+ const oldest = byConversationId.keys().next().value;
40
+ if (!oldest)
41
+ break;
42
+ byConversationId.delete(oldest);
43
+ materializedByConversationId.delete(oldest);
44
+ }
45
+ }
46
+ /** Frozen stable RequestContext base for this conversation, if any. */
47
+ export function getFrozenRequestContext(conversationId) {
48
+ const frozen = byConversationId.get(conversationId);
49
+ if (!frozen)
50
+ return undefined;
51
+ // Touch for LRU.
52
+ byConversationId.delete(conversationId);
53
+ byConversationId.set(conversationId, frozen);
54
+ return frozen;
55
+ }
56
+ /** Replace the frozen stable base, stripping any live capability fields. */
57
+ export function setFrozenRequestContext(conversationId, context) {
58
+ if (!conversationId)
59
+ return;
60
+ remember(conversationId, requestContextBase(context));
61
+ }
62
+ /** Drop a conversation's frozen RequestContext (compaction / binding reset). */
63
+ export function clearFrozenRequestContext(conversationId) {
64
+ byConversationId.delete(conversationId);
65
+ materializedByConversationId.delete(conversationId);
66
+ }
67
+ /**
68
+ * Move a stable workspace base across a conversation-id reset.
69
+ *
70
+ * Compaction changes Cursor's state/checkpoint identity, not the OpenCode
71
+ * workspace. Preserve the expensive base and the prior materialized bytes as a
72
+ * comparison seed; getOrBuildRequestContext still rediscovers live capability
73
+ * overlays and only reuses the complete context when those bytes also match.
74
+ */
75
+ export function transferFrozenRequestContext(previousConversationId, nextConversationId) {
76
+ if (!previousConversationId || !nextConversationId)
77
+ return false;
78
+ const base = byConversationId.get(previousConversationId);
79
+ const materialized = materializedByConversationId.get(previousConversationId);
80
+ clearFrozenRequestContext(previousConversationId);
81
+ clearFrozenRequestContext(nextConversationId);
82
+ if (!base)
83
+ return false;
84
+ remember(nextConversationId, base);
85
+ if (materialized) {
86
+ materializedByConversationId.set(nextConversationId, {
87
+ context: materialized.context,
88
+ bytes: Uint8Array.from(materialized.bytes),
89
+ });
90
+ }
91
+ return true;
92
+ }
93
+ /** Test helper — wipe all frozen contexts. */
94
+ export function resetFrozenRequestContextsForTests() {
95
+ byConversationId.clear();
96
+ materializedByConversationId.clear();
97
+ buildsByConversationId.clear();
98
+ }
99
+ function sameBytes(a, b) {
100
+ if (a.length !== b.length)
101
+ return false;
102
+ for (let i = 0; i < a.length; i++)
103
+ if (a[i] !== b[i])
104
+ return false;
105
+ return true;
106
+ }
107
+ function rememberMaterialized(conversationId, context) {
108
+ const bytes = encodeMessage("RequestContext", context);
109
+ const previous = materializedByConversationId.get(conversationId);
110
+ if (previous && sameBytes(previous.bytes, bytes)) {
111
+ return { context: previous.context, reused: true };
112
+ }
113
+ const frozen = freezeSnapshot(structuredClone(context));
114
+ materializedByConversationId.set(conversationId, {
115
+ context: frozen,
116
+ bytes: Uint8Array.from(bytes),
117
+ });
118
+ return { context: frozen, reused: false };
119
+ }
120
+ /**
121
+ * Return a stable-base + live-overlay RequestContext for `conversationId`.
122
+ * The base is built once; capability sections are rediscovered every Run.
123
+ */
124
+ export async function getOrBuildRequestContext(conversationId, input, opts) {
125
+ if (!opts?.refresh && conversationId) {
126
+ const base = getFrozenRequestContext(conversationId);
127
+ if (base) {
128
+ const dynamic = await buildDynamicRequestContext(input);
129
+ const materialized = rememberMaterialized(conversationId, materializeRequestContext(base, dynamic));
130
+ trace(`request_context: materialized conversationId=${conversationId} ` +
131
+ `tools=${Array.isArray(materialized.context.tools) ? materialized.context.tools.length : 0} ` +
132
+ `reused=${materialized.reused}`);
133
+ return materialized;
134
+ }
135
+ }
136
+ // Two overlapping model calls can open Runs for the same conversation. Share
137
+ // the first build so both Runs receive the same snapshot instead of racing
138
+ // two independently collected git/layout views into the cache.
139
+ if (!opts?.refresh && conversationId) {
140
+ const inFlight = buildsByConversationId.get(conversationId);
141
+ if (inFlight) {
142
+ await inFlight;
143
+ return getOrBuildRequestContext(conversationId, input);
144
+ }
145
+ }
146
+ const build = buildRequestContext(input);
147
+ if (conversationId)
148
+ buildsByConversationId.set(conversationId, build);
149
+ let context;
150
+ try {
151
+ context = await build;
152
+ }
153
+ finally {
154
+ if (conversationId && buildsByConversationId.get(conversationId) === build) {
155
+ buildsByConversationId.delete(conversationId);
156
+ }
157
+ }
158
+ if (conversationId)
159
+ setFrozenRequestContext(conversationId, context);
160
+ // The first full build already contains both base and live overlay.
161
+ const materialized = conversationId
162
+ ? rememberMaterialized(conversationId, context)
163
+ : { context: freezeSnapshot(structuredClone(context)), reused: false };
164
+ trace(`request_context: built+frozen conversationId=${conversationId || "(none)"} ` +
165
+ `tools=${Array.isArray(materialized.context.tools) ? materialized.context.tools.length : 0} ` +
166
+ `refresh=${!!opts?.refresh}`);
167
+ return { context: materialized.context, reused: false };
168
+ }
@@ -1,2 +1,3 @@
1
- export { buildRequestContext, type BuildRequestContextInput } from "./build.js";
2
- export { adoptCompatHostCacheDir, getHostCacheDirOverride, opencodeGlobalCacheDir, opencodeGlobalConfigDir, opencodeGlobalDataDir, resolveHostCacheDir, setHostCacheDirOverride, } from "./paths.js";
1
+ export { buildDynamicRequestContext, buildRequestContext, DYNAMIC_REQUEST_CONTEXT_KEYS, materializeRequestContext, requestContextBase, type BuildRequestContextInput, } from "./build.js";
2
+ export { clearFrozenRequestContext, getFrozenRequestContext, getOrBuildRequestContext, MAX_FROZEN_REQUEST_CONTEXTS, resetFrozenRequestContextsForTests, setFrozenRequestContext, transferFrozenRequestContext, } from "./frozen.js";
3
+ export { HOST_PATH_BRIDGE, getHostCacheDirOverride, opencodeGlobalCacheDir, opencodeGlobalConfigDir, opencodeGlobalDataDir, hostGlobalDataDir, resolveHostCacheDir, setHostCacheDirOverride, } from "./paths.js";
@@ -1,2 +1,3 @@
1
- export { buildRequestContext } from "./build.js";
2
- export { adoptCompatHostCacheDir, getHostCacheDirOverride, opencodeGlobalCacheDir, opencodeGlobalConfigDir, opencodeGlobalDataDir, resolveHostCacheDir, setHostCacheDirOverride, } from "./paths.js";
1
+ export { buildDynamicRequestContext, buildRequestContext, DYNAMIC_REQUEST_CONTEXT_KEYS, materializeRequestContext, requestContextBase, } from "./build.js";
2
+ export { clearFrozenRequestContext, getFrozenRequestContext, getOrBuildRequestContext, MAX_FROZEN_REQUEST_CONTEXTS, resetFrozenRequestContextsForTests, setFrozenRequestContext, transferFrozenRequestContext, } from "./frozen.js";
3
+ export { HOST_PATH_BRIDGE, getHostCacheDirOverride, opencodeGlobalCacheDir, opencodeGlobalConfigDir, opencodeGlobalDataDir, hostGlobalDataDir, resolveHostCacheDir, setHostCacheDirOverride, } from "./paths.js";
@@ -1,79 +1,61 @@
1
1
  export type HostPathEnv = NodeJS.ProcessEnv;
2
- /** Host-neutral bridge installed by OCP before an unchanged provider is loaded. */
3
- export declare const OPENCODE_PATH_BRIDGE: unique symbol;
2
+ /** Structural host-path capability installed before an unchanged provider loads. */
3
+ export declare const HOST_PATH_BRIDGE: unique symbol;
4
4
  export type OpenCodePathBridge = {
5
5
  projectConfigDirs: (workspaceRoot: string) => string[];
6
6
  globalConfigDirs: () => string[];
7
+ /** Optional host-owned durable data root; absent means native OpenCode defaults. */
8
+ globalDataDir?: () => string;
9
+ /** Optional host-owned cache root; absent means native OpenCode defaults. */
10
+ globalCacheDir?: () => string;
7
11
  configFileNames?: string[];
8
12
  };
9
13
  export declare function opencodeProjectConfigDirs(workspaceRoot: string): string[];
10
14
  export declare function opencodeGlobalConfigDirs(): string[];
11
15
  export declare function opencodeConfigFileNames(): string[];
12
- type CompatDetectResult = {
13
- id: string;
14
- supported: boolean;
15
- source?: string;
16
- profile: {
17
- paths: {
18
- cacheDir: string;
19
- };
20
- };
21
- };
22
- type CompatDetector = () => CompatDetectResult;
23
16
  /**
24
17
  * Pin the process-wide cache root. Highest precedence for {@link opencodeGlobalCacheDir}.
25
18
  * Use for host-injected `Path.cache` or an explicit `createCursor({ cacheDir })`.
26
19
  */
27
20
  export declare function setHostCacheDirOverride(dir: string | undefined): void;
28
21
  export declare function getHostCacheDirOverride(): string | undefined;
29
- declare const HOST_CACHE_DIRS: readonly ["mimocode", "kilo", "opencode"];
30
- export type HostCacheDir = (typeof HOST_CACHE_DIRS)[number];
31
- /**
32
- * Host cache-dir name inferred from the running binary (`argv[0]` /
33
- * `process.execPath`). A source-checkout provider can't be located by install
34
- * path, but the binary that loaded it is authoritative: when this process IS
35
- * `mimo` / `kilocode`, the cache root is that host's, not OpenCode's. Mirrors
36
- * OCP `detect()` binaryHint semantics and tolerates the leading-dot Kilo binary
37
- * (`.kilo`). Only `argv[0]` is inspected — later argv entries are arguments,
38
- * not binary identity, and could otherwise cause false positives (e.g. an
39
- * `--opencode-config=…` flag under a fork).
40
- */
41
- export declare function hostCacheDirFromProcess(argv?: readonly string[], execPath?: string): HostCacheDir | undefined;
42
- /**
43
- * Resolve the host cache directory without an override.
44
- *
45
- * Explicit host environment wins. Otherwise, an installed provider inherits
46
- * the host-named cache containing its module; a source checkout is attributed
47
- * by the running binary identity (see {@link hostCacheDirFromProcess}). Merely
48
- * having another host's config directory installed is not evidence that it
49
- * owns this process.
50
- */
51
- export declare function resolveHostCacheDir(env?: HostPathEnv, moduleUrl?: string, processIdentity?: {
52
- argv?: readonly string[];
53
- execPath?: string;
54
- }): string;
55
- /**
56
- * Best-effort: if `@opencode-compat/profile` is installed, adopt `detect().profile.paths.cacheDir`
57
- * when the host is supported. No-op when OCP is absent or detection fails.
58
- */
59
- export declare function adoptCompatHostCacheDir(detector?: CompatDetector): Promise<string | undefined>;
60
- /** OpenCode / host global config dir (`~/.config/<app>`). Still OpenCode-named for rule discovery. */
22
+ /** Resolve the native OpenCode cache root when no host bridge is installed. */
23
+ export declare function resolveHostCacheDir(env?: HostPathEnv): string;
24
+ /** Native OpenCode global config dir. */
61
25
  export declare function opencodeGlobalConfigDir(): string;
62
26
  /**
63
27
  * Host global cache dir for Cursor project metadata + model/version caches.
64
28
  *
65
29
  * Precedence:
66
30
  * 1. {@link setHostCacheDirOverride} / `createCursor({ cacheDir })` (host `Path.cache`)
67
- * 2. Strong OCP `detect()` identity when {@link adoptCompatHostCacheDir} ran successfully
68
- * 3. Explicit host environment / provider install path ({@link resolveHostCacheDir})
31
+ * 2. An injected structural host path bridge
32
+ * 3. Native OpenCode XDG defaults ({@link resolveHostCacheDir})
69
33
  */
70
34
  export declare function opencodeGlobalCacheDir(): string;
35
+ /** Native OpenCode global data root. */
36
+ export declare function opencodeGlobalDataDir(env?: HostPathEnv): string;
37
+ /** Host-portable durable data root; falls back to native OpenCode. */
38
+ export declare function hostGlobalDataDir(env?: HostPathEnv): string;
71
39
  /**
72
- * OpenCode global data dir (`~/.local/share/opencode`).
73
- * Uses `$XDG_DATA_HOME/opencode` when set, otherwise `$HOME/.local/share/opencode`.
74
- * Auth credentials live here in `auth.json`.
40
+ * Directory for host plan files — always `<hostGlobalDataDir()>/plans`, and
41
+ * never inside the user's repository.
42
+ *
43
+ * OpenCode's own `Session.plan` branches on VCS and puts plans in the worktree
44
+ * (`<worktree>/.opencode/plans`) for a git project. The provider deliberately
45
+ * does *not* mirror that branch. Writing there means a throwaway plan lands in
46
+ * the user's tree untracked-but-unignored, and — because OpenCode installs
47
+ * `@opencode-ai/plugin` into every `.opencode` directory it discovers walking up
48
+ * from the cwd — creating that directory also bootstraps a project-local
49
+ * `node_modules`. The provider must add nothing to a repository it did not
50
+ * already contain.
51
+ *
52
+ * The global-data location is not a degraded fallback: it is the branch OpenCode
53
+ * itself uses when there is no VCS, and its plan agent allow-lists that path for
54
+ * `edit` / `external_directory` alongside the in-worktree one. {@link
55
+ * hostGlobalDataDir} carries an optional injected host translation; without a
56
+ * bridge it is the native OpenCode data root.
75
57
  */
76
- export declare function opencodeGlobalDataDir(): string;
58
+ export declare function hostPlansDir(_workspaceRoot?: string): string;
77
59
  /**
78
60
  * Cursor-compatible path slug (`/Users/a/b` → `Users-a-b`).
79
61
  * Used for per-workspace metadata under the host cache.
@@ -81,7 +63,7 @@ export declare function opencodeGlobalDataDir(): string;
81
63
  export declare function slugifyWorkspacePath(workspaceRoot: string): string;
82
64
  /**
83
65
  * Cursor-style project metadata root for a workspace.
84
- * Lives at `<host-cache>/projects/<slug>/` (OpenCode / MiMo / Kilo cache root).
66
+ * Lives at `<host-cache>/projects/<slug>/` under the resolved OpenCode/host cache root.
85
67
  *
86
68
  * This is what Cursor's RequestContextEnv.project_folder / MCP
87
69
  * workspace_project_dir point at — agent-tools, terminals, transcripts, etc.
@@ -91,4 +73,3 @@ export declare function opencodeProjectDir(workspaceRoot: string): string;
91
73
  /** Ensure {@link opencodeProjectDir} exists (mode 0o700) and return it. */
92
74
  export declare function ensureOpencodeProjectDir(workspaceRoot: string): string;
93
75
  export declare function resolveHomeRelative(p: string): string;
94
- export {};
@@ -2,12 +2,11 @@ import { createHash } from "node:crypto";
2
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";
6
5
  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");
6
+ /** Structural host-path capability installed before an unchanged provider loads. */
7
+ export const HOST_PATH_BRIDGE = Symbol.for("opencode.host.path-bridge");
9
8
  function pathBridge() {
10
- const value = globalThis[OPENCODE_PATH_BRIDGE];
9
+ const value = globalThis[HOST_PATH_BRIDGE];
11
10
  if (!value || typeof value !== "object")
12
11
  return undefined;
13
12
  const bridge = value;
@@ -15,6 +14,23 @@ function pathBridge() {
15
14
  ? bridge
16
15
  : undefined;
17
16
  }
17
+ function openCodeGlobalDataDir(env = process.env) {
18
+ if (env.XDG_DATA_HOME && env.XDG_DATA_HOME.length > 0) {
19
+ return path.join(env.XDG_DATA_HOME, "opencode");
20
+ }
21
+ return path.join(resolveHome(env), ".local", "share", "opencode");
22
+ }
23
+ function openCodeGlobalCacheDir(env = process.env) {
24
+ return path.join(xdgCacheHome(env), "opencode");
25
+ }
26
+ function bridgeGlobalDataDir() {
27
+ const value = pathBridge()?.globalDataDir?.();
28
+ return typeof value === "string" && value.length > 0 ? path.resolve(value) : undefined;
29
+ }
30
+ function bridgeGlobalCacheDir() {
31
+ const value = pathBridge()?.globalCacheDir?.();
32
+ return typeof value === "string" && value.length > 0 ? path.resolve(value) : undefined;
33
+ }
18
34
  export function opencodeProjectConfigDirs(workspaceRoot) {
19
35
  return pathBridge()?.projectConfigDirs(path.resolve(workspaceRoot)) ?? [
20
36
  path.join(path.resolve(workspaceRoot), ".opencode"),
@@ -48,110 +64,11 @@ export function setHostCacheDirOverride(dir) {
48
64
  export function getHostCacheDirOverride() {
49
65
  return hostCacheDirOverride;
50
66
  }
51
- const HOST_CACHE_DIRS = ["mimocode", "kilo", "opencode"];
52
- /**
53
- * Host cache-dir name inferred from the running binary (`argv[0]` /
54
- * `process.execPath`). A source-checkout provider can't be located by install
55
- * path, but the binary that loaded it is authoritative: when this process IS
56
- * `mimo` / `kilocode`, the cache root is that host's, not OpenCode's. Mirrors
57
- * OCP `detect()` binaryHint semantics and tolerates the leading-dot Kilo binary
58
- * (`.kilo`). Only `argv[0]` is inspected — later argv entries are arguments,
59
- * not binary identity, and could otherwise cause false positives (e.g. an
60
- * `--opencode-config=…` flag under a fork).
61
- */
62
- export function hostCacheDirFromProcess(argv = process.argv, execPath = process.execPath) {
63
- for (const raw of [argv[0], execPath]) {
64
- if (!raw)
65
- continue;
66
- const name = path.basename(String(raw)).toLowerCase().replace(/^\.+/, "");
67
- if (name === "mimo" ||
68
- name === "mimocode" ||
69
- name.startsWith("mimo-") ||
70
- name.includes("mimocode")) {
71
- return "mimocode";
72
- }
73
- if (name === "kilo" ||
74
- name === "kilocode" ||
75
- name.startsWith("kilo-") ||
76
- name.includes("kilocode")) {
77
- return "kilo";
78
- }
79
- if (name === "opencode" ||
80
- name.startsWith("opencode-") ||
81
- name.includes("opencode")) {
82
- return "opencode";
83
- }
84
- }
85
- return undefined;
86
- }
87
- /**
88
- * Resolve the host cache directory without an override.
89
- *
90
- * Explicit host environment wins. Otherwise, an installed provider inherits
91
- * the host-named cache containing its module; a source checkout is attributed
92
- * by the running binary identity (see {@link hostCacheDirFromProcess}). Merely
93
- * having another host's config directory installed is not evidence that it
94
- * owns this process.
95
- */
96
- export function resolveHostCacheDir(env = process.env, moduleUrl = import.meta.url, processIdentity = {}) {
97
- const mimoHome = env.MIMOCODE_HOME;
98
- if (mimoHome && mimoHome.length > 0) {
99
- return path.join(mimoHome, "cache");
100
- }
101
- const cacheHome = xdgCacheHome(env);
102
- const kiloConfig = env.KILO_CONFIG_DIR;
103
- if (kiloConfig && kiloConfig.length > 0) {
104
- return path.join(cacheHome, "kilo");
105
- }
106
- let modulePath;
107
- try {
108
- modulePath = moduleUrl.startsWith("file:") ? fileURLToPath(moduleUrl) : path.resolve(moduleUrl);
109
- }
110
- catch {
111
- modulePath = undefined;
112
- }
113
- if (modulePath) {
114
- for (const host of HOST_CACHE_DIRS) {
115
- const root = path.resolve(cacheHome, host);
116
- if (modulePath === root || modulePath.startsWith(`${root}${path.sep}`))
117
- return root;
118
- }
119
- }
120
- // Source checkout: the module path can't name the host, but the binary that
121
- // loaded us can (running as `mimo` / `kilocode` / `opencode`).
122
- const host = hostCacheDirFromProcess(processIdentity.argv ?? process.argv, processIdentity.execPath ?? process.execPath);
123
- if (host)
124
- return path.join(cacheHome, host);
125
- return path.join(cacheHome, "opencode");
126
- }
127
- /**
128
- * Best-effort: if `@opencode-compat/profile` is installed, adopt `detect().profile.paths.cacheDir`
129
- * when the host is supported. No-op when OCP is absent or detection fails.
130
- */
131
- export async function adoptCompatHostCacheDir(detector) {
132
- if (hostCacheDirOverride)
133
- return hostCacheDirOverride;
134
- try {
135
- const detect = detector ?? (await import("@opencode-compat/profile")).detect;
136
- const result = detect();
137
- if (!result.supported || result.id === "unknown")
138
- return undefined;
139
- if (!result.source || !["env", "binary", "package"].includes(result.source)) {
140
- trace(`host-cache: ignored weak OCP detect host=${result.id} source=${result.source ?? "unknown"}`);
141
- return undefined;
142
- }
143
- const cacheDir = result.profile.paths.cacheDir;
144
- if (!cacheDir || cacheDir.length === 0)
145
- return undefined;
146
- setHostCacheDirOverride(cacheDir);
147
- trace(`host-cache: adopted OCP detect cacheDir=${cacheDir} host=${result.id}`);
148
- return cacheDir;
149
- }
150
- catch {
151
- return undefined;
152
- }
67
+ /** Resolve the native OpenCode cache root when no host bridge is installed. */
68
+ export function resolveHostCacheDir(env = process.env) {
69
+ return bridgeGlobalCacheDir() ?? openCodeGlobalCacheDir(env);
153
70
  }
154
- /** OpenCode / host global config dir (`~/.config/<app>`). Still OpenCode-named for rule discovery. */
71
+ /** Native OpenCode global config dir. */
155
72
  export function opencodeGlobalConfigDir() {
156
73
  return path.join(resolveHome(), ".config", "opencode");
157
74
  }
@@ -160,24 +77,43 @@ export function opencodeGlobalConfigDir() {
160
77
  *
161
78
  * Precedence:
162
79
  * 1. {@link setHostCacheDirOverride} / `createCursor({ cacheDir })` (host `Path.cache`)
163
- * 2. Strong OCP `detect()` identity when {@link adoptCompatHostCacheDir} ran successfully
164
- * 3. Explicit host environment / provider install path ({@link resolveHostCacheDir})
80
+ * 2. An injected structural host path bridge
81
+ * 3. Native OpenCode XDG defaults ({@link resolveHostCacheDir})
165
82
  */
166
83
  export function opencodeGlobalCacheDir() {
167
84
  if (hostCacheDirOverride)
168
85
  return hostCacheDirOverride;
169
86
  return resolveHostCacheDir();
170
87
  }
88
+ /** Native OpenCode global data root. */
89
+ export function opencodeGlobalDataDir(env = process.env) {
90
+ return openCodeGlobalDataDir(env);
91
+ }
92
+ /** Host-portable durable data root; falls back to native OpenCode. */
93
+ export function hostGlobalDataDir(env = process.env) {
94
+ return bridgeGlobalDataDir() ?? openCodeGlobalDataDir(env);
95
+ }
171
96
  /**
172
- * OpenCode global data dir (`~/.local/share/opencode`).
173
- * Uses `$XDG_DATA_HOME/opencode` when set, otherwise `$HOME/.local/share/opencode`.
174
- * Auth credentials live here in `auth.json`.
97
+ * Directory for host plan files — always `<hostGlobalDataDir()>/plans`, and
98
+ * never inside the user's repository.
99
+ *
100
+ * OpenCode's own `Session.plan` branches on VCS and puts plans in the worktree
101
+ * (`<worktree>/.opencode/plans`) for a git project. The provider deliberately
102
+ * does *not* mirror that branch. Writing there means a throwaway plan lands in
103
+ * the user's tree untracked-but-unignored, and — because OpenCode installs
104
+ * `@opencode-ai/plugin` into every `.opencode` directory it discovers walking up
105
+ * from the cwd — creating that directory also bootstraps a project-local
106
+ * `node_modules`. The provider must add nothing to a repository it did not
107
+ * already contain.
108
+ *
109
+ * The global-data location is not a degraded fallback: it is the branch OpenCode
110
+ * itself uses when there is no VCS, and its plan agent allow-lists that path for
111
+ * `edit` / `external_directory` alongside the in-worktree one. {@link
112
+ * hostGlobalDataDir} carries an optional injected host translation; without a
113
+ * bridge it is the native OpenCode data root.
175
114
  */
176
- export function opencodeGlobalDataDir() {
177
- if (process.env.XDG_DATA_HOME) {
178
- return path.join(process.env.XDG_DATA_HOME, "opencode");
179
- }
180
- return path.join(resolveHome(), ".local", "share", "opencode");
115
+ export function hostPlansDir(_workspaceRoot) {
116
+ return path.join(hostGlobalDataDir(), "plans");
181
117
  }
182
118
  /**
183
119
  * Cursor-compatible path slug (`/Users/a/b` → `Users-a-b`).
@@ -193,7 +129,7 @@ export function slugifyWorkspacePath(workspaceRoot) {
193
129
  }
194
130
  /**
195
131
  * Cursor-style project metadata root for a workspace.
196
- * Lives at `<host-cache>/projects/<slug>/` (OpenCode / MiMo / Kilo cache root).
132
+ * Lives at `<host-cache>/projects/<slug>/` under the resolved OpenCode/host cache root.
197
133
  *
198
134
  * This is what Cursor's RequestContextEnv.project_folder / MCP
199
135
  * workspace_project_dir point at — agent-tools, terminals, transcripts, etc.
@@ -9,6 +9,7 @@ export type OpencodeJson = {
9
9
  plugins?: string[];
10
10
  mcp?: Record<string, unknown>;
11
11
  };
12
+ export declare function findGitWorktree(start: string): Promise<string>;
12
13
  /** Same truthy rule as OpenCode's Flag.OPENCODE_DISABLE_PROJECT_CONFIG. */
13
14
  export declare function isProjectConfigDisabled(): boolean;
14
15
  /** Fetch a remote instruction with one deadline covering headers and body. */
@@ -27,7 +27,7 @@ async function readJsonConfig(dir) {
27
27
  }
28
28
  return {};
29
29
  }
30
- async function findGitWorktree(start) {
30
+ export async function findGitWorktree(start) {
31
31
  let dir = path.resolve(start);
32
32
  for (;;) {
33
33
  if (await exists(path.join(dir, ".git")))
@@ -167,7 +167,7 @@ export async function loadMergedConfig(workspaceRoot) {
167
167
  if (isProjectConfigDisabled())
168
168
  return mergeConfig({}, globalConfig);
169
169
  // The bridge supplies native project config roots for an unchanged plugin:
170
- // .opencode on OpenCode, .mimocode on MiMo, and .kilo/.kilocode on Kilo.
170
+ // The active host's project config directories are supplied by the path bridge; OpenCode defaults to .opencode.
171
171
  // Later roots have higher precedence, matching the host's native ordering.
172
172
  let projectConfig = await readJsonConfig(workspaceRoot);
173
173
  for (const configDir of opencodeProjectConfigDirs(workspaceRoot)) {
@@ -0,0 +1,19 @@
1
+ import { executeCursorImageSave } from "./image-save.js";
2
+ /** Build the classic OpenCode image-save tool with the host's own Zod helper. */
3
+ export declare function createCursorImageSaveTool(factory: {
4
+ tool: (input: Record<string, unknown>) => Record<string, unknown>;
5
+ schema: {
6
+ string: () => any;
7
+ };
8
+ }): Record<string, unknown>;
9
+ /** Host-neutral JSON-schema fallback for hosts with no classic helper. */
10
+ export declare const cursorImageSaveTool: {
11
+ description: string;
12
+ args: {
13
+ image_id: {
14
+ type: string;
15
+ description: string;
16
+ };
17
+ };
18
+ execute: typeof executeCursorImageSave;
19
+ };
@@ -0,0 +1,25 @@
1
+ import { executeCursorImageSave } from "./image-save.js";
2
+ /** Build the classic OpenCode image-save tool with the host's own Zod helper. */
3
+ export function createCursorImageSaveTool(factory) {
4
+ return factory.tool({
5
+ description: "Save an image that Cursor generated during this session to its target path. "
6
+ + "Takes only the id of an already-generated image — it cannot write arbitrary "
7
+ + "files, and it is not a general-purpose file writer. You do not normally call "
8
+ + "this: the Cursor provider issues it after an image is generated.",
9
+ args: {
10
+ image_id: factory.schema.string().describe("Id of the pending Cursor-generated image to save"),
11
+ },
12
+ execute: executeCursorImageSave,
13
+ });
14
+ }
15
+ /** Host-neutral JSON-schema fallback for hosts with no classic helper. */
16
+ export const cursorImageSaveTool = {
17
+ description: "Save an image that Cursor generated during this session to its target path. "
18
+ + "Takes only the id of an already-generated image — it cannot write arbitrary "
19
+ + "files, and it is not a general-purpose file writer. You do not normally call "
20
+ + "this: the Cursor provider issues it after an image is generated.",
21
+ args: {
22
+ image_id: { type: "string", description: "Id of the pending Cursor-generated image to save" },
23
+ },
24
+ execute: executeCursorImageSave,
25
+ };
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Commit a staged Cursor image to disk, gated by OpenCode's own permission.
3
+ *
4
+ * Host-neutral on purpose: `./image-save-tool.ts` performs the `tool()` value
5
+ * import from `@opencode-ai/plugin`, which the OpenCode 2.0 entrypoint must not
6
+ * pull in (see the note in `./web-search-tool.ts`). Only that file may import
7
+ * the plugin package; this module is safe on every graph.
8
+ */
9
+ /**
10
+ * Marks a refusal that must reach Cursor as `WriteResult.permission_denied`
11
+ * rather than a generic error — the variant its own executor returns and its
12
+ * agent branches on. Both sides of this string are ours, so matching on it is
13
+ * a contract, not a guess at someone else's message.
14
+ */
15
+ export declare const IMAGE_PERMISSION_DENIED_PREFIX = "CursorImagePermissionDenied:";
16
+ /** The subset of OpenCode's plugin ToolContext this needs. */
17
+ export type ImageSaveToolContext = {
18
+ worktree: string;
19
+ directory: string;
20
+ ask(input: {
21
+ permission: string;
22
+ patterns: string[];
23
+ always: string[];
24
+ metadata: Record<string, unknown>;
25
+ }): Promise<void>;
26
+ };
27
+ export type ImageSaveResult = {
28
+ title: string;
29
+ output: string;
30
+ attachments?: Array<{
31
+ type: "file";
32
+ mime: string;
33
+ url: string;
34
+ filename?: string;
35
+ }>;
36
+ };
37
+ /**
38
+ * Prove the staged target still resolves inside one of the roots it was mapped
39
+ * into. `remapCursorImageWritePath` already chose the location; this re-checks
40
+ * it against the *real* paths, because a symlink planted between mapping and
41
+ * commit could otherwise redirect the write. Mirrors the containment rule the
42
+ * correlated-edit read applies before answering with complete file content.
43
+ */
44
+ export declare function resolveContainedImagePath(target: string, allowedRoots: readonly string[]): {
45
+ path: string;
46
+ relative: string;
47
+ root: string;
48
+ } | {
49
+ error: string;
50
+ };
51
+ /**
52
+ * Execute the commit. `image_id` is the only input: the path and bytes come
53
+ * from this process's staging table, so a caller without a real handle — the
54
+ * model of any provider, replaying a used id, or guessing — writes nothing.
55
+ */
56
+ export declare function executeCursorImageSave(args: {
57
+ image_id?: unknown;
58
+ }, ctx: ImageSaveToolContext): Promise<ImageSaveResult | string>;