@clinebot/core 0.0.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/README.md +88 -0
- package/dist/account/cline-account-service.d.ts +34 -0
- package/dist/account/index.d.ts +3 -0
- package/dist/account/rpc.d.ts +38 -0
- package/dist/account/types.d.ts +74 -0
- package/dist/agents/agent-config-loader.d.ts +18 -0
- package/dist/agents/agent-config-parser.d.ts +25 -0
- package/dist/agents/hooks-config-loader.d.ts +23 -0
- package/dist/agents/index.d.ts +11 -0
- package/dist/agents/plugin-config-loader.d.ts +22 -0
- package/dist/agents/plugin-loader.d.ts +9 -0
- package/dist/agents/plugin-sandbox.d.ts +12 -0
- package/dist/agents/unified-config-file-watcher.d.ts +77 -0
- package/dist/agents/user-instruction-config-loader.d.ts +63 -0
- package/dist/auth/client.d.ts +11 -0
- package/dist/auth/cline.d.ts +41 -0
- package/dist/auth/codex.d.ts +39 -0
- package/dist/auth/oca.d.ts +22 -0
- package/dist/auth/server.d.ts +22 -0
- package/dist/auth/types.d.ts +72 -0
- package/dist/auth/utils.d.ts +32 -0
- package/dist/chat/chat-schema.d.ts +145 -0
- package/dist/default-tools/constants.d.ts +23 -0
- package/dist/default-tools/definitions.d.ts +96 -0
- package/dist/default-tools/executors/apply-patch-parser.d.ts +68 -0
- package/dist/default-tools/executors/apply-patch.d.ts +26 -0
- package/dist/default-tools/executors/bash.d.ts +49 -0
- package/dist/default-tools/executors/editor.d.ts +31 -0
- package/dist/default-tools/executors/file-read.d.ts +40 -0
- package/dist/default-tools/executors/index.d.ts +44 -0
- package/dist/default-tools/executors/search.d.ts +50 -0
- package/dist/default-tools/executors/web-fetch.d.ts +58 -0
- package/dist/default-tools/index.d.ts +57 -0
- package/dist/default-tools/presets.d.ts +124 -0
- package/dist/default-tools/schemas.d.ts +121 -0
- package/dist/default-tools/types.d.ts +237 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +220 -0
- package/dist/input/file-indexer.d.ts +5 -0
- package/dist/input/index.d.ts +4 -0
- package/dist/input/mention-enricher.d.ts +12 -0
- package/dist/mcp/config-loader.d.ts +15 -0
- package/dist/mcp/index.d.ts +4 -0
- package/dist/mcp/manager.d.ts +24 -0
- package/dist/mcp/types.d.ts +66 -0
- package/dist/runtime/hook-file-hooks.d.ts +18 -0
- package/dist/runtime/rules.d.ts +5 -0
- package/dist/runtime/runtime-builder.d.ts +5 -0
- package/dist/runtime/sandbox/subprocess-sandbox.d.ts +19 -0
- package/dist/runtime/session-runtime.d.ts +36 -0
- package/dist/runtime/tool-approval.d.ts +9 -0
- package/dist/runtime/workflows.d.ts +13 -0
- package/dist/server/index.d.ts +47 -0
- package/dist/server/index.js +641 -0
- package/dist/session/default-session-manager.d.ts +77 -0
- package/dist/session/rpc-session-service.d.ts +12 -0
- package/dist/session/runtime-oauth-token-manager.d.ts +28 -0
- package/dist/session/session-artifacts.d.ts +19 -0
- package/dist/session/session-graph.d.ts +15 -0
- package/dist/session/session-host.d.ts +21 -0
- package/dist/session/session-manager.d.ts +50 -0
- package/dist/session/session-manifest.d.ts +30 -0
- package/dist/session/session-service.d.ts +113 -0
- package/dist/session/sqlite-rpc-session-backend.d.ts +30 -0
- package/dist/session/unified-session-persistence-service.d.ts +93 -0
- package/dist/session/workspace-manager.d.ts +28 -0
- package/dist/session/workspace-manifest.d.ts +25 -0
- package/dist/storage/provider-settings-legacy-migration.d.ts +13 -0
- package/dist/storage/provider-settings-manager.d.ts +20 -0
- package/dist/storage/sqlite-session-store.d.ts +29 -0
- package/dist/storage/sqlite-team-store.d.ts +31 -0
- package/dist/storage/team-store.d.ts +2 -0
- package/dist/team/index.d.ts +1 -0
- package/dist/team/projections.d.ts +8 -0
- package/dist/types/common.d.ts +10 -0
- package/dist/types/config.d.ts +37 -0
- package/dist/types/events.d.ts +54 -0
- package/dist/types/provider-settings.d.ts +20 -0
- package/dist/types/sessions.d.ts +9 -0
- package/dist/types/storage.d.ts +37 -0
- package/dist/types/workspace.d.ts +7 -0
- package/dist/types.d.ts +26 -0
- package/package.json +63 -0
- package/src/account/cline-account-service.test.ts +101 -0
- package/src/account/cline-account-service.ts +267 -0
- package/src/account/index.ts +20 -0
- package/src/account/rpc.test.ts +62 -0
- package/src/account/rpc.ts +172 -0
- package/src/account/types.ts +80 -0
- package/src/agents/agent-config-loader.test.ts +234 -0
- package/src/agents/agent-config-loader.ts +107 -0
- package/src/agents/agent-config-parser.ts +191 -0
- package/src/agents/hooks-config-loader.ts +97 -0
- package/src/agents/index.ts +84 -0
- package/src/agents/plugin-config-loader.test.ts +91 -0
- package/src/agents/plugin-config-loader.ts +160 -0
- package/src/agents/plugin-loader.test.ts +102 -0
- package/src/agents/plugin-loader.ts +105 -0
- package/src/agents/plugin-sandbox.test.ts +120 -0
- package/src/agents/plugin-sandbox.ts +471 -0
- package/src/agents/unified-config-file-watcher.test.ts +196 -0
- package/src/agents/unified-config-file-watcher.ts +483 -0
- package/src/agents/user-instruction-config-loader.test.ts +158 -0
- package/src/agents/user-instruction-config-loader.ts +438 -0
- package/src/auth/client.test.ts +40 -0
- package/src/auth/client.ts +25 -0
- package/src/auth/cline.test.ts +130 -0
- package/src/auth/cline.ts +414 -0
- package/src/auth/codex.test.ts +170 -0
- package/src/auth/codex.ts +466 -0
- package/src/auth/oca.test.ts +215 -0
- package/src/auth/oca.ts +546 -0
- package/src/auth/server.ts +216 -0
- package/src/auth/types.ts +78 -0
- package/src/auth/utils.test.ts +128 -0
- package/src/auth/utils.ts +247 -0
- package/src/chat/chat-schema.ts +82 -0
- package/src/default-tools/constants.ts +35 -0
- package/src/default-tools/definitions.test.ts +233 -0
- package/src/default-tools/definitions.ts +632 -0
- package/src/default-tools/executors/apply-patch-parser.ts +520 -0
- package/src/default-tools/executors/apply-patch.ts +359 -0
- package/src/default-tools/executors/bash.ts +205 -0
- package/src/default-tools/executors/editor.ts +231 -0
- package/src/default-tools/executors/file-read.test.ts +25 -0
- package/src/default-tools/executors/file-read.ts +94 -0
- package/src/default-tools/executors/index.ts +75 -0
- package/src/default-tools/executors/search.ts +278 -0
- package/src/default-tools/executors/web-fetch.ts +259 -0
- package/src/default-tools/index.ts +161 -0
- package/src/default-tools/presets.test.ts +63 -0
- package/src/default-tools/presets.ts +168 -0
- package/src/default-tools/schemas.ts +228 -0
- package/src/default-tools/types.ts +324 -0
- package/src/index.ts +119 -0
- package/src/input/file-indexer.d.ts +11 -0
- package/src/input/file-indexer.test.ts +87 -0
- package/src/input/file-indexer.ts +280 -0
- package/src/input/index.ts +7 -0
- package/src/input/mention-enricher.test.ts +82 -0
- package/src/input/mention-enricher.ts +119 -0
- package/src/mcp/config-loader.test.ts +238 -0
- package/src/mcp/config-loader.ts +219 -0
- package/src/mcp/index.ts +26 -0
- package/src/mcp/manager.test.ts +106 -0
- package/src/mcp/manager.ts +262 -0
- package/src/mcp/types.ts +88 -0
- package/src/runtime/hook-file-hooks.test.ts +106 -0
- package/src/runtime/hook-file-hooks.ts +736 -0
- package/src/runtime/index.ts +27 -0
- package/src/runtime/rules.ts +34 -0
- package/src/runtime/runtime-builder.team-persistence.test.ts +203 -0
- package/src/runtime/runtime-builder.test.ts +215 -0
- package/src/runtime/runtime-builder.ts +515 -0
- package/src/runtime/runtime-parity.test.ts +132 -0
- package/src/runtime/sandbox/subprocess-sandbox.ts +207 -0
- package/src/runtime/session-runtime.ts +44 -0
- package/src/runtime/tool-approval.ts +104 -0
- package/src/runtime/workflows.test.ts +119 -0
- package/src/runtime/workflows.ts +54 -0
- package/src/server/index.ts +282 -0
- package/src/session/default-session-manager.e2e.test.ts +354 -0
- package/src/session/default-session-manager.test.ts +816 -0
- package/src/session/default-session-manager.ts +1286 -0
- package/src/session/index.ts +37 -0
- package/src/session/rpc-session-service.ts +189 -0
- package/src/session/runtime-oauth-token-manager.test.ts +137 -0
- package/src/session/runtime-oauth-token-manager.ts +265 -0
- package/src/session/session-artifacts.ts +106 -0
- package/src/session/session-graph.ts +90 -0
- package/src/session/session-host.ts +190 -0
- package/src/session/session-manager.ts +56 -0
- package/src/session/session-manifest.ts +29 -0
- package/src/session/session-service.team-persistence.test.ts +48 -0
- package/src/session/session-service.ts +610 -0
- package/src/session/sqlite-rpc-session-backend.ts +303 -0
- package/src/session/unified-session-persistence-service.ts +781 -0
- package/src/session/workspace-manager.ts +98 -0
- package/src/session/workspace-manifest.ts +100 -0
- package/src/storage/artifact-store.ts +1 -0
- package/src/storage/index.ts +11 -0
- package/src/storage/provider-settings-legacy-migration.test.ts +175 -0
- package/src/storage/provider-settings-legacy-migration.ts +637 -0
- package/src/storage/provider-settings-manager.test.ts +111 -0
- package/src/storage/provider-settings-manager.ts +129 -0
- package/src/storage/session-store.ts +1 -0
- package/src/storage/sqlite-session-store.ts +270 -0
- package/src/storage/sqlite-team-store.ts +443 -0
- package/src/storage/team-store.ts +5 -0
- package/src/team/index.ts +4 -0
- package/src/team/projections.ts +285 -0
- package/src/types/common.ts +14 -0
- package/src/types/config.ts +64 -0
- package/src/types/events.ts +46 -0
- package/src/types/index.ts +24 -0
- package/src/types/provider-settings.ts +43 -0
- package/src/types/sessions.ts +16 -0
- package/src/types/storage.ts +64 -0
- package/src/types/workspace.ts +7 -0
- package/src/types.ts +127 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export { DefaultSessionManager } from "./default-session-manager";
|
|
2
|
+
export { RpcCoreSessionService } from "./rpc-session-service";
|
|
3
|
+
export {
|
|
4
|
+
deriveSubsessionStatus,
|
|
5
|
+
makeSubSessionId,
|
|
6
|
+
makeTeamTaskSubSessionId,
|
|
7
|
+
sanitizeSessionToken,
|
|
8
|
+
} from "./session-graph";
|
|
9
|
+
export type { CreateSessionHostOptions, SessionHost } from "./session-host";
|
|
10
|
+
export { createSessionHost } from "./session-host";
|
|
11
|
+
export type {
|
|
12
|
+
SendSessionInput,
|
|
13
|
+
SessionManager,
|
|
14
|
+
StartSessionInput,
|
|
15
|
+
StartSessionResult,
|
|
16
|
+
} from "./session-manager";
|
|
17
|
+
export type { SessionManifest } from "./session-manifest";
|
|
18
|
+
export type {
|
|
19
|
+
CreateRootSessionWithArtifactsInput,
|
|
20
|
+
RootSessionArtifacts,
|
|
21
|
+
} from "./session-service";
|
|
22
|
+
export { CoreSessionService } from "./session-service";
|
|
23
|
+
export type {
|
|
24
|
+
WorkspaceManager,
|
|
25
|
+
WorkspaceManagerEvent,
|
|
26
|
+
} from "./workspace-manager";
|
|
27
|
+
export { InMemoryWorkspaceManager } from "./workspace-manager";
|
|
28
|
+
export type { WorkspaceManifest } from "./workspace-manifest";
|
|
29
|
+
export {
|
|
30
|
+
buildWorkspaceMetadata,
|
|
31
|
+
emptyWorkspaceManifest,
|
|
32
|
+
generateWorkspaceInfo,
|
|
33
|
+
normalizeWorkspacePath,
|
|
34
|
+
upsertWorkspaceInfo,
|
|
35
|
+
WorkspaceInfoSchema,
|
|
36
|
+
WorkspaceManifestSchema,
|
|
37
|
+
} from "./workspace-manifest";
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { RpcSessionClient, type RpcSessionRow } from "@clinebot/rpc";
|
|
3
|
+
import { nowIso } from "./session-artifacts";
|
|
4
|
+
import type { SessionRowShape } from "./session-service";
|
|
5
|
+
import type {
|
|
6
|
+
PersistedSessionUpdateInput,
|
|
7
|
+
SessionPersistenceAdapter,
|
|
8
|
+
} from "./unified-session-persistence-service";
|
|
9
|
+
import { UnifiedSessionPersistenceService } from "./unified-session-persistence-service";
|
|
10
|
+
|
|
11
|
+
function toShape(row: RpcSessionRow): SessionRowShape {
|
|
12
|
+
return {
|
|
13
|
+
session_id: row.sessionId,
|
|
14
|
+
source: row.source,
|
|
15
|
+
pid: row.pid,
|
|
16
|
+
started_at: row.startedAt,
|
|
17
|
+
ended_at: row.endedAt ?? null,
|
|
18
|
+
exit_code: row.exitCode ?? null,
|
|
19
|
+
status: row.status,
|
|
20
|
+
status_lock: row.statusLock,
|
|
21
|
+
interactive: row.interactive ? 1 : 0,
|
|
22
|
+
provider: row.provider,
|
|
23
|
+
model: row.model,
|
|
24
|
+
cwd: row.cwd,
|
|
25
|
+
workspace_root: row.workspaceRoot,
|
|
26
|
+
team_name: row.teamName ?? null,
|
|
27
|
+
enable_tools: row.enableTools ? 1 : 0,
|
|
28
|
+
enable_spawn: row.enableSpawn ? 1 : 0,
|
|
29
|
+
enable_teams: row.enableTeams ? 1 : 0,
|
|
30
|
+
parent_session_id: row.parentSessionId ?? null,
|
|
31
|
+
parent_agent_id: row.parentAgentId ?? null,
|
|
32
|
+
agent_id: row.agentId ?? null,
|
|
33
|
+
conversation_id: row.conversationId ?? null,
|
|
34
|
+
is_subagent: row.isSubagent ? 1 : 0,
|
|
35
|
+
prompt: row.prompt ?? null,
|
|
36
|
+
metadata_json: row.metadata ? JSON.stringify(row.metadata) : null,
|
|
37
|
+
transcript_path: row.transcriptPath,
|
|
38
|
+
hook_path: row.hookPath,
|
|
39
|
+
messages_path: row.messagesPath ?? null,
|
|
40
|
+
updated_at: row.updatedAt,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function fromShape(row: SessionRowShape): RpcSessionRow {
|
|
45
|
+
return {
|
|
46
|
+
sessionId: row.session_id,
|
|
47
|
+
source: row.source,
|
|
48
|
+
pid: row.pid,
|
|
49
|
+
startedAt: row.started_at,
|
|
50
|
+
endedAt: row.ended_at ?? null,
|
|
51
|
+
exitCode: row.exit_code ?? null,
|
|
52
|
+
status: row.status,
|
|
53
|
+
statusLock: row.status_lock ?? 0,
|
|
54
|
+
interactive: row.interactive === 1,
|
|
55
|
+
provider: row.provider,
|
|
56
|
+
model: row.model,
|
|
57
|
+
cwd: row.cwd,
|
|
58
|
+
workspaceRoot: row.workspace_root,
|
|
59
|
+
teamName: row.team_name ?? undefined,
|
|
60
|
+
enableTools: row.enable_tools === 1,
|
|
61
|
+
enableSpawn: row.enable_spawn === 1,
|
|
62
|
+
enableTeams: row.enable_teams === 1,
|
|
63
|
+
parentSessionId: row.parent_session_id ?? undefined,
|
|
64
|
+
parentAgentId: row.parent_agent_id ?? undefined,
|
|
65
|
+
agentId: row.agent_id ?? undefined,
|
|
66
|
+
conversationId: row.conversation_id ?? undefined,
|
|
67
|
+
isSubagent: row.is_subagent === 1,
|
|
68
|
+
prompt: row.prompt ?? undefined,
|
|
69
|
+
metadata: (() => {
|
|
70
|
+
if (!row.metadata_json) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
const parsed = JSON.parse(row.metadata_json) as unknown;
|
|
75
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
76
|
+
return parsed as Record<string, unknown>;
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
// Ignore malformed metadata payloads.
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
})(),
|
|
83
|
+
transcriptPath: row.transcript_path,
|
|
84
|
+
hookPath: row.hook_path,
|
|
85
|
+
messagesPath: row.messages_path ?? undefined,
|
|
86
|
+
updatedAt: row.updated_at ?? nowIso(),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
class RpcSessionPersistenceAdapter implements SessionPersistenceAdapter {
|
|
91
|
+
constructor(private readonly client: RpcSessionClient) {}
|
|
92
|
+
|
|
93
|
+
ensureSessionsDir(): string {
|
|
94
|
+
return "";
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async upsertSession(row: SessionRowShape): Promise<void> {
|
|
98
|
+
await this.client.upsertSession(fromShape(row));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async getSession(sessionId: string): Promise<SessionRowShape | undefined> {
|
|
102
|
+
const row = await this.client.getSession(sessionId);
|
|
103
|
+
return row ? toShape(row) : undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async listSessions(options: {
|
|
107
|
+
limit: number;
|
|
108
|
+
parentSessionId?: string;
|
|
109
|
+
status?: string;
|
|
110
|
+
}): Promise<SessionRowShape[]> {
|
|
111
|
+
const rows = await this.client.listSessions(options);
|
|
112
|
+
return rows.map((row) => toShape(row));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async updateSession(
|
|
116
|
+
input: PersistedSessionUpdateInput,
|
|
117
|
+
): Promise<{ updated: boolean; statusLock: number }> {
|
|
118
|
+
const changed = await this.client.updateSession({
|
|
119
|
+
sessionId: input.sessionId,
|
|
120
|
+
status: input.status,
|
|
121
|
+
endedAt: input.endedAt,
|
|
122
|
+
exitCode: input.exitCode,
|
|
123
|
+
prompt: input.prompt,
|
|
124
|
+
metadata:
|
|
125
|
+
input.metadataJson === undefined
|
|
126
|
+
? undefined
|
|
127
|
+
: input.metadataJson
|
|
128
|
+
? (JSON.parse(input.metadataJson) as Record<string, unknown>)
|
|
129
|
+
: null,
|
|
130
|
+
parentSessionId: input.parentSessionId,
|
|
131
|
+
parentAgentId: input.parentAgentId,
|
|
132
|
+
agentId: input.agentId,
|
|
133
|
+
conversationId: input.conversationId,
|
|
134
|
+
expectedStatusLock: input.expectedStatusLock,
|
|
135
|
+
setRunning: input.setRunning,
|
|
136
|
+
});
|
|
137
|
+
return changed;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async deleteSession(sessionId: string, cascade: boolean): Promise<boolean> {
|
|
141
|
+
return await this.client.deleteSession(sessionId, cascade);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async enqueueSpawnRequest(input: {
|
|
145
|
+
rootSessionId: string;
|
|
146
|
+
parentAgentId: string;
|
|
147
|
+
task?: string;
|
|
148
|
+
systemPrompt?: string;
|
|
149
|
+
}): Promise<void> {
|
|
150
|
+
await this.client.enqueueSpawnRequest(input);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async claimSpawnRequest(
|
|
154
|
+
rootSessionId: string,
|
|
155
|
+
parentAgentId: string,
|
|
156
|
+
): Promise<string | undefined> {
|
|
157
|
+
return await this.client.claimSpawnRequest(rootSessionId, parentAgentId);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface RpcCoreSessionServiceOptions {
|
|
162
|
+
address?: string;
|
|
163
|
+
sessionsDir: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export class RpcCoreSessionService extends UnifiedSessionPersistenceService {
|
|
167
|
+
private readonly sessionsDirPath: string;
|
|
168
|
+
private readonly client: RpcSessionClient;
|
|
169
|
+
|
|
170
|
+
constructor(options: RpcCoreSessionServiceOptions) {
|
|
171
|
+
const client = new RpcSessionClient({
|
|
172
|
+
address: options.address?.trim() || "127.0.0.1:4317",
|
|
173
|
+
});
|
|
174
|
+
super(new RpcSessionPersistenceAdapter(client));
|
|
175
|
+
this.sessionsDirPath = options.sessionsDir;
|
|
176
|
+
this.client = client;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
override ensureSessionsDir(): string {
|
|
180
|
+
if (!existsSync(this.sessionsDirPath)) {
|
|
181
|
+
mkdirSync(this.sessionsDirPath, { recursive: true });
|
|
182
|
+
}
|
|
183
|
+
return this.sessionsDirPath;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
close(): void {
|
|
187
|
+
this.client.close();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
OAuthReauthRequiredError,
|
|
4
|
+
RuntimeOAuthTokenManager,
|
|
5
|
+
} from "./runtime-oauth-token-manager";
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
getValidOpenAICodexCredentials,
|
|
9
|
+
getValidClineCredentials,
|
|
10
|
+
getValidOcaCredentials,
|
|
11
|
+
} = vi.hoisted(() => ({
|
|
12
|
+
getValidOpenAICodexCredentials: vi.fn(),
|
|
13
|
+
getValidClineCredentials: vi.fn(),
|
|
14
|
+
getValidOcaCredentials: vi.fn(),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock("../auth/codex", () => ({
|
|
18
|
+
getValidOpenAICodexCredentials,
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock("../auth/cline", () => ({
|
|
22
|
+
getValidClineCredentials,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
vi.mock("../auth/oca", () => ({
|
|
26
|
+
getValidOcaCredentials,
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
describe("RuntimeOAuthTokenManager", () => {
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
vi.clearAllMocks();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("refreshes and persists OpenAI Codex OAuth credentials", async () => {
|
|
35
|
+
const getProviderSettings = vi.fn().mockReturnValue({
|
|
36
|
+
provider: "openai-codex",
|
|
37
|
+
auth: {
|
|
38
|
+
accessToken: "access-old",
|
|
39
|
+
refreshToken: "refresh-old",
|
|
40
|
+
expiresAt: Date.now() - 1_000,
|
|
41
|
+
accountId: "acct-old",
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
const saveProviderSettings = vi.fn();
|
|
45
|
+
|
|
46
|
+
getValidOpenAICodexCredentials.mockResolvedValueOnce({
|
|
47
|
+
access: "access-new",
|
|
48
|
+
refresh: "refresh-new",
|
|
49
|
+
expires: 4_000_000_000_000,
|
|
50
|
+
accountId: "acct-new",
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const manager = new RuntimeOAuthTokenManager({
|
|
54
|
+
providerSettingsManager: {
|
|
55
|
+
getProviderSettings,
|
|
56
|
+
saveProviderSettings,
|
|
57
|
+
} as never,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const result = await manager.resolveProviderApiKey({
|
|
61
|
+
providerId: "openai-codex",
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(result).toMatchObject({
|
|
65
|
+
providerId: "openai-codex",
|
|
66
|
+
apiKey: "access-new",
|
|
67
|
+
accountId: "acct-new",
|
|
68
|
+
refreshed: true,
|
|
69
|
+
});
|
|
70
|
+
expect(saveProviderSettings).toHaveBeenCalledWith(
|
|
71
|
+
expect.objectContaining({
|
|
72
|
+
auth: expect.objectContaining({
|
|
73
|
+
accessToken: "access-new",
|
|
74
|
+
refreshToken: "refresh-new",
|
|
75
|
+
accountId: "acct-new",
|
|
76
|
+
expiresAt: 4_000_000_000_000,
|
|
77
|
+
}),
|
|
78
|
+
}),
|
|
79
|
+
{ setLastUsed: false, tokenSource: "oauth" },
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("throws re-auth required when refresh returns null", async () => {
|
|
84
|
+
getValidOpenAICodexCredentials.mockResolvedValueOnce(null);
|
|
85
|
+
const manager = new RuntimeOAuthTokenManager({
|
|
86
|
+
providerSettingsManager: {
|
|
87
|
+
getProviderSettings: vi.fn().mockReturnValue({
|
|
88
|
+
provider: "openai-codex",
|
|
89
|
+
auth: {
|
|
90
|
+
accessToken: "access-old",
|
|
91
|
+
refreshToken: "refresh-old",
|
|
92
|
+
expiresAt: Date.now() - 1_000,
|
|
93
|
+
},
|
|
94
|
+
}),
|
|
95
|
+
saveProviderSettings: vi.fn(),
|
|
96
|
+
} as never,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
await expect(
|
|
100
|
+
manager.resolveProviderApiKey({ providerId: "openai-codex" }),
|
|
101
|
+
).rejects.toBeInstanceOf(OAuthReauthRequiredError);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("de-duplicates concurrent refresh calls per provider", async () => {
|
|
105
|
+
const refreshBarrier = Promise.resolve().then(() => ({
|
|
106
|
+
access: "access-new",
|
|
107
|
+
refresh: "refresh-new",
|
|
108
|
+
expires: Date.now() + 60_000,
|
|
109
|
+
}));
|
|
110
|
+
getValidOpenAICodexCredentials.mockImplementationOnce(
|
|
111
|
+
async () => refreshBarrier,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const manager = new RuntimeOAuthTokenManager({
|
|
115
|
+
providerSettingsManager: {
|
|
116
|
+
getProviderSettings: vi.fn().mockReturnValue({
|
|
117
|
+
provider: "openai-codex",
|
|
118
|
+
auth: {
|
|
119
|
+
accessToken: "access-old",
|
|
120
|
+
refreshToken: "refresh-old",
|
|
121
|
+
expiresAt: Date.now() - 1_000,
|
|
122
|
+
},
|
|
123
|
+
}),
|
|
124
|
+
saveProviderSettings: vi.fn(),
|
|
125
|
+
} as never,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const [first, second] = await Promise.all([
|
|
129
|
+
manager.resolveProviderApiKey({ providerId: "openai-codex" }),
|
|
130
|
+
manager.resolveProviderApiKey({ providerId: "openai-codex" }),
|
|
131
|
+
]);
|
|
132
|
+
|
|
133
|
+
expect(first?.apiKey).toBe("access-new");
|
|
134
|
+
expect(second?.apiKey).toBe("access-new");
|
|
135
|
+
expect(getValidOpenAICodexCredentials).toHaveBeenCalledTimes(1);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import type { providers as LlmsProviders } from "@clinebot/llms";
|
|
2
|
+
import {
|
|
3
|
+
type ClineOAuthCredentials,
|
|
4
|
+
getValidClineCredentials,
|
|
5
|
+
} from "../auth/cline";
|
|
6
|
+
import { getValidOpenAICodexCredentials } from "../auth/codex";
|
|
7
|
+
import { getValidOcaCredentials } from "../auth/oca";
|
|
8
|
+
import { decodeJwtPayload } from "../auth/utils";
|
|
9
|
+
import { ProviderSettingsManager } from "../storage/provider-settings-manager";
|
|
10
|
+
|
|
11
|
+
const DEFAULT_CLINE_API_BASE_URL = "https://api.cline.bot";
|
|
12
|
+
const WORKOS_TOKEN_PREFIX = "workos:";
|
|
13
|
+
|
|
14
|
+
const MANAGED_OAUTH_PROVIDERS = ["cline", "oca", "openai-codex"] as const;
|
|
15
|
+
type ManagedOAuthProviderId = (typeof MANAGED_OAUTH_PROVIDERS)[number];
|
|
16
|
+
|
|
17
|
+
function isManagedOAuthProviderId(
|
|
18
|
+
providerId: string,
|
|
19
|
+
): providerId is ManagedOAuthProviderId {
|
|
20
|
+
return (MANAGED_OAUTH_PROVIDERS as readonly string[]).includes(providerId);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function toStoredAccessToken(
|
|
24
|
+
providerId: ManagedOAuthProviderId,
|
|
25
|
+
accessToken: string,
|
|
26
|
+
): string {
|
|
27
|
+
if (providerId === "cline") {
|
|
28
|
+
return `${WORKOS_TOKEN_PREFIX}${accessToken}`;
|
|
29
|
+
}
|
|
30
|
+
return accessToken;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function fromStoredAccessToken(
|
|
34
|
+
providerId: ManagedOAuthProviderId,
|
|
35
|
+
accessToken: string,
|
|
36
|
+
): string {
|
|
37
|
+
if (
|
|
38
|
+
providerId === "cline" &&
|
|
39
|
+
accessToken.toLowerCase().startsWith(WORKOS_TOKEN_PREFIX)
|
|
40
|
+
) {
|
|
41
|
+
return accessToken.slice(WORKOS_TOKEN_PREFIX.length);
|
|
42
|
+
}
|
|
43
|
+
return accessToken;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readExpiryFromToken(accessToken: string): number | null {
|
|
47
|
+
const payload = decodeJwtPayload(accessToken);
|
|
48
|
+
const exp = payload?.exp;
|
|
49
|
+
if (typeof exp === "number" && exp > 0) {
|
|
50
|
+
return exp * 1000;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function deriveCredentialExpiry(
|
|
56
|
+
settings: LlmsProviders.ProviderSettings,
|
|
57
|
+
normalizedAccessToken: string,
|
|
58
|
+
): number {
|
|
59
|
+
const explicitExpiry = (
|
|
60
|
+
settings.auth as
|
|
61
|
+
| (LlmsProviders.ProviderSettings["auth"] & { expiresAt?: number })
|
|
62
|
+
| undefined
|
|
63
|
+
)?.expiresAt;
|
|
64
|
+
if (
|
|
65
|
+
typeof explicitExpiry === "number" &&
|
|
66
|
+
Number.isFinite(explicitExpiry) &&
|
|
67
|
+
explicitExpiry > 0
|
|
68
|
+
) {
|
|
69
|
+
return explicitExpiry;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const jwtExpiry = readExpiryFromToken(normalizedAccessToken);
|
|
73
|
+
if (jwtExpiry) {
|
|
74
|
+
return jwtExpiry;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Unknown expiry should trigger refresh on next resolution.
|
|
78
|
+
return Date.now() - 1;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function toCredentials(
|
|
82
|
+
providerId: ManagedOAuthProviderId,
|
|
83
|
+
settings: LlmsProviders.ProviderSettings,
|
|
84
|
+
): ClineOAuthCredentials | null {
|
|
85
|
+
const rawAccess = settings.auth?.accessToken?.trim();
|
|
86
|
+
const refreshToken = settings.auth?.refreshToken?.trim();
|
|
87
|
+
if (!rawAccess || !refreshToken) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const access = fromStoredAccessToken(providerId, rawAccess);
|
|
91
|
+
if (!access) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
access,
|
|
97
|
+
refresh: refreshToken,
|
|
98
|
+
expires: deriveCredentialExpiry(settings, access),
|
|
99
|
+
accountId: settings.auth?.accountId,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function authSettingsEqual(
|
|
104
|
+
a: LlmsProviders.ProviderSettings["auth"] | undefined,
|
|
105
|
+
b: LlmsProviders.ProviderSettings["auth"] | undefined,
|
|
106
|
+
): boolean {
|
|
107
|
+
const aExpiry = (
|
|
108
|
+
a as
|
|
109
|
+
| (LlmsProviders.ProviderSettings["auth"] & { expiresAt?: number })
|
|
110
|
+
| undefined
|
|
111
|
+
)?.expiresAt;
|
|
112
|
+
const bExpiry = (
|
|
113
|
+
b as
|
|
114
|
+
| (LlmsProviders.ProviderSettings["auth"] & { expiresAt?: number })
|
|
115
|
+
| undefined
|
|
116
|
+
)?.expiresAt;
|
|
117
|
+
return (
|
|
118
|
+
a?.accessToken === b?.accessToken &&
|
|
119
|
+
a?.refreshToken === b?.refreshToken &&
|
|
120
|
+
a?.accountId === b?.accountId &&
|
|
121
|
+
aExpiry === bExpiry
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export class OAuthReauthRequiredError extends Error {
|
|
126
|
+
public readonly providerId: ManagedOAuthProviderId;
|
|
127
|
+
|
|
128
|
+
constructor(providerId: ManagedOAuthProviderId) {
|
|
129
|
+
super(
|
|
130
|
+
`OAuth credentials for provider "${providerId}" are no longer valid. Re-run authentication for this provider.`,
|
|
131
|
+
);
|
|
132
|
+
this.name = "OAuthReauthRequiredError";
|
|
133
|
+
this.providerId = providerId;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export type RuntimeOAuthResolution = {
|
|
138
|
+
providerId: ManagedOAuthProviderId;
|
|
139
|
+
apiKey: string;
|
|
140
|
+
accountId?: string;
|
|
141
|
+
refreshed: boolean;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export class RuntimeOAuthTokenManager {
|
|
145
|
+
private readonly providerSettingsManager: ProviderSettingsManager;
|
|
146
|
+
private readonly refreshInFlight = new Map<
|
|
147
|
+
ManagedOAuthProviderId,
|
|
148
|
+
Promise<RuntimeOAuthResolution | null>
|
|
149
|
+
>();
|
|
150
|
+
|
|
151
|
+
constructor(options?: { providerSettingsManager?: ProviderSettingsManager }) {
|
|
152
|
+
this.providerSettingsManager =
|
|
153
|
+
options?.providerSettingsManager ?? new ProviderSettingsManager();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
public async resolveProviderApiKey(input: {
|
|
157
|
+
providerId: string;
|
|
158
|
+
forceRefresh?: boolean;
|
|
159
|
+
}): Promise<RuntimeOAuthResolution | null> {
|
|
160
|
+
if (!isManagedOAuthProviderId(input.providerId)) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
return this.resolveWithSingleFlight(input.providerId, input.forceRefresh);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private async resolveWithSingleFlight(
|
|
167
|
+
providerId: ManagedOAuthProviderId,
|
|
168
|
+
forceRefresh = false,
|
|
169
|
+
): Promise<RuntimeOAuthResolution | null> {
|
|
170
|
+
const currentInFlight = this.refreshInFlight.get(providerId);
|
|
171
|
+
if (currentInFlight) {
|
|
172
|
+
return currentInFlight;
|
|
173
|
+
}
|
|
174
|
+
const pending = this.resolveProviderApiKeyInternal(providerId, forceRefresh)
|
|
175
|
+
.catch((error) => {
|
|
176
|
+
throw error;
|
|
177
|
+
})
|
|
178
|
+
.finally(() => {
|
|
179
|
+
this.refreshInFlight.delete(providerId);
|
|
180
|
+
});
|
|
181
|
+
this.refreshInFlight.set(providerId, pending);
|
|
182
|
+
return pending;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private async resolveProviderApiKeyInternal(
|
|
186
|
+
providerId: ManagedOAuthProviderId,
|
|
187
|
+
forceRefresh: boolean,
|
|
188
|
+
): Promise<RuntimeOAuthResolution | null> {
|
|
189
|
+
const settings =
|
|
190
|
+
this.providerSettingsManager.getProviderSettings(providerId);
|
|
191
|
+
if (!settings) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const currentCredentials = toCredentials(providerId, settings);
|
|
196
|
+
if (!currentCredentials) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const nextCredentials = await this.resolveCredentials(
|
|
201
|
+
providerId,
|
|
202
|
+
settings,
|
|
203
|
+
currentCredentials,
|
|
204
|
+
forceRefresh,
|
|
205
|
+
);
|
|
206
|
+
if (!nextCredentials) {
|
|
207
|
+
throw new OAuthReauthRequiredError(providerId);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const persistedAccessToken = toStoredAccessToken(
|
|
211
|
+
providerId,
|
|
212
|
+
nextCredentials.access,
|
|
213
|
+
);
|
|
214
|
+
const nextAuth = {
|
|
215
|
+
...(settings.auth ?? {}),
|
|
216
|
+
accessToken: persistedAccessToken,
|
|
217
|
+
refreshToken: nextCredentials.refresh,
|
|
218
|
+
accountId: nextCredentials.accountId,
|
|
219
|
+
} as LlmsProviders.ProviderSettings["auth"] & { expiresAt?: number };
|
|
220
|
+
nextAuth.expiresAt = nextCredentials.expires;
|
|
221
|
+
const nextSettings: LlmsProviders.ProviderSettings = {
|
|
222
|
+
...settings,
|
|
223
|
+
auth: nextAuth,
|
|
224
|
+
};
|
|
225
|
+
const wasRefreshed = !authSettingsEqual(settings.auth, nextSettings.auth);
|
|
226
|
+
if (wasRefreshed) {
|
|
227
|
+
this.providerSettingsManager.saveProviderSettings(nextSettings, {
|
|
228
|
+
setLastUsed: false,
|
|
229
|
+
tokenSource: "oauth",
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
providerId,
|
|
235
|
+
apiKey: persistedAccessToken,
|
|
236
|
+
accountId: nextCredentials.accountId,
|
|
237
|
+
refreshed: wasRefreshed,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private async resolveCredentials(
|
|
242
|
+
providerId: ManagedOAuthProviderId,
|
|
243
|
+
settings: LlmsProviders.ProviderSettings,
|
|
244
|
+
currentCredentials: ClineOAuthCredentials,
|
|
245
|
+
forceRefresh: boolean,
|
|
246
|
+
): Promise<ClineOAuthCredentials | null> {
|
|
247
|
+
if (providerId === "cline") {
|
|
248
|
+
return getValidClineCredentials(
|
|
249
|
+
currentCredentials,
|
|
250
|
+
{
|
|
251
|
+
apiBaseUrl: settings.baseUrl?.trim() || DEFAULT_CLINE_API_BASE_URL,
|
|
252
|
+
},
|
|
253
|
+
{ forceRefresh },
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
if (providerId === "oca") {
|
|
257
|
+
return getValidOcaCredentials(
|
|
258
|
+
currentCredentials,
|
|
259
|
+
{ forceRefresh },
|
|
260
|
+
{ mode: settings.oca?.mode },
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
return getValidOpenAICodexCredentials(currentCredentials, { forceRefresh });
|
|
264
|
+
}
|
|
265
|
+
}
|