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.
- package/README.md +33 -23
- package/dist/context/agents.d.ts +1 -1
- package/dist/context/agents.js +1 -1
- package/dist/context/auth-store.js +2 -2
- package/dist/context/build.d.ts +11 -1
- package/dist/context/build.js +76 -25
- package/dist/context/frozen.d.ts +29 -0
- package/dist/context/frozen.js +168 -0
- package/dist/context/index.d.ts +3 -2
- package/dist/context/index.js +3 -2
- package/dist/context/paths.d.ts +34 -53
- package/dist/context/paths.js +54 -118
- package/dist/context/rules.d.ts +1 -0
- package/dist/context/rules.js +2 -2
- package/dist/image-save-tool.d.ts +19 -0
- package/dist/image-save-tool.js +25 -0
- package/dist/image-save.d.ts +58 -0
- package/dist/image-save.js +189 -0
- package/dist/image-staging.d.ts +59 -0
- package/dist/image-staging.js +91 -0
- package/dist/index.d.ts +1 -1
- package/dist/language-model.d.ts +13 -11
- package/dist/language-model.js +957 -150
- package/dist/plan-execution-kickoff.d.ts +55 -0
- package/dist/plan-execution-kickoff.js +162 -0
- package/dist/plugin-opencode2.js +52 -3
- package/dist/plugin-v2.d.ts +11 -9
- package/dist/plugin-v2.js +6 -13
- package/dist/plugin.d.ts +9 -0
- package/dist/plugin.js +61 -5
- package/dist/pricing-data.d.ts +16 -4
- package/dist/pricing-data.js +16 -4
- package/dist/protocol/ask-question.d.ts +133 -0
- package/dist/protocol/ask-question.js +320 -0
- package/dist/protocol/blob-reachability.d.ts +15 -0
- package/dist/protocol/blob-reachability.js +140 -0
- package/dist/protocol/blob-store.d.ts +37 -0
- package/dist/protocol/blob-store.js +115 -0
- package/dist/protocol/conversation-bind.d.ts +13 -0
- package/dist/protocol/conversation-bind.js +27 -1
- package/dist/protocol/conversation-persistence.d.ts +32 -0
- package/dist/protocol/conversation-persistence.js +504 -0
- package/dist/protocol/conversation-state.d.ts +16 -0
- package/dist/protocol/conversation-state.js +62 -0
- package/dist/protocol/create-plan.d.ts +141 -0
- package/dist/protocol/create-plan.js +373 -0
- package/dist/protocol/generate-image.d.ts +54 -0
- package/dist/protocol/generate-image.js +101 -0
- package/dist/protocol/interactions.d.ts +110 -8
- package/dist/protocol/interactions.js +295 -35
- package/dist/protocol/kv.d.ts +1 -0
- package/dist/protocol/kv.js +2 -0
- package/dist/protocol/messages.d.ts +2 -0
- package/dist/protocol/messages.js +148 -8
- package/dist/protocol/request.d.ts +2 -0
- package/dist/protocol/request.js +2 -0
- package/dist/protocol/switch-mode.d.ts +178 -0
- package/dist/protocol/switch-mode.js +417 -0
- package/dist/protocol/token-details.d.ts +33 -0
- package/dist/protocol/token-details.js +155 -0
- package/dist/protocol/tool-call-bridge.js +10 -5
- package/dist/protocol/tools.d.ts +44 -11
- package/dist/protocol/tools.js +171 -41
- package/dist/session.d.ts +39 -11
- package/dist/shared.d.ts +3 -0
- package/dist/shared.js +3 -0
- package/dist/usage.d.ts +42 -37
- package/dist/usage.js +201 -70
- package/dist/web-search-tool.d.ts +22 -26
- package/dist/web-search-tool.js +25 -15
- 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
|
+
}
|
package/dist/context/index.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export { buildRequestContext, type BuildRequestContextInput } from "./build.js";
|
|
2
|
-
export {
|
|
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";
|
package/dist/context/index.js
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export { buildRequestContext } from "./build.js";
|
|
2
|
-
export {
|
|
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";
|
package/dist/context/paths.d.ts
CHANGED
|
@@ -1,79 +1,61 @@
|
|
|
1
1
|
export type HostPathEnv = NodeJS.ProcessEnv;
|
|
2
|
-
/**
|
|
3
|
-
export declare const
|
|
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
|
-
|
|
30
|
-
export
|
|
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.
|
|
68
|
-
* 3.
|
|
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
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
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
|
|
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>/`
|
|
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 {};
|
package/dist/context/paths.js
CHANGED
|
@@ -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
|
-
/**
|
|
8
|
-
export const
|
|
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[
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
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.
|
|
164
|
-
* 3.
|
|
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
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
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
|
|
177
|
-
|
|
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>/`
|
|
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.
|
package/dist/context/rules.d.ts
CHANGED
|
@@ -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. */
|
package/dist/context/rules.js
CHANGED
|
@@ -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
|
-
//
|
|
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>;
|