openllmprovider 0.1.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 (91) hide show
  1. package/README.md +192 -0
  2. package/dist/auth/index.cjs +6 -0
  3. package/dist/auth/index.d.cts +3 -0
  4. package/dist/auth/index.d.mts +3 -0
  5. package/dist/auth/index.mjs +3 -0
  6. package/dist/auto-C2hXJY13.d.cts +33 -0
  7. package/dist/auto-C2hXJY13.d.cts.map +1 -0
  8. package/dist/auto-CBqNYBXs.mjs +48 -0
  9. package/dist/auto-CBqNYBXs.mjs.map +1 -0
  10. package/dist/auto-CInerwvs.d.mts +33 -0
  11. package/dist/auto-CInerwvs.d.mts.map +1 -0
  12. package/dist/auto-D77wgMqO.cjs +59 -0
  13. package/dist/auto-D77wgMqO.cjs.map +1 -0
  14. package/dist/file-DB-rxfzi.mjs +77 -0
  15. package/dist/file-DB-rxfzi.mjs.map +1 -0
  16. package/dist/file-DZ7FGcSW.cjs +73 -0
  17. package/dist/file-DZ7FGcSW.cjs.map +1 -0
  18. package/dist/index.cjs +1909 -0
  19. package/dist/index.cjs.map +1 -0
  20. package/dist/index.d.cts +1239 -0
  21. package/dist/index.d.cts.map +1 -0
  22. package/dist/index.d.mts +1241 -0
  23. package/dist/index.d.mts.map +1 -0
  24. package/dist/index.mjs +1891 -0
  25. package/dist/index.mjs.map +1 -0
  26. package/dist/logger-BsHpI_fH.mjs +11 -0
  27. package/dist/logger-BsHpI_fH.mjs.map +1 -0
  28. package/dist/logger-jRimlMFR.cjs +69 -0
  29. package/dist/logger-jRimlMFR.cjs.map +1 -0
  30. package/dist/plugin/index.cjs +29 -0
  31. package/dist/plugin/index.cjs.map +1 -0
  32. package/dist/plugin/index.d.cts +10 -0
  33. package/dist/plugin/index.d.cts.map +1 -0
  34. package/dist/plugin/index.d.mts +10 -0
  35. package/dist/plugin/index.d.mts.map +1 -0
  36. package/dist/plugin/index.mjs +25 -0
  37. package/dist/plugin/index.mjs.map +1 -0
  38. package/dist/plugin-BkeUu5LW.d.mts +46 -0
  39. package/dist/plugin-BkeUu5LW.d.mts.map +1 -0
  40. package/dist/plugin-wK7RmJhZ.d.cts +46 -0
  41. package/dist/plugin-wK7RmJhZ.d.cts.map +1 -0
  42. package/dist/resolver-BA7LWSJO.mjs +645 -0
  43. package/dist/resolver-BA7LWSJO.mjs.map +1 -0
  44. package/dist/resolver-BMTvzTt9.cjs +662 -0
  45. package/dist/resolver-BMTvzTt9.cjs.map +1 -0
  46. package/dist/resolver-MgJryMWG.d.cts +75 -0
  47. package/dist/resolver-MgJryMWG.d.cts.map +1 -0
  48. package/dist/resolver-_gfXzr_S.d.mts +76 -0
  49. package/dist/resolver-_gfXzr_S.d.mts.map +1 -0
  50. package/dist/storage/index.cjs +7 -0
  51. package/dist/storage/index.d.cts +12 -0
  52. package/dist/storage/index.d.cts.map +1 -0
  53. package/dist/storage/index.d.mts +12 -0
  54. package/dist/storage/index.d.mts.map +1 -0
  55. package/dist/storage/index.mjs +4 -0
  56. package/package.json +137 -0
  57. package/src/auth/.gitkeep +0 -0
  58. package/src/auth/index.ts +10 -0
  59. package/src/auth/resolver.ts +46 -0
  60. package/src/auth/scanners.ts +462 -0
  61. package/src/auth/store.ts +357 -0
  62. package/src/catalog/.gitkeep +0 -0
  63. package/src/catalog/catalog.ts +302 -0
  64. package/src/catalog/index.ts +17 -0
  65. package/src/catalog/mapper.ts +129 -0
  66. package/src/catalog/merger.ts +99 -0
  67. package/src/index.ts +37 -0
  68. package/src/logger.ts +7 -0
  69. package/src/plugin/.gitkeep +0 -0
  70. package/src/plugin/anthropic.test.ts +505 -0
  71. package/src/plugin/anthropic.ts +324 -0
  72. package/src/plugin/codex.ts +656 -0
  73. package/src/plugin/copilot.ts +161 -0
  74. package/src/plugin/google.ts +454 -0
  75. package/src/plugin/index.ts +30 -0
  76. package/src/provider/.gitkeep +0 -0
  77. package/src/provider/bundled.ts +59 -0
  78. package/src/provider/index.ts +249 -0
  79. package/src/provider/state.ts +163 -0
  80. package/src/storage/.gitkeep +0 -0
  81. package/src/storage/auto.ts +32 -0
  82. package/src/storage/file.ts +84 -0
  83. package/src/storage/index.ts +10 -0
  84. package/src/storage/memory.ts +23 -0
  85. package/src/types/.gitkeep +0 -0
  86. package/src/types/auth.ts +18 -0
  87. package/src/types/errors.ts +87 -0
  88. package/src/types/index.ts +26 -0
  89. package/src/types/model.ts +88 -0
  90. package/src/types/plugin.ts +49 -0
  91. package/src/types/provider.ts +48 -0
@@ -0,0 +1,462 @@
1
+ import { createLogger } from '../logger.js'
2
+
3
+ const log = createLogger('auth:scanners')
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Core types
7
+ // ---------------------------------------------------------------------------
8
+
9
+ export interface DiskScanResult {
10
+ providerId: string
11
+ source: string
12
+ key?: string
13
+ credentialType?: 'api' | 'oauth' | 'wellknown'
14
+ refresh?: string
15
+ accountId?: string
16
+ expires?: number
17
+ }
18
+
19
+ export interface DiskScanner {
20
+ name: string
21
+ scan(ctx: ScanContext): Promise<DiskScanResult[]>
22
+ }
23
+
24
+ export interface ScanContext {
25
+ readFile(path: string): Promise<string | undefined>
26
+ homedir(): string
27
+ platform(): string
28
+ env(name: string): string | undefined
29
+ exec?(command: string): Promise<string | undefined>
30
+ }
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Default ScanContext (Node.js)
34
+ // ---------------------------------------------------------------------------
35
+
36
+ export function createNodeScanContext(): ScanContext {
37
+ return {
38
+ async readFile(path: string): Promise<string | undefined> {
39
+ try {
40
+ const fs = await import('node:fs/promises')
41
+ return await fs.readFile(path, 'utf-8')
42
+ } catch {
43
+ return undefined
44
+ }
45
+ },
46
+ homedir(): string {
47
+ return process.env.HOME ?? process.env.USERPROFILE ?? ''
48
+ },
49
+ platform(): string {
50
+ return process.platform
51
+ },
52
+ env(name: string): string | undefined {
53
+ return process.env[name]
54
+ },
55
+ async exec(command: string): Promise<string | undefined> {
56
+ try {
57
+ const { execSync } = await import('node:child_process')
58
+ return execSync(command, { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).trim()
59
+ } catch {
60
+ return undefined
61
+ }
62
+ },
63
+ }
64
+ }
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // Helpers
68
+ // ---------------------------------------------------------------------------
69
+
70
+ function join(base: string, ...segments: string[]): string {
71
+ return [base, ...segments].join('/')
72
+ }
73
+
74
+ function parseJson(raw: string): Record<string, unknown> | undefined {
75
+ try {
76
+ const parsed = JSON.parse(raw)
77
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
78
+ ? (parsed as Record<string, unknown>)
79
+ : undefined
80
+ } catch {
81
+ return undefined
82
+ }
83
+ }
84
+
85
+ function stripJsonComments(raw: string): string {
86
+ return raw.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '')
87
+ }
88
+
89
+ async function readJson(ctx: ScanContext, path: string): Promise<Record<string, unknown> | undefined> {
90
+ const raw = await ctx.readFile(path)
91
+ if (!raw) return undefined
92
+ return parseJson(raw) ?? parseJson(stripJsonComments(raw))
93
+ }
94
+
95
+ function configDir(ctx: ScanContext): string {
96
+ const xdg = ctx.env('XDG_CONFIG_HOME')
97
+ if (xdg) return xdg
98
+ return join(ctx.homedir(), '.config')
99
+ }
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // Scanners
103
+ // ---------------------------------------------------------------------------
104
+
105
+ const copilotScanner: DiskScanner = {
106
+ name: 'github-copilot',
107
+ async scan(ctx) {
108
+ const results: DiskScanResult[] = []
109
+ const base = join(configDir(ctx), 'github-copilot')
110
+ const files = ['hosts.json', 'apps.json']
111
+
112
+ for (const file of files) {
113
+ const path = join(base, file)
114
+ const json = await readJson(ctx, path)
115
+ if (!json) continue
116
+
117
+ for (const [key, value] of Object.entries(json)) {
118
+ if (!key.includes('github.com')) continue
119
+ const token = value && typeof value === 'object' ? (value as Record<string, unknown>).oauth_token : undefined
120
+ if (typeof token === 'string' && token.length > 0) {
121
+ log('copilot: found token in %s', path)
122
+ results.push({ providerId: 'github-copilot', source: path, key: token })
123
+ return results
124
+ }
125
+ }
126
+ }
127
+
128
+ return results
129
+ },
130
+ }
131
+
132
+ const claudeCodeScanner: DiskScanner = {
133
+ name: 'claude-code',
134
+ async scan(ctx) {
135
+ const results: DiskScanResult[] = []
136
+ const base = join(ctx.homedir(), '.claude')
137
+ const files = ['settings.json', 'settings.local.json']
138
+
139
+ for (const file of files) {
140
+ const path = join(base, file)
141
+ const json = await readJson(ctx, path)
142
+ if (!json) continue
143
+
144
+ for (const field of ['anthropicApiKey', 'anthropic_api_key', 'ANTHROPIC_API_KEY', 'apiKey']) {
145
+ const value = json[field]
146
+ if (typeof value === 'string' && value.length > 0) {
147
+ log('claude-code: found key in %s (%s)', path, field)
148
+ results.push({ providerId: 'anthropic', source: path, key: value })
149
+ return results
150
+ }
151
+ }
152
+ }
153
+
154
+ return results
155
+ },
156
+ }
157
+
158
+ const codexCliScanner: DiskScanner = {
159
+ name: 'codex-cli',
160
+ async scan(ctx) {
161
+ const results: DiskScanResult[] = []
162
+ const authPath = join(ctx.homedir(), '.codex', 'auth.json')
163
+ const json = await readJson(ctx, authPath)
164
+ if (!json) return results
165
+ // Check for OPENAI_API_KEY first (user-set API key takes priority)
166
+ const apiKey = json.OPENAI_API_KEY
167
+ if (typeof apiKey === 'string' && apiKey.length > 0) {
168
+ log('codex-cli: found OPENAI_API_KEY in %s', authPath)
169
+ results.push({ providerId: 'openai', source: authPath, key: apiKey, credentialType: 'api' })
170
+ return results
171
+ }
172
+
173
+ // OAuth tokens from codex login
174
+ const tokens = json.tokens
175
+ if (tokens !== null && typeof tokens === 'object' && !Array.isArray(tokens)) {
176
+ const t = tokens as Record<string, unknown>
177
+ const accessToken = t.access_token
178
+ if (typeof accessToken === 'string' && accessToken.length > 0) {
179
+ const refreshToken = typeof t.refresh_token === 'string' ? t.refresh_token : undefined
180
+ const accountId = typeof t.account_id === 'string' ? t.account_id : undefined
181
+ log('codex-cli: found OAuth tokens in %s', authPath)
182
+ results.push({
183
+ providerId: 'openai',
184
+ source: authPath,
185
+ key: accessToken,
186
+ credentialType: 'oauth',
187
+ refresh: refreshToken,
188
+ accountId,
189
+ })
190
+ return results
191
+ }
192
+ }
193
+
194
+ // Fallback: flat token or apiKey field
195
+ const token = json.token ?? json.apiKey
196
+ if (typeof token === 'string' && token.length > 0) {
197
+ results.push({ providerId: 'openai', source: authPath, key: token })
198
+ }
199
+ return results
200
+ },
201
+ }
202
+
203
+ const geminiCliScanner: DiskScanner = {
204
+ name: 'gemini-cli',
205
+ async scan(ctx) {
206
+ const results: DiskScanResult[] = []
207
+ const geminiHome = ctx.env('GEMINI_CLI_HOME') ?? join(ctx.homedir(), '.gemini')
208
+ const files = ['oauth_creds.json', 'google_accounts.json']
209
+
210
+ for (const file of files) {
211
+ const path = join(geminiHome, file)
212
+ const json = await readJson(ctx, path)
213
+ if (!json) continue
214
+ const accessToken = typeof json.access_token === 'string' ? json.access_token : undefined
215
+ const refreshToken = typeof json.refresh_token === 'string' ? json.refresh_token : undefined
216
+ const expiryDate = typeof json.expiry_date === 'number' ? json.expiry_date : undefined
217
+
218
+ if (accessToken || refreshToken) {
219
+ log('gemini-cli: found credentials in %s', path)
220
+ results.push({
221
+ providerId: 'google',
222
+ source: path,
223
+ key: accessToken,
224
+ credentialType: 'oauth',
225
+ refresh: refreshToken,
226
+ expires: expiryDate,
227
+ })
228
+ return results
229
+ }
230
+
231
+ // Fallback: google_accounts.json with accounts array
232
+ if (Array.isArray(json.accounts) && (json.accounts as unknown[]).length > 0) {
233
+ log('gemini-cli: found credentials in %s', path)
234
+ results.push({ providerId: 'google', source: path })
235
+ return results
236
+ }
237
+ }
238
+
239
+ return results
240
+ },
241
+ }
242
+
243
+ const gcloudAdcScanner: DiskScanner = {
244
+ name: 'gcloud-adc',
245
+ async scan(ctx) {
246
+ const results: DiskScanResult[] = []
247
+ const cloudSdkConfig = ctx.env('CLOUDSDK_CONFIG')
248
+ const paths = [
249
+ ctx.env('GOOGLE_APPLICATION_CREDENTIALS'),
250
+ cloudSdkConfig ? join(cloudSdkConfig, 'application_default_credentials.json') : undefined,
251
+ join(configDir(ctx), 'gcloud', 'application_default_credentials.json'),
252
+ ].filter((p): p is string => typeof p === 'string')
253
+
254
+ for (const path of paths) {
255
+ const json = await readJson(ctx, path)
256
+ if (!json) continue
257
+
258
+ if ('refresh_token' in json || 'type' in json || 'private_key' in json) {
259
+ log('gcloud-adc: found ADC in %s', path)
260
+ results.push({ providerId: 'google-vertex', source: path })
261
+ return results
262
+ }
263
+ }
264
+
265
+ return results
266
+ },
267
+ }
268
+
269
+ const awsCredentialsScanner: DiskScanner = {
270
+ name: 'aws-credentials',
271
+ async scan(ctx) {
272
+ const results: DiskScanResult[] = []
273
+ const credPath = ctx.env('AWS_SHARED_CREDENTIALS_FILE') ?? join(ctx.homedir(), '.aws', 'credentials')
274
+ const raw = await ctx.readFile(credPath)
275
+ if (!raw) return results
276
+
277
+ const profile = ctx.env('AWS_PROFILE') ?? 'default'
278
+ const key = parseIniProfileKey(raw, profile, 'aws_access_key_id')
279
+
280
+ if (key) {
281
+ log('aws: found credentials for profile [%s] in %s', profile, credPath)
282
+ results.push({ providerId: 'amazon-bedrock', source: credPath })
283
+ }
284
+
285
+ return results
286
+ },
287
+ }
288
+
289
+ // Scan opencode's auth.json for saved credentials (API keys only).
290
+ // NOTE: OAuth credentials from opencode should NOT be reused. Providers like
291
+ // Anthropic use refresh token rotation — each refresh invalidates the old
292
+ // refresh_token and issues a new one. If two applications share the same OAuth
293
+ // credential, whichever refreshes first will break the other. API key credentials
294
+ // (type: 'api') are safe to share since they don't expire or rotate.
295
+ const opencodeAuthScanner: DiskScanner = {
296
+ name: 'opencode-auth',
297
+ async scan(ctx) {
298
+ const results: DiskScanResult[] = []
299
+ const xdgData = ctx.env('XDG_DATA_HOME')
300
+ const home = ctx.homedir()
301
+ const platform = ctx.platform()
302
+
303
+ const paths = [
304
+ xdgData ? join(xdgData, 'opencode', 'auth.json') : undefined,
305
+ platform === 'darwin' ? join(home, 'Library', 'Application Support', 'opencode', 'auth.json') : undefined,
306
+ join(home, '.local', 'share', 'opencode', 'auth.json'),
307
+ join(home, '.config', 'opencode', 'auth.json'),
308
+ ].filter((p): p is string => typeof p === 'string')
309
+
310
+ for (const path of paths) {
311
+ const json = await readJson(ctx, path)
312
+ if (!json) continue
313
+
314
+ for (const [providerId, entry] of Object.entries(json)) {
315
+ if (!entry || typeof entry !== 'object') continue
316
+ const typed = entry as Record<string, unknown>
317
+ const type = typed.type
318
+ if (type !== 'api' && type !== 'oauth' && type !== 'wellknown') continue
319
+
320
+ const key = typeof typed.key === 'string' ? typed.key : undefined
321
+ const refresh = typeof typed.refresh === 'string' ? typed.refresh : undefined
322
+ const accountId = typeof typed.accountId === 'string' ? typed.accountId : undefined
323
+ const expires = typeof typed.expires === 'number' ? typed.expires : undefined
324
+ log('opencode-auth: found %s (%s) in %s', providerId, type, path)
325
+ results.push({
326
+ providerId,
327
+ source: path,
328
+ key,
329
+ credentialType: type as DiskScanResult['credentialType'],
330
+ refresh,
331
+ accountId,
332
+ expires,
333
+ })
334
+ }
335
+
336
+ if (results.length > 0) return results
337
+ }
338
+
339
+ return results
340
+ },
341
+ }
342
+
343
+ const vscodeSettingsScanner: DiskScanner = {
344
+ name: 'vscode',
345
+ async scan(ctx) {
346
+ const results: DiskScanResult[] = []
347
+ const home = ctx.homedir()
348
+ const platform = ctx.platform()
349
+
350
+ const vscodePaths = [
351
+ platform === 'darwin'
352
+ ? join(home, 'Library', 'Application Support', 'Code', 'User', 'globalStorage', 'github.copilot', 'hosts.json')
353
+ : undefined,
354
+ join(configDir(ctx), 'Code', 'User', 'globalStorage', 'github.copilot', 'hosts.json'),
355
+ ].filter((p): p is string => typeof p === 'string')
356
+
357
+ for (const path of vscodePaths) {
358
+ const json = await readJson(ctx, path)
359
+ if (!json) continue
360
+
361
+ for (const [key, value] of Object.entries(json)) {
362
+ if (!key.includes('github.com')) continue
363
+ const token = value && typeof value === 'object' ? (value as Record<string, unknown>).oauth_token : undefined
364
+ if (typeof token === 'string' && token.length > 0) {
365
+ log('vscode: found copilot token in %s', path)
366
+ results.push({ providerId: 'github-copilot', source: path, key: token })
367
+ return results
368
+ }
369
+ }
370
+ }
371
+
372
+ return results
373
+ },
374
+ }
375
+
376
+ // ---------------------------------------------------------------------------
377
+ // INI parser (minimal, for AWS credentials)
378
+ // ---------------------------------------------------------------------------
379
+
380
+ function parseIniProfileKey(raw: string, profile: string, key: string): string | undefined {
381
+ const lines = raw.split('\n')
382
+ let inProfile = false
383
+
384
+ for (const line of lines) {
385
+ const trimmed = line.trim()
386
+ if (trimmed.startsWith('[')) {
387
+ const name = trimmed.slice(1, trimmed.indexOf(']')).trim()
388
+ inProfile = name === profile
389
+ continue
390
+ }
391
+ if (inProfile && trimmed.startsWith(key)) {
392
+ const eq = trimmed.indexOf('=')
393
+ if (eq !== -1) return trimmed.slice(eq + 1).trim()
394
+ }
395
+ }
396
+ return undefined
397
+ }
398
+
399
+ // ---------------------------------------------------------------------------
400
+ // Registry
401
+ // ---------------------------------------------------------------------------
402
+
403
+ const cursorScanner: DiskScanner = {
404
+ name: 'cursor',
405
+ async scan(ctx) {
406
+ const results: DiskScanResult[] = []
407
+ const home = ctx.homedir()
408
+ const platform = ctx.platform()
409
+
410
+ const dbPaths = [
411
+ platform === 'darwin'
412
+ ? join(home, 'Library', 'Application Support', 'Cursor', 'User', 'globalStorage', 'state.vscdb')
413
+ : undefined,
414
+ platform === 'win32'
415
+ ? join(ctx.env('APPDATA') ?? join(home, 'AppData', 'Roaming'), 'Cursor', 'User', 'globalStorage', 'state.vscdb')
416
+ : undefined,
417
+ join(configDir(ctx), 'Cursor', 'User', 'globalStorage', 'state.vscdb'),
418
+ ].filter((p): p is string => typeof p === 'string')
419
+
420
+ if (!ctx.exec) return results
421
+
422
+ for (const dbPath of dbPaths) {
423
+ const query = `SELECT value FROM ItemTable WHERE key='cursorAuth/openAIKey'`
424
+ const value = await ctx.exec(`sqlite3 "${dbPath}" "${query}"`)
425
+ if (value && value.length > 0) {
426
+ log('cursor: found openAIKey in %s', dbPath)
427
+ results.push({ providerId: 'cursor', source: dbPath, key: value })
428
+ return results
429
+ }
430
+ }
431
+
432
+ return results
433
+ },
434
+ }
435
+
436
+ export const DEFAULT_SCANNERS: DiskScanner[] = [
437
+ copilotScanner,
438
+ vscodeSettingsScanner,
439
+ claudeCodeScanner,
440
+ codexCliScanner,
441
+ geminiCliScanner,
442
+ gcloudAdcScanner,
443
+ awsCredentialsScanner,
444
+ cursorScanner,
445
+ opencodeAuthScanner,
446
+ ]
447
+
448
+ export async function runDiskScanners(scanners: DiskScanner[], ctx?: ScanContext): Promise<DiskScanResult[]> {
449
+ const scanCtx = ctx ?? createNodeScanContext()
450
+ const results: DiskScanResult[] = []
451
+
452
+ for (const scanner of scanners) {
453
+ try {
454
+ const found = await scanner.scan(scanCtx)
455
+ results.push(...found)
456
+ } catch (err: unknown) {
457
+ log('scanner %s failed: %s', scanner.name, err instanceof Error ? err.message : String(err))
458
+ }
459
+ }
460
+
461
+ return results
462
+ }