cursor-opencode-provider 0.2.7 → 0.2.8
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 +10 -6
- package/dist/agent-url.d.ts +8 -5
- package/dist/auth.js +59 -27
- package/dist/context/build.js +6 -0
- package/dist/context/paths.d.ts +21 -6
- package/dist/context/paths.js +32 -18
- package/dist/deadline.d.ts +8 -0
- package/dist/deadline.js +25 -0
- package/dist/language-model.js +123 -69
- package/dist/models.d.ts +2 -0
- package/dist/plugin.js +8 -1
- package/dist/protocol/client-version.js +7 -6
- package/dist/protocol/messages.js +4 -0
- package/dist/protocol/struct.d.ts +18 -0
- package/dist/protocol/struct.js +82 -0
- package/dist/protocol/tools.d.ts +18 -0
- package/dist/protocol/tools.js +74 -5
- package/dist/replay-safety.d.ts +23 -0
- package/dist/replay-safety.js +126 -0
- package/dist/session.d.ts +3 -0
- package/dist/transport/connect.d.ts +2 -0
- package/dist/transport/connect.js +106 -82
- package/dist/web-tools.d.ts +34 -0
- package/dist/web-tools.js +127 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ Add the package name to OpenCode config. OpenCode installs npm plugins with Bun
|
|
|
47
47
|
}
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
Pin a version if you want: `"cursor-opencode-provider@0.2.
|
|
50
|
+
Pin a version if you want: `"cursor-opencode-provider@0.2.8"`.
|
|
51
51
|
|
|
52
52
|
### From a local clone
|
|
53
53
|
|
|
@@ -109,10 +109,13 @@ After login, the plugin fetches your available models and writes them to `<host-
|
|
|
109
109
|
Model/version caches and Cursor project metadata live under a **host cache root**, resolved in this order:
|
|
110
110
|
|
|
111
111
|
1. Explicit `createCursor({ cacheDir })` / Effect v2 `Path.cache`
|
|
112
|
-
2. Optional `@opencode-compat/profile` `detect()` when
|
|
113
|
-
3.
|
|
112
|
+
2. Optional `@opencode-compat/profile` `detect()` when it reports strong environment, binary, or package identity
|
|
113
|
+
3. Explicit `$MIMOCODE_HOME` / `KILO_CONFIG_DIR`, then the provider's host-named install path under `$XDG_CACHE_HOME`
|
|
114
114
|
4. Default `~/.cache/opencode/`
|
|
115
115
|
|
|
116
|
+
Config-directory presence is deliberately ignored: having MiMo or Kilo installed
|
|
117
|
+
must not redirect a native OpenCode process into that host's cache.
|
|
118
|
+
|
|
116
119
|
| Kind | Default (OpenCode) | Notes |
|
|
117
120
|
|------|--------------------|-------|
|
|
118
121
|
| Model / version **cache** | `~/.cache/opencode/` | MiMo: `$MIMOCODE_HOME/cache` or `~/.cache/mimocode/`; Kilo: `~/.cache/kilo/` |
|
|
@@ -187,7 +190,7 @@ Pass either `accessToken` (JWT from OAuth or key exchange) or `apiKey` (raw `sk-
|
|
|
187
190
|
| `CURSOR_PROVIDER_DEBUG_FILE` | Debug log path (default: `debug-<pid>.log` under `$TMPDIR/cursor-provider-logs-<uid>/`) |
|
|
188
191
|
| `XDG_CACHE_HOME` | Base for host cache dirs (`$XDG_CACHE_HOME/opencode/`, `…/mimocode/`, or `…/kilo/`) when no explicit `cacheDir` / OCP detect override |
|
|
189
192
|
| `MIMOCODE_HOME` | When set, host cache is `$MIMOCODE_HOME/cache` (MiMo) |
|
|
190
|
-
| `KILO_CONFIG_DIR` | When set
|
|
193
|
+
| `KILO_CONFIG_DIR` | When set, host cache is `$XDG_CACHE_HOME/kilo` |
|
|
191
194
|
| `XDG_DATA_HOME` | When set, OpenCode `auth.json` is read from `$XDG_DATA_HOME/opencode/` instead of `~/.local/share/opencode/` |
|
|
192
195
|
|
|
193
196
|
`createCursor({ agentBaseURL })` overrides the agent Run host. When unset, the provider resolves the host from Cursor's `GetServerConfig` API (`agentUrlConfig.agentnUrl`, region-specific — e.g. `agentn.us.api5.cursor.sh`, `agent-gcpp-uswest.api5.cursor.sh`) once per process and holds it in memory (never written to disk), so a held-open Run stream is never repointed mid-session. Explicit agent overrides and GetServerConfig results are validated as HTTPS `*.cursor.sh` hosts (Cursor's agent hostnames vary and may change); non-`cursor.sh` hosts are rejected. Shared HTTP/2 connections are rotated before they become server-aged, while existing Runs may finish on their original connection. The lookup sends `{ "telem_enabled": false }` by default; set `telemetryEnabled: true` in provider config, or `CURSOR_GET_SERVER_CONFIG_TELEMETRY=1` for OpenCode/plugin usage, to opt in. If the lookup fails or does not return a valid Cursor agent host, the model call fails clearly instead of falling back to `agentn.global.api5.cursor.sh`.
|
|
@@ -271,8 +274,9 @@ Project `instructions` may reference absolute or `~/` paths (OpenCode parity). S
|
|
|
271
274
|
- **Configured MCP tools keep their upstream server id** — OpenCode builtins and plugin/custom tools are advertised under a synthetic `opencode` MCP server. Tools whose flattened name matches an MCP server in merged `opencode.json` configuration (`github_create_pull_request`, …) are grouped into that server's `mcp_descriptors` / `provider_identifier` (`github`, …). Unknown underscore-containing names stay under `opencode` rather than being guessed incorrectly. Cursor's MCP-state exec probe is answered from the same advertised descriptors before the actual tool request, using the full canonical tool-definition identity required by native `get_mcp_tools`; exec still reconstructs the full OpenCode tool id.
|
|
272
275
|
- **Display completions are notifications, not execution requests** — Cursor `tool_call_*` frames use a typed `ToolCall` oneof. The provider decodes them for diagnostics but only mirrors finalized todo/plan state (`update_todos_tool_call` / `create_plan_tool_call`) into advertised OpenCode `todowrite`; the completed payload already contains the authoritative final list. Interactive, data-returning, and side-effecting completions are never replayed as new tools because their result could not be returned to Cursor. Exec-backed native subagent/Task and Pi read/bash/edit/write/grep/find/ls calls use their typed request/result fields instead. Unknown display variants are logged. All 37 Cursor CLI exec request/result pairs are inventoried by field and name; known-but-unsupported and future unknown exec variants fail explicitly rather than receiving a guessed response that could deadlock the Run.
|
|
273
276
|
- **Tool availability is per OpenCode agent** — Cursor can request native capabilities such as Task even when a child or restricted OpenCode agent did not advertise the corresponding host tool. The provider prompts Cursor with the exact current catalog and checks every decoded host-tool exec request against it. An unavailable request is answered on Cursor's correlated typed result channel and is never emitted as OpenCode's `invalid` tool.
|
|
277
|
+
- **Host web tools use collision-safe aliases** — Cursor sees `custom_websearch` / `custom_webfetch`, and the held Run maps each alias back to an executable OpenCode tool with its schema, permission check, and correlated result intact. The plugin registers its search fallback directly as `custom_websearch`, avoiding OpenCode's reserved `websearch` id filter for third-party providers and taking precedence over MCP search providers such as Brave without host environment configuration. Exact host `webfetch` is translated the same way. Cursor's UI-bound native web interactions stay disabled because their approval replies cannot carry OpenCode tool results.
|
|
274
278
|
- **Background shells are non-interactive** — Cursor's native background-shell spawn and soft-background shell-stream timeouts are bridged through OpenCode's foreground-only `bash` tool. With bash/zsh, the classic plugin keeps the original permission/UI command and executes the wrapper through `shell.env` (`BASH_ENV` / `ZDOTDIR`); OpenCode's non-interactive sh/dash argv ignores those startup variables, so the plugin uses a short `exec /bin/sh '<wrapper-file>'` command backed by the same private wrapper. Native background-spawn requests also carry a self-contained marker-producing fallback when the classic hooks are absent. Spawn/soft-bg wrappers detach with `nohup`, redirect output under `${TMPDIR:-/tmp}/cursor-opencode-{bg,shell}.*`, and return the real PID (or typed timeout/exit) to Cursor. Private markers and OpenCode's `<shell_metadata>` envelope are stripped before storage/render, with a short still-running / started / timed-out status line left for the bash bubble. Requests that require `write_shell_stdin` are rejected explicitly because OpenCode does not expose an interactive background-process lifecycle through its AI SDK tool interface. The POSIX wrap path is not implemented for native Windows PowerShell/`cmd`.
|
|
275
|
-
- **Cursor-native interaction queries remain headless** — Cursor UI/approval *queries* (as distinct from display tool calls) still cannot be surfaced through the AI SDK provider interface. The normal system prompt redirects questions, planning, plan-mode transitions, and
|
|
279
|
+
- **Cursor-native interaction queries remain headless** — Cursor UI/approval *queries* (as distinct from display tool calls) still cannot be surfaced through the AI SDK provider interface. The normal system prompt redirects questions, planning, plan-mode transitions, and available web capabilities to executable host tools (`question`, `todowrite`, `plan_enter` / `plan_exit`, `custom_websearch`, `custom_webfetch`); native web/PR/MCP/image/SCM requests are declined so they remain behind host tool permissions. Separately from display `create_plan_tool_call` → `todowrite` mirroring, interaction `create_plan_request_query` is auto-acked (CLI headless parity) with success and an empty `plan_uri`, so Cursor may treat the plan as accepted without an OpenCode UI confirm. Compaction prompts are unchanged. Unknown future interaction variants fail the turn explicitly instead of hanging the Run stream.
|
|
276
280
|
- **Compaction resets Cursor conversation state** — the classic plugin marks OpenCode's `compaction` agent explicitly. On those turns the provider mints an isolated Cursor `conversation_id`, drops the prior checkpoint + KV blobs, preserves real tool outputs as OpenCode-host observations in the seed history, and re-advertises the session's last tool catalog while refusing execution during the summary itself. The first normal turn then rebases once more onto a fresh conversation seeded with OpenCode's compacted prompt and normal system instructions, so the summary-agent checkpoint cannot suppress later tool calls. Ordinary no-tool / `toolChoice:none` calls do not reset conversation state.
|
|
277
281
|
- **Conversation bindings and compaction catalogs are bounded** — process-global per-session bindings, prior tool catalogs, and pending post-compaction rebases use a 256-session LRU bound. Evicting a conversation binding also drops its checkpoint and KV blobs.
|
|
278
282
|
- **Interrupted Runs resume from checkpoints** — a remote EOF, Connect end-stream, or trailer error is never emitted as a successful `stop`. When the failed Run produced an eligible checkpoint, the provider opens a new RPC for the same conversation and sends that state with `ResumeAction`, so completed text and tool work are not replayed. Before any stateful output, an interruption without a checkpoint can still rebase from OpenCode history. Stateful interruptions without a checkpoint are surfaced because replay would be ambiguous; retry exhaustion remains explicit. A transport closure after `turn_ended` is treated as successful completion.
|
|
@@ -280,4 +284,4 @@ Project `instructions` may reference absolute or `~/` paths (OpenCode parity). S
|
|
|
280
284
|
|
|
281
285
|
## License
|
|
282
286
|
|
|
283
|
-
MIT
|
|
287
|
+
MIT
|
package/dist/agent-url.d.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
type AgentUrlOptions = {
|
|
2
|
+
apiBaseURL?: string;
|
|
3
|
+
baseURL?: string;
|
|
4
|
+
telemetryEnabled?: boolean;
|
|
5
|
+
timeoutMs?: number;
|
|
6
|
+
};
|
|
1
7
|
/**
|
|
2
8
|
* Resolve the Run stream origin for this account via the `GetServerConfig`
|
|
3
9
|
* Connect RPC. Memoized for the process lifetime.
|
|
@@ -8,10 +14,7 @@
|
|
|
8
14
|
*
|
|
9
15
|
* Concurrent callers share a single in-flight fetch.
|
|
10
16
|
*/
|
|
11
|
-
export declare function resolveAgentUrl(token: string, options?:
|
|
12
|
-
apiBaseURL?: string;
|
|
13
|
-
baseURL?: string;
|
|
14
|
-
telemetryEnabled?: boolean;
|
|
15
|
-
}): Promise<string>;
|
|
17
|
+
export declare function resolveAgentUrl(token: string, options?: AgentUrlOptions): Promise<string>;
|
|
16
18
|
/** Reset the in-process memo. Tests only. */
|
|
17
19
|
export declare function resetAgentUrlCache(): void;
|
|
20
|
+
export {};
|
package/dist/auth.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { CURSOR_API_HOST, CURSOR_WEBSITE_HOST } from "./shared.js";
|
|
2
|
+
import { withAbortDeadline } from "./deadline.js";
|
|
2
3
|
const API_BASE = `https://${CURSOR_API_HOST}`;
|
|
4
|
+
const AUTH_REQUEST_TIMEOUT_MS = 5_000;
|
|
3
5
|
export class AuthExchangeError extends Error {
|
|
4
6
|
cause;
|
|
5
7
|
constructor(message, cause) {
|
|
@@ -72,37 +74,67 @@ export function useAuthToken(token) {
|
|
|
72
74
|
return { accessToken: token };
|
|
73
75
|
}
|
|
74
76
|
export async function exchangeApiKey(apiKey, baseUrl = API_BASE) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
return withAbortDeadline(AUTH_REQUEST_TIMEOUT_MS, () => new AuthExchangeError("API key exchange timed out"), async (signal) => {
|
|
78
|
+
let res;
|
|
79
|
+
try {
|
|
80
|
+
res = await fetch(`${baseUrl}/auth/exchange_user_api_key`, {
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers: {
|
|
83
|
+
"Content-Type": "application/json",
|
|
84
|
+
Authorization: `Bearer ${apiKey}`,
|
|
85
|
+
},
|
|
86
|
+
body: "{}",
|
|
87
|
+
signal,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
catch (cause) {
|
|
91
|
+
throw new AuthExchangeError("API key exchange request failed", cause);
|
|
92
|
+
}
|
|
93
|
+
if (!res.ok) {
|
|
94
|
+
throw new AuthExchangeError(`API key exchange failed: ${res.status} ${res.statusText}`);
|
|
95
|
+
}
|
|
96
|
+
let body;
|
|
97
|
+
try {
|
|
98
|
+
body = await res.json();
|
|
99
|
+
}
|
|
100
|
+
catch (cause) {
|
|
101
|
+
throw new AuthExchangeError("API key exchange returned malformed JSON", cause);
|
|
102
|
+
}
|
|
103
|
+
if (typeof body.accessToken !== "string" || typeof body.refreshToken !== "string") {
|
|
104
|
+
throw new AuthExchangeError("Exchange response missing tokens");
|
|
105
|
+
}
|
|
106
|
+
return { accessToken: body.accessToken, refreshToken: body.refreshToken };
|
|
82
107
|
});
|
|
83
|
-
if (!res.ok) {
|
|
84
|
-
throw new AuthExchangeError(`API key exchange failed: ${res.status} ${res.statusText}`);
|
|
85
|
-
}
|
|
86
|
-
const body = await res.json();
|
|
87
|
-
if (!body.accessToken || !body.refreshToken) {
|
|
88
|
-
throw new AuthExchangeError("Exchange response missing tokens");
|
|
89
|
-
}
|
|
90
|
-
return { accessToken: body.accessToken, refreshToken: body.refreshToken };
|
|
91
108
|
}
|
|
92
109
|
export async function refreshAccessToken(refreshToken, baseUrl = API_BASE) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
110
|
+
return withAbortDeadline(AUTH_REQUEST_TIMEOUT_MS, () => new AuthRefreshError("Token refresh timed out"), async (signal) => {
|
|
111
|
+
let res;
|
|
112
|
+
try {
|
|
113
|
+
res = await fetch(`${baseUrl}/auth/token`, {
|
|
114
|
+
method: "POST",
|
|
115
|
+
headers: { "Content-Type": "application/json" },
|
|
116
|
+
body: JSON.stringify({ refreshToken }),
|
|
117
|
+
signal,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
catch (cause) {
|
|
121
|
+
throw new AuthRefreshError("Token refresh request failed", cause);
|
|
122
|
+
}
|
|
123
|
+
if (!res.ok) {
|
|
124
|
+
throw new AuthRefreshError(`Token refresh failed: ${res.status} ${res.statusText}`);
|
|
125
|
+
}
|
|
126
|
+
let body;
|
|
127
|
+
try {
|
|
128
|
+
body = await res.json();
|
|
129
|
+
}
|
|
130
|
+
catch (cause) {
|
|
131
|
+
throw new AuthRefreshError("Token refresh returned malformed JSON", cause);
|
|
132
|
+
}
|
|
133
|
+
if (typeof body.accessToken !== "string" || typeof body.refreshToken !== "string") {
|
|
134
|
+
throw new AuthRefreshError("Refresh response missing tokens");
|
|
135
|
+
}
|
|
136
|
+
return { accessToken: body.accessToken, refreshToken: body.refreshToken };
|
|
97
137
|
});
|
|
98
|
-
if (!res.ok) {
|
|
99
|
-
throw new AuthRefreshError(`Token refresh failed: ${res.status} ${res.statusText}`);
|
|
100
|
-
}
|
|
101
|
-
const body = await res.json();
|
|
102
|
-
if (!body.accessToken || !body.refreshToken) {
|
|
103
|
-
throw new AuthRefreshError("Refresh response missing tokens");
|
|
104
|
-
}
|
|
105
|
-
return { accessToken: body.accessToken, refreshToken: body.refreshToken };
|
|
106
138
|
}
|
|
107
139
|
// Cache JWTs obtained via API-key exchange so doStream doesn't re-exchange
|
|
108
140
|
// on every turn when the caller only supplied `apiKey`.
|
package/dist/context/build.js
CHANGED
|
@@ -61,6 +61,12 @@ export async function buildRequestContext(input) {
|
|
|
61
61
|
enabled: true,
|
|
62
62
|
mcp_descriptors: nested,
|
|
63
63
|
},
|
|
64
|
+
// This provider always rejects native web_search/web_fetch interaction
|
|
65
|
+
// queries with a headless-UI reason (see interactions.ts). Advertise that
|
|
66
|
+
// unavailability up front so Cursor prefers the collision-safe
|
|
67
|
+
// custom_web* aliases instead of routing through a query doomed to fail.
|
|
68
|
+
web_search_enabled: false,
|
|
69
|
+
web_fetch_enabled: false,
|
|
64
70
|
// Completeness: true only for sections we actually gathered.
|
|
65
71
|
rules_info_complete: true,
|
|
66
72
|
env_info_complete: true,
|
package/dist/context/paths.d.ts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
export type HostPathEnv = NodeJS.ProcessEnv;
|
|
2
|
+
type CompatDetectResult = {
|
|
3
|
+
id: string;
|
|
4
|
+
supported: boolean;
|
|
5
|
+
source?: string;
|
|
6
|
+
profile: {
|
|
7
|
+
paths: {
|
|
8
|
+
cacheDir: string;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
type CompatDetector = () => CompatDetectResult;
|
|
2
13
|
/**
|
|
3
14
|
* Pin the process-wide cache root. Highest precedence for {@link opencodeGlobalCacheDir}.
|
|
4
15
|
* Use for host-injected `Path.cache` or an explicit `createCursor({ cacheDir })`.
|
|
@@ -7,15 +18,18 @@ export declare function setHostCacheDirOverride(dir: string | undefined): void;
|
|
|
7
18
|
export declare function getHostCacheDirOverride(): string | undefined;
|
|
8
19
|
/**
|
|
9
20
|
* Resolve the host cache directory without an override.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
21
|
+
*
|
|
22
|
+
* Explicit host environment wins. Otherwise, an installed provider inherits
|
|
23
|
+
* the host-named cache containing its module. A source checkout or otherwise
|
|
24
|
+
* unidentifiable install defaults to OpenCode. Merely having another host's
|
|
25
|
+
* config directory installed is not evidence that it owns this process.
|
|
12
26
|
*/
|
|
13
|
-
export declare function resolveHostCacheDir(env?: HostPathEnv): string;
|
|
27
|
+
export declare function resolveHostCacheDir(env?: HostPathEnv, moduleUrl?: string): string;
|
|
14
28
|
/**
|
|
15
29
|
* Best-effort: if `@opencode-compat/profile` is installed, adopt `detect().profile.paths.cacheDir`
|
|
16
30
|
* when the host is supported. No-op when OCP is absent or detection fails.
|
|
17
31
|
*/
|
|
18
|
-
export declare function adoptCompatHostCacheDir(): Promise<string | undefined>;
|
|
32
|
+
export declare function adoptCompatHostCacheDir(detector?: CompatDetector): Promise<string | undefined>;
|
|
19
33
|
/** OpenCode / host global config dir (`~/.config/<app>`). Still OpenCode-named for rule discovery. */
|
|
20
34
|
export declare function opencodeGlobalConfigDir(): string;
|
|
21
35
|
/**
|
|
@@ -23,8 +37,8 @@ export declare function opencodeGlobalConfigDir(): string;
|
|
|
23
37
|
*
|
|
24
38
|
* Precedence:
|
|
25
39
|
* 1. {@link setHostCacheDirOverride} / `createCursor({ cacheDir })` (host `Path.cache`)
|
|
26
|
-
* 2. OCP `detect()` when {@link adoptCompatHostCacheDir} ran successfully
|
|
27
|
-
* 3.
|
|
40
|
+
* 2. Strong OCP `detect()` identity when {@link adoptCompatHostCacheDir} ran successfully
|
|
41
|
+
* 3. Explicit host environment / provider install path ({@link resolveHostCacheDir})
|
|
28
42
|
*/
|
|
29
43
|
export declare function opencodeGlobalCacheDir(): string;
|
|
30
44
|
/**
|
|
@@ -50,3 +64,4 @@ export declare function opencodeProjectDir(workspaceRoot: string): string;
|
|
|
50
64
|
/** Ensure {@link opencodeProjectDir} exists (mode 0o700) and return it. */
|
|
51
65
|
export declare function ensureOpencodeProjectDir(workspaceRoot: string): string;
|
|
52
66
|
export declare function resolveHomeRelative(p: string): string;
|
|
67
|
+
export {};
|
package/dist/context/paths.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { mkdirSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { trace } from "../debug.js";
|
|
6
7
|
/** Explicit host cache root (e.g. Effect v2 `Path.cache`, or `createCursor({ cacheDir })`). */
|
|
7
8
|
let hostCacheDirOverride;
|
|
@@ -13,11 +14,6 @@ function xdgCacheHome(env = process.env) {
|
|
|
13
14
|
return env.XDG_CACHE_HOME;
|
|
14
15
|
return path.join(resolveHome(env), ".cache");
|
|
15
16
|
}
|
|
16
|
-
function xdgConfigHome(env = process.env) {
|
|
17
|
-
if (env.XDG_CONFIG_HOME && env.XDG_CONFIG_HOME.length > 0)
|
|
18
|
-
return env.XDG_CONFIG_HOME;
|
|
19
|
-
return path.join(resolveHome(env), ".config");
|
|
20
|
-
}
|
|
21
17
|
/**
|
|
22
18
|
* Pin the process-wide cache root. Highest precedence for {@link opencodeGlobalCacheDir}.
|
|
23
19
|
* Use for host-injected `Path.cache` or an explicit `createCursor({ cacheDir })`.
|
|
@@ -30,23 +26,35 @@ export function getHostCacheDirOverride() {
|
|
|
30
26
|
}
|
|
31
27
|
/**
|
|
32
28
|
* Resolve the host cache directory without an override.
|
|
33
|
-
*
|
|
34
|
-
*
|
|
29
|
+
*
|
|
30
|
+
* Explicit host environment wins. Otherwise, an installed provider inherits
|
|
31
|
+
* the host-named cache containing its module. A source checkout or otherwise
|
|
32
|
+
* unidentifiable install defaults to OpenCode. Merely having another host's
|
|
33
|
+
* config directory installed is not evidence that it owns this process.
|
|
35
34
|
*/
|
|
36
|
-
export function resolveHostCacheDir(env = process.env) {
|
|
35
|
+
export function resolveHostCacheDir(env = process.env, moduleUrl = import.meta.url) {
|
|
37
36
|
const mimoHome = env.MIMOCODE_HOME;
|
|
38
37
|
if (mimoHome && mimoHome.length > 0) {
|
|
39
38
|
return path.join(mimoHome, "cache");
|
|
40
39
|
}
|
|
41
|
-
const configHome = xdgConfigHome(env);
|
|
42
40
|
const cacheHome = xdgCacheHome(env);
|
|
43
41
|
const kiloConfig = env.KILO_CONFIG_DIR;
|
|
44
|
-
|
|
45
|
-
if ((kiloConfig && kiloConfig.length > 0) || existsSync(path.join(configHome, "kilo"))) {
|
|
42
|
+
if (kiloConfig && kiloConfig.length > 0) {
|
|
46
43
|
return path.join(cacheHome, "kilo");
|
|
47
44
|
}
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
let modulePath;
|
|
46
|
+
try {
|
|
47
|
+
modulePath = moduleUrl.startsWith("file:") ? fileURLToPath(moduleUrl) : path.resolve(moduleUrl);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
modulePath = undefined;
|
|
51
|
+
}
|
|
52
|
+
if (modulePath) {
|
|
53
|
+
for (const host of ["mimocode", "kilo", "opencode"]) {
|
|
54
|
+
const root = path.resolve(cacheHome, host);
|
|
55
|
+
if (modulePath === root || modulePath.startsWith(`${root}${path.sep}`))
|
|
56
|
+
return root;
|
|
57
|
+
}
|
|
50
58
|
}
|
|
51
59
|
return path.join(cacheHome, "opencode");
|
|
52
60
|
}
|
|
@@ -54,12 +62,18 @@ export function resolveHostCacheDir(env = process.env) {
|
|
|
54
62
|
* Best-effort: if `@opencode-compat/profile` is installed, adopt `detect().profile.paths.cacheDir`
|
|
55
63
|
* when the host is supported. No-op when OCP is absent or detection fails.
|
|
56
64
|
*/
|
|
57
|
-
export async function adoptCompatHostCacheDir() {
|
|
65
|
+
export async function adoptCompatHostCacheDir(detector) {
|
|
66
|
+
if (hostCacheDirOverride)
|
|
67
|
+
return hostCacheDirOverride;
|
|
58
68
|
try {
|
|
59
|
-
const
|
|
69
|
+
const detect = detector ?? (await import("@opencode-compat/profile")).detect;
|
|
60
70
|
const result = detect();
|
|
61
71
|
if (!result.supported || result.id === "unknown")
|
|
62
72
|
return undefined;
|
|
73
|
+
if (!result.source || !["env", "binary", "package"].includes(result.source)) {
|
|
74
|
+
trace(`host-cache: ignored weak OCP detect host=${result.id} source=${result.source ?? "unknown"}`);
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
63
77
|
const cacheDir = result.profile.paths.cacheDir;
|
|
64
78
|
if (!cacheDir || cacheDir.length === 0)
|
|
65
79
|
return undefined;
|
|
@@ -80,8 +94,8 @@ export function opencodeGlobalConfigDir() {
|
|
|
80
94
|
*
|
|
81
95
|
* Precedence:
|
|
82
96
|
* 1. {@link setHostCacheDirOverride} / `createCursor({ cacheDir })` (host `Path.cache`)
|
|
83
|
-
* 2. OCP `detect()` when {@link adoptCompatHostCacheDir} ran successfully
|
|
84
|
-
* 3.
|
|
97
|
+
* 2. Strong OCP `detect()` identity when {@link adoptCompatHostCacheDir} ran successfully
|
|
98
|
+
* 3. Explicit host environment / provider install path ({@link resolveHostCacheDir})
|
|
85
99
|
*/
|
|
86
100
|
export function opencodeGlobalCacheDir() {
|
|
87
101
|
if (hostCacheDirOverride)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bound a complete async operation, not only fetch(). Response body readers can
|
|
3
|
+
* ignore abort signals, so Promise.race remains the authoritative deadline.
|
|
4
|
+
*
|
|
5
|
+
* Rejects with `timeoutError()` before aborting so callers observe the domain
|
|
6
|
+
* timeout error rather than a generic AbortError.
|
|
7
|
+
*/
|
|
8
|
+
export declare function withAbortDeadline<T>(timeoutMs: number, timeoutError: () => Error, run: (signal: AbortSignal) => Promise<T>): Promise<T>;
|
package/dist/deadline.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bound a complete async operation, not only fetch(). Response body readers can
|
|
3
|
+
* ignore abort signals, so Promise.race remains the authoritative deadline.
|
|
4
|
+
*
|
|
5
|
+
* Rejects with `timeoutError()` before aborting so callers observe the domain
|
|
6
|
+
* timeout error rather than a generic AbortError.
|
|
7
|
+
*/
|
|
8
|
+
export async function withAbortDeadline(timeoutMs, timeoutError, run) {
|
|
9
|
+
const controller = new AbortController();
|
|
10
|
+
let timer;
|
|
11
|
+
const deadline = new Promise((_, reject) => {
|
|
12
|
+
timer = setTimeout(() => {
|
|
13
|
+
reject(timeoutError());
|
|
14
|
+
controller.abort();
|
|
15
|
+
}, timeoutMs);
|
|
16
|
+
timer.unref?.();
|
|
17
|
+
});
|
|
18
|
+
try {
|
|
19
|
+
return await Promise.race([run(controller.signal), deadline]);
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
if (timer)
|
|
23
|
+
clearTimeout(timer);
|
|
24
|
+
}
|
|
25
|
+
}
|