codemaxxing 0.1.3 → 0.1.4
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/dist/auth-cli.d.ts +16 -0
- package/dist/auth-cli.js +265 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +27 -0
- package/dist/index.js +24 -0
- package/dist/utils/auth.d.ts +59 -0
- package/dist/utils/auth.js +495 -0
- package/package.json +3 -2
- package/src/auth-cli.ts +286 -0
- package/src/config.ts +33 -0
- package/src/index.tsx +24 -0
- package/src/utils/auth.ts +606 -0
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth management for Codemaxxing
|
|
3
|
+
*
|
|
4
|
+
* Supports:
|
|
5
|
+
* - OpenRouter OAuth PKCE (browser login, no API key needed)
|
|
6
|
+
* - Anthropic setup-token (via Claude Code CLI)
|
|
7
|
+
* - OpenAI/ChatGPT (via Codex CLI cached token)
|
|
8
|
+
* - Qwen (via Qwen CLI cached credentials)
|
|
9
|
+
* - GitHub Copilot (device flow)
|
|
10
|
+
* - Manual API key entry (any provider)
|
|
11
|
+
*
|
|
12
|
+
* Credentials stored in ~/.codemaxxing/auth.json with 0o600 permissions.
|
|
13
|
+
*/
|
|
14
|
+
import { readFileSync, writeFileSync, existsSync, chmodSync, mkdirSync } from "fs";
|
|
15
|
+
import { homedir } from "os";
|
|
16
|
+
import { join } from "path";
|
|
17
|
+
import { createServer } from "http";
|
|
18
|
+
import { randomBytes, createHash } from "crypto";
|
|
19
|
+
import { execSync, exec } from "child_process";
|
|
20
|
+
// ── Paths ──
|
|
21
|
+
const CONFIG_DIR = join(homedir(), ".codemaxxing");
|
|
22
|
+
const AUTH_FILE = join(CONFIG_DIR, "auth.json");
|
|
23
|
+
export const PROVIDERS = [
|
|
24
|
+
{
|
|
25
|
+
id: "openrouter",
|
|
26
|
+
name: "OpenRouter",
|
|
27
|
+
methods: ["oauth", "api-key"],
|
|
28
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
29
|
+
consoleUrl: "https://openrouter.ai/keys",
|
|
30
|
+
description: "200+ models (Claude, GPT, Gemini, Llama, etc.) — one login",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: "anthropic",
|
|
34
|
+
name: "Anthropic (Claude)",
|
|
35
|
+
methods: ["setup-token", "api-key"],
|
|
36
|
+
baseUrl: "https://api.anthropic.com/v1",
|
|
37
|
+
consoleUrl: "https://console.anthropic.com/settings/keys",
|
|
38
|
+
description: "Claude Opus, Sonnet, Haiku — use your subscription or API key",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: "openai",
|
|
42
|
+
name: "OpenAI (ChatGPT)",
|
|
43
|
+
methods: ["cached-token", "api-key"],
|
|
44
|
+
baseUrl: "https://api.openai.com/v1",
|
|
45
|
+
consoleUrl: "https://platform.openai.com/api-keys",
|
|
46
|
+
description: "GPT-4o, GPT-5, o1 — use your ChatGPT subscription or API key",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: "qwen",
|
|
50
|
+
name: "Qwen",
|
|
51
|
+
methods: ["cached-token", "api-key"],
|
|
52
|
+
baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
53
|
+
consoleUrl: "https://dashscope.console.aliyun.com/apiKey",
|
|
54
|
+
description: "Qwen 3.5, Qwen Coder — use your Qwen CLI login or API key",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "gemini",
|
|
58
|
+
name: "Google Gemini",
|
|
59
|
+
methods: ["api-key"],
|
|
60
|
+
baseUrl: "https://generativelanguage.googleapis.com/v1beta/openai/",
|
|
61
|
+
consoleUrl: "https://aistudio.google.com/apikey",
|
|
62
|
+
description: "Gemini 2.5, Gemini Flash",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: "copilot",
|
|
66
|
+
name: "GitHub Copilot",
|
|
67
|
+
methods: ["device-flow"],
|
|
68
|
+
baseUrl: "https://api.githubcopilot.com",
|
|
69
|
+
description: "Use your GitHub Copilot subscription",
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "local",
|
|
73
|
+
name: "Local (LM Studio / Ollama)",
|
|
74
|
+
methods: ["none"],
|
|
75
|
+
baseUrl: "http://localhost:1234/v1",
|
|
76
|
+
description: "No auth needed — auto-detected",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: "custom",
|
|
80
|
+
name: "Custom Provider",
|
|
81
|
+
methods: ["api-key"],
|
|
82
|
+
baseUrl: "",
|
|
83
|
+
description: "Any OpenAI-compatible API endpoint",
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
// ── Auth Store ──
|
|
87
|
+
function loadAuthStore() {
|
|
88
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
89
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
90
|
+
}
|
|
91
|
+
if (!existsSync(AUTH_FILE)) {
|
|
92
|
+
return { version: 1, credentials: [] };
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const raw = readFileSync(AUTH_FILE, "utf-8");
|
|
96
|
+
return JSON.parse(raw);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return { version: 1, credentials: [] };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function saveAuthStore(store) {
|
|
103
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
104
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
105
|
+
}
|
|
106
|
+
writeFileSync(AUTH_FILE, JSON.stringify(store, null, 2), { mode: 0o600 });
|
|
107
|
+
try {
|
|
108
|
+
chmodSync(AUTH_FILE, 0o600);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Windows doesn't support chmod — ignore
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
export function getCredentials() {
|
|
115
|
+
return loadAuthStore().credentials;
|
|
116
|
+
}
|
|
117
|
+
export function getCredential(providerId) {
|
|
118
|
+
return loadAuthStore().credentials.find((c) => c.provider === providerId);
|
|
119
|
+
}
|
|
120
|
+
export function saveCredential(cred) {
|
|
121
|
+
const store = loadAuthStore();
|
|
122
|
+
// Replace existing credential for this provider, or add new
|
|
123
|
+
const idx = store.credentials.findIndex((c) => c.provider === cred.provider);
|
|
124
|
+
if (idx >= 0) {
|
|
125
|
+
store.credentials[idx] = cred;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
store.credentials.push(cred);
|
|
129
|
+
}
|
|
130
|
+
saveAuthStore(store);
|
|
131
|
+
}
|
|
132
|
+
export function removeCredential(providerId) {
|
|
133
|
+
const store = loadAuthStore();
|
|
134
|
+
const before = store.credentials.length;
|
|
135
|
+
store.credentials = store.credentials.filter((c) => c.provider !== providerId);
|
|
136
|
+
if (store.credentials.length < before) {
|
|
137
|
+
saveAuthStore(store);
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
// ── OpenRouter OAuth PKCE ──
|
|
143
|
+
function generatePKCE() {
|
|
144
|
+
const verifier = randomBytes(32).toString("base64url");
|
|
145
|
+
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
146
|
+
return { verifier, challenge };
|
|
147
|
+
}
|
|
148
|
+
export async function openRouterOAuth(onStatus) {
|
|
149
|
+
const { verifier, challenge } = generatePKCE();
|
|
150
|
+
return new Promise((resolve, reject) => {
|
|
151
|
+
// Start local callback server
|
|
152
|
+
const server = createServer(async (req, res) => {
|
|
153
|
+
const url = new URL(req.url ?? "/", `http://localhost`);
|
|
154
|
+
if (url.pathname === "/callback") {
|
|
155
|
+
const code = url.searchParams.get("code");
|
|
156
|
+
if (!code) {
|
|
157
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
158
|
+
res.end("<h1>Error: No code received</h1><p>Please try again.</p>");
|
|
159
|
+
server.close();
|
|
160
|
+
reject(new Error("No authorization code received"));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
// Exchange code for API key
|
|
164
|
+
onStatus?.("Exchanging code for API key...");
|
|
165
|
+
try {
|
|
166
|
+
const exchangeRes = await fetch("https://openrouter.ai/api/v1/auth/keys", {
|
|
167
|
+
method: "POST",
|
|
168
|
+
headers: { "Content-Type": "application/json" },
|
|
169
|
+
body: JSON.stringify({
|
|
170
|
+
code,
|
|
171
|
+
code_verifier: verifier,
|
|
172
|
+
code_challenge_method: "S256",
|
|
173
|
+
}),
|
|
174
|
+
});
|
|
175
|
+
if (!exchangeRes.ok) {
|
|
176
|
+
const errText = await exchangeRes.text();
|
|
177
|
+
throw new Error(`Exchange failed (${exchangeRes.status}): ${errText}`);
|
|
178
|
+
}
|
|
179
|
+
const data = (await exchangeRes.json());
|
|
180
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
181
|
+
res.end(`
|
|
182
|
+
<html>
|
|
183
|
+
<body style="font-family: monospace; background: #1a1a2e; color: #0ff; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0;">
|
|
184
|
+
<div style="text-align: center;">
|
|
185
|
+
<h1>💪 Authenticated!</h1>
|
|
186
|
+
<p>You can close this tab and return to Codemaxxing.</p>
|
|
187
|
+
</div>
|
|
188
|
+
</body>
|
|
189
|
+
</html>
|
|
190
|
+
`);
|
|
191
|
+
server.close();
|
|
192
|
+
const cred = {
|
|
193
|
+
provider: "openrouter",
|
|
194
|
+
method: "oauth",
|
|
195
|
+
apiKey: data.key,
|
|
196
|
+
baseUrl: "https://openrouter.ai/api/v1",
|
|
197
|
+
label: "OpenRouter (OAuth)",
|
|
198
|
+
createdAt: new Date().toISOString(),
|
|
199
|
+
};
|
|
200
|
+
saveCredential(cred);
|
|
201
|
+
resolve(cred);
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
res.writeHead(500, { "Content-Type": "text/html" });
|
|
205
|
+
res.end(`<h1>Error</h1><p>${err.message}</p>`);
|
|
206
|
+
server.close();
|
|
207
|
+
reject(err);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
// Listen on random port
|
|
212
|
+
server.listen(0, "127.0.0.1", () => {
|
|
213
|
+
const addr = server.address();
|
|
214
|
+
if (!addr || typeof addr === "string") {
|
|
215
|
+
reject(new Error("Failed to start callback server"));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const port = addr.port;
|
|
219
|
+
const callbackUrl = `http://localhost:${port}/callback`;
|
|
220
|
+
const authUrl = `https://openrouter.ai/auth?callback_url=${encodeURIComponent(callbackUrl)}&code_challenge=${challenge}&code_challenge_method=S256`;
|
|
221
|
+
onStatus?.(`Opening browser for OpenRouter login...`);
|
|
222
|
+
// Open browser
|
|
223
|
+
try {
|
|
224
|
+
const cmd = process.platform === "darwin"
|
|
225
|
+
? `open "${authUrl}"`
|
|
226
|
+
: process.platform === "win32"
|
|
227
|
+
? `start "" "${authUrl}"`
|
|
228
|
+
: `xdg-open "${authUrl}"`;
|
|
229
|
+
exec(cmd);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
onStatus?.(`Could not open browser. Please visit:\n${authUrl}`);
|
|
233
|
+
}
|
|
234
|
+
onStatus?.("Waiting for authorization...");
|
|
235
|
+
// Timeout after 5 minutes
|
|
236
|
+
setTimeout(() => {
|
|
237
|
+
server.close();
|
|
238
|
+
reject(new Error("OAuth timed out after 5 minutes"));
|
|
239
|
+
}, 5 * 60 * 1000);
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
// ── Anthropic Setup Token (via Claude Code CLI) ──
|
|
244
|
+
export function detectClaudeCLI() {
|
|
245
|
+
try {
|
|
246
|
+
execSync("which claude 2>/dev/null || where claude 2>nul", { stdio: "pipe" });
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
export async function anthropicSetupToken(onStatus) {
|
|
254
|
+
if (!detectClaudeCLI()) {
|
|
255
|
+
throw new Error("Claude Code CLI not found.\n" +
|
|
256
|
+
"Install it first: curl -fsSL https://claude.ai/install.sh | bash\n" +
|
|
257
|
+
"Then run: claude setup-token");
|
|
258
|
+
}
|
|
259
|
+
onStatus?.("Running 'claude setup-token'...");
|
|
260
|
+
onStatus?.("A browser window will open — log in with your Anthropic account.");
|
|
261
|
+
return new Promise((resolve, reject) => {
|
|
262
|
+
const child = exec("claude setup-token", { timeout: 120000 }, (err, stdout, stderr) => {
|
|
263
|
+
if (err) {
|
|
264
|
+
reject(new Error(`setup-token failed: ${stderr || err.message}`));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
// The output should contain the token
|
|
268
|
+
const token = stdout.trim();
|
|
269
|
+
if (!token) {
|
|
270
|
+
reject(new Error("No token received from claude setup-token"));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const cred = {
|
|
274
|
+
provider: "anthropic",
|
|
275
|
+
method: "setup-token",
|
|
276
|
+
apiKey: token,
|
|
277
|
+
baseUrl: "https://api.anthropic.com/v1",
|
|
278
|
+
label: "Anthropic (Claude subscription)",
|
|
279
|
+
createdAt: new Date().toISOString(),
|
|
280
|
+
};
|
|
281
|
+
saveCredential(cred);
|
|
282
|
+
resolve(cred);
|
|
283
|
+
});
|
|
284
|
+
// Forward stdout in real-time for the interactive flow
|
|
285
|
+
child.stdout?.on("data", (data) => {
|
|
286
|
+
const text = data.toString().trim();
|
|
287
|
+
if (text)
|
|
288
|
+
onStatus?.(text);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
// ── OpenAI / Codex CLI Cached Token ──
|
|
293
|
+
export function detectCodexToken() {
|
|
294
|
+
// Codex CLI stores OAuth tokens — check common locations
|
|
295
|
+
const locations = [
|
|
296
|
+
join(homedir(), ".codex", "auth.json"),
|
|
297
|
+
join(homedir(), ".config", "codex", "auth.json"),
|
|
298
|
+
join(homedir(), ".codex-cli", "auth.json"),
|
|
299
|
+
];
|
|
300
|
+
for (const loc of locations) {
|
|
301
|
+
if (existsSync(loc)) {
|
|
302
|
+
try {
|
|
303
|
+
const data = JSON.parse(readFileSync(loc, "utf-8"));
|
|
304
|
+
// Look for an API key or access token
|
|
305
|
+
if (data.api_key)
|
|
306
|
+
return data.api_key;
|
|
307
|
+
if (data.access_token)
|
|
308
|
+
return data.access_token;
|
|
309
|
+
if (data.token)
|
|
310
|
+
return data.token;
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
export function importCodexToken(onStatus) {
|
|
320
|
+
const token = detectCodexToken();
|
|
321
|
+
if (!token)
|
|
322
|
+
return null;
|
|
323
|
+
onStatus?.("Found existing Codex CLI credentials — importing...");
|
|
324
|
+
const cred = {
|
|
325
|
+
provider: "openai",
|
|
326
|
+
method: "cached-token",
|
|
327
|
+
apiKey: token,
|
|
328
|
+
baseUrl: "https://api.openai.com/v1",
|
|
329
|
+
label: "OpenAI (from Codex CLI)",
|
|
330
|
+
createdAt: new Date().toISOString(),
|
|
331
|
+
};
|
|
332
|
+
saveCredential(cred);
|
|
333
|
+
return cred;
|
|
334
|
+
}
|
|
335
|
+
// ── Qwen CLI Cached Credentials ──
|
|
336
|
+
export function detectQwenToken() {
|
|
337
|
+
const qwenAuth = join(homedir(), ".qwen", "oauth_creds.json");
|
|
338
|
+
if (existsSync(qwenAuth)) {
|
|
339
|
+
try {
|
|
340
|
+
const data = JSON.parse(readFileSync(qwenAuth, "utf-8"));
|
|
341
|
+
if (data.access_token)
|
|
342
|
+
return data.access_token;
|
|
343
|
+
if (data.token)
|
|
344
|
+
return data.token;
|
|
345
|
+
if (data.api_key)
|
|
346
|
+
return data.api_key;
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
// ignore
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
export function importQwenToken(onStatus) {
|
|
355
|
+
const token = detectQwenToken();
|
|
356
|
+
if (!token)
|
|
357
|
+
return null;
|
|
358
|
+
onStatus?.("Found existing Qwen CLI credentials — importing...");
|
|
359
|
+
const cred = {
|
|
360
|
+
provider: "qwen",
|
|
361
|
+
method: "cached-token",
|
|
362
|
+
apiKey: token,
|
|
363
|
+
baseUrl: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
364
|
+
label: "Qwen (from Qwen CLI)",
|
|
365
|
+
createdAt: new Date().toISOString(),
|
|
366
|
+
};
|
|
367
|
+
saveCredential(cred);
|
|
368
|
+
return cred;
|
|
369
|
+
}
|
|
370
|
+
// ── GitHub Copilot Device Flow ──
|
|
371
|
+
export async function copilotDeviceFlow(onStatus) {
|
|
372
|
+
// Step 1: Request device code
|
|
373
|
+
onStatus?.("Starting GitHub Copilot device flow...");
|
|
374
|
+
const deviceRes = await fetch("https://github.com/login/device/code", {
|
|
375
|
+
method: "POST",
|
|
376
|
+
headers: {
|
|
377
|
+
"Content-Type": "application/json",
|
|
378
|
+
"Accept": "application/json",
|
|
379
|
+
},
|
|
380
|
+
body: JSON.stringify({
|
|
381
|
+
client_id: "Iv1.b507a08c87ecfe98", // GitHub Copilot's public client_id
|
|
382
|
+
scope: "copilot",
|
|
383
|
+
}),
|
|
384
|
+
});
|
|
385
|
+
if (!deviceRes.ok) {
|
|
386
|
+
throw new Error(`Device code request failed: ${deviceRes.status}`);
|
|
387
|
+
}
|
|
388
|
+
const deviceData = (await deviceRes.json());
|
|
389
|
+
onStatus?.(`\nGo to: ${deviceData.verification_uri}`);
|
|
390
|
+
onStatus?.(`Enter code: ${deviceData.user_code}\n`);
|
|
391
|
+
// Try to open browser
|
|
392
|
+
try {
|
|
393
|
+
const cmd = process.platform === "darwin"
|
|
394
|
+
? `open "${deviceData.verification_uri}"`
|
|
395
|
+
: process.platform === "win32"
|
|
396
|
+
? `start "" "${deviceData.verification_uri}"`
|
|
397
|
+
: `xdg-open "${deviceData.verification_uri}"`;
|
|
398
|
+
exec(cmd);
|
|
399
|
+
}
|
|
400
|
+
catch { /* ignore */ }
|
|
401
|
+
// Step 2: Poll for token
|
|
402
|
+
const interval = (deviceData.interval || 5) * 1000;
|
|
403
|
+
const expiresAt = Date.now() + deviceData.expires_in * 1000;
|
|
404
|
+
onStatus?.("Waiting for authorization...");
|
|
405
|
+
while (Date.now() < expiresAt) {
|
|
406
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
407
|
+
const tokenRes = await fetch("https://github.com/login/oauth/access_token", {
|
|
408
|
+
method: "POST",
|
|
409
|
+
headers: {
|
|
410
|
+
"Content-Type": "application/json",
|
|
411
|
+
"Accept": "application/json",
|
|
412
|
+
},
|
|
413
|
+
body: JSON.stringify({
|
|
414
|
+
client_id: "Iv1.b507a08c87ecfe98",
|
|
415
|
+
device_code: deviceData.device_code,
|
|
416
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
417
|
+
}),
|
|
418
|
+
});
|
|
419
|
+
const tokenData = (await tokenRes.json());
|
|
420
|
+
if (tokenData.access_token) {
|
|
421
|
+
const cred = {
|
|
422
|
+
provider: "copilot",
|
|
423
|
+
method: "oauth",
|
|
424
|
+
apiKey: tokenData.access_token,
|
|
425
|
+
baseUrl: "https://api.githubcopilot.com",
|
|
426
|
+
label: "GitHub Copilot",
|
|
427
|
+
createdAt: new Date().toISOString(),
|
|
428
|
+
};
|
|
429
|
+
saveCredential(cred);
|
|
430
|
+
return cred;
|
|
431
|
+
}
|
|
432
|
+
if (tokenData.error === "authorization_pending") {
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
if (tokenData.error === "slow_down") {
|
|
436
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
throw new Error(`Authorization failed: ${tokenData.error}`);
|
|
440
|
+
}
|
|
441
|
+
throw new Error("Device flow timed out");
|
|
442
|
+
}
|
|
443
|
+
// ── Manual API Key ──
|
|
444
|
+
export function saveApiKey(providerId, apiKey, baseUrl, label) {
|
|
445
|
+
const providerDef = PROVIDERS.find((p) => p.id === providerId);
|
|
446
|
+
const cred = {
|
|
447
|
+
provider: providerId,
|
|
448
|
+
method: "api-key",
|
|
449
|
+
apiKey,
|
|
450
|
+
baseUrl: baseUrl ?? providerDef?.baseUrl ?? "",
|
|
451
|
+
label: label ?? providerDef?.name ?? providerId,
|
|
452
|
+
createdAt: new Date().toISOString(),
|
|
453
|
+
};
|
|
454
|
+
saveCredential(cred);
|
|
455
|
+
return cred;
|
|
456
|
+
}
|
|
457
|
+
// ── Auto-Detection ──
|
|
458
|
+
/**
|
|
459
|
+
* Check which provider CLIs / cached tokens are available on this machine
|
|
460
|
+
*/
|
|
461
|
+
export function detectAvailableAuth() {
|
|
462
|
+
const available = [];
|
|
463
|
+
if (detectClaudeCLI()) {
|
|
464
|
+
available.push({
|
|
465
|
+
provider: "anthropic",
|
|
466
|
+
method: "setup-token",
|
|
467
|
+
description: "Claude Code CLI detected — can link your Anthropic subscription",
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
if (detectCodexToken()) {
|
|
471
|
+
available.push({
|
|
472
|
+
provider: "openai",
|
|
473
|
+
method: "cached-token",
|
|
474
|
+
description: "Codex CLI credentials found — can import your ChatGPT subscription",
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
if (detectQwenToken()) {
|
|
478
|
+
available.push({
|
|
479
|
+
provider: "qwen",
|
|
480
|
+
method: "cached-token",
|
|
481
|
+
description: "Qwen CLI credentials found — can import your Qwen access",
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
return available;
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* Resolve provider config: check auth store first, then fall back to config
|
|
488
|
+
*/
|
|
489
|
+
export function resolveProviderAuth(providerId) {
|
|
490
|
+
const cred = getCredential(providerId);
|
|
491
|
+
if (cred) {
|
|
492
|
+
return { apiKey: cred.apiKey, baseUrl: cred.baseUrl };
|
|
493
|
+
}
|
|
494
|
+
return null;
|
|
495
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codemaxxing",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Open-source terminal coding agent. Connect any LLM. Max your code.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"codemaxxing": "./dist/index.js"
|
|
7
|
+
"codemaxxing": "./dist/index.js",
|
|
8
|
+
"codemaxxing-auth": "./dist/auth-cli.js"
|
|
8
9
|
},
|
|
9
10
|
"type": "module",
|
|
10
11
|
"scripts": {
|