opencode-ext-connector 0.3.3 → 0.5.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 +34 -0
- package/README.md +165 -28
- package/dist/core/options.d.ts +20 -0
- package/dist/core/options.js +66 -5
- package/dist/opencode/host-options.d.ts +9 -1
- package/dist/opencode/host-options.js +27 -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/process/production-supervisor.d.ts +20 -0
- package/dist/process/production-supervisor.js +171 -0
- package/dist/providers/claude/credential-authority-scheduler.d.ts +23 -0
- package/dist/providers/claude/credential-authority-scheduler.js +181 -0
- 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 +55 -13
- package/docs/README.ko.md +165 -28
- package/package.json +1 -1
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import { type OllamaCatalogState } from "../providers/ollama/catalog-state.js";
|
|
2
|
+
import { type OllamaEndpoints } from "../providers/ollama/endpoints.js";
|
|
2
3
|
import { type OllamaRuntime } from "../providers/ollama/runtime.js";
|
|
4
|
+
export type ProductionOllamaBundle = {
|
|
5
|
+
readonly endpoints: OllamaEndpoints;
|
|
6
|
+
readonly catalog: OllamaCatalogState;
|
|
7
|
+
readonly runtime: OllamaRuntime;
|
|
8
|
+
};
|
|
9
|
+
export declare function getProductionOllamaBundle(input?: unknown): ProductionOllamaBundle;
|
|
3
10
|
export declare const productionOllamaCatalog: OllamaCatalogState;
|
|
4
11
|
export declare const productionOllamaRuntime: OllamaRuntime;
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
import { createOllamaCatalogState, } from "../providers/ollama/catalog-state.js";
|
|
2
|
+
import { parseOllamaEndpoints } from "../providers/ollama/endpoints.js";
|
|
2
3
|
import { productionOllamaFetch } from "../providers/ollama/http.js";
|
|
3
4
|
import { createOllamaRuntime } from "../providers/ollama/runtime.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
});
|
|
5
|
+
const bundles = new Map();
|
|
6
|
+
export function getProductionOllamaBundle(input) {
|
|
7
|
+
const endpoints = parseOllamaEndpoints(input);
|
|
8
|
+
const existing = bundles.get(endpoints.baseURL);
|
|
9
|
+
if (existing !== undefined)
|
|
10
|
+
return existing;
|
|
11
|
+
const catalog = createOllamaCatalogState({ fetch: productionOllamaFetch });
|
|
12
|
+
const bundle = Object.freeze({
|
|
13
|
+
endpoints,
|
|
14
|
+
catalog,
|
|
15
|
+
runtime: createOllamaRuntime({ endpoints, catalog, fetch: productionOllamaFetch }),
|
|
16
|
+
});
|
|
17
|
+
bundles.set(endpoints.baseURL, bundle);
|
|
18
|
+
return bundle;
|
|
19
|
+
}
|
|
20
|
+
const defaultBundle = getProductionOllamaBundle();
|
|
21
|
+
export const productionOllamaCatalog = defaultBundle.catalog;
|
|
22
|
+
export const productionOllamaRuntime = defaultBundle.runtime;
|
|
@@ -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 {
|
|
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",
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ProcessCommand, ProcessExit, ProcessSupervisor } from "../core/process.js";
|
|
2
|
+
export type ProcessSpawnOptions = {
|
|
3
|
+
readonly shell: false;
|
|
4
|
+
readonly stdio: "ignore";
|
|
5
|
+
readonly windowsHide: true;
|
|
6
|
+
};
|
|
7
|
+
export interface SpawnedChild {
|
|
8
|
+
readonly exitCode: number | null;
|
|
9
|
+
readonly signalCode: string | null;
|
|
10
|
+
onSpawn(listener: () => void): () => void;
|
|
11
|
+
onError(listener: (error: Error) => void): () => void;
|
|
12
|
+
onExit(listener: (exit: ProcessExit) => void): () => void;
|
|
13
|
+
kill(signal: "SIGTERM" | "SIGKILL"): boolean;
|
|
14
|
+
}
|
|
15
|
+
export type ProcessSpawn = (command: ProcessCommand, options: ProcessSpawnOptions) => SpawnedChild;
|
|
16
|
+
export type ProductionProcessSupervisorOptions = {
|
|
17
|
+
readonly spawn?: ProcessSpawn;
|
|
18
|
+
readonly terminationGraceMs?: number;
|
|
19
|
+
};
|
|
20
|
+
export declare function createProductionProcessSupervisor(options?: ProductionProcessSupervisorOptions): ProcessSupervisor;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { OperationCancelledError, ProcessSupervisorError, ResourceDisposedError, } from "../core/errors.js";
|
|
3
|
+
import { createAsyncDisposable } from "../core/lifecycle.js";
|
|
4
|
+
const spawnOptions = {
|
|
5
|
+
shell: false,
|
|
6
|
+
stdio: "ignore",
|
|
7
|
+
windowsHide: true,
|
|
8
|
+
};
|
|
9
|
+
function spawnNodeChild(command, options) {
|
|
10
|
+
const child = spawn(command.executable, [...command.arguments], {
|
|
11
|
+
cwd: command.cwd ?? undefined,
|
|
12
|
+
shell: options.shell,
|
|
13
|
+
stdio: options.stdio,
|
|
14
|
+
windowsHide: options.windowsHide,
|
|
15
|
+
});
|
|
16
|
+
return {
|
|
17
|
+
get exitCode() {
|
|
18
|
+
return child.exitCode;
|
|
19
|
+
},
|
|
20
|
+
get signalCode() {
|
|
21
|
+
return child.signalCode;
|
|
22
|
+
},
|
|
23
|
+
onSpawn: (listener) => {
|
|
24
|
+
child.once("spawn", listener);
|
|
25
|
+
return () => child.removeListener("spawn", listener);
|
|
26
|
+
},
|
|
27
|
+
onError: (listener) => {
|
|
28
|
+
child.once("error", listener);
|
|
29
|
+
return () => child.removeListener("error", listener);
|
|
30
|
+
},
|
|
31
|
+
onExit: (listener) => {
|
|
32
|
+
const onExit = (code, signal) => {
|
|
33
|
+
listener(code === null ? { kind: "signal", signal: signal ?? "unknown" } : { kind: "code", code });
|
|
34
|
+
};
|
|
35
|
+
child.once("exit", onExit);
|
|
36
|
+
return () => child.removeListener("exit", onExit);
|
|
37
|
+
},
|
|
38
|
+
kill: (signal) => child.kill(signal),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function processFailure(operation, cause) {
|
|
42
|
+
return new ProcessSupervisorError({ operation, retryable: true, cause });
|
|
43
|
+
}
|
|
44
|
+
class ProductionSupervisedProcess {
|
|
45
|
+
completion;
|
|
46
|
+
spawned;
|
|
47
|
+
disposal;
|
|
48
|
+
constructor(child, terminationGraceMs) {
|
|
49
|
+
this.spawned = new Promise((resolve, reject) => {
|
|
50
|
+
const removeSpawn = child.onSpawn(() => {
|
|
51
|
+
removeError();
|
|
52
|
+
resolve();
|
|
53
|
+
});
|
|
54
|
+
const removeError = child.onError((error) => {
|
|
55
|
+
removeSpawn();
|
|
56
|
+
reject(processFailure("spawn", error));
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
this.completion = new Promise((resolve, reject) => {
|
|
60
|
+
const removeExit = child.onExit((exit) => {
|
|
61
|
+
removeError();
|
|
62
|
+
resolve(exit);
|
|
63
|
+
});
|
|
64
|
+
const removeError = child.onError((error) => {
|
|
65
|
+
removeExit();
|
|
66
|
+
reject(processFailure("wait", error));
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
this.disposal = createAsyncDisposable(async () => {
|
|
70
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
71
|
+
return;
|
|
72
|
+
child.kill("SIGTERM");
|
|
73
|
+
let timer;
|
|
74
|
+
await Promise.race([
|
|
75
|
+
this.completion,
|
|
76
|
+
new Promise((resolve) => {
|
|
77
|
+
timer = setTimeout(resolve, terminationGraceMs);
|
|
78
|
+
timer.unref();
|
|
79
|
+
}),
|
|
80
|
+
]);
|
|
81
|
+
if (timer !== undefined)
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
84
|
+
if (!child.kill("SIGKILL"))
|
|
85
|
+
throw processFailure("terminate", null);
|
|
86
|
+
await this.completion;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
ready(signal) {
|
|
91
|
+
return this.raceCancellation(this.spawned, signal, "start-process");
|
|
92
|
+
}
|
|
93
|
+
wait(signal) {
|
|
94
|
+
return this.raceCancellation(this.completion, signal, "wait-process");
|
|
95
|
+
}
|
|
96
|
+
terminate() {
|
|
97
|
+
return this.disposal.dispose();
|
|
98
|
+
}
|
|
99
|
+
dispose() {
|
|
100
|
+
return this.disposal.dispose();
|
|
101
|
+
}
|
|
102
|
+
[Symbol.asyncDispose]() {
|
|
103
|
+
return this.disposal.dispose();
|
|
104
|
+
}
|
|
105
|
+
raceCancellation(operation, signal, name) {
|
|
106
|
+
if (signal.aborted)
|
|
107
|
+
return Promise.reject(new OperationCancelledError(name));
|
|
108
|
+
const deferred = Promise.withResolvers();
|
|
109
|
+
const onAbort = () => deferred.reject(new OperationCancelledError(name));
|
|
110
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
111
|
+
operation.then((value) => {
|
|
112
|
+
signal.removeEventListener("abort", onAbort);
|
|
113
|
+
deferred.resolve(value);
|
|
114
|
+
}, (error) => {
|
|
115
|
+
signal.removeEventListener("abort", onAbort);
|
|
116
|
+
deferred.reject(error);
|
|
117
|
+
});
|
|
118
|
+
return deferred.promise;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
export function createProductionProcessSupervisor(options = {}) {
|
|
122
|
+
const spawnProcess = options.spawn ?? spawnNodeChild;
|
|
123
|
+
const terminationGraceMs = options.terminationGraceMs ?? 1_000;
|
|
124
|
+
const active = new Set();
|
|
125
|
+
const disposalController = new AbortController();
|
|
126
|
+
let disposalStarted = false;
|
|
127
|
+
const disposal = createAsyncDisposable(async () => {
|
|
128
|
+
disposalStarted = true;
|
|
129
|
+
disposalController.abort();
|
|
130
|
+
await Promise.all([...active].map((process) => process.terminate()));
|
|
131
|
+
});
|
|
132
|
+
const start = async (command, signal) => {
|
|
133
|
+
if (signal.aborted)
|
|
134
|
+
throw new OperationCancelledError("start-process");
|
|
135
|
+
if (disposalStarted)
|
|
136
|
+
throw new ResourceDisposedError("process-supervisor");
|
|
137
|
+
let child;
|
|
138
|
+
try {
|
|
139
|
+
child = spawnProcess(command, spawnOptions);
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
throw processFailure("spawn", error);
|
|
143
|
+
}
|
|
144
|
+
const process = new ProductionSupervisedProcess(child, terminationGraceMs);
|
|
145
|
+
active.add(process);
|
|
146
|
+
try {
|
|
147
|
+
await process.ready(AbortSignal.any([signal, disposalController.signal]));
|
|
148
|
+
if (disposalStarted) {
|
|
149
|
+
await process.terminate();
|
|
150
|
+
throw new ResourceDisposedError("process-supervisor");
|
|
151
|
+
}
|
|
152
|
+
return process;
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
active.delete(process);
|
|
156
|
+
if (error instanceof OperationCancelledError)
|
|
157
|
+
await process.terminate();
|
|
158
|
+
if (disposalStarted)
|
|
159
|
+
throw new ResourceDisposedError("process-supervisor");
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
void process.wait(new AbortController().signal).then(() => active.delete(process), () => active.delete(process));
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
return {
|
|
167
|
+
start,
|
|
168
|
+
dispose: disposal.dispose,
|
|
169
|
+
[Symbol.asyncDispose]: disposal[Symbol.asyncDispose],
|
|
170
|
+
};
|
|
171
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Clock } from "../../core/clock.js";
|
|
2
|
+
import { type AsyncDisposableHandle } from "../../core/lifecycle.js";
|
|
3
|
+
import type { ConnectorLogger } from "../../core/logger.js";
|
|
4
|
+
import type { ProcessSupervisor } from "../../core/process.js";
|
|
5
|
+
import type { ClaudeCredentials } from "./credentials.js";
|
|
6
|
+
export type ClaudeCredentialReader = (env: Readonly<Record<string, string | undefined>>, signal: AbortSignal) => Promise<ClaudeCredentials | null>;
|
|
7
|
+
export type ClaudeCredentialAuthorityEnvironment = Readonly<Record<string, string | undefined>> & {
|
|
8
|
+
readonly HOME?: string;
|
|
9
|
+
readonly XDG_STATE_HOME?: string;
|
|
10
|
+
};
|
|
11
|
+
export type ClaudeCredentialAuthoritySchedulerOptions = {
|
|
12
|
+
readonly enabled: boolean;
|
|
13
|
+
readonly platform?: string;
|
|
14
|
+
readonly clock: Clock;
|
|
15
|
+
readonly leadMs: number;
|
|
16
|
+
readonly retryMs: number;
|
|
17
|
+
readonly env: ClaudeCredentialAuthorityEnvironment;
|
|
18
|
+
readonly processSupervisor: ProcessSupervisor;
|
|
19
|
+
readonly logger: ConnectorLogger;
|
|
20
|
+
readonly readCredentials?: ClaudeCredentialReader;
|
|
21
|
+
readonly ensureStateDirectory?: (path: string) => Promise<void>;
|
|
22
|
+
};
|
|
23
|
+
export declare function createClaudeCredentialAuthorityScheduler(options: ClaudeCredentialAuthoritySchedulerOptions): AsyncDisposableHandle;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import { isAbsolute, join } from "node:path";
|
|
3
|
+
import { InvalidArgumentError, OperationCancelledError, ProcessSupervisorError, } from "../../core/errors.js";
|
|
4
|
+
import { createAsyncDisposable } from "../../core/lifecycle.js";
|
|
5
|
+
import { readClaudeCredentials } from "./auth.js";
|
|
6
|
+
const AUTHORITY_DIRECTORY = "claude-credential-authority";
|
|
7
|
+
const LOCK_FILE = "authority.lock";
|
|
8
|
+
const LOCK_CONFLICT_EXIT_CODE = 75;
|
|
9
|
+
const MAXIMUM_DELAY_MS = 2_147_483_647;
|
|
10
|
+
const AUTHORITY_PROMPT = "Reply OK without using tools.";
|
|
11
|
+
function authorityStateDirectory(env) {
|
|
12
|
+
const configuredState = env.XDG_STATE_HOME;
|
|
13
|
+
if (configuredState !== undefined && configuredState.length > 0 && isAbsolute(configuredState)) {
|
|
14
|
+
return join(configuredState, "opencode-ext-connector", AUTHORITY_DIRECTORY);
|
|
15
|
+
}
|
|
16
|
+
const home = env.HOME;
|
|
17
|
+
if (home === undefined || home.length === 0 || !isAbsolute(home))
|
|
18
|
+
return null;
|
|
19
|
+
return join(home, ".local", "state", "opencode-ext-connector", AUTHORITY_DIRECTORY);
|
|
20
|
+
}
|
|
21
|
+
async function createStateDirectory(path) {
|
|
22
|
+
await mkdir(path, { recursive: true, mode: 0o700 });
|
|
23
|
+
}
|
|
24
|
+
function assertNeverProcessExit(exit) {
|
|
25
|
+
throw new InvalidArgumentError("processExit", exit);
|
|
26
|
+
}
|
|
27
|
+
export function createClaudeCredentialAuthorityScheduler(options) {
|
|
28
|
+
if (!Number.isSafeInteger(options.leadMs) || options.leadMs < 0) {
|
|
29
|
+
throw new InvalidArgumentError("leadMs");
|
|
30
|
+
}
|
|
31
|
+
if (!Number.isSafeInteger(options.retryMs) || options.retryMs <= 0) {
|
|
32
|
+
throw new InvalidArgumentError("retryMs");
|
|
33
|
+
}
|
|
34
|
+
if (!options.enabled || (options.platform ?? process.platform) !== "linux") {
|
|
35
|
+
return createAsyncDisposable(() => undefined);
|
|
36
|
+
}
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
const readCredentials = options.readCredentials ?? readClaudeCredentials;
|
|
39
|
+
const ensureStateDirectory = options.ensureStateDirectory ?? createStateDirectory;
|
|
40
|
+
const stateDirectory = authorityStateDirectory(options.env);
|
|
41
|
+
let scheduled;
|
|
42
|
+
let activeProcess;
|
|
43
|
+
let activeEvaluation;
|
|
44
|
+
let disposalStarted = false;
|
|
45
|
+
const arm = (delayMs) => {
|
|
46
|
+
if (disposalStarted)
|
|
47
|
+
return;
|
|
48
|
+
scheduled?.cancel();
|
|
49
|
+
scheduled = options.clock.schedule(Math.min(MAXIMUM_DELAY_MS, Math.max(0, Math.trunc(delayMs))), () => {
|
|
50
|
+
scheduled = undefined;
|
|
51
|
+
startEvaluation(true);
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
const runAuthority = async () => {
|
|
55
|
+
if (stateDirectory === null)
|
|
56
|
+
return null;
|
|
57
|
+
await ensureStateDirectory(stateDirectory);
|
|
58
|
+
const process = await options.processSupervisor.start({
|
|
59
|
+
executable: "flock",
|
|
60
|
+
arguments: [
|
|
61
|
+
"--exclusive",
|
|
62
|
+
"--nonblock",
|
|
63
|
+
"--conflict-exit-code",
|
|
64
|
+
String(LOCK_CONFLICT_EXIT_CODE),
|
|
65
|
+
"--no-fork",
|
|
66
|
+
"--",
|
|
67
|
+
join(stateDirectory, LOCK_FILE),
|
|
68
|
+
"claude",
|
|
69
|
+
"--restricted",
|
|
70
|
+
"-p",
|
|
71
|
+
AUTHORITY_PROMPT,
|
|
72
|
+
"--permission-prompts",
|
|
73
|
+
"none",
|
|
74
|
+
"--max-turns",
|
|
75
|
+
"1",
|
|
76
|
+
"--output-format",
|
|
77
|
+
"json",
|
|
78
|
+
],
|
|
79
|
+
cwd: stateDirectory,
|
|
80
|
+
}, controller.signal);
|
|
81
|
+
activeProcess = process;
|
|
82
|
+
try {
|
|
83
|
+
return await process.wait(controller.signal);
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
activeProcess = undefined;
|
|
87
|
+
await process.dispose();
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
const evaluate = async (canInvoke) => {
|
|
91
|
+
const credentials = await readCredentials(options.env, controller.signal);
|
|
92
|
+
if (disposalStarted)
|
|
93
|
+
return;
|
|
94
|
+
const expiresAtMs = credentials?.expiresAtMs ?? null;
|
|
95
|
+
if (expiresAtMs === null) {
|
|
96
|
+
arm(options.retryMs);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const untilLeadMs = expiresAtMs - options.leadMs - options.clock.nowMs();
|
|
100
|
+
if (untilLeadMs > 0) {
|
|
101
|
+
arm(untilLeadMs);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (!canInvoke) {
|
|
105
|
+
arm(options.retryMs);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const exit = await runAuthority();
|
|
109
|
+
if (disposalStarted)
|
|
110
|
+
return;
|
|
111
|
+
if (exit === null) {
|
|
112
|
+
options.logger.log("warn", "claude.credential-authority.state-unavailable", {});
|
|
113
|
+
arm(options.retryMs);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
switch (exit.kind) {
|
|
117
|
+
case "signal":
|
|
118
|
+
options.logger.log("warn", "claude.credential-authority.signal-exit", {
|
|
119
|
+
signal: exit.signal,
|
|
120
|
+
});
|
|
121
|
+
arm(options.retryMs);
|
|
122
|
+
return;
|
|
123
|
+
case "code":
|
|
124
|
+
if (exit.code === LOCK_CONFLICT_EXIT_CODE) {
|
|
125
|
+
arm(options.retryMs);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (exit.code !== 0) {
|
|
129
|
+
options.logger.log("warn", "claude.credential-authority.nonzero-exit", {
|
|
130
|
+
exitCode: exit.code,
|
|
131
|
+
});
|
|
132
|
+
arm(options.retryMs);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
await evaluate(false);
|
|
136
|
+
return;
|
|
137
|
+
default:
|
|
138
|
+
return assertNeverProcessExit(exit);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
const startEvaluation = (canInvoke) => {
|
|
142
|
+
if (disposalStarted || activeEvaluation !== undefined)
|
|
143
|
+
return;
|
|
144
|
+
const operation = evaluate(canInvoke).catch((error) => {
|
|
145
|
+
if (error instanceof OperationCancelledError && disposalStarted)
|
|
146
|
+
return;
|
|
147
|
+
if (error instanceof ProcessSupervisorError) {
|
|
148
|
+
options.logger.log("warn", "claude.credential-authority.process-failed", {});
|
|
149
|
+
arm(options.retryMs);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (error instanceof Error) {
|
|
153
|
+
options.logger.log("warn", "claude.credential-authority.evaluation-failed", {});
|
|
154
|
+
arm(options.retryMs);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
throw error;
|
|
158
|
+
});
|
|
159
|
+
activeEvaluation = operation;
|
|
160
|
+
void operation.then(() => {
|
|
161
|
+
if (activeEvaluation === operation)
|
|
162
|
+
activeEvaluation = undefined;
|
|
163
|
+
}, () => {
|
|
164
|
+
if (activeEvaluation === operation)
|
|
165
|
+
activeEvaluation = undefined;
|
|
166
|
+
});
|
|
167
|
+
};
|
|
168
|
+
const disposal = createAsyncDisposable(async () => {
|
|
169
|
+
disposalStarted = true;
|
|
170
|
+
scheduled?.cancel();
|
|
171
|
+
scheduled = undefined;
|
|
172
|
+
controller.abort();
|
|
173
|
+
await activeProcess?.terminate();
|
|
174
|
+
await activeEvaluation;
|
|
175
|
+
});
|
|
176
|
+
startEvaluation(true);
|
|
177
|
+
return {
|
|
178
|
+
dispose: disposal.dispose,
|
|
179
|
+
[Symbol.asyncDispose]: disposal[Symbol.asyncDispose],
|
|
180
|
+
};
|
|
181
|
+
}
|