egaki 0.4.1 → 0.6.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.
@@ -2,12 +2,13 @@
2
2
  // Reads and writes API keys from ~/.config/egaki/credentials.json.
3
3
  // Keys are stored per-provider and injected as env vars at runtime.
4
4
  //
5
- // Most providers store a plain API key string. The 'chatgpt' provider stores
6
- // a structured ChatGptAuth object with OAuth tokens for the Codex backend flow.
5
+ // Most providers store a plain API key string. OAuth providers store structured
6
+ // token objects for their custom backend flows.
7
7
  import fs from 'node:fs'
8
8
  import path from 'node:path'
9
9
  import os from 'node:os'
10
10
  import type { ChatGptAuth } from './chatgpt-auth.js'
11
+ import type { XaiAuth } from './xai-auth.js'
11
12
 
12
13
  export type ProviderInfo = {
13
14
  envVar: string
@@ -57,6 +58,17 @@ export const PROVIDERS: Record<string, ProviderInfo> = {
57
58
  label: 'fal.ai (Flux, SDXL, Recraft)',
58
59
  hint: 'Get your key at https://fal.ai/dashboard/keys',
59
60
  },
61
+ xai: {
62
+ envVar: 'XAI_API_KEY',
63
+ label: 'xAI (API key)',
64
+ hint: 'Get your key at https://console.x.ai/team/default/api-keys',
65
+ },
66
+ 'xai-oauth': {
67
+ envVar: 'XAI_API_KEY',
68
+ label: 'xAI Grok Build (use your subscription)',
69
+ hint: 'Sign in with your xAI / Grok account via browser',
70
+ oauth: true,
71
+ },
60
72
  }
61
73
 
62
74
  export const EGAKI_GATEWAY_URL = 'https://egaki.org/v3/ai'
@@ -72,8 +84,8 @@ function getCredentialsPath(): string {
72
84
  return path.join(getConfigDir(), 'credentials.json')
73
85
  }
74
86
 
75
- /** A credential value is either a plain API key string or a ChatGPT OAuth object. */
76
- export type CredentialValue = string | ChatGptAuth
87
+ /** A credential value is a plain API key string, a ChatGPT OAuth object, or an xAI OAuth object. */
88
+ export type CredentialValue = string | ChatGptAuth | XaiAuth
77
89
  export type Credentials = Record<string, CredentialValue>
78
90
 
79
91
  export function readCredentials(): Credentials {
@@ -115,6 +127,19 @@ export function getChatGptAuth(): ChatGptAuth | undefined {
115
127
  return undefined
116
128
  }
117
129
 
130
+ export function saveXaiAuth(auth: XaiAuth): void {
131
+ const creds = readCredentials()
132
+ creds['xai-oauth'] = auth
133
+ writeCredentials(creds)
134
+ }
135
+
136
+ export function getXaiAuth(): XaiAuth | undefined {
137
+ const creds = readCredentials()
138
+ const val = creds['xai-oauth']
139
+ if (!val || typeof val !== 'object') return undefined
140
+ return val
141
+ }
142
+
118
143
  export function removeProviderKey(provider: string): void {
119
144
  const creds = readCredentials()
120
145
  delete creds[provider]
@@ -127,17 +152,26 @@ export function getProviderKey(provider: string): string | undefined {
127
152
  return typeof val === 'string' ? val : undefined
128
153
  }
129
154
 
130
- // Inject all stored non-ChatGPT credentials as env vars so the AI SDK
155
+ // Inject all stored non-OAuth credentials as env vars so the AI SDK
131
156
  // providers pick them up automatically. Env vars already set by the user take
132
157
  // precedence (we don't overwrite them).
158
+ //
159
+ // Special case: if xAI OAuth tokens exist, skip injecting the stored xai API
160
+ // key so OAuth takes priority. Only an explicit XAI_API_KEY env var (set by
161
+ // the user before egaki runs) will override OAuth.
133
162
  export function injectCredentialsToEnv(): void {
134
163
  const creds = readCredentials()
164
+ const hasXaiOAuth = Boolean(getXaiAuth())
135
165
  for (const [provider, value] of Object.entries(creds)) {
136
166
  const info = PROVIDERS[provider]
137
167
  if (!info) continue
138
168
 
139
- // Skip ChatGPT OAuth entries — handled separately
169
+ // Skip OAuth entries — handled separately via custom fetch
140
170
  if (provider === 'chatgpt') continue
171
+ if (provider === 'xai-oauth') continue
172
+
173
+ // Skip stored xai key when OAuth tokens exist (OAuth takes priority)
174
+ if (provider === 'xai' && hasXaiOAuth && !process.env[info.envVar]) continue
141
175
 
142
176
  if (typeof value === 'string' && !process.env[info.envVar]) {
143
177
  process.env[info.envVar] = value
@@ -153,6 +187,16 @@ export function shouldUseChatGptBackend(): boolean {
153
187
  return !process.env['OPENAI_API_KEY'] && Boolean(getChatGptAuth())
154
188
  }
155
189
 
190
+ /**
191
+ * Returns true when xAI models should use the OAuth flow instead of a direct
192
+ * XAI_API_KEY. Priority: explicit env var > xai-oauth tokens > stored xai key.
193
+ * Only an explicit XAI_API_KEY (set before egaki runs) overrides OAuth.
194
+ */
195
+ export function shouldUseXaiOAuth(): boolean {
196
+ if (process.env['XAI_API_KEY']) return false
197
+ return Boolean(getXaiAuth())
198
+ }
199
+
156
200
  // Check if a provider has a key available (from env or stored credentials).
157
201
  // Returns the source of the key for display purposes.
158
202
  export function getKeyStatus(provider: string): {
@@ -164,7 +208,7 @@ export function getKeyStatus(provider: string): {
164
208
  return { available: false, source: 'none' }
165
209
  }
166
210
 
167
- // ChatGPT OAuth is a special case
211
+ // OAuth providers are special cases
168
212
  if (provider === 'chatgpt') {
169
213
  const auth = getChatGptAuth()
170
214
  if (auth) {
@@ -173,6 +217,25 @@ export function getKeyStatus(provider: string): {
173
217
  return { available: false, source: 'none' }
174
218
  }
175
219
 
220
+ if (provider === 'xai-oauth') {
221
+ const auth = getXaiAuth()
222
+ if (auth) {
223
+ return { available: true, source: 'oauth' }
224
+ }
225
+ return { available: false, source: 'none' }
226
+ }
227
+
228
+ // For xai provider, report the same priority used by generation:
229
+ // explicit env var > xai-oauth tokens > stored xai key.
230
+ if (provider === 'xai') {
231
+ if (process.env[info.envVar]) return { available: true, source: 'env' }
232
+ const oauthAuth = getXaiAuth()
233
+ if (oauthAuth) return { available: true, source: 'oauth' }
234
+ const stored = getProviderKey(provider)
235
+ if (stored) return { available: true, source: 'stored' }
236
+ return { available: false, source: 'none' }
237
+ }
238
+
176
239
  if (process.env[info.envVar]) {
177
240
  return { available: true, source: 'env' }
178
241
  }
package/src/login.ts CHANGED
@@ -20,8 +20,11 @@ import {
20
20
  getKeyStatus,
21
21
  getChatGptAuth,
22
22
  saveChatGptAuth,
23
+ getXaiAuth,
24
+ saveXaiAuth,
23
25
  } from './credentials.js'
24
26
  import { chatGptOAuthLogin, extractPlanType } from './chatgpt-auth.js'
27
+ import { xaiOAuthLogin } from './xai-auth.js'
25
28
 
26
29
  export async function loginInteractive(): Promise<void> {
27
30
  intro(pc.bold('egaki login'))
@@ -36,9 +39,17 @@ export async function loginInteractive(): Promise<void> {
36
39
  return pc.green('(saved)')
37
40
  }
38
41
  if (status.source === 'oauth') {
39
- const auth = getChatGptAuth()
40
- const email = auth?.email
41
- return pc.green(`(signed in${email ? ` as ${email}` : ''})`)
42
+ if (key === 'chatgpt') {
43
+ const auth = getChatGptAuth()
44
+ const email = auth?.email
45
+ return pc.green(`(signed in${email ? ` as ${email}` : ''})`)
46
+ }
47
+ if (key === 'xai-oauth') {
48
+ const auth = getXaiAuth()
49
+ const email = auth?.email
50
+ return pc.green(`(signed in${email ? ` as ${email}` : ''})`)
51
+ }
52
+ return pc.green('(signed in)')
42
53
  }
43
54
  return pc.dim('(not configured)')
44
55
  })()
@@ -77,6 +88,14 @@ export async function loginInteractive(): Promise<void> {
77
88
  return
78
89
  }
79
90
 
91
+ // xAI Grok Build uses browser OAuth
92
+ if (provider === 'xai-oauth') {
93
+ const auth = await xaiOAuthLogin()
94
+ saveXaiAuth(auth)
95
+ outro('Done — xAI OAuth saved')
96
+ return
97
+ }
98
+
80
99
  log.info(
81
100
  `${pc.dim('Env var:')} ${info.envVar} ${pc.dim('(also used by the CLI when set)')}`,
82
101
  )
@@ -131,6 +150,14 @@ export async function loginNonInteractive({
131
150
  return
132
151
  }
133
152
 
153
+ // xAI Grok Build uses browser OAuth — key flag is not needed
154
+ if (provider === 'xai-oauth') {
155
+ const auth = await xaiOAuthLogin()
156
+ saveXaiAuth(auth)
157
+ console.log(pc.green('xAI OAuth saved'))
158
+ return
159
+ }
160
+
134
161
  if (!key || key.trim().length === 0) {
135
162
  console.error(pc.red('API key cannot be empty'))
136
163
  process.exit(1)
@@ -165,6 +192,24 @@ export function showLoginStatus(): void {
165
192
  continue
166
193
  }
167
194
 
195
+ if (key === 'xai-oauth') {
196
+ const auth = getXaiAuth()
197
+ if (auth) {
198
+ const email = auth.email ?? 'unknown'
199
+ const expired = auth.expires < Date.now()
200
+ const expiryLabel = expired
201
+ ? pc.yellow('(token expired, will auto-refresh)')
202
+ : pc.dim('(token valid)')
203
+ console.log(`${pc.green('*')} ${info.label} ${pc.green('(signed in)')}`)
204
+ console.log(pc.dim(` account: ${email}`))
205
+ console.log(` ${expiryLabel}`)
206
+ } else {
207
+ console.log(`${pc.dim('-')} ${info.label} ${pc.dim('(not signed in)')}`)
208
+ console.log(pc.dim(` run: egaki login → select xAI Grok Build`))
209
+ }
210
+ continue
211
+ }
212
+
168
213
  const icon = status.available ? pc.green('*') : pc.dim('-')
169
214
  const source = (() => {
170
215
  if (status.source === 'env') {