@gajae-code/ai 0.2.5 → 0.3.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 CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.3.0] - 2026-06-03
6
+
7
+ ### Added
8
+
9
+ - Added xAI to the `/login` provider catalog as a Grok OAuth login with PKCE, refresh-token storage, and mocked login/refresh coverage.
10
+
5
11
  ## [0.2.4] - 2026-06-02
6
12
 
7
13
  ### Added
package/README.md CHANGED
@@ -1014,6 +1014,7 @@ Several providers support OAuth authentication (some also support static API key
1014
1014
  - **Google Gemini CLI** (Gemini 2.0/2.5 via Google Cloud Code Assist; free tier or paid subscription)
1015
1015
  - **Antigravity** (Free Gemini 3, Anthropic model, GPT-OSS via Google Cloud)
1016
1016
  - **Qwen Portal** (Qwen OAuth token or API key)
1017
+ - **xAI** (Grok OAuth login via xAI account)
1017
1018
 
1018
1019
  For paid Cloud Code Assist subscriptions, set `GOOGLE_CLOUD_PROJECT` or `GOOGLE_CLOUD_PROJECT_ID` to your project ID.
1019
1020
 
@@ -1063,12 +1064,13 @@ The quickest way to authenticate:
1063
1064
  bunx @gajae-code/ai login # interactive provider selection
1064
1065
  bunx @gajae-code/ai login anthropic # login to specific provider
1065
1066
  bunx @gajae-code/ai login vllm # store vLLM API key (or placeholder for local no-auth)
1067
+ bunx @gajae-code/ai login xai # sign in with xAI/Grok OAuth
1066
1068
  bunx @gajae-code/ai list # list available providers
1067
1069
  ```
1068
1070
 
1069
- Credentials are saved to `agent.db` in the agent directory. `/login qianfan` opens the Qianfan console and stores the pasted API key.
1071
+ Credentials are saved to `agent.db` in the agent directory. `/login qianfan` opens the Qianfan console and stores the pasted API key; `/login xai` opens xAI/Grok OAuth login and stores refreshable OAuth credentials.
1070
1072
 
1071
- `login` supports OAuth providers (Anthropic, OpenAI code provider, GitHub Copilot, Gemini CLI, Antigravity) and API-key onboarding flows.
1073
+ `login` supports OAuth providers (Anthropic, OpenAI code provider, GitHub Copilot, Gemini CLI, Antigravity, xAI) and API-key onboarding flows.
1072
1074
 
1073
1075
  For the current API-key onboarding flows, the library covers Together, Moonshot, Qianfan, NVIDIA, NanoGPT, Hugging Face, Venice, Xiaomi, vLLM, LiteLLM, Cloudflare AI Gateway, Qwen Portal, and Ollama Cloud. Ollama remains the local runtime integration; set `OLLAMA_API_KEY` only when your local or self-hosted deployment enforces bearer auth.
1074
1076
 
@@ -7,7 +7,7 @@ export type OAuthCredentials = {
7
7
  email?: string;
8
8
  accountId?: string;
9
9
  };
10
- export type OAuthProvider = "alibaba-coding-plan" | "anthropic" | "cerebras" | "cloudflare-ai-gateway" | "cursor" | "deepseek" | "fireworks" | "firepass" | "github-copilot" | "google-gemini-cli" | "google-antigravity" | "gitlab-duo" | "huggingface" | "kimi-code" | "kilo" | "kagi" | "litellm" | "lm-studio" | "minimax-code" | "minimax-code-cn" | "moonshot" | "nvidia" | "nanogpt" | "ollama" | "ollama-cloud" | "openai-codex" | "openai-codex-device" | "opencode-go" | "opencode-zen" | "parallel" | "perplexity" | "qianfan" | "qwen-portal" | "synthetic" | "tavily" | "together" | "venice" | "vercel-ai-gateway" | "vllm" | "xiaomi" | "zenmux" | "zai";
10
+ export type OAuthProvider = "alibaba-coding-plan" | "anthropic" | "cerebras" | "cloudflare-ai-gateway" | "cursor" | "deepseek" | "fireworks" | "firepass" | "github-copilot" | "google-gemini-cli" | "google-antigravity" | "gitlab-duo" | "huggingface" | "kimi-code" | "kilo" | "kagi" | "litellm" | "lm-studio" | "minimax-code" | "minimax-code-cn" | "moonshot" | "nvidia" | "nanogpt" | "ollama" | "ollama-cloud" | "openai-codex" | "openai-codex-device" | "opencode-go" | "opencode-zen" | "parallel" | "perplexity" | "qianfan" | "qwen-portal" | "synthetic" | "tavily" | "together" | "venice" | "vercel-ai-gateway" | "vllm" | "xai" | "xiaomi" | "zenmux" | "zai";
11
11
  export type OAuthProviderId = OAuthProvider | (string & {});
12
12
  export type OAuthPrompt = {
13
13
  message: string;
@@ -0,0 +1,23 @@
1
+ /** xAI OAuth flow (Grok account login). */
2
+ import { OAuthCallbackFlow } from "./callback-server";
3
+ import type { OAuthController, OAuthCredentials } from "./types";
4
+ export declare const XAI_OAUTH_DISCOVERY_URL = "https://auth.x.ai/.well-known/openid-configuration";
5
+ export declare const XAI_OAUTH_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
6
+ export declare const XAI_OAUTH_SCOPE = "openid profile email offline_access grok-cli:access api:access";
7
+ interface XaiDiscovery {
8
+ authorizationEndpoint: string;
9
+ tokenEndpoint: string;
10
+ }
11
+ export declare function discoverXaiOAuthEndpoints(signal?: AbortSignal): Promise<XaiDiscovery>;
12
+ export declare class XaiOAuthFlow extends OAuthCallbackFlow {
13
+ #private;
14
+ constructor(ctrl: OAuthController);
15
+ generateAuthUrl(state: string, redirectUri: string): Promise<{
16
+ url: string;
17
+ instructions?: string;
18
+ }>;
19
+ exchangeToken(code: string, _state: string, redirectUri: string): Promise<OAuthCredentials>;
20
+ }
21
+ export declare function loginXai(ctrl: OAuthController): Promise<OAuthCredentials>;
22
+ export declare function refreshXaiToken(refreshToken: string, signal?: AbortSignal): Promise<OAuthCredentials>;
23
+ export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@gajae-code/ai",
4
- "version": "0.2.5",
4
+ "version": "0.3.0",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://gaebal-gajae.dev",
7
7
  "author": "Yeachan-Heo",
@@ -43,7 +43,7 @@
43
43
  "dependencies": {
44
44
  "@anthropic-ai/sdk": "^0.94.0",
45
45
  "@bufbuild/protobuf": "^2.12.0",
46
- "@gajae-code/utils": "0.2.5",
46
+ "@gajae-code/utils": "0.3.0",
47
47
  "openai": "^6.36.0",
48
48
  "partial-json": "^0.1.7",
49
49
  "zod": "4.4.3"
@@ -1484,6 +1484,14 @@ export class AuthStorage {
1484
1484
  await saveApiKeyCredential(apiKey);
1485
1485
  return;
1486
1486
  }
1487
+ case "xai": {
1488
+ const { loginXai } = await import("./utils/oauth/xai");
1489
+ credentials = await loginXai({
1490
+ ...ctrl,
1491
+ onManualCodeInput: ctrl.onManualCodeInput ?? manualCodeInput,
1492
+ });
1493
+ break;
1494
+ }
1487
1495
  case "fireworks": {
1488
1496
  const { loginFireworks } = await import("./utils/oauth/fireworks");
1489
1497
  const apiKey = await loginFireworks(ctrl);
@@ -1637,6 +1645,13 @@ export class AuthStorage {
1637
1645
  }
1638
1646
  }
1639
1647
  const newCredential: OAuthCredential = { type: "oauth", ...credentials };
1648
+ if (provider === "xai") {
1649
+ const existingOAuthCredentials = this.#getCredentialsForProvider(provider).filter(
1650
+ (credential): credential is OAuthCredential => credential.type === "oauth",
1651
+ );
1652
+ await this.set(provider, [...existingOAuthCredentials, newCredential]);
1653
+ return;
1654
+ }
1640
1655
  await this.#upsertOAuthCredential(provider, newCredential);
1641
1656
  }
1642
1657
 
package/src/cli.ts CHANGED
@@ -110,6 +110,7 @@ Providers:
110
110
  tavily Tavily
111
111
  zai Z.AI (GLM Coding Plan)
112
112
  deepseek DeepSeek
113
+ xai xAI
113
114
  nanogpt NanoGPT
114
115
  minimax-code MiniMax Coding Plan (International)
115
116
  minimax-code-cn MiniMax Coding Plan (China)
@@ -60,6 +60,11 @@ const builtInOAuthProviders: OAuthProviderInfo[] = [
60
60
  name: "DeepSeek",
61
61
  available: true,
62
62
  },
63
+ {
64
+ id: "xai",
65
+ name: "xAI",
66
+ available: true,
67
+ },
63
68
  {
64
69
  id: "fireworks",
65
70
  name: "Fireworks",
@@ -310,6 +315,11 @@ export async function refreshOAuthToken(
310
315
  newCredentials = await refreshCursorToken(credentials.refresh);
311
316
  break;
312
317
  }
318
+ case "xai": {
319
+ const { refreshXaiToken } = await import("./xai");
320
+ newCredentials = await refreshXaiToken(credentials.refresh);
321
+ break;
322
+ }
313
323
  case "kilo":
314
324
  case "perplexity":
315
325
  case "huggingface":
@@ -48,6 +48,7 @@ export type OAuthProvider =
48
48
  | "venice"
49
49
  | "vercel-ai-gateway"
50
50
  | "vllm"
51
+ | "xai"
51
52
  | "xiaomi"
52
53
  | "zenmux"
53
54
  | "zai";
@@ -0,0 +1,210 @@
1
+ /** xAI OAuth flow (Grok account login). */
2
+ import { OAuthCallbackFlow, type OAuthCallbackFlowOptions } from "./callback-server";
3
+ import { generatePKCE } from "./pkce";
4
+ import type { OAuthController, OAuthCredentials } from "./types";
5
+
6
+ const XAI_OAUTH_ISSUER = "https://auth.x.ai";
7
+ export const XAI_OAUTH_DISCOVERY_URL = `${XAI_OAUTH_ISSUER}/.well-known/openid-configuration`;
8
+ export const XAI_OAUTH_CLIENT_ID = "b1a00492-073a-47ea-816f-4c329264a828";
9
+ export const XAI_OAUTH_SCOPE = "openid profile email offline_access grok-cli:access api:access";
10
+ const XAI_OAUTH_CALLBACK_PORT = 56121;
11
+ const XAI_OAUTH_CALLBACK_PATH = "/callback";
12
+ const XAI_OAUTH_REFRESH_SKEW_MS = 2 * 60 * 1000;
13
+ const TOKEN_REQUEST_TIMEOUT_MS = 30_000;
14
+
15
+ interface XaiDiscovery {
16
+ authorizationEndpoint: string;
17
+ tokenEndpoint: string;
18
+ }
19
+
20
+ interface XaiDiscoveryPayload {
21
+ authorization_endpoint?: unknown;
22
+ token_endpoint?: unknown;
23
+ }
24
+
25
+ interface XaiTokenPayload {
26
+ access_token?: unknown;
27
+ refresh_token?: unknown;
28
+ expires_in?: unknown;
29
+ id_token?: unknown;
30
+ token_type?: unknown;
31
+ }
32
+
33
+ interface XaiJwtPayload {
34
+ sub?: unknown;
35
+ email?: unknown;
36
+ [key: string]: unknown;
37
+ }
38
+
39
+ function requestSignal(signal: AbortSignal | undefined): AbortSignal {
40
+ const timeoutSignal = AbortSignal.timeout(TOKEN_REQUEST_TIMEOUT_MS);
41
+ return signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
42
+ }
43
+
44
+ function validateXaiEndpoint(rawUrl: string): string {
45
+ const parsed = new URL(rawUrl);
46
+ const host = parsed.hostname.toLowerCase();
47
+ if (parsed.protocol !== "https:" || (host !== "x.ai" && !host.endsWith(".x.ai"))) {
48
+ throw new Error(`xAI OAuth discovery returned an unexpected endpoint: ${rawUrl}`);
49
+ }
50
+ return parsed.toString();
51
+ }
52
+
53
+ export async function discoverXaiOAuthEndpoints(signal?: AbortSignal): Promise<XaiDiscovery> {
54
+ const response = await fetch(XAI_OAUTH_DISCOVERY_URL, {
55
+ headers: { Accept: "application/json" },
56
+ signal: requestSignal(signal),
57
+ });
58
+ if (!response.ok) {
59
+ throw new Error(`xAI OAuth discovery failed: ${response.status} ${await response.text()}`);
60
+ }
61
+
62
+ const payload = (await response.json()) as XaiDiscoveryPayload;
63
+ if (typeof payload.authorization_endpoint !== "string" || typeof payload.token_endpoint !== "string") {
64
+ throw new Error("xAI OAuth discovery response missing authorization/token endpoints");
65
+ }
66
+
67
+ return {
68
+ authorizationEndpoint: validateXaiEndpoint(payload.authorization_endpoint),
69
+ tokenEndpoint: validateXaiEndpoint(payload.token_endpoint),
70
+ };
71
+ }
72
+
73
+ function decodeJwtPayload(token: string): XaiJwtPayload | undefined {
74
+ const parts = token.split(".");
75
+ const payload = parts[1];
76
+ if (parts.length !== 3 || !payload) return undefined;
77
+ try {
78
+ return JSON.parse(Buffer.from(payload, "base64url").toString("utf8")) as XaiJwtPayload;
79
+ } catch {
80
+ return undefined;
81
+ }
82
+ }
83
+
84
+ function getTokenIdentity(accessToken: string, idToken: string | undefined): { accountId?: string; email?: string } {
85
+ const payload = (idToken ? decodeJwtPayload(idToken) : undefined) ?? decodeJwtPayload(accessToken);
86
+ const accountId = typeof payload?.sub === "string" && payload.sub.length > 0 ? payload.sub : undefined;
87
+ const email =
88
+ typeof payload?.email === "string" && payload.email.length > 0 ? payload.email.toLowerCase() : undefined;
89
+ return { accountId, email };
90
+ }
91
+
92
+ async function postXaiToken(
93
+ tokenEndpoint: string,
94
+ body: Record<string, string>,
95
+ signal?: AbortSignal,
96
+ ): Promise<XaiTokenPayload> {
97
+ const response = await fetch(tokenEndpoint, {
98
+ method: "POST",
99
+ headers: {
100
+ Accept: "application/json",
101
+ "Content-Type": "application/x-www-form-urlencoded",
102
+ },
103
+ body: new URLSearchParams(body).toString(),
104
+ signal: requestSignal(signal),
105
+ });
106
+ if (!response.ok) {
107
+ throw new Error(`xAI token request failed: ${response.status} ${await response.text()}`);
108
+ }
109
+ return (await response.json()) as XaiTokenPayload;
110
+ }
111
+
112
+ function credentialsFromTokenPayload(payload: XaiTokenPayload, refreshFallback = ""): OAuthCredentials {
113
+ if (typeof payload.access_token !== "string" || payload.access_token.length === 0) {
114
+ throw new Error("xAI token response did not include an access token");
115
+ }
116
+ const refresh =
117
+ typeof payload.refresh_token === "string" && payload.refresh_token.length > 0
118
+ ? payload.refresh_token
119
+ : refreshFallback;
120
+ if (!refresh) {
121
+ throw new Error("xAI token response did not include a refresh token");
122
+ }
123
+ const expiresIn =
124
+ typeof payload.expires_in === "number" && Number.isFinite(payload.expires_in) ? payload.expires_in : 3600;
125
+ const idToken = typeof payload.id_token === "string" ? payload.id_token : undefined;
126
+ const { accountId, email } = getTokenIdentity(payload.access_token, idToken);
127
+ return {
128
+ refresh,
129
+ access: payload.access_token,
130
+ expires: Date.now() + expiresIn * 1000 - XAI_OAUTH_REFRESH_SKEW_MS,
131
+ accountId,
132
+ email,
133
+ };
134
+ }
135
+
136
+ export class XaiOAuthFlow extends OAuthCallbackFlow {
137
+ #verifier = "";
138
+ #discovery: XaiDiscovery | undefined;
139
+
140
+ constructor(ctrl: OAuthController) {
141
+ super(ctrl, {
142
+ preferredPort: XAI_OAUTH_CALLBACK_PORT,
143
+ callbackPath: XAI_OAUTH_CALLBACK_PATH,
144
+ callbackHostname: "127.0.0.1",
145
+ callbackBindHostname: "127.0.0.1",
146
+ redirectUri: `http://127.0.0.1:${XAI_OAUTH_CALLBACK_PORT}${XAI_OAUTH_CALLBACK_PATH}`,
147
+ } satisfies OAuthCallbackFlowOptions);
148
+ }
149
+
150
+ async generateAuthUrl(state: string, redirectUri: string): Promise<{ url: string; instructions?: string }> {
151
+ const pkce = await generatePKCE();
152
+ this.#verifier = pkce.verifier;
153
+ this.#discovery = await discoverXaiOAuthEndpoints(this.ctrl.signal);
154
+ const params = new URLSearchParams({
155
+ response_type: "code",
156
+ client_id: XAI_OAUTH_CLIENT_ID,
157
+ redirect_uri: redirectUri,
158
+ scope: XAI_OAUTH_SCOPE,
159
+ code_challenge: pkce.challenge,
160
+ code_challenge_method: "S256",
161
+ state,
162
+ nonce: crypto.randomUUID(),
163
+ });
164
+ return {
165
+ url: `${this.#discovery.authorizationEndpoint}?${params.toString()}`,
166
+ instructions:
167
+ "Complete xAI/Grok login in your browser. If the browser cannot reach this machine, paste the final redirect URL or authorization code when prompted.",
168
+ };
169
+ }
170
+
171
+ async exchangeToken(code: string, _state: string, redirectUri: string): Promise<OAuthCredentials> {
172
+ if (!this.#verifier) {
173
+ throw new Error("xAI OAuth PKCE verifier was not initialized");
174
+ }
175
+ const discovery = this.#discovery ?? (await discoverXaiOAuthEndpoints(this.ctrl.signal));
176
+ const tokenPayload = await postXaiToken(
177
+ discovery.tokenEndpoint,
178
+ {
179
+ grant_type: "authorization_code",
180
+ client_id: XAI_OAUTH_CLIENT_ID,
181
+ code,
182
+ redirect_uri: redirectUri,
183
+ code_verifier: this.#verifier,
184
+ },
185
+ this.ctrl.signal,
186
+ );
187
+ return credentialsFromTokenPayload(tokenPayload);
188
+ }
189
+ }
190
+
191
+ export async function loginXai(ctrl: OAuthController): Promise<OAuthCredentials> {
192
+ return new XaiOAuthFlow(ctrl).login();
193
+ }
194
+
195
+ export async function refreshXaiToken(refreshToken: string, signal?: AbortSignal): Promise<OAuthCredentials> {
196
+ if (!refreshToken) {
197
+ throw new Error("xAI credentials are expired and do not include a refresh token");
198
+ }
199
+ const discovery = await discoverXaiOAuthEndpoints(signal);
200
+ const tokenPayload = await postXaiToken(
201
+ discovery.tokenEndpoint,
202
+ {
203
+ grant_type: "refresh_token",
204
+ client_id: XAI_OAUTH_CLIENT_ID,
205
+ refresh_token: refreshToken,
206
+ },
207
+ signal,
208
+ );
209
+ return credentialsFromTokenPayload(tokenPayload, refreshToken);
210
+ }