opencode-ext-connector 0.3.2 → 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.
Files changed (46) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +160 -40
  3. package/dist/core/options.d.ts +4 -0
  4. package/dist/core/options.js +35 -6
  5. package/dist/opencode/host-options.d.ts +8 -1
  6. package/dist/opencode/host-options.js +24 -7
  7. package/dist/opencode/language-factory.d.ts +1 -1
  8. package/dist/opencode/language-factory.js +3 -0
  9. package/dist/opencode/ollama-probe.d.ts +2 -1
  10. package/dist/opencode/ollama-probe.js +2 -2
  11. package/dist/opencode/ollama-production.d.ts +7 -0
  12. package/dist/opencode/ollama-production.js +19 -7
  13. package/dist/opencode/provider-entry.d.ts +1 -0
  14. package/dist/opencode/providers.d.ts +2 -0
  15. package/dist/opencode/providers.js +12 -5
  16. package/dist/opencode/v1-catalog.js +6 -1
  17. package/dist/opencode/v1-language.js +5 -2
  18. package/dist/opencode/v1-session-auth.d.ts +2 -1
  19. package/dist/opencode/v1-session-auth.js +7 -6
  20. package/dist/providers/command-code/language-model.js +46 -39
  21. package/dist/providers/command-code/open-generation.d.ts +16 -0
  22. package/dist/providers/command-code/open-generation.js +45 -0
  23. package/dist/providers/cursor/credential-retry.d.ts +14 -0
  24. package/dist/providers/cursor/credential-retry.js +26 -0
  25. package/dist/providers/cursor/direct-run-failure.d.ts +8 -0
  26. package/dist/providers/cursor/direct-run-failure.js +26 -0
  27. package/dist/providers/cursor/direct-run-types.d.ts +1 -0
  28. package/dist/providers/cursor/direct-run.js +40 -27
  29. package/dist/providers/cursor/direct-runtime.js +26 -39
  30. package/dist/providers/cursor/run-session.js +1 -1
  31. package/dist/providers/ollama/adapter.d.ts +2 -0
  32. package/dist/providers/ollama/adapter.js +3 -1
  33. package/dist/providers/ollama/endpoints.d.ts +8 -0
  34. package/dist/providers/ollama/endpoints.js +38 -0
  35. package/dist/providers/ollama/index.d.ts +1 -0
  36. package/dist/providers/ollama/index.js +1 -0
  37. package/dist/providers/ollama/language-model.d.ts +2 -0
  38. package/dist/providers/ollama/language-model.js +2 -0
  39. package/dist/providers/ollama/local-catalog.d.ts +2 -1
  40. package/dist/providers/ollama/local-catalog.js +3 -3
  41. package/dist/providers/ollama/runtime.d.ts +2 -0
  42. package/dist/providers/ollama/runtime.js +5 -5
  43. package/dist/sdk/ollama.d.ts +4 -1
  44. package/dist/server.js +33 -7
  45. package/docs/README.ko.md +160 -40
  46. package/package.json +1 -1
@@ -19,6 +19,7 @@ export type ProviderEntry = {
19
19
  readonly integrationId: string;
20
20
  readonly integrationMethod: IntegrationEnvMethod;
21
21
  readonly fallbackModelIds?: readonly string[];
22
+ readonly providerOptions?: Readonly<Record<string, unknown>>;
22
23
  readonly createAdapter: (deps: ProviderEntryDeps) => ProviderAdapter;
23
24
  readonly createAuthHook: (deps: ProviderEntryDeps) => AuthHook;
24
25
  readonly isConnected: (deps: ProviderEntryDeps) => Promise<boolean>;
@@ -1,5 +1,6 @@
1
1
  import type { ClaudeCredentials } from "../providers/claude/credentials.js";
2
2
  import type { OllamaCatalogState } from "../providers/ollama/catalog-state.js";
3
+ import type { OllamaEndpoints } from "../providers/ollama/endpoints.js";
3
4
  import { type OllamaFetch } from "../providers/ollama/http.js";
4
5
  import type { ProviderEntry } from "./provider-entry.js";
5
6
  export type ProviderConnectionActive = (integrationId: string) => Promise<boolean>;
@@ -9,6 +10,7 @@ export type ProviderRegistryOptions = {
9
10
  readonly ollama?: {
10
11
  readonly fetch: OllamaFetch;
11
12
  readonly catalog: OllamaCatalogState;
13
+ readonly endpoints?: OllamaEndpoints;
12
14
  };
13
15
  };
14
16
  export declare function selectConfiguredProviders(entries: readonly ProviderEntry[], providerIds: readonly string[]): readonly ProviderEntry[];
@@ -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 { productionOllamaCatalog } from "./ollama-production.js";
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 ollama = options.ollama ?? {
46
+ const productionOllama = getProductionOllamaBundle();
47
+ const configuredOllama = options.ollama ?? {
47
48
  fetch: productionOllamaFetch,
48
- catalog: productionOllamaCatalog,
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: { npm, name: entry.displayName, models },
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 { productionOllamaRuntime } from "./ollama-production.js";
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: productionOllamaRuntime,
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 local Ollama daemon. Cloud access remains managed by `ollama signin`; this plugin does not run sign-in.";
9
- const OLLAMA_UNAVAILABLE_INSTRUCTIONS = "Start the local Ollama daemon, then retry.";
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 local daemon",
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 { commandCodeHttpError, createCommandCodeRequestLifecycle, readCommandCodeErrorBody, } from "./request-lifecycle.js";
10
+ import { createCommandCodeRequestLifecycle } from "./request-lifecycle.js";
11
11
  import { createCommandCodeSessionId } from "./session.js";
12
- function buildRequestOptions(options, call, token, cliVersion) {
13
- const bodyOptions = {
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: options.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: `${(options.baseURL ?? "https://api.commandcode.ai").replace(/\/+$/, "")}/alpha/generate`,
25
- headers: {
26
- ...buildHeaders(headerOptions),
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 requestOptions = buildRequestOptions(options, call, token, cliVersion);
70
- let opened;
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
- opened = await openHttpBody(options.transport, {
73
- method: "POST",
74
- url: requestOptions.url,
75
- headers: requestOptions.headers,
76
- body: requestOptions.body,
77
- }, lifecycle.signal);
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
- if (opened.status < 200 || opened.status >= 300) {
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(requestOptions.body) },
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, mode) {
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: options.token,
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, "initial");
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 failRun(logical, session, new OperationCancelledError("cursor-direct-stream"));
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 failRun(logical, session, recoveryError);
205
+ await failCursorDirectRun(logical, session, recoveryError);
194
206
  }
195
207
  return;
196
208
  }
197
209
  try {
198
- const retireForRetry = session.retireForRetry;
199
- if (retireForRetry === undefined)
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, decision.mode);
213
+ session = await openAttempt(options, logical, {
214
+ mode: decision.mode,
215
+ token: credential.currentToken(),
216
+ });
204
217
  }
205
218
  catch (retryError) {
206
- await failRun(logical, session, retryError);
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 ?? new AbortController().signal;
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 setup = new AbortController();
63
- const cancel = () => {
64
- if (!setup.signal.aborted) {
65
- setup.abort(new OperationCancelledError("cursor-direct-stream"));
66
- }
67
- };
68
- lifecycle.signal.addEventListener("abort", cancel, { once: true });
69
- call.abortSignal?.addEventListener("abort", cancel, { once: true });
70
- if (lifecycle.signal.aborted || signal.aborted)
71
- cancel();
72
- try {
73
- const token = await options.readAccessToken(setup.signal);
74
- if (setup.signal.aborted)
75
- throw new OperationCancelledError("cursor-direct-stream");
76
- if (token === null)
77
- throw cursorFailure("cursor-auth-unavailable");
78
- return await startCursorDirectRun({
79
- bridge: await bridge(),
80
- call,
81
- clock: options.clock,
82
- createId,
83
- idleTimeoutMs: options.idleTimeoutMs ?? 60_000,
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
  };