cursor-opencode-provider 0.7.6 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +79 -0
- package/README.md +36 -15
- package/SECURITY.md +43 -7
- package/dist/agent-url.js +2 -2
- package/dist/auth-renewal.d.ts +85 -0
- package/dist/auth-renewal.js +284 -0
- package/dist/auth.d.ts +38 -22
- package/dist/auth.js +79 -96
- package/dist/classic-tools.d.ts +15 -0
- package/dist/classic-tools.js +45 -0
- package/dist/context/build.d.ts +62 -7
- package/dist/context/build.js +127 -95
- package/dist/context/dynamic-catalog.d.ts +2 -58
- package/dist/context/dynamic-catalog.js +16 -161
- package/dist/context/epoch.d.ts +6 -4
- package/dist/context/epoch.js +22 -10
- package/dist/context/frozen.js +42 -15
- package/dist/context/index.d.ts +3 -1
- package/dist/context/index.js +3 -1
- package/dist/context/overlay.d.ts +2 -23
- package/dist/context/overlay.js +6 -30
- package/dist/context/paths.d.ts +51 -21
- package/dist/context/paths.js +78 -27
- package/dist/context/rules.d.ts +3 -15
- package/dist/context/rules.js +10 -186
- package/dist/context/skills-bridge.d.ts +22 -0
- package/dist/context/skills-bridge.js +20 -0
- package/dist/context/skills.d.ts +31 -8
- package/dist/context/skills.js +187 -86
- package/dist/debug.d.ts +2 -0
- package/dist/debug.js +4 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.js +27 -3
- package/dist/host-agent-mode.d.ts +53 -4
- package/dist/host-agent-mode.js +119 -15
- package/dist/host-plan-file.d.ts +28 -0
- package/dist/host-plan-file.js +76 -0
- package/dist/image-input.d.ts +18 -0
- package/dist/image-input.js +119 -28
- package/dist/image-save.d.ts +10 -2
- package/dist/image-save.js +35 -23
- package/dist/index.d.ts +10 -0
- package/dist/language-model.d.ts +86 -25
- package/dist/language-model.js +1295 -369
- package/dist/model-config.d.ts +9 -0
- package/dist/model-config.js +28 -1
- package/dist/opencode2/catalog.d.ts +1 -0
- package/dist/opencode2/catalog.js +2 -0
- package/dist/opencode2/image-save-tool.d.ts +8 -0
- package/dist/opencode2/image-save-tool.js +84 -0
- package/dist/opencode2/integration.d.ts +30 -7
- package/dist/opencode2/integration.js +75 -46
- package/dist/opencode2/types.d.ts +15 -0
- package/dist/parallel-step.d.ts +65 -0
- package/dist/parallel-step.js +102 -0
- package/dist/plugin-opencode2.js +58 -56
- package/dist/plugin.d.ts +0 -10
- package/dist/plugin.js +207 -127
- package/dist/pricing-data.d.ts +14 -1
- package/dist/pricing-data.js +14 -1
- package/dist/protocol/ask-question.d.ts +12 -1
- package/dist/protocol/ask-question.js +77 -3
- package/dist/protocol/blob-store.js +3 -2
- package/dist/protocol/conversation-bind.js +1 -1
- package/dist/protocol/conversation-persistence.d.ts +2 -0
- package/dist/protocol/conversation-persistence.js +11 -1
- package/dist/protocol/conversation-state.d.ts +2 -0
- package/dist/protocol/conversation-state.js +19 -0
- package/dist/protocol/create-plan.d.ts +47 -25
- package/dist/protocol/create-plan.js +35 -24
- package/dist/protocol/interactions.d.ts +26 -17
- package/dist/protocol/interactions.js +80 -31
- package/dist/protocol/messages.js +96 -18
- package/dist/protocol/request.d.ts +16 -9
- package/dist/protocol/request.js +17 -25
- package/dist/protocol/switch-mode.d.ts +66 -27
- package/dist/protocol/switch-mode.js +147 -73
- package/dist/protocol/token-details.d.ts +2 -0
- package/dist/protocol/token-details.js +11 -1
- package/dist/protocol/tool-call-bridge.d.ts +4 -0
- package/dist/protocol/tool-call-bridge.js +14 -0
- package/dist/protocol/tools.d.ts +33 -11
- package/dist/protocol/tools.js +165 -103
- package/dist/protocol/turn-provenance.d.ts +52 -0
- package/dist/protocol/turn-provenance.js +174 -0
- package/dist/replay-safety.d.ts +6 -0
- package/dist/replay-safety.js +56 -6
- package/dist/session.d.ts +44 -2
- package/dist/session.js +45 -1
- package/dist/shared.d.ts +7 -0
- package/dist/shared.js +7 -0
- package/dist/todo-store.d.ts +2 -0
- package/dist/todo-store.js +19 -0
- package/dist/transport/connect.d.ts +2 -0
- package/dist/transport/connect.js +23 -6
- package/dist/usage.js +23 -6
- package/package.json +3 -2
- package/dist/context/agents.d.ts +0 -12
- package/dist/context/agents.js +0 -136
- package/dist/plan-execution-kickoff.d.ts +0 -57
- package/dist/plan-execution-kickoff.js +0 -186
package/dist/context/paths.d.ts
CHANGED
|
@@ -9,7 +9,31 @@ export type OpenCodePathBridge = {
|
|
|
9
9
|
/** Optional host-owned cache root; absent means native OpenCode defaults. */
|
|
10
10
|
globalCacheDir?: () => string;
|
|
11
11
|
configFileNames?: string[];
|
|
12
|
+
/**
|
|
13
|
+
* Optional host plan file for a session (the host's own plan location, which
|
|
14
|
+
* its `plan_exit` review reads). Absent on an installed bridge means the host
|
|
15
|
+
* defines none; without a bridge OpenCode's own location applies.
|
|
16
|
+
*/
|
|
17
|
+
planFile?: (input: {
|
|
18
|
+
worktree: string;
|
|
19
|
+
vcs: boolean;
|
|
20
|
+
created: number;
|
|
21
|
+
slug: string;
|
|
22
|
+
}) => string | undefined;
|
|
12
23
|
};
|
|
24
|
+
/**
|
|
25
|
+
* The plan file of a session: OpenCode's own `Session.plan` location
|
|
26
|
+
* (`<worktree>/.opencode/plans` in a VCS project, else `<data>/plans`, named
|
|
27
|
+
* `<created>-<slug>.md`), which its plan agent and `plan_exit` use. An injected
|
|
28
|
+
* host path bridge owns host paths, so with one installed only its `planFile`
|
|
29
|
+
* defines the location.
|
|
30
|
+
*/
|
|
31
|
+
export declare function hostPlanFilePath(input: {
|
|
32
|
+
worktree: string;
|
|
33
|
+
vcs: boolean;
|
|
34
|
+
created: number;
|
|
35
|
+
slug: string;
|
|
36
|
+
}, env?: HostPathEnv): string | undefined;
|
|
13
37
|
export declare function opencodeProjectConfigDirs(workspaceRoot: string): string[];
|
|
14
38
|
export declare function opencodeGlobalConfigDirs(): string[];
|
|
15
39
|
export declare function opencodeConfigFileNames(): string[];
|
|
@@ -21,8 +45,12 @@ export declare function setHostCacheDirOverride(dir: string | undefined): void;
|
|
|
21
45
|
export declare function getHostCacheDirOverride(): string | undefined;
|
|
22
46
|
/** Resolve the native OpenCode cache root when no host bridge is installed. */
|
|
23
47
|
export declare function resolveHostCacheDir(env?: HostPathEnv): string;
|
|
24
|
-
/**
|
|
25
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Native OpenCode global config dir: `$XDG_CONFIG_HOME/opencode`, otherwise
|
|
50
|
+
* `~/.config/opencode` — OpenCode's `Global.Path.config` (`xdg-basedir`
|
|
51
|
+
* `xdgConfig`, `packages/core/src/global.ts`), the same in 1.x and 2.0.
|
|
52
|
+
*/
|
|
53
|
+
export declare function opencodeGlobalConfigDir(env?: HostPathEnv): string;
|
|
26
54
|
/**
|
|
27
55
|
* Host global cache dir for Cursor project metadata + model/version caches.
|
|
28
56
|
*
|
|
@@ -37,27 +65,30 @@ export declare function opencodeGlobalDataDir(env?: HostPathEnv): string;
|
|
|
37
65
|
/** Host-portable durable data root; falls back to native OpenCode. */
|
|
38
66
|
export declare function hostGlobalDataDir(env?: HostPathEnv): string;
|
|
39
67
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
|
|
68
|
+
* Select the entrypoint's native OpenCode plan directory. OpenCode 2.0 sets its
|
|
69
|
+
* Plan directory ({@link opencode2PlanDir}); OpenCode 1.x keeps the default.
|
|
70
|
+
* The returned disposer removes only this selection, so an older plugin setup
|
|
71
|
+
* disposed after a newer one leaves the newer selection in place. `undefined`
|
|
72
|
+
* removes all.
|
|
73
|
+
*/
|
|
74
|
+
export declare function setNativePlansDir(dir: string | undefined): () => void;
|
|
75
|
+
/**
|
|
76
|
+
* OpenCode 2.0's Plan directory: `<home>/.opencode/plan`, where its Plan agent
|
|
77
|
+
* may write plan files (home is `OPENCODE_TEST_HOME`, else the OS home).
|
|
78
|
+
*/
|
|
79
|
+
export declare function opencode2PlanDir(env?: HostPathEnv): string;
|
|
80
|
+
/**
|
|
81
|
+
* Directory for a new plan file when the session's own plan file
|
|
82
|
+
* ({@link hostPlanFilePath}) is not known.
|
|
51
83
|
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* bridge it is the native OpenCode data root.
|
|
84
|
+
* An injected host path bridge owns host paths: `<globalDataDir()>/plans`.
|
|
85
|
+
* Otherwise the native OpenCode location of the running entrypoint: OpenCode
|
|
86
|
+
* 2.0's Plan directory, or OpenCode 1.x's no-VCS `Session.plan` base
|
|
87
|
+
* (`<data>/plans`).
|
|
57
88
|
*/
|
|
58
|
-
export declare function hostPlansDir(_workspaceRoot?: string): string;
|
|
89
|
+
export declare function hostPlansDir(_workspaceRoot?: string, env?: HostPathEnv): string;
|
|
59
90
|
/**
|
|
60
|
-
* Cursor-compatible path slug (`/
|
|
91
|
+
* Cursor-compatible path slug (`/workspace/a/b` → `workspace-a-b`).
|
|
61
92
|
* Used for per-workspace metadata under the host cache.
|
|
62
93
|
*/
|
|
63
94
|
export declare function slugifyWorkspacePath(workspaceRoot: string): string;
|
|
@@ -72,4 +103,3 @@ export declare function slugifyWorkspacePath(workspaceRoot: string): string;
|
|
|
72
103
|
export declare function opencodeProjectDir(workspaceRoot: string): string;
|
|
73
104
|
/** Ensure {@link opencodeProjectDir} exists (mode 0o700) and return it. */
|
|
74
105
|
export declare function ensureOpencodeProjectDir(workspaceRoot: string): string;
|
|
75
|
-
export declare function resolveHomeRelative(p: string): string;
|
package/dist/context/paths.js
CHANGED
|
@@ -27,6 +27,31 @@ function bridgeGlobalDataDir() {
|
|
|
27
27
|
const value = pathBridge()?.globalDataDir?.();
|
|
28
28
|
return typeof value === "string" && value.length > 0 ? path.resolve(value) : undefined;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* The plan file of a session: OpenCode's own `Session.plan` location
|
|
32
|
+
* (`<worktree>/.opencode/plans` in a VCS project, else `<data>/plans`, named
|
|
33
|
+
* `<created>-<slug>.md`), which its plan agent and `plan_exit` use. An injected
|
|
34
|
+
* host path bridge owns host paths, so with one installed only its `planFile`
|
|
35
|
+
* defines the location.
|
|
36
|
+
*/
|
|
37
|
+
export function hostPlanFilePath(input, env = process.env) {
|
|
38
|
+
const bridge = pathBridge();
|
|
39
|
+
if (bridge) {
|
|
40
|
+
if (typeof bridge.planFile !== "function")
|
|
41
|
+
return undefined;
|
|
42
|
+
const value = bridge.planFile(input);
|
|
43
|
+
return typeof value === "string" && value.length > 0 ? path.resolve(value) : undefined;
|
|
44
|
+
}
|
|
45
|
+
const slug = input.slug.trim();
|
|
46
|
+
if (!slug || slug.includes("/") || slug.includes("\\") || slug.startsWith("."))
|
|
47
|
+
return undefined;
|
|
48
|
+
if (!Number.isSafeInteger(input.created) || input.created <= 0)
|
|
49
|
+
return undefined;
|
|
50
|
+
const base = input.vcs
|
|
51
|
+
? path.join(path.resolve(input.worktree), ".opencode", "plans")
|
|
52
|
+
: path.join(openCodeGlobalDataDir(env), "plans");
|
|
53
|
+
return path.join(base, `${input.created}-${slug}.md`);
|
|
54
|
+
}
|
|
30
55
|
function bridgeGlobalCacheDir() {
|
|
31
56
|
const value = pathBridge()?.globalCacheDir?.();
|
|
32
57
|
return typeof value === "string" && value.length > 0 ? path.resolve(value) : undefined;
|
|
@@ -68,9 +93,18 @@ export function getHostCacheDirOverride() {
|
|
|
68
93
|
export function resolveHostCacheDir(env = process.env) {
|
|
69
94
|
return bridgeGlobalCacheDir() ?? openCodeGlobalCacheDir(env);
|
|
70
95
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
96
|
+
function xdgConfigHome(env = process.env) {
|
|
97
|
+
if (env.XDG_CONFIG_HOME && env.XDG_CONFIG_HOME.length > 0)
|
|
98
|
+
return env.XDG_CONFIG_HOME;
|
|
99
|
+
return path.join(resolveHome(env), ".config");
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Native OpenCode global config dir: `$XDG_CONFIG_HOME/opencode`, otherwise
|
|
103
|
+
* `~/.config/opencode` — OpenCode's `Global.Path.config` (`xdg-basedir`
|
|
104
|
+
* `xdgConfig`, `packages/core/src/global.ts`), the same in 1.x and 2.0.
|
|
105
|
+
*/
|
|
106
|
+
export function opencodeGlobalConfigDir(env = process.env) {
|
|
107
|
+
return path.join(xdgConfigHome(env), "opencode");
|
|
74
108
|
}
|
|
75
109
|
/**
|
|
76
110
|
* Host global cache dir for Cursor project metadata + model/version caches.
|
|
@@ -93,30 +127,52 @@ export function opencodeGlobalDataDir(env = process.env) {
|
|
|
93
127
|
export function hostGlobalDataDir(env = process.env) {
|
|
94
128
|
return bridgeGlobalDataDir() ?? openCodeGlobalDataDir(env);
|
|
95
129
|
}
|
|
130
|
+
/** Entrypoint plan directories, newest last (see {@link setNativePlansDir}). */
|
|
131
|
+
const nativePlansDirs = [];
|
|
96
132
|
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
133
|
+
* Select the entrypoint's native OpenCode plan directory. OpenCode 2.0 sets its
|
|
134
|
+
* Plan directory ({@link opencode2PlanDir}); OpenCode 1.x keeps the default.
|
|
135
|
+
* The returned disposer removes only this selection, so an older plugin setup
|
|
136
|
+
* disposed after a newer one leaves the newer selection in place. `undefined`
|
|
137
|
+
* removes all.
|
|
138
|
+
*/
|
|
139
|
+
export function setNativePlansDir(dir) {
|
|
140
|
+
if (!dir) {
|
|
141
|
+
nativePlansDirs.length = 0;
|
|
142
|
+
return () => { };
|
|
143
|
+
}
|
|
144
|
+
const entry = { dir: path.resolve(dir) };
|
|
145
|
+
nativePlansDirs.push(entry);
|
|
146
|
+
return () => {
|
|
147
|
+
const index = nativePlansDirs.lastIndexOf(entry);
|
|
148
|
+
if (index >= 0)
|
|
149
|
+
nativePlansDirs.splice(index, 1);
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* OpenCode 2.0's Plan directory: `<home>/.opencode/plan`, where its Plan agent
|
|
154
|
+
* may write plan files (home is `OPENCODE_TEST_HOME`, else the OS home).
|
|
155
|
+
*/
|
|
156
|
+
export function opencode2PlanDir(env = process.env) {
|
|
157
|
+
return path.join(env.OPENCODE_TEST_HOME ?? homedir(), ".opencode", "plan");
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Directory for a new plan file when the session's own plan file
|
|
161
|
+
* ({@link hostPlanFilePath}) is not known.
|
|
108
162
|
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
113
|
-
* bridge it is the native OpenCode data root.
|
|
163
|
+
* An injected host path bridge owns host paths: `<globalDataDir()>/plans`.
|
|
164
|
+
* Otherwise the native OpenCode location of the running entrypoint: OpenCode
|
|
165
|
+
* 2.0's Plan directory, or OpenCode 1.x's no-VCS `Session.plan` base
|
|
166
|
+
* (`<data>/plans`).
|
|
114
167
|
*/
|
|
115
|
-
export function hostPlansDir(_workspaceRoot) {
|
|
116
|
-
|
|
168
|
+
export function hostPlansDir(_workspaceRoot, env = process.env) {
|
|
169
|
+
const bridged = bridgeGlobalDataDir();
|
|
170
|
+
if (bridged)
|
|
171
|
+
return path.join(bridged, "plans");
|
|
172
|
+
return nativePlansDirs[nativePlansDirs.length - 1]?.dir ?? path.join(openCodeGlobalDataDir(env), "plans");
|
|
117
173
|
}
|
|
118
174
|
/**
|
|
119
|
-
* Cursor-compatible path slug (`/
|
|
175
|
+
* Cursor-compatible path slug (`/workspace/a/b` → `workspace-a-b`).
|
|
120
176
|
* Used for per-workspace metadata under the host cache.
|
|
121
177
|
*/
|
|
122
178
|
export function slugifyWorkspacePath(workspaceRoot) {
|
|
@@ -157,8 +213,3 @@ export function ensureOpencodeProjectDir(workspaceRoot) {
|
|
|
157
213
|
`xdg_cache_home=${process.env.XDG_CACHE_HOME ?? "(unset)"}`);
|
|
158
214
|
return dir;
|
|
159
215
|
}
|
|
160
|
-
export function resolveHomeRelative(p) {
|
|
161
|
-
if (p.startsWith("~/"))
|
|
162
|
-
return path.join(homedir(), p.slice(2));
|
|
163
|
-
return p;
|
|
164
|
-
}
|
package/dist/context/rules.d.ts
CHANGED
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
export type CollectedRule = {
|
|
2
|
-
fullPath: string;
|
|
3
|
-
content: string;
|
|
4
|
-
};
|
|
5
1
|
export type OpencodeJson = {
|
|
6
2
|
instructions?: string[];
|
|
7
3
|
permission?: unknown;
|
|
@@ -9,18 +5,10 @@ export type OpencodeJson = {
|
|
|
9
5
|
plugins?: string[];
|
|
10
6
|
mcp?: Record<string, unknown>;
|
|
11
7
|
};
|
|
12
|
-
export declare function findGitWorktree(start: string): Promise<string>;
|
|
13
8
|
/** Same truthy rule as OpenCode's Flag.OPENCODE_DISABLE_PROJECT_CONFIG. */
|
|
14
9
|
export declare function isProjectConfigDisabled(): boolean;
|
|
15
|
-
/** Fetch a remote instruction with one deadline covering headers and body. */
|
|
16
|
-
export declare function fetchRemoteInstruction(url: string, timeoutMs?: number): Promise<string | undefined>;
|
|
17
|
-
export declare function loadMergedConfig(workspaceRoot: string): Promise<OpencodeJson>;
|
|
18
10
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
11
|
+
* Merged `opencode.json` / `opencode.jsonc` for MCP server ids, plugin lists,
|
|
12
|
+
* and interaction guidance. Instruction file bodies are not collected here.
|
|
21
13
|
*/
|
|
22
|
-
export declare function
|
|
23
|
-
rules: CollectedRule[];
|
|
24
|
-
config: OpencodeJson;
|
|
25
|
-
worktree: string;
|
|
26
|
-
}>;
|
|
14
|
+
export declare function loadMergedConfig(workspaceRoot: string): Promise<OpencodeJson>;
|
package/dist/context/rules.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { readFile,
|
|
2
|
-
import { homedir } from "node:os";
|
|
1
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
2
|
import path from "node:path";
|
|
4
|
-
import { opencodeConfigFileNames, opencodeGlobalConfigDirs, opencodeProjectConfigDirs
|
|
3
|
+
import { opencodeConfigFileNames, opencodeGlobalConfigDirs, opencodeProjectConfigDirs } from "./paths.js";
|
|
5
4
|
async function exists(file) {
|
|
6
5
|
try {
|
|
7
6
|
await stat(file);
|
|
@@ -18,7 +17,10 @@ async function readJsonConfig(dir) {
|
|
|
18
17
|
continue;
|
|
19
18
|
try {
|
|
20
19
|
const raw = await readFile(file, "utf-8");
|
|
21
|
-
|
|
20
|
+
// Match strings first so comment markers inside URLs/plugin specifiers
|
|
21
|
+
// survive. Remove comments before trailing commas (which may precede one).
|
|
22
|
+
const uncommented = raw.replace(/"(?:\\.|[^"\\])*"|\/\*[\s\S]*?\*\/|\/\/[^\r\n]*/g, token => token.startsWith('"') ? token : " ");
|
|
23
|
+
const stripped = uncommented.replace(/"(?:\\.|[^"\\])*"|,\s*(?=[}\]])/g, token => token.startsWith('"') ? token : "");
|
|
22
24
|
return JSON.parse(stripped);
|
|
23
25
|
}
|
|
24
26
|
catch {
|
|
@@ -27,131 +29,11 @@ async function readJsonConfig(dir) {
|
|
|
27
29
|
}
|
|
28
30
|
return {};
|
|
29
31
|
}
|
|
30
|
-
export async function findGitWorktree(start) {
|
|
31
|
-
let dir = path.resolve(start);
|
|
32
|
-
for (;;) {
|
|
33
|
-
if (await exists(path.join(dir, ".git")))
|
|
34
|
-
return dir;
|
|
35
|
-
const parent = path.dirname(dir);
|
|
36
|
-
if (parent === dir)
|
|
37
|
-
return path.resolve(start);
|
|
38
|
-
dir = parent;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
async function findUp(name, start, stop) {
|
|
42
|
-
let dir = path.resolve(start);
|
|
43
|
-
const root = path.resolve(stop);
|
|
44
|
-
for (;;) {
|
|
45
|
-
const candidate = path.join(dir, name);
|
|
46
|
-
if (await exists(candidate))
|
|
47
|
-
return candidate;
|
|
48
|
-
if (dir === root)
|
|
49
|
-
return undefined;
|
|
50
|
-
const parent = path.dirname(dir);
|
|
51
|
-
if (parent === dir)
|
|
52
|
-
return undefined;
|
|
53
|
-
dir = parent;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
async function readRule(file) {
|
|
57
|
-
if (!(await exists(file)))
|
|
58
|
-
return undefined;
|
|
59
|
-
try {
|
|
60
|
-
const content = await readFile(file, "utf-8");
|
|
61
|
-
if (!content.trim())
|
|
62
|
-
return undefined;
|
|
63
|
-
return { fullPath: path.resolve(file), content };
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
return undefined;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
function globToRegExp(glob) {
|
|
70
|
-
const norm = glob.replace(/\\/g, "/");
|
|
71
|
-
let re = "^";
|
|
72
|
-
for (let i = 0; i < norm.length; i++) {
|
|
73
|
-
const c = norm[i];
|
|
74
|
-
if (c === "*") {
|
|
75
|
-
if (norm[i + 1] === "*") {
|
|
76
|
-
re += ".*";
|
|
77
|
-
i++;
|
|
78
|
-
if (norm[i + 1] === "/")
|
|
79
|
-
i++;
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
re += "[^/]*";
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
else if (".$^+?()[]{}|".includes(c) || c === "\\") {
|
|
86
|
-
re += "\\" + c;
|
|
87
|
-
}
|
|
88
|
-
else {
|
|
89
|
-
re += c;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
return new RegExp(re + "$");
|
|
93
|
-
}
|
|
94
|
-
async function expandGlob(pattern, workspaceRoot) {
|
|
95
|
-
const abs = path.isAbsolute(pattern) ? pattern : path.join(workspaceRoot, pattern);
|
|
96
|
-
if (!abs.includes("*"))
|
|
97
|
-
return (await exists(abs)) ? [abs] : [];
|
|
98
|
-
const out = [];
|
|
99
|
-
const base = abs.split("*")[0] || workspaceRoot;
|
|
100
|
-
const startDir = path.dirname(base.endsWith("/") ? base : base);
|
|
101
|
-
const regex = globToRegExp(abs);
|
|
102
|
-
async function walk(dir, depth) {
|
|
103
|
-
if (depth > 8)
|
|
104
|
-
return;
|
|
105
|
-
let entries;
|
|
106
|
-
try {
|
|
107
|
-
entries = await readdir(dir);
|
|
108
|
-
}
|
|
109
|
-
catch {
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
entries.sort();
|
|
113
|
-
for (const name of entries) {
|
|
114
|
-
if (name === "node_modules" || name === ".git")
|
|
115
|
-
continue;
|
|
116
|
-
const full = path.join(dir, name);
|
|
117
|
-
let st;
|
|
118
|
-
try {
|
|
119
|
-
st = await stat(full);
|
|
120
|
-
}
|
|
121
|
-
catch {
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
if (st.isDirectory())
|
|
125
|
-
await walk(full, depth + 1);
|
|
126
|
-
else if (regex.test(full.replace(/\\/g, "/")))
|
|
127
|
-
out.push(full);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
await walk(startDir, 0);
|
|
131
|
-
return out;
|
|
132
|
-
}
|
|
133
32
|
/** Same truthy rule as OpenCode's Flag.OPENCODE_DISABLE_PROJECT_CONFIG. */
|
|
134
33
|
export function isProjectConfigDisabled() {
|
|
135
34
|
const value = process.env.OPENCODE_DISABLE_PROJECT_CONFIG?.toLowerCase();
|
|
136
35
|
return value === "true" || value === "1";
|
|
137
36
|
}
|
|
138
|
-
/** Fetch a remote instruction with one deadline covering headers and body. */
|
|
139
|
-
export async function fetchRemoteInstruction(url, timeoutMs = 5000) {
|
|
140
|
-
const ctrl = new AbortController();
|
|
141
|
-
const timer = setTimeout(() => ctrl.abort(), Math.max(1, timeoutMs));
|
|
142
|
-
try {
|
|
143
|
-
const res = await fetch(url, { signal: ctrl.signal });
|
|
144
|
-
if (!res.ok)
|
|
145
|
-
return undefined;
|
|
146
|
-
return await res.text();
|
|
147
|
-
}
|
|
148
|
-
catch {
|
|
149
|
-
return undefined;
|
|
150
|
-
}
|
|
151
|
-
finally {
|
|
152
|
-
clearTimeout(timer);
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
37
|
function mergeConfig(base, overlay) {
|
|
156
38
|
return {
|
|
157
39
|
...base,
|
|
@@ -163,6 +45,10 @@ function mergeConfig(base, overlay) {
|
|
|
163
45
|
permission: overlay.permission ?? base.permission,
|
|
164
46
|
};
|
|
165
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Merged `opencode.json` / `opencode.jsonc` for MCP server ids, plugin lists,
|
|
50
|
+
* and interaction guidance. Instruction file bodies are not collected here.
|
|
51
|
+
*/
|
|
166
52
|
export async function loadMergedConfig(workspaceRoot) {
|
|
167
53
|
const globalConfig = await readJsonConfig(opencodeGlobalConfigDirs()[0] ?? "");
|
|
168
54
|
if (isProjectConfigDisabled())
|
|
@@ -176,65 +62,3 @@ export async function loadMergedConfig(workspaceRoot) {
|
|
|
176
62
|
}
|
|
177
63
|
return mergeConfig(globalConfig, projectConfig);
|
|
178
64
|
}
|
|
179
|
-
/**
|
|
180
|
-
* Collect OpenCode instruction files.
|
|
181
|
-
* `preloadedConfig` reuses a merged config already loaded on this Run.
|
|
182
|
-
*/
|
|
183
|
-
export async function collectRules(workspaceRoot, preloadedConfig) {
|
|
184
|
-
const worktree = await findGitWorktree(workspaceRoot);
|
|
185
|
-
const rules = [];
|
|
186
|
-
const seen = new Set();
|
|
187
|
-
const config = preloadedConfig ?? await loadMergedConfig(workspaceRoot);
|
|
188
|
-
const add = async (file) => {
|
|
189
|
-
if (!file)
|
|
190
|
-
return;
|
|
191
|
-
const resolved = path.resolve(file);
|
|
192
|
-
if (seen.has(resolved))
|
|
193
|
-
return;
|
|
194
|
-
const rule = await readRule(resolved);
|
|
195
|
-
if (!rule)
|
|
196
|
-
return;
|
|
197
|
-
seen.add(resolved);
|
|
198
|
-
rules.push(rule);
|
|
199
|
-
};
|
|
200
|
-
// Match OpenCode: OPENCODE_DISABLE_PROJECT_CONFIG skips project AGENTS/CLAUDE/CONTEXT
|
|
201
|
-
// discovery and project opencode.json (see loadMergedConfig).
|
|
202
|
-
if (!isProjectConfigDisabled()) {
|
|
203
|
-
for (const name of ["AGENTS.md", "CLAUDE.md", "CONTEXT.md"]) {
|
|
204
|
-
const hit = await findUp(name, workspaceRoot, worktree);
|
|
205
|
-
if (hit) {
|
|
206
|
-
await add(hit);
|
|
207
|
-
break;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
for (const globalDir of opencodeGlobalConfigDirs()) {
|
|
212
|
-
await add(path.join(globalDir, "AGENTS.md"));
|
|
213
|
-
}
|
|
214
|
-
await add(path.join(homedir(), ".claude", "CLAUDE.md"));
|
|
215
|
-
for (const raw of config.instructions ?? []) {
|
|
216
|
-
if (raw.startsWith("http://") || raw.startsWith("https://")) {
|
|
217
|
-
// HTTPS-only. Redirects (incl. to a local proxy) are intentional — do not
|
|
218
|
-
// disable follow-redirects or reject localhost/private/metadata hosts.
|
|
219
|
-
let remoteUrl;
|
|
220
|
-
try {
|
|
221
|
-
remoteUrl = new URL(raw);
|
|
222
|
-
}
|
|
223
|
-
catch {
|
|
224
|
-
continue;
|
|
225
|
-
}
|
|
226
|
-
if (remoteUrl.protocol !== "https:")
|
|
227
|
-
continue;
|
|
228
|
-
const content = await fetchRemoteInstruction(remoteUrl.href);
|
|
229
|
-
if (!content?.trim() || seen.has(raw))
|
|
230
|
-
continue;
|
|
231
|
-
seen.add(raw);
|
|
232
|
-
rules.push({ fullPath: raw, content });
|
|
233
|
-
continue;
|
|
234
|
-
}
|
|
235
|
-
const expanded = resolveHomeRelative(raw);
|
|
236
|
-
for (const m of await expandGlob(expanded, workspaceRoot))
|
|
237
|
-
await add(m);
|
|
238
|
-
}
|
|
239
|
-
return { rules, config, worktree };
|
|
240
|
-
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural host-skills capability installed before an unchanged provider loads.
|
|
3
|
+
* Same install timing as `opencode.host.path-bridge`.
|
|
4
|
+
*/
|
|
5
|
+
export declare const HOST_SKILLS_BRIDGE: unique symbol;
|
|
6
|
+
/** One host catalog skill the provider may advertise to Cursor. */
|
|
7
|
+
export type HostSkill = {
|
|
8
|
+
name: string;
|
|
9
|
+
id?: string;
|
|
10
|
+
description: string;
|
|
11
|
+
/** Absolute path, `file:` URL, or host URI (`skill://…`). Omit built-ins / markers. */
|
|
12
|
+
location?: string;
|
|
13
|
+
};
|
|
14
|
+
export type OpenCodeSkillsBridge = {
|
|
15
|
+
list(input: {
|
|
16
|
+
directory: string;
|
|
17
|
+
sessionID?: string;
|
|
18
|
+
}): Promise<HostSkill[]>;
|
|
19
|
+
};
|
|
20
|
+
export declare function skillsBridge(): OpenCodeSkillsBridge | undefined;
|
|
21
|
+
/** Test helper: install or clear the structural skills bridge. */
|
|
22
|
+
export declare function setHostSkillsBridgeForTests(bridge: OpenCodeSkillsBridge | undefined): void;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural host-skills capability installed before an unchanged provider loads.
|
|
3
|
+
* Same install timing as `opencode.host.path-bridge`.
|
|
4
|
+
*/
|
|
5
|
+
export const HOST_SKILLS_BRIDGE = Symbol.for("opencode.host.skills");
|
|
6
|
+
export function skillsBridge() {
|
|
7
|
+
const value = globalThis[HOST_SKILLS_BRIDGE];
|
|
8
|
+
if (!value || typeof value !== "object")
|
|
9
|
+
return undefined;
|
|
10
|
+
const bridge = value;
|
|
11
|
+
return typeof bridge.list === "function" ? bridge : undefined;
|
|
12
|
+
}
|
|
13
|
+
/** Test helper: install or clear the structural skills bridge. */
|
|
14
|
+
export function setHostSkillsBridgeForTests(bridge) {
|
|
15
|
+
const globals = globalThis;
|
|
16
|
+
if (bridge)
|
|
17
|
+
globals[HOST_SKILLS_BRIDGE] = bridge;
|
|
18
|
+
else
|
|
19
|
+
delete globals[HOST_SKILLS_BRIDGE];
|
|
20
|
+
}
|
package/dist/context/skills.d.ts
CHANGED
|
@@ -1,11 +1,34 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import type { OpencodeToolDef } from "../protocol/tools.js";
|
|
2
|
+
import { type HostSkill } from "./skills-bridge.js";
|
|
3
|
+
export type HostSkillFileEntry = {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly path: string;
|
|
6
6
|
};
|
|
7
|
+
export type AgentSkillWire = {
|
|
8
|
+
readonly full_path: string;
|
|
9
|
+
readonly description: string;
|
|
10
|
+
};
|
|
11
|
+
/** Locations Cursor can advertise; markers / builtins are omitted. */
|
|
12
|
+
export declare function isUsableSkillLocation(location: string | undefined): boolean;
|
|
13
|
+
export declare function normalizeSkillLocation(location: string): string;
|
|
14
|
+
export declare function skillToolAdvertised(tools: readonly OpencodeToolDef[] | undefined): boolean;
|
|
15
|
+
/** Record skill files a Location's host currently registers (OpenCode 2 plugin). */
|
|
16
|
+
export declare function rememberHostSkillFiles(directory: string, skills: readonly HostSkillFileEntry[]): void;
|
|
17
|
+
export declare function hostSkillFiles(directory: string | undefined): ReadonlyMap<string, string> | undefined;
|
|
18
|
+
/** Test helper. */
|
|
19
|
+
export declare function resetHostSkillFilesForTests(): void;
|
|
7
20
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
21
|
+
* path-desc `agent_skills` for cataloged skills that have a usable location,
|
|
22
|
+
* in catalog order. Descriptions come from the catalog (epoch-stable).
|
|
10
23
|
*/
|
|
11
|
-
export declare function
|
|
24
|
+
export declare function agentSkillsForCursor(systemText: string | undefined, options: {
|
|
25
|
+
skillToolAdvertised: boolean;
|
|
26
|
+
skillFiles?: ReadonlyMap<string, string>;
|
|
27
|
+
bridgeSkills?: readonly HostSkill[];
|
|
28
|
+
}): AgentSkillWire[];
|
|
29
|
+
/** Fetch locations from the structural bridge when installed. */
|
|
30
|
+
export declare function loadBridgeSkills(input: {
|
|
31
|
+
workspaceRoot: string;
|
|
32
|
+
sessionID?: string;
|
|
33
|
+
}): Promise<HostSkill[] | undefined>;
|
|
34
|
+
export declare function applyAgentSkillsToContext(context: Record<string, unknown>, skills: readonly AgentSkillWire[]): void;
|