opencode-ext-connector 0.3.3 → 0.4.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/CHANGELOG.md +23 -0
- package/README.md +129 -28
- package/dist/core/options.d.ts +4 -0
- package/dist/core/options.js +35 -6
- package/dist/opencode/host-options.d.ts +8 -1
- package/dist/opencode/host-options.js +24 -7
- package/dist/opencode/language-factory.d.ts +1 -1
- package/dist/opencode/language-factory.js +3 -0
- package/dist/opencode/ollama-probe.d.ts +2 -1
- package/dist/opencode/ollama-probe.js +2 -2
- package/dist/opencode/ollama-production.d.ts +7 -0
- package/dist/opencode/ollama-production.js +19 -7
- package/dist/opencode/provider-entry.d.ts +1 -0
- package/dist/opencode/providers.d.ts +2 -0
- package/dist/opencode/providers.js +12 -5
- package/dist/opencode/v1-catalog.js +6 -1
- package/dist/opencode/v1-language.js +5 -2
- package/dist/opencode/v1-session-auth.d.ts +2 -1
- package/dist/opencode/v1-session-auth.js +7 -6
- package/dist/providers/command-code/language-model.js +46 -39
- package/dist/providers/command-code/open-generation.d.ts +16 -0
- package/dist/providers/command-code/open-generation.js +45 -0
- package/dist/providers/cursor/credential-retry.d.ts +14 -0
- package/dist/providers/cursor/credential-retry.js +26 -0
- package/dist/providers/cursor/direct-run-failure.d.ts +8 -0
- package/dist/providers/cursor/direct-run-failure.js +26 -0
- package/dist/providers/cursor/direct-run-types.d.ts +1 -0
- package/dist/providers/cursor/direct-run.js +40 -27
- package/dist/providers/cursor/direct-runtime.js +26 -39
- package/dist/providers/cursor/run-session.js +1 -1
- package/dist/providers/ollama/adapter.d.ts +2 -0
- package/dist/providers/ollama/adapter.js +3 -1
- package/dist/providers/ollama/endpoints.d.ts +8 -0
- package/dist/providers/ollama/endpoints.js +38 -0
- package/dist/providers/ollama/index.d.ts +1 -0
- package/dist/providers/ollama/index.js +1 -0
- package/dist/providers/ollama/language-model.d.ts +2 -0
- package/dist/providers/ollama/language-model.js +2 -0
- package/dist/providers/ollama/local-catalog.d.ts +2 -1
- package/dist/providers/ollama/local-catalog.js +3 -3
- package/dist/providers/ollama/runtime.d.ts +2 -0
- package/dist/providers/ollama/runtime.js +5 -5
- package/dist/sdk/ollama.d.ts +4 -1
- package/dist/server.js +33 -7
- package/docs/README.ko.md +129 -28
- package/package.json +1 -1
|
@@ -11,7 +11,7 @@ import { listCursorUsableModels } from "../providers/cursor/models.js";
|
|
|
11
11
|
import { createOllamaAdapter } from "../providers/ollama/adapter.js";
|
|
12
12
|
import { productionOllamaFetch } from "../providers/ollama/http.js";
|
|
13
13
|
import { probeLocalOllama } from "./ollama-probe.js";
|
|
14
|
-
import {
|
|
14
|
+
import { getProductionOllamaBundle } from "./ollama-production.js";
|
|
15
15
|
import { createAnthropicCliAuth } from "./v1-anthropic-auth.js";
|
|
16
16
|
import { createCommandCodeSessionAuth, createCursorSessionAuth, createOllamaSessionAuth, } from "./v1-session-auth.js";
|
|
17
17
|
async function readCommandCodeToken(deps, signal) {
|
|
@@ -43,9 +43,15 @@ export async function selectActiveProviders(entries, isActive) {
|
|
|
43
43
|
}
|
|
44
44
|
export function createProviderRegistry(options = {}) {
|
|
45
45
|
const writeClaudeCredentials = options.writeClaudeCredentials;
|
|
46
|
-
const
|
|
46
|
+
const productionOllama = getProductionOllamaBundle();
|
|
47
|
+
const configuredOllama = options.ollama ?? {
|
|
47
48
|
fetch: productionOllamaFetch,
|
|
48
|
-
catalog:
|
|
49
|
+
catalog: productionOllama.catalog,
|
|
50
|
+
endpoints: productionOllama.endpoints,
|
|
51
|
+
};
|
|
52
|
+
const ollama = {
|
|
53
|
+
...configuredOllama,
|
|
54
|
+
endpoints: configuredOllama.endpoints ?? productionOllama.endpoints,
|
|
49
55
|
};
|
|
50
56
|
return [
|
|
51
57
|
{
|
|
@@ -128,12 +134,13 @@ export function createProviderRegistry(options = {}) {
|
|
|
128
134
|
displayName: "Ollama",
|
|
129
135
|
integrationId: "ollama",
|
|
130
136
|
integrationMethod: { type: "env", names: ["OLLAMA_EXT_CONNECTOR_ENABLED"] },
|
|
137
|
+
providerOptions: Object.freeze({ ollamaBaseURL: ollama.endpoints.baseURL }),
|
|
131
138
|
createAdapter: () => createOllamaAdapter(ollama),
|
|
132
|
-
createAuthHook: () => createOllamaSessionAuth(ollama.fetch),
|
|
139
|
+
createAuthHook: () => createOllamaSessionAuth(ollama.fetch, ollama.endpoints),
|
|
133
140
|
isConnected: async (deps) => {
|
|
134
141
|
if ((await deps.authStore.matchAuth("ollama")) === null)
|
|
135
142
|
return false;
|
|
136
|
-
return probeLocalOllama(ollama.fetch);
|
|
143
|
+
return probeLocalOllama(ollama.fetch, ollama.endpoints);
|
|
137
144
|
},
|
|
138
145
|
},
|
|
139
146
|
];
|
|
@@ -36,7 +36,12 @@ export function createV1CatalogProjector(options) {
|
|
|
36
36
|
}
|
|
37
37
|
const provider = attachedConfig.provider ?? {};
|
|
38
38
|
Object.defineProperty(provider, entry.id, {
|
|
39
|
-
value: {
|
|
39
|
+
value: {
|
|
40
|
+
npm,
|
|
41
|
+
name: entry.displayName,
|
|
42
|
+
models,
|
|
43
|
+
...(entry.providerOptions === undefined ? {} : { options: entry.providerOptions }),
|
|
44
|
+
},
|
|
40
45
|
enumerable: true,
|
|
41
46
|
configurable: true,
|
|
42
47
|
writable: true,
|
|
@@ -4,7 +4,7 @@ import { createClaudeTokenReader } from "../providers/claude/auth.js";
|
|
|
4
4
|
import { readCursorAccessToken } from "../providers/cursor/auth.js";
|
|
5
5
|
import { createCursorDirectRuntime } from "../providers/cursor/direct-runtime.js";
|
|
6
6
|
import { createConnectorLanguage } from "./language-factory.js";
|
|
7
|
-
import {
|
|
7
|
+
import { getProductionOllamaBundle } from "./ollama-production.js";
|
|
8
8
|
const clock = {
|
|
9
9
|
nowMs: () => Date.now(),
|
|
10
10
|
schedule: (delayMs, callback) => {
|
|
@@ -47,12 +47,15 @@ function apiKeyFromOptions(options) {
|
|
|
47
47
|
}
|
|
48
48
|
export function languageForV1Provider(providerID, modelId, options) {
|
|
49
49
|
const commandCodeApiKey = apiKeyFromOptions(options);
|
|
50
|
+
const ollamaRuntime = providerID === "ollama"
|
|
51
|
+
? getProductionOllamaBundle(options["ollamaBaseURL"]).runtime
|
|
52
|
+
: undefined;
|
|
50
53
|
const createLanguage = createConnectorLanguage({
|
|
51
54
|
env,
|
|
52
55
|
transport,
|
|
53
56
|
readClaudeToken,
|
|
54
57
|
cursorRuntime,
|
|
55
|
-
ollamaRuntime:
|
|
58
|
+
...(ollamaRuntime === undefined ? {} : { ollamaRuntime }),
|
|
56
59
|
...(commandCodeApiKey === undefined ? {} : { commandCodeApiKey }),
|
|
57
60
|
});
|
|
58
61
|
const model = createLanguage(providerID, modelId);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AuthHook } from "@opencode-ai/plugin";
|
|
2
|
+
import { type OllamaEndpoints } from "../providers/ollama/endpoints.js";
|
|
2
3
|
import { type OllamaFetch } from "../providers/ollama/http.js";
|
|
3
4
|
export declare function createCursorSessionAuth(env: Readonly<Record<string, string | undefined>>): AuthHook;
|
|
4
5
|
export declare function createCommandCodeSessionAuth(env: Readonly<Record<string, string | undefined>>): AuthHook;
|
|
5
|
-
export declare function createOllamaSessionAuth(fetch?: OllamaFetch): AuthHook;
|
|
6
|
+
export declare function createOllamaSessionAuth(fetch?: OllamaFetch, endpoints?: OllamaEndpoints): AuthHook;
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { readCommandCodeAccessToken } from "../providers/command-code/auth.js";
|
|
2
2
|
import { readCursorAccessToken } from "../providers/cursor/auth.js";
|
|
3
|
+
import { parseOllamaEndpoints } from "../providers/ollama/endpoints.js";
|
|
3
4
|
import { productionOllamaFetch } from "../providers/ollama/http.js";
|
|
4
5
|
import { probeLocalOllama } from "./ollama-probe.js";
|
|
5
6
|
const CURSOR_SESSION_MARKER = "cli-session:cursor";
|
|
6
7
|
const COMMAND_CODE_SESSION_MARKER = "cli-session:command-code";
|
|
7
8
|
const OLLAMA_SESSION_MARKER = "cli-session:ollama";
|
|
8
|
-
const OLLAMA_AVAILABLE_INSTRUCTIONS = "The connector will reuse the running
|
|
9
|
-
const OLLAMA_UNAVAILABLE_INSTRUCTIONS = "Start the
|
|
9
|
+
const OLLAMA_AVAILABLE_INSTRUCTIONS = "The connector will reuse the running configured Ollama daemon. Cloud access remains managed by `ollama signin`; this plugin does not run sign-in.";
|
|
10
|
+
const OLLAMA_UNAVAILABLE_INSTRUCTIONS = "Start the configured Ollama daemon, then retry.";
|
|
10
11
|
function sessionMethod(options) {
|
|
11
12
|
return {
|
|
12
13
|
type: "oauth",
|
|
@@ -68,22 +69,22 @@ export function createCommandCodeSessionAuth(env) {
|
|
|
68
69
|
],
|
|
69
70
|
};
|
|
70
71
|
}
|
|
71
|
-
export function createOllamaSessionAuth(fetch = productionOllamaFetch) {
|
|
72
|
+
export function createOllamaSessionAuth(fetch = productionOllamaFetch, endpoints = parseOllamaEndpoints(undefined)) {
|
|
72
73
|
return {
|
|
73
74
|
provider: "ollama",
|
|
74
75
|
methods: [
|
|
75
76
|
{
|
|
76
77
|
type: "oauth",
|
|
77
|
-
label: "Ollama
|
|
78
|
+
label: "Ollama daemon",
|
|
78
79
|
authorize: async () => {
|
|
79
|
-
const available = await probeLocalOllama(fetch);
|
|
80
|
+
const available = await probeLocalOllama(fetch, endpoints);
|
|
80
81
|
return {
|
|
81
82
|
url: "",
|
|
82
83
|
instructions: available
|
|
83
84
|
? OLLAMA_AVAILABLE_INSTRUCTIONS
|
|
84
85
|
: OLLAMA_UNAVAILABLE_INSTRUCTIONS,
|
|
85
86
|
method: "auto",
|
|
86
|
-
callback: async () => (await probeLocalOllama(fetch))
|
|
87
|
+
callback: async () => (await probeLocalOllama(fetch, endpoints))
|
|
87
88
|
? {
|
|
88
89
|
type: "success",
|
|
89
90
|
provider: "ollama",
|
|
@@ -2,32 +2,32 @@
|
|
|
2
2
|
// Licensed under MIT. See THIRD_PARTY_NOTICES.md.
|
|
3
3
|
import { AdapterError, OperationCancelledError } from "../../core/errors.js";
|
|
4
4
|
import { parseProviderId } from "../../core/ids.js";
|
|
5
|
-
import { openHttpBody } from "../../http/read-body.js";
|
|
6
5
|
import { createCommandCodeVersionResolver } from "./cli-version.js";
|
|
7
6
|
import { emitCommandCodeChunks } from "./emit-stream.js";
|
|
8
7
|
import { commandCodeMissingBodyError } from "./errors.js";
|
|
8
|
+
import { openCommandCodeGeneration } from "./open-generation.js";
|
|
9
9
|
import { buildBody, buildHeaders, } from "./request.js";
|
|
10
|
-
import {
|
|
10
|
+
import { createCommandCodeRequestLifecycle } from "./request-lifecycle.js";
|
|
11
11
|
import { createCommandCodeSessionId } from "./session.js";
|
|
12
|
-
function buildRequestOptions(options
|
|
13
|
-
const
|
|
14
|
-
modelId: options.modelId,
|
|
15
|
-
call,
|
|
16
|
-
sessionId: options.sessionId,
|
|
17
|
-
};
|
|
12
|
+
function buildRequestOptions(options) {
|
|
13
|
+
const { runtime, call, token, cliVersion, bodySnapshot } = options;
|
|
18
14
|
const headerOptions = {
|
|
19
15
|
token,
|
|
20
16
|
cliVersion,
|
|
21
|
-
sessionId:
|
|
17
|
+
sessionId: runtime.sessionId,
|
|
22
18
|
};
|
|
19
|
+
const headers = {};
|
|
20
|
+
for (const source of [buildHeaders(headerOptions), runtime.headers ?? {}, call.headers ?? {}]) {
|
|
21
|
+
for (const [name, value] of Object.entries(source)) {
|
|
22
|
+
if (value !== undefined) {
|
|
23
|
+
headers[name.toLowerCase()] = value;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
23
27
|
return {
|
|
24
|
-
url: `${(
|
|
25
|
-
headers
|
|
26
|
-
|
|
27
|
-
...options.headers,
|
|
28
|
-
...Object.fromEntries(Object.entries(call.headers ?? {}).filter((entry) => entry[1] !== undefined)),
|
|
29
|
-
},
|
|
30
|
-
body: new TextEncoder().encode(JSON.stringify(buildBody(bodyOptions))),
|
|
28
|
+
url: `${(runtime.baseURL ?? "https://api.commandcode.ai").replace(/\/+$/, "")}/alpha/generate`,
|
|
29
|
+
headers,
|
|
30
|
+
body: new Uint8Array(bodySnapshot),
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
33
|
function createUsage() {
|
|
@@ -66,34 +66,41 @@ async function streamCommandCode(options, call) {
|
|
|
66
66
|
providerId: parseProviderId("command-code"),
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
|
-
const
|
|
70
|
-
|
|
69
|
+
const bodyOptions = {
|
|
70
|
+
modelId: options.modelId,
|
|
71
|
+
call,
|
|
72
|
+
sessionId: options.sessionId,
|
|
73
|
+
};
|
|
74
|
+
const bodySnapshot = new TextEncoder().encode(JSON.stringify(buildBody(bodyOptions)));
|
|
75
|
+
let generation;
|
|
71
76
|
try {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
generation = await openCommandCodeGeneration({
|
|
78
|
+
transport: options.transport,
|
|
79
|
+
lifecycle,
|
|
80
|
+
initialToken: token,
|
|
81
|
+
readAccessToken: options.readAccessToken,
|
|
82
|
+
createRequest: (accessToken) => {
|
|
83
|
+
const request = buildRequestOptions({
|
|
84
|
+
runtime: options,
|
|
85
|
+
call,
|
|
86
|
+
token: accessToken,
|
|
87
|
+
cliVersion,
|
|
88
|
+
bodySnapshot,
|
|
89
|
+
});
|
|
90
|
+
return {
|
|
91
|
+
method: "POST",
|
|
92
|
+
url: request.url,
|
|
93
|
+
headers: request.headers,
|
|
94
|
+
body: request.body,
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
});
|
|
78
98
|
}
|
|
79
99
|
catch (error) {
|
|
80
100
|
lifecycle.dispose();
|
|
81
101
|
throw error;
|
|
82
102
|
}
|
|
83
|
-
|
|
84
|
-
let errorBody;
|
|
85
|
-
try {
|
|
86
|
-
errorBody = await readCommandCodeErrorBody(opened.chunks);
|
|
87
|
-
}
|
|
88
|
-
catch (error) {
|
|
89
|
-
lifecycle.abort();
|
|
90
|
-
throw error;
|
|
91
|
-
}
|
|
92
|
-
finally {
|
|
93
|
-
lifecycle.dispose();
|
|
94
|
-
}
|
|
95
|
-
throw commandCodeHttpError(opened.status, errorBody);
|
|
96
|
-
}
|
|
103
|
+
const { opened, request } = generation;
|
|
97
104
|
if (!opened.bodyPresent) {
|
|
98
105
|
lifecycle.dispose();
|
|
99
106
|
throw commandCodeMissingBodyError(opened.status);
|
|
@@ -124,7 +131,7 @@ async function streamCommandCode(options, call) {
|
|
|
124
131
|
});
|
|
125
132
|
return {
|
|
126
133
|
stream,
|
|
127
|
-
request: { body: new TextDecoder().decode(
|
|
134
|
+
request: { body: request.body === null ? undefined : new TextDecoder().decode(request.body) },
|
|
128
135
|
response: { headers: opened.headers },
|
|
129
136
|
};
|
|
130
137
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { HttpRequest, HttpTransport } from "../../core/http.js";
|
|
2
|
+
import { type HttpBodyStream } from "../../http/read-body.js";
|
|
3
|
+
import type { CommandCodeRequestLifecycle } from "./request-lifecycle.js";
|
|
4
|
+
type OpenGenerationOptions = {
|
|
5
|
+
readonly transport: HttpTransport;
|
|
6
|
+
readonly lifecycle: CommandCodeRequestLifecycle;
|
|
7
|
+
readonly initialToken: string;
|
|
8
|
+
readonly readAccessToken: (signal: AbortSignal) => Promise<string | null>;
|
|
9
|
+
readonly createRequest: (token: string) => HttpRequest;
|
|
10
|
+
};
|
|
11
|
+
export type OpenedCommandCodeGeneration = {
|
|
12
|
+
readonly opened: HttpBodyStream;
|
|
13
|
+
readonly request: HttpRequest;
|
|
14
|
+
};
|
|
15
|
+
export declare function openCommandCodeGeneration(options: OpenGenerationOptions): Promise<OpenedCommandCodeGeneration>;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Derived from brent-weatherall/opencode-commandcode-provider src/model.ts.
|
|
2
|
+
// Licensed under MIT. See THIRD_PARTY_NOTICES.md.
|
|
3
|
+
import { OperationCancelledError } from "../../core/errors.js";
|
|
4
|
+
import { openHttpBody } from "../../http/read-body.js";
|
|
5
|
+
import { commandCodeHttpError, readCommandCodeErrorBody } from "./request-lifecycle.js";
|
|
6
|
+
async function openAttempt(options, request) {
|
|
7
|
+
const opened = await openHttpBody(options.transport, request, options.lifecycle.signal);
|
|
8
|
+
return { opened, request };
|
|
9
|
+
}
|
|
10
|
+
async function responseError(opened, lifecycle) {
|
|
11
|
+
try {
|
|
12
|
+
const body = await readCommandCodeErrorBody(opened.chunks);
|
|
13
|
+
return commandCodeHttpError(opened.status, body);
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
lifecycle.abort();
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function successful(opened) {
|
|
21
|
+
return opened.status >= 200 && opened.status < 300;
|
|
22
|
+
}
|
|
23
|
+
export async function openCommandCodeGeneration(options) {
|
|
24
|
+
const firstRequest = options.createRequest(options.initialToken);
|
|
25
|
+
const first = await openAttempt(options, firstRequest);
|
|
26
|
+
if (successful(first.opened))
|
|
27
|
+
return first;
|
|
28
|
+
const firstError = await responseError(first.opened, options.lifecycle);
|
|
29
|
+
if (first.opened.status !== 401)
|
|
30
|
+
throw firstError;
|
|
31
|
+
const reloadedToken = await options.readAccessToken(options.lifecycle.signal);
|
|
32
|
+
if (options.lifecycle.signal.aborted) {
|
|
33
|
+
throw new OperationCancelledError("command-code-stream");
|
|
34
|
+
}
|
|
35
|
+
if (reloadedToken === null)
|
|
36
|
+
throw firstError;
|
|
37
|
+
const candidateRequest = options.createRequest(reloadedToken);
|
|
38
|
+
if (candidateRequest.headers["authorization"] === firstRequest.headers["authorization"]) {
|
|
39
|
+
throw firstError;
|
|
40
|
+
}
|
|
41
|
+
const second = await openAttempt(options, candidateRequest);
|
|
42
|
+
if (successful(second.opened))
|
|
43
|
+
return second;
|
|
44
|
+
throw await responseError(second.opened, options.lifecycle);
|
|
45
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CursorReplayState } from "./recovery.js";
|
|
2
|
+
export declare function isCursorCredentialRetryEligible(input: {
|
|
3
|
+
readonly error: unknown;
|
|
4
|
+
readonly replay: CursorReplayState;
|
|
5
|
+
readonly retryUsed: boolean;
|
|
6
|
+
}): boolean;
|
|
7
|
+
export type CursorCredentialRetry = {
|
|
8
|
+
readonly currentToken: () => string;
|
|
9
|
+
readonly reload: (error: unknown, replay: CursorReplayState, signal: AbortSignal) => Promise<boolean>;
|
|
10
|
+
};
|
|
11
|
+
export declare function createCursorCredentialRetry(options: {
|
|
12
|
+
readonly initialToken: string;
|
|
13
|
+
readonly reloadAccessToken: (signal: AbortSignal) => Promise<string | null>;
|
|
14
|
+
}): CursorCredentialRetry;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { CursorDirectStreamError } from "./direct-stream.js";
|
|
2
|
+
export function isCursorCredentialRetryEligible(input) {
|
|
3
|
+
return (!input.retryUsed &&
|
|
4
|
+
input.error instanceof CursorDirectStreamError &&
|
|
5
|
+
input.error.bridgeCode === "http-401" &&
|
|
6
|
+
input.replay.outputEpoch === 0 &&
|
|
7
|
+
input.replay.checkpointEpoch === null &&
|
|
8
|
+
!input.replay.toolBoundary);
|
|
9
|
+
}
|
|
10
|
+
export function createCursorCredentialRetry(options) {
|
|
11
|
+
let currentToken = options.initialToken;
|
|
12
|
+
let retryUsed = false;
|
|
13
|
+
return {
|
|
14
|
+
currentToken: () => currentToken,
|
|
15
|
+
reload: async (error, replay, signal) => {
|
|
16
|
+
if (!isCursorCredentialRetryEligible({ error, replay, retryUsed }))
|
|
17
|
+
return false;
|
|
18
|
+
retryUsed = true;
|
|
19
|
+
const reloadedToken = await options.reloadAccessToken(signal);
|
|
20
|
+
if (reloadedToken === null || reloadedToken === currentToken)
|
|
21
|
+
return false;
|
|
22
|
+
currentToken = reloadedToken;
|
|
23
|
+
return true;
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CursorDirectSetupCleanup } from "./direct-run-types.js";
|
|
2
|
+
import type { CursorRunSession } from "./run-session.js";
|
|
3
|
+
import type { CursorStreamAdapter } from "./stream-adapter.js";
|
|
4
|
+
export declare function retireCursorSessionForRetry(session: CursorRunSession, cause: unknown): Promise<void>;
|
|
5
|
+
export declare function failCursorDirectRun(logical: {
|
|
6
|
+
readonly adapter: CursorStreamAdapter;
|
|
7
|
+
readonly cleanup: CursorDirectSetupCleanup;
|
|
8
|
+
}, session: CursorRunSession | null, error: unknown): Promise<void>;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export async function retireCursorSessionForRetry(session, cause) {
|
|
2
|
+
try {
|
|
3
|
+
const retireForRetry = session.retireForRetry;
|
|
4
|
+
if (retireForRetry === undefined)
|
|
5
|
+
throw new TypeError("retry retirement is unavailable");
|
|
6
|
+
await retireForRetry();
|
|
7
|
+
}
|
|
8
|
+
catch (retirementError) {
|
|
9
|
+
throw new AggregateError([cause, retirementError], "Cursor retry retirement failed");
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export async function failCursorDirectRun(logical, session, error) {
|
|
13
|
+
try {
|
|
14
|
+
if (session === null) {
|
|
15
|
+
logical.cleanup.invalidateCheckpoint();
|
|
16
|
+
logical.cleanup.invalidateSession();
|
|
17
|
+
}
|
|
18
|
+
else
|
|
19
|
+
await session.abort();
|
|
20
|
+
}
|
|
21
|
+
catch (cleanupError) {
|
|
22
|
+
logical.adapter.fail(new AggregateError([error, cleanupError], "Cursor recovery cleanup failed"));
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
logical.adapter.fail(error);
|
|
26
|
+
}
|
|
@@ -25,6 +25,7 @@ export type CursorDirectRunOptions = {
|
|
|
25
25
|
readonly createSetupCleanup?: (resources: CursorDirectSetupCleanupResources) => CursorDirectSetupCleanup;
|
|
26
26
|
readonly idleTimeoutMs: number;
|
|
27
27
|
readonly modelId: string;
|
|
28
|
+
readonly reloadAccessToken: (signal: AbortSignal) => Promise<string | null>;
|
|
28
29
|
readonly registry: CursorRunSessionRegistry;
|
|
29
30
|
readonly signal: AbortSignal;
|
|
30
31
|
readonly token: string;
|
|
@@ -3,6 +3,8 @@ import { parseModelId } from "../../core/ids.js";
|
|
|
3
3
|
import { createCursorBlobStore } from "./blob-store.js";
|
|
4
4
|
import { createCursorCheckpointStore } from "./checkpoint-store.js";
|
|
5
5
|
import { encodeConnectFrame } from "./connect-frame.js";
|
|
6
|
+
import { createCursorCredentialRetry } from "./credential-retry.js";
|
|
7
|
+
import { failCursorDirectRun, retireCursorSessionForRetry } from "./direct-run-failure.js";
|
|
6
8
|
import { consumeCursorDirectAttempt, isCursorRetryableStreamError } from "./direct-stream.js";
|
|
7
9
|
import { cursorMcpDefinitions } from "./exec-reply.js";
|
|
8
10
|
import { cursorPromptText } from "./prompt.js";
|
|
@@ -76,13 +78,13 @@ function buildRequest(options, logical, mode) {
|
|
|
76
78
|
},
|
|
77
79
|
});
|
|
78
80
|
}
|
|
79
|
-
async function openAttempt(options, logical,
|
|
80
|
-
const request = buildRequest(options, logical, mode);
|
|
81
|
+
async function openAttempt(options, logical, attempt) {
|
|
82
|
+
const request = buildRequest(options, logical, attempt.mode);
|
|
81
83
|
let stream = null;
|
|
82
84
|
try {
|
|
83
85
|
stream = await options.bridge.open({
|
|
84
86
|
id: options.createId(),
|
|
85
|
-
accessToken:
|
|
87
|
+
accessToken: attempt.token,
|
|
86
88
|
path: CURSOR_RUN_PATH,
|
|
87
89
|
headers: { "content-type": "application/connect+proto", "connect-protocol-version": "1" },
|
|
88
90
|
signal: options.signal,
|
|
@@ -123,27 +125,19 @@ async function openAttempt(options, logical, mode) {
|
|
|
123
125
|
throw error;
|
|
124
126
|
}
|
|
125
127
|
}
|
|
126
|
-
async function failRun(logical, session, error) {
|
|
127
|
-
try {
|
|
128
|
-
if (session === null) {
|
|
129
|
-
logical.cleanup.invalidateCheckpoint();
|
|
130
|
-
logical.cleanup.invalidateSession();
|
|
131
|
-
}
|
|
132
|
-
else
|
|
133
|
-
await session.abort();
|
|
134
|
-
}
|
|
135
|
-
catch (cleanupError) {
|
|
136
|
-
logical.adapter.fail(new AggregateError([error, cleanupError], "Cursor recovery cleanup failed"));
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
logical.adapter.fail(error);
|
|
140
|
-
}
|
|
141
128
|
export async function startCursorDirectRun(options) {
|
|
142
129
|
const logical = createLogicalRun(options);
|
|
143
130
|
const planner = createCursorRecoveryPlanner();
|
|
131
|
+
const credential = createCursorCredentialRetry({
|
|
132
|
+
initialToken: options.token,
|
|
133
|
+
reloadAccessToken: options.reloadAccessToken,
|
|
134
|
+
});
|
|
144
135
|
let session;
|
|
145
136
|
try {
|
|
146
|
-
session = await openAttempt(options, logical,
|
|
137
|
+
session = await openAttempt(options, logical, {
|
|
138
|
+
mode: "initial",
|
|
139
|
+
token: credential.currentToken(),
|
|
140
|
+
});
|
|
147
141
|
}
|
|
148
142
|
catch (error) {
|
|
149
143
|
logical.cleanup.releaseOwnership();
|
|
@@ -177,7 +171,25 @@ export async function startCursorDirectRun(options) {
|
|
|
177
171
|
const idle = watchdog.expired();
|
|
178
172
|
watchdog.dispose();
|
|
179
173
|
if (options.signal.aborted) {
|
|
180
|
-
await
|
|
174
|
+
await failCursorDirectRun(logical, session, new OperationCancelledError("cursor-direct-stream"));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
const changed = await credential.reload(error, logical.adapter.replayState(), options.signal);
|
|
179
|
+
if (options.signal.aborted)
|
|
180
|
+
throw new OperationCancelledError("cursor-direct-stream");
|
|
181
|
+
if (changed) {
|
|
182
|
+
await retireCursorSessionForRetry(session, error);
|
|
183
|
+
session = null;
|
|
184
|
+
session = await openAttempt(options, logical, {
|
|
185
|
+
mode: "initial",
|
|
186
|
+
token: credential.currentToken(),
|
|
187
|
+
});
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch (retryError) {
|
|
192
|
+
await failCursorDirectRun(logical, session, retryError);
|
|
181
193
|
return;
|
|
182
194
|
}
|
|
183
195
|
const decision = planner.next({
|
|
@@ -190,20 +202,21 @@ export async function startCursorDirectRun(options) {
|
|
|
190
202
|
planner.requireRetry(decision, error);
|
|
191
203
|
}
|
|
192
204
|
catch (recoveryError) {
|
|
193
|
-
await
|
|
205
|
+
await failCursorDirectRun(logical, session, recoveryError);
|
|
194
206
|
}
|
|
195
207
|
return;
|
|
196
208
|
}
|
|
197
209
|
try {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
throw new TypeError("retry retirement is unavailable");
|
|
201
|
-
await retireForRetry();
|
|
210
|
+
await retireCursorSessionForRetry(session, error);
|
|
211
|
+
session = null;
|
|
202
212
|
logical.adapter.suspendForRetry();
|
|
203
|
-
session = await openAttempt(options, logical,
|
|
213
|
+
session = await openAttempt(options, logical, {
|
|
214
|
+
mode: decision.mode,
|
|
215
|
+
token: credential.currentToken(),
|
|
216
|
+
});
|
|
204
217
|
}
|
|
205
218
|
catch (retryError) {
|
|
206
|
-
await
|
|
219
|
+
await failCursorDirectRun(logical, session, retryError);
|
|
207
220
|
return;
|
|
208
221
|
}
|
|
209
222
|
}
|
|
@@ -41,7 +41,9 @@ export function createCursorDirectRuntime(options) {
|
|
|
41
41
|
return bridgeClient;
|
|
42
42
|
};
|
|
43
43
|
const doStream = async (call, modelId) => {
|
|
44
|
-
const signal = call.abortSignal
|
|
44
|
+
const signal = call.abortSignal === undefined
|
|
45
|
+
? lifecycle.signal
|
|
46
|
+
: AbortSignal.any([lifecycle.signal, call.abortSignal]);
|
|
45
47
|
if (signal.aborted)
|
|
46
48
|
throw new OperationCancelledError("cursor-direct-stream");
|
|
47
49
|
const resultCallIds = [];
|
|
@@ -59,46 +61,31 @@ export function createCursorDirectRuntime(options) {
|
|
|
59
61
|
await existing.writeContinuations(continuations, signal);
|
|
60
62
|
return { stream: await consumeCursorDirectSession({ session: existing, signal, registry }) };
|
|
61
63
|
}
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
modelId,
|
|
85
|
-
registry,
|
|
86
|
-
signal,
|
|
87
|
-
token,
|
|
88
|
-
tools: toolsFromCall(call),
|
|
89
|
-
ttlMs: options.ttlMs ?? 300_000,
|
|
90
|
-
...(options.createSetupCleanup === undefined
|
|
91
|
-
? {}
|
|
92
|
-
: { createSetupCleanup: options.createSetupCleanup }),
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
finally {
|
|
96
|
-
lifecycle.signal.removeEventListener("abort", cancel);
|
|
97
|
-
call.abortSignal?.removeEventListener("abort", cancel);
|
|
98
|
-
}
|
|
64
|
+
const token = await options.readAccessToken(signal);
|
|
65
|
+
if (signal.aborted)
|
|
66
|
+
throw new OperationCancelledError("cursor-direct-stream");
|
|
67
|
+
if (token === null)
|
|
68
|
+
throw cursorFailure("cursor-auth-unavailable");
|
|
69
|
+
return await startCursorDirectRun({
|
|
70
|
+
bridge: await bridge(),
|
|
71
|
+
call,
|
|
72
|
+
clock: options.clock,
|
|
73
|
+
createId,
|
|
74
|
+
idleTimeoutMs: options.idleTimeoutMs ?? 60_000,
|
|
75
|
+
modelId,
|
|
76
|
+
reloadAccessToken: options.readAccessToken,
|
|
77
|
+
registry,
|
|
78
|
+
signal,
|
|
79
|
+
token,
|
|
80
|
+
tools: toolsFromCall(call),
|
|
81
|
+
ttlMs: options.ttlMs ?? 300_000,
|
|
82
|
+
...(options.createSetupCleanup === undefined
|
|
83
|
+
? {}
|
|
84
|
+
: { createSetupCleanup: options.createSetupCleanup }),
|
|
85
|
+
});
|
|
99
86
|
};
|
|
100
87
|
const disposal = createAsyncDisposable(async () => {
|
|
101
|
-
lifecycle.abort();
|
|
88
|
+
lifecycle.abort(new OperationCancelledError("cursor-direct-stream"));
|
|
102
89
|
await settleCursorCleanup([
|
|
103
90
|
registry.dispose,
|
|
104
91
|
async () => {
|
|
@@ -150,12 +150,12 @@ export function createCursorRunSessionRegistry(_options) {
|
|
|
150
150
|
retireForRetry: async () => {
|
|
151
151
|
if (resources.dispatcher.parkedCalls.size > 0 || reserved.size > 0)
|
|
152
152
|
throw new CursorRunSessionError("retry-boundary");
|
|
153
|
-
retired = true;
|
|
154
153
|
cancelTimer();
|
|
155
154
|
sessions.delete(identity.sessionId);
|
|
156
155
|
removeOwnership(identity.sessionId);
|
|
157
156
|
resources.dispatcher.parkedCalls.clear();
|
|
158
157
|
await settleCursorCleanup([resources.stream.abort, resources.ownership.release]);
|
|
158
|
+
retired = true;
|
|
159
159
|
},
|
|
160
160
|
dispose: disposal.dispose,
|
|
161
161
|
};
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { ProviderAdapter } from "../../core/adapter.js";
|
|
2
2
|
import type { OllamaCatalogState } from "./catalog-state.js";
|
|
3
|
+
import { type OllamaEndpoints } from "./endpoints.js";
|
|
3
4
|
import type { OllamaFetch } from "./http.js";
|
|
4
5
|
export type OllamaAdapterOptions = {
|
|
5
6
|
readonly fetch: OllamaFetch;
|
|
6
7
|
readonly catalog: OllamaCatalogState;
|
|
8
|
+
readonly endpoints?: OllamaEndpoints;
|
|
7
9
|
};
|
|
8
10
|
export declare function createOllamaAdapter(options: OllamaAdapterOptions): ProviderAdapter;
|