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.
Files changed (101) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +36 -15
  3. package/SECURITY.md +43 -7
  4. package/dist/agent-url.js +2 -2
  5. package/dist/auth-renewal.d.ts +85 -0
  6. package/dist/auth-renewal.js +284 -0
  7. package/dist/auth.d.ts +38 -22
  8. package/dist/auth.js +79 -96
  9. package/dist/classic-tools.d.ts +15 -0
  10. package/dist/classic-tools.js +45 -0
  11. package/dist/context/build.d.ts +62 -7
  12. package/dist/context/build.js +127 -95
  13. package/dist/context/dynamic-catalog.d.ts +2 -58
  14. package/dist/context/dynamic-catalog.js +16 -161
  15. package/dist/context/epoch.d.ts +6 -4
  16. package/dist/context/epoch.js +22 -10
  17. package/dist/context/frozen.js +42 -15
  18. package/dist/context/index.d.ts +3 -1
  19. package/dist/context/index.js +3 -1
  20. package/dist/context/overlay.d.ts +2 -23
  21. package/dist/context/overlay.js +6 -30
  22. package/dist/context/paths.d.ts +51 -21
  23. package/dist/context/paths.js +78 -27
  24. package/dist/context/rules.d.ts +3 -15
  25. package/dist/context/rules.js +10 -186
  26. package/dist/context/skills-bridge.d.ts +22 -0
  27. package/dist/context/skills-bridge.js +20 -0
  28. package/dist/context/skills.d.ts +31 -8
  29. package/dist/context/skills.js +187 -86
  30. package/dist/debug.d.ts +2 -0
  31. package/dist/debug.js +4 -0
  32. package/dist/errors.d.ts +13 -0
  33. package/dist/errors.js +27 -3
  34. package/dist/host-agent-mode.d.ts +53 -4
  35. package/dist/host-agent-mode.js +119 -15
  36. package/dist/host-plan-file.d.ts +28 -0
  37. package/dist/host-plan-file.js +76 -0
  38. package/dist/image-input.d.ts +18 -0
  39. package/dist/image-input.js +119 -28
  40. package/dist/image-save.d.ts +10 -2
  41. package/dist/image-save.js +35 -23
  42. package/dist/index.d.ts +10 -0
  43. package/dist/language-model.d.ts +86 -25
  44. package/dist/language-model.js +1295 -369
  45. package/dist/model-config.d.ts +9 -0
  46. package/dist/model-config.js +28 -1
  47. package/dist/opencode2/catalog.d.ts +1 -0
  48. package/dist/opencode2/catalog.js +2 -0
  49. package/dist/opencode2/image-save-tool.d.ts +8 -0
  50. package/dist/opencode2/image-save-tool.js +84 -0
  51. package/dist/opencode2/integration.d.ts +30 -7
  52. package/dist/opencode2/integration.js +75 -46
  53. package/dist/opencode2/types.d.ts +15 -0
  54. package/dist/parallel-step.d.ts +65 -0
  55. package/dist/parallel-step.js +102 -0
  56. package/dist/plugin-opencode2.js +58 -56
  57. package/dist/plugin.d.ts +0 -10
  58. package/dist/plugin.js +207 -127
  59. package/dist/pricing-data.d.ts +14 -1
  60. package/dist/pricing-data.js +14 -1
  61. package/dist/protocol/ask-question.d.ts +12 -1
  62. package/dist/protocol/ask-question.js +77 -3
  63. package/dist/protocol/blob-store.js +3 -2
  64. package/dist/protocol/conversation-bind.js +1 -1
  65. package/dist/protocol/conversation-persistence.d.ts +2 -0
  66. package/dist/protocol/conversation-persistence.js +11 -1
  67. package/dist/protocol/conversation-state.d.ts +2 -0
  68. package/dist/protocol/conversation-state.js +19 -0
  69. package/dist/protocol/create-plan.d.ts +47 -25
  70. package/dist/protocol/create-plan.js +35 -24
  71. package/dist/protocol/interactions.d.ts +26 -17
  72. package/dist/protocol/interactions.js +80 -31
  73. package/dist/protocol/messages.js +96 -18
  74. package/dist/protocol/request.d.ts +16 -9
  75. package/dist/protocol/request.js +17 -25
  76. package/dist/protocol/switch-mode.d.ts +66 -27
  77. package/dist/protocol/switch-mode.js +147 -73
  78. package/dist/protocol/token-details.d.ts +2 -0
  79. package/dist/protocol/token-details.js +11 -1
  80. package/dist/protocol/tool-call-bridge.d.ts +4 -0
  81. package/dist/protocol/tool-call-bridge.js +14 -0
  82. package/dist/protocol/tools.d.ts +33 -11
  83. package/dist/protocol/tools.js +165 -103
  84. package/dist/protocol/turn-provenance.d.ts +52 -0
  85. package/dist/protocol/turn-provenance.js +174 -0
  86. package/dist/replay-safety.d.ts +6 -0
  87. package/dist/replay-safety.js +56 -6
  88. package/dist/session.d.ts +44 -2
  89. package/dist/session.js +45 -1
  90. package/dist/shared.d.ts +7 -0
  91. package/dist/shared.js +7 -0
  92. package/dist/todo-store.d.ts +2 -0
  93. package/dist/todo-store.js +19 -0
  94. package/dist/transport/connect.d.ts +2 -0
  95. package/dist/transport/connect.js +23 -6
  96. package/dist/usage.js +23 -6
  97. package/package.json +3 -2
  98. package/dist/context/agents.d.ts +0 -12
  99. package/dist/context/agents.js +0 -136
  100. package/dist/plan-execution-kickoff.d.ts +0 -57
  101. package/dist/plan-execution-kickoff.js +0 -186
@@ -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
- /** Native OpenCode global config dir. */
25
- export declare function opencodeGlobalConfigDir(): string;
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
- * 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.
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
- * 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.
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 (`/Users/a/b` → `Users-a-b`).
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;
@@ -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
- /** Native OpenCode global config dir. */
72
- export function opencodeGlobalConfigDir() {
73
- return path.join(resolveHome(), ".config", "opencode");
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
- * 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.
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
- * 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.
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
- return path.join(hostGlobalDataDir(), "plans");
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 (`/Users/a/b` → `Users-a-b`).
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
- }
@@ -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
- * Collect OpenCode instruction files.
20
- * `preloadedConfig` reuses a merged config already loaded on this Run.
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 collectRules(workspaceRoot: string, preloadedConfig?: OpencodeJson): Promise<{
23
- rules: CollectedRule[];
24
- config: OpencodeJson;
25
- worktree: string;
26
- }>;
14
+ export declare function loadMergedConfig(workspaceRoot: string): Promise<OpencodeJson>;
@@ -1,7 +1,6 @@
1
- import { readFile, readdir, stat } from "node:fs/promises";
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, resolveHomeRelative } from "./paths.js";
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
- const stripped = raw.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
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
+ }
@@ -1,11 +1,34 @@
1
- export type CollectedSkill = {
2
- fullPath: string;
3
- name: string;
4
- description: string;
5
- content: string;
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
- * Discover skills the way OpenCode does: `.opencode/skills`, global opencode,
9
- * plus `.claude/skills` and `.agents/skills` (project + home).
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 collectSkills(workspaceRoot: string, worktree: string): Promise<CollectedSkill[]>;
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;