feat(auth): sync OAuth from Claude/Codex CLIs
Add source profiles anthropic:claude-cli and openai-codex:codex-cli; surface them in onboarding/configure. Co-authored-by: pepicrft <pepicrft@users.noreply.github.com>
This commit is contained in:
@@ -57,6 +57,7 @@
|
|||||||
- Auth: lock auth profile refreshes to avoid multi-instance OAuth logouts; keep credentials on refresh failure.
|
- Auth: lock auth profile refreshes to avoid multi-instance OAuth logouts; keep credentials on refresh failure.
|
||||||
- Auth/Doctor: migrate Anthropic OAuth configs from `anthropic:default` → `anthropic:<email>` and surface a doctor hint on refresh failures. Thanks @RandyVentures for PR #361. (#363)
|
- Auth/Doctor: migrate Anthropic OAuth configs from `anthropic:default` → `anthropic:<email>` and surface a doctor hint on refresh failures. Thanks @RandyVentures for PR #361. (#363)
|
||||||
- Auth: delete legacy `auth.json` after migration to prevent stale OAuth token overwrites. Thanks @reeltimeapps for PR #368.
|
- Auth: delete legacy `auth.json` after migration to prevent stale OAuth token overwrites. Thanks @reeltimeapps for PR #368.
|
||||||
|
- Auth: auto-sync OAuth creds from Claude CLI/Codex CLI into `anthropic:claude-cli`/`openai-codex:codex-cli` and offer them as onboarding/config choices (avoids `refresh_token_reused`). Thanks @pepicrft for PR #374.
|
||||||
- Gateway/CLI: stop forcing localhost URL in remote mode so remote gateway config works. Thanks @oswalpalash for PR #293.
|
- Gateway/CLI: stop forcing localhost URL in remote mode so remote gateway config works. Thanks @oswalpalash for PR #293.
|
||||||
- Onboarding: prompt immediately for OpenAI Codex redirect URL on remote/headless logins.
|
- Onboarding: prompt immediately for OpenAI Codex redirect URL on remote/headless logins.
|
||||||
- Configure: add OpenAI Codex (ChatGPT OAuth) auth choice (align with onboarding).
|
- Configure: add OpenAI Codex (ChatGPT OAuth) auth choice (align with onboarding).
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { describe, expect, it } from "vitest";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
type AuthProfileStore,
|
type AuthProfileStore,
|
||||||
|
CLAUDE_CLI_PROFILE_ID,
|
||||||
|
CODEX_CLI_PROFILE_ID,
|
||||||
calculateAuthProfileCooldownMs,
|
calculateAuthProfileCooldownMs,
|
||||||
ensureAuthProfileStore,
|
ensureAuthProfileStore,
|
||||||
resolveAuthProfileOrder,
|
resolveAuthProfileOrder,
|
||||||
@@ -339,3 +341,263 @@ describe("auth profile cooldowns", () => {
|
|||||||
expect(calculateAuthProfileCooldownMs(5)).toBe(60 * 60_000);
|
expect(calculateAuthProfileCooldownMs(5)).toBe(60 * 60_000);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("external CLI credential sync", () => {
|
||||||
|
it("syncs Claude CLI credentials into anthropic:claude-cli", () => {
|
||||||
|
const agentDir = fs.mkdtempSync(
|
||||||
|
path.join(os.tmpdir(), "clawdbot-cli-sync-"),
|
||||||
|
);
|
||||||
|
const originalHome = process.env.HOME;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create a temp home with Claude CLI credentials
|
||||||
|
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-home-"));
|
||||||
|
process.env.HOME = tempHome;
|
||||||
|
|
||||||
|
// Create Claude CLI credentials
|
||||||
|
const claudeDir = path.join(tempHome, ".claude");
|
||||||
|
fs.mkdirSync(claudeDir, { recursive: true });
|
||||||
|
const claudeCreds = {
|
||||||
|
claudeAiOauth: {
|
||||||
|
accessToken: "fresh-access-token",
|
||||||
|
refreshToken: "fresh-refresh-token",
|
||||||
|
expiresAt: Date.now() + 60 * 60 * 1000, // 1 hour from now
|
||||||
|
},
|
||||||
|
};
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(claudeDir, ".credentials.json"),
|
||||||
|
JSON.stringify(claudeCreds),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create empty auth-profiles.json
|
||||||
|
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||||
|
fs.writeFileSync(
|
||||||
|
authPath,
|
||||||
|
JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
profiles: {
|
||||||
|
"anthropic:default": {
|
||||||
|
type: "api_key",
|
||||||
|
provider: "anthropic",
|
||||||
|
key: "sk-default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Load the store - should sync from CLI
|
||||||
|
const store = ensureAuthProfileStore(agentDir);
|
||||||
|
|
||||||
|
expect(store.profiles["anthropic:default"]).toBeDefined();
|
||||||
|
expect((store.profiles["anthropic:default"] as { key: string }).key).toBe(
|
||||||
|
"sk-default",
|
||||||
|
);
|
||||||
|
expect(store.profiles[CLAUDE_CLI_PROFILE_ID]).toBeDefined();
|
||||||
|
expect(
|
||||||
|
(store.profiles[CLAUDE_CLI_PROFILE_ID] as { access: string }).access,
|
||||||
|
).toBe("fresh-access-token");
|
||||||
|
expect(
|
||||||
|
(store.profiles[CLAUDE_CLI_PROFILE_ID] as { expires: number }).expires,
|
||||||
|
).toBeGreaterThan(Date.now());
|
||||||
|
} finally {
|
||||||
|
process.env.HOME = originalHome;
|
||||||
|
fs.rmSync(agentDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("syncs Codex CLI credentials into openai-codex:codex-cli", () => {
|
||||||
|
const agentDir = fs.mkdtempSync(
|
||||||
|
path.join(os.tmpdir(), "clawdbot-codex-sync-"),
|
||||||
|
);
|
||||||
|
const originalHome = process.env.HOME;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-home-"));
|
||||||
|
process.env.HOME = tempHome;
|
||||||
|
|
||||||
|
// Create Codex CLI credentials
|
||||||
|
const codexDir = path.join(tempHome, ".codex");
|
||||||
|
fs.mkdirSync(codexDir, { recursive: true });
|
||||||
|
const codexCreds = {
|
||||||
|
tokens: {
|
||||||
|
access_token: "codex-access-token",
|
||||||
|
refresh_token: "codex-refresh-token",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const codexAuthPath = path.join(codexDir, "auth.json");
|
||||||
|
fs.writeFileSync(codexAuthPath, JSON.stringify(codexCreds));
|
||||||
|
|
||||||
|
// Create empty auth-profiles.json
|
||||||
|
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||||
|
fs.writeFileSync(
|
||||||
|
authPath,
|
||||||
|
JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
profiles: {},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const store = ensureAuthProfileStore(agentDir);
|
||||||
|
|
||||||
|
expect(store.profiles[CODEX_CLI_PROFILE_ID]).toBeDefined();
|
||||||
|
expect(
|
||||||
|
(store.profiles[CODEX_CLI_PROFILE_ID] as { access: string }).access,
|
||||||
|
).toBe("codex-access-token");
|
||||||
|
} finally {
|
||||||
|
process.env.HOME = originalHome;
|
||||||
|
fs.rmSync(agentDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not overwrite API keys when syncing external CLI creds", () => {
|
||||||
|
const agentDir = fs.mkdtempSync(
|
||||||
|
path.join(os.tmpdir(), "clawdbot-no-overwrite-"),
|
||||||
|
);
|
||||||
|
const originalHome = process.env.HOME;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-home-"));
|
||||||
|
process.env.HOME = tempHome;
|
||||||
|
|
||||||
|
// Create Claude CLI credentials
|
||||||
|
const claudeDir = path.join(tempHome, ".claude");
|
||||||
|
fs.mkdirSync(claudeDir, { recursive: true });
|
||||||
|
const claudeCreds = {
|
||||||
|
claudeAiOauth: {
|
||||||
|
accessToken: "cli-access",
|
||||||
|
refreshToken: "cli-refresh",
|
||||||
|
expiresAt: Date.now() + 30 * 60 * 1000,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(claudeDir, ".credentials.json"),
|
||||||
|
JSON.stringify(claudeCreds),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create auth-profiles.json with an API key
|
||||||
|
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||||
|
fs.writeFileSync(
|
||||||
|
authPath,
|
||||||
|
JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
profiles: {
|
||||||
|
"anthropic:default": {
|
||||||
|
type: "api_key",
|
||||||
|
provider: "anthropic",
|
||||||
|
key: "sk-store",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const store = ensureAuthProfileStore(agentDir);
|
||||||
|
|
||||||
|
// Should keep the store's API key and still add the CLI profile.
|
||||||
|
expect((store.profiles["anthropic:default"] as { key: string }).key).toBe(
|
||||||
|
"sk-store",
|
||||||
|
);
|
||||||
|
expect(store.profiles[CLAUDE_CLI_PROFILE_ID]).toBeDefined();
|
||||||
|
} finally {
|
||||||
|
process.env.HOME = originalHome;
|
||||||
|
fs.rmSync(agentDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not overwrite fresher store OAuth with older Claude CLI credentials", () => {
|
||||||
|
const agentDir = fs.mkdtempSync(
|
||||||
|
path.join(os.tmpdir(), "clawdbot-cli-no-downgrade-"),
|
||||||
|
);
|
||||||
|
const originalHome = process.env.HOME;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-home-"));
|
||||||
|
process.env.HOME = tempHome;
|
||||||
|
|
||||||
|
const claudeDir = path.join(tempHome, ".claude");
|
||||||
|
fs.mkdirSync(claudeDir, { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(claudeDir, ".credentials.json"),
|
||||||
|
JSON.stringify({
|
||||||
|
claudeAiOauth: {
|
||||||
|
accessToken: "cli-access",
|
||||||
|
refreshToken: "cli-refresh",
|
||||||
|
expiresAt: Date.now() + 30 * 60 * 1000,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||||
|
fs.writeFileSync(
|
||||||
|
authPath,
|
||||||
|
JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
profiles: {
|
||||||
|
[CLAUDE_CLI_PROFILE_ID]: {
|
||||||
|
type: "oauth",
|
||||||
|
provider: "anthropic",
|
||||||
|
access: "store-access",
|
||||||
|
refresh: "store-refresh",
|
||||||
|
expires: Date.now() + 60 * 60 * 1000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const store = ensureAuthProfileStore(agentDir);
|
||||||
|
expect(
|
||||||
|
(store.profiles[CLAUDE_CLI_PROFILE_ID] as { access: string }).access,
|
||||||
|
).toBe("store-access");
|
||||||
|
} finally {
|
||||||
|
process.env.HOME = originalHome;
|
||||||
|
fs.rmSync(agentDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates codex-cli profile when Codex CLI refresh token changes", () => {
|
||||||
|
const agentDir = fs.mkdtempSync(
|
||||||
|
path.join(os.tmpdir(), "clawdbot-codex-refresh-sync-"),
|
||||||
|
);
|
||||||
|
const originalHome = process.env.HOME;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), "clawdbot-home-"));
|
||||||
|
process.env.HOME = tempHome;
|
||||||
|
|
||||||
|
const codexDir = path.join(tempHome, ".codex");
|
||||||
|
fs.mkdirSync(codexDir, { recursive: true });
|
||||||
|
const codexAuthPath = path.join(codexDir, "auth.json");
|
||||||
|
fs.writeFileSync(
|
||||||
|
codexAuthPath,
|
||||||
|
JSON.stringify({
|
||||||
|
tokens: { access_token: "same-access", refresh_token: "new-refresh" },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
fs.utimesSync(codexAuthPath, new Date(), new Date());
|
||||||
|
|
||||||
|
const authPath = path.join(agentDir, "auth-profiles.json");
|
||||||
|
fs.writeFileSync(
|
||||||
|
authPath,
|
||||||
|
JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
profiles: {
|
||||||
|
[CODEX_CLI_PROFILE_ID]: {
|
||||||
|
type: "oauth",
|
||||||
|
provider: "openai-codex",
|
||||||
|
access: "same-access",
|
||||||
|
refresh: "old-refresh",
|
||||||
|
expires: Date.now() - 1000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const store = ensureAuthProfileStore(agentDir);
|
||||||
|
expect(
|
||||||
|
(store.profiles[CODEX_CLI_PROFILE_ID] as { refresh: string }).refresh,
|
||||||
|
).toBe("new-refresh");
|
||||||
|
} finally {
|
||||||
|
process.env.HOME = originalHome;
|
||||||
|
fs.rmSync(agentDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -19,6 +19,14 @@ import { normalizeProviderId } from "./model-selection.js";
|
|||||||
const AUTH_STORE_VERSION = 1;
|
const AUTH_STORE_VERSION = 1;
|
||||||
const AUTH_PROFILE_FILENAME = "auth-profiles.json";
|
const AUTH_PROFILE_FILENAME = "auth-profiles.json";
|
||||||
const LEGACY_AUTH_FILENAME = "auth.json";
|
const LEGACY_AUTH_FILENAME = "auth.json";
|
||||||
|
|
||||||
|
// External CLI credential file locations
|
||||||
|
const CLAUDE_CLI_CREDENTIALS_RELATIVE_PATH = ".claude/.credentials.json";
|
||||||
|
const CODEX_CLI_AUTH_RELATIVE_PATH = ".codex/auth.json";
|
||||||
|
|
||||||
|
export const CLAUDE_CLI_PROFILE_ID = "anthropic:claude-cli";
|
||||||
|
export const CODEX_CLI_PROFILE_ID = "openai-codex:codex-cli";
|
||||||
|
|
||||||
const AUTH_STORE_LOCK_OPTIONS = {
|
const AUTH_STORE_LOCK_OPTIONS = {
|
||||||
retries: {
|
retries: {
|
||||||
retries: 10,
|
retries: 10,
|
||||||
@@ -267,11 +275,177 @@ function mergeOAuthFileIntoStore(store: AuthProfileStore): boolean {
|
|||||||
return mutated;
|
return mutated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read Anthropic OAuth credentials from Claude CLI's credential file.
|
||||||
|
* Claude CLI stores credentials at ~/.claude/.credentials.json
|
||||||
|
*/
|
||||||
|
function readClaudeCliCredentials(): OAuthCredential | null {
|
||||||
|
const credPath = path.join(
|
||||||
|
resolveUserPath("~"),
|
||||||
|
CLAUDE_CLI_CREDENTIALS_RELATIVE_PATH,
|
||||||
|
);
|
||||||
|
const raw = loadJsonFile(credPath);
|
||||||
|
if (!raw || typeof raw !== "object") return null;
|
||||||
|
|
||||||
|
const data = raw as Record<string, unknown>;
|
||||||
|
const claudeOauth = data.claudeAiOauth as Record<string, unknown> | undefined;
|
||||||
|
if (!claudeOauth || typeof claudeOauth !== "object") return null;
|
||||||
|
|
||||||
|
const accessToken = claudeOauth.accessToken;
|
||||||
|
const refreshToken = claudeOauth.refreshToken;
|
||||||
|
const expiresAt = claudeOauth.expiresAt;
|
||||||
|
|
||||||
|
if (typeof accessToken !== "string" || !accessToken) return null;
|
||||||
|
if (typeof refreshToken !== "string" || !refreshToken) return null;
|
||||||
|
if (typeof expiresAt !== "number" || expiresAt <= 0) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "oauth",
|
||||||
|
provider: "anthropic",
|
||||||
|
access: accessToken,
|
||||||
|
refresh: refreshToken,
|
||||||
|
expires: expiresAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read OpenAI Codex OAuth credentials from Codex CLI's auth file.
|
||||||
|
* Codex CLI stores credentials at ~/.codex/auth.json
|
||||||
|
*/
|
||||||
|
function readCodexCliCredentials(): OAuthCredential | null {
|
||||||
|
const authPath = path.join(
|
||||||
|
resolveUserPath("~"),
|
||||||
|
CODEX_CLI_AUTH_RELATIVE_PATH,
|
||||||
|
);
|
||||||
|
const raw = loadJsonFile(authPath);
|
||||||
|
if (!raw || typeof raw !== "object") return null;
|
||||||
|
|
||||||
|
const data = raw as Record<string, unknown>;
|
||||||
|
const tokens = data.tokens as Record<string, unknown> | undefined;
|
||||||
|
if (!tokens || typeof tokens !== "object") return null;
|
||||||
|
|
||||||
|
const accessToken = tokens.access_token;
|
||||||
|
const refreshToken = tokens.refresh_token;
|
||||||
|
|
||||||
|
if (typeof accessToken !== "string" || !accessToken) return null;
|
||||||
|
if (typeof refreshToken !== "string" || !refreshToken) return null;
|
||||||
|
|
||||||
|
// Codex CLI doesn't store expiry, estimate 1 hour from file mtime or now
|
||||||
|
let expires: number;
|
||||||
|
try {
|
||||||
|
const stat = fs.statSync(authPath);
|
||||||
|
// Assume token is valid for ~1 hour from when the file was last modified
|
||||||
|
expires = stat.mtimeMs + 60 * 60 * 1000;
|
||||||
|
} catch {
|
||||||
|
expires = Date.now() + 60 * 60 * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "oauth",
|
||||||
|
provider: "openai-codex" as unknown as OAuthProvider,
|
||||||
|
access: accessToken,
|
||||||
|
refresh: refreshToken,
|
||||||
|
expires,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function shallowEqualOAuthCredentials(
|
||||||
|
a: OAuthCredential | undefined,
|
||||||
|
b: OAuthCredential,
|
||||||
|
): boolean {
|
||||||
|
if (!a) return false;
|
||||||
|
if (a.type !== "oauth") return false;
|
||||||
|
return (
|
||||||
|
a.provider === b.provider &&
|
||||||
|
a.access === b.access &&
|
||||||
|
a.refresh === b.refresh &&
|
||||||
|
a.expires === b.expires &&
|
||||||
|
a.email === b.email &&
|
||||||
|
a.enterpriseUrl === b.enterpriseUrl &&
|
||||||
|
a.projectId === b.projectId &&
|
||||||
|
a.accountId === b.accountId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync OAuth credentials from external CLI tools (Claude CLI, Codex CLI) into the store.
|
||||||
|
* This allows clawdbot to use the same credentials as these tools without requiring
|
||||||
|
* separate authentication, and keeps credentials in sync when CLI tools refresh tokens.
|
||||||
|
*
|
||||||
|
* Returns true if any credentials were updated.
|
||||||
|
*/
|
||||||
|
function syncExternalCliCredentials(store: AuthProfileStore): boolean {
|
||||||
|
let mutated = false;
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// Sync from Claude CLI
|
||||||
|
const claudeCreds = readClaudeCliCredentials();
|
||||||
|
if (claudeCreds) {
|
||||||
|
const existing = store.profiles[CLAUDE_CLI_PROFILE_ID];
|
||||||
|
const existingOAuth = existing?.type === "oauth" ? existing : undefined;
|
||||||
|
|
||||||
|
// Update if: no existing profile, existing is not oauth, or CLI has newer/valid token
|
||||||
|
const shouldUpdate =
|
||||||
|
!existingOAuth ||
|
||||||
|
existingOAuth.provider !== "anthropic" ||
|
||||||
|
existingOAuth.expires <= now ||
|
||||||
|
(claudeCreds.expires > now &&
|
||||||
|
claudeCreds.expires > existingOAuth.expires);
|
||||||
|
|
||||||
|
if (
|
||||||
|
shouldUpdate &&
|
||||||
|
!shallowEqualOAuthCredentials(existingOAuth, claudeCreds)
|
||||||
|
) {
|
||||||
|
store.profiles[CLAUDE_CLI_PROFILE_ID] = claudeCreds;
|
||||||
|
mutated = true;
|
||||||
|
log.info("synced anthropic credentials from claude cli", {
|
||||||
|
profileId: CLAUDE_CLI_PROFILE_ID,
|
||||||
|
expires: new Date(claudeCreds.expires).toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync from Codex CLI
|
||||||
|
const codexCreds = readCodexCliCredentials();
|
||||||
|
if (codexCreds) {
|
||||||
|
const existing = store.profiles[CODEX_CLI_PROFILE_ID];
|
||||||
|
const existingOAuth = existing?.type === "oauth" ? existing : undefined;
|
||||||
|
|
||||||
|
// Codex creds don't carry expiry; use file mtime heuristic for freshness.
|
||||||
|
const shouldUpdate =
|
||||||
|
!existingOAuth ||
|
||||||
|
existingOAuth.provider !== ("openai-codex" as unknown as OAuthProvider) ||
|
||||||
|
existingOAuth.expires <= now ||
|
||||||
|
codexCreds.expires > existingOAuth.expires;
|
||||||
|
|
||||||
|
if (
|
||||||
|
shouldUpdate &&
|
||||||
|
!shallowEqualOAuthCredentials(existingOAuth, codexCreds)
|
||||||
|
) {
|
||||||
|
store.profiles[CODEX_CLI_PROFILE_ID] = codexCreds;
|
||||||
|
mutated = true;
|
||||||
|
log.info("synced openai-codex credentials from codex cli", {
|
||||||
|
profileId: CODEX_CLI_PROFILE_ID,
|
||||||
|
expires: new Date(codexCreds.expires).toISOString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mutated;
|
||||||
|
}
|
||||||
|
|
||||||
export function loadAuthProfileStore(): AuthProfileStore {
|
export function loadAuthProfileStore(): AuthProfileStore {
|
||||||
const authPath = resolveAuthStorePath();
|
const authPath = resolveAuthStorePath();
|
||||||
const raw = loadJsonFile(authPath);
|
const raw = loadJsonFile(authPath);
|
||||||
const asStore = coerceAuthStore(raw);
|
const asStore = coerceAuthStore(raw);
|
||||||
if (asStore) return asStore;
|
if (asStore) {
|
||||||
|
// Sync from external CLI tools on every load
|
||||||
|
const synced = syncExternalCliCredentials(asStore);
|
||||||
|
if (synced) {
|
||||||
|
saveJsonFile(authPath, asStore);
|
||||||
|
}
|
||||||
|
return asStore;
|
||||||
|
}
|
||||||
|
|
||||||
const legacyRaw = loadJsonFile(resolveLegacyAuthStorePath());
|
const legacyRaw = loadJsonFile(resolveLegacyAuthStorePath());
|
||||||
const legacy = coerceLegacyStore(legacyRaw);
|
const legacy = coerceLegacyStore(legacyRaw);
|
||||||
@@ -303,17 +477,27 @@ export function loadAuthProfileStore(): AuthProfileStore {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
syncExternalCliCredentials(store);
|
||||||
return store;
|
return store;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { version: AUTH_STORE_VERSION, profiles: {} };
|
const store: AuthProfileStore = { version: AUTH_STORE_VERSION, profiles: {} };
|
||||||
|
syncExternalCliCredentials(store);
|
||||||
|
return store;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ensureAuthProfileStore(agentDir?: string): AuthProfileStore {
|
export function ensureAuthProfileStore(agentDir?: string): AuthProfileStore {
|
||||||
const authPath = resolveAuthStorePath(agentDir);
|
const authPath = resolveAuthStorePath(agentDir);
|
||||||
const raw = loadJsonFile(authPath);
|
const raw = loadJsonFile(authPath);
|
||||||
const asStore = coerceAuthStore(raw);
|
const asStore = coerceAuthStore(raw);
|
||||||
if (asStore) return asStore;
|
if (asStore) {
|
||||||
|
// Sync from external CLI tools on every load
|
||||||
|
const synced = syncExternalCliCredentials(asStore);
|
||||||
|
if (synced) {
|
||||||
|
saveJsonFile(authPath, asStore);
|
||||||
|
}
|
||||||
|
return asStore;
|
||||||
|
}
|
||||||
|
|
||||||
const legacyRaw = loadJsonFile(resolveLegacyAuthStorePath(agentDir));
|
const legacyRaw = loadJsonFile(resolveLegacyAuthStorePath(agentDir));
|
||||||
const legacy = coerceLegacyStore(legacyRaw);
|
const legacy = coerceLegacyStore(legacyRaw);
|
||||||
@@ -348,7 +532,8 @@ export function ensureAuthProfileStore(agentDir?: string): AuthProfileStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const mergedOAuth = mergeOAuthFileIntoStore(store);
|
const mergedOAuth = mergeOAuthFileIntoStore(store);
|
||||||
const shouldWrite = legacy !== null || mergedOAuth;
|
const syncedCli = syncExternalCliCredentials(store);
|
||||||
|
const shouldWrite = legacy !== null || mergedOAuth || syncedCli;
|
||||||
if (shouldWrite) {
|
if (shouldWrite) {
|
||||||
saveJsonFile(authPath, store);
|
saveJsonFile(authPath, store);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
resolveAgentDir,
|
resolveAgentDir,
|
||||||
resolveAgentWorkspaceDir,
|
resolveAgentWorkspaceDir,
|
||||||
} from "../agents/agent-scope.js";
|
} from "../agents/agent-scope.js";
|
||||||
|
import { ensureAuthProfileStore } from "../agents/auth-profiles.js";
|
||||||
import type { ClawdbotConfig } from "../config/config.js";
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
import {
|
import {
|
||||||
CONFIG_PATH_CLAWDBOT,
|
CONFIG_PATH_CLAWDBOT,
|
||||||
@@ -21,6 +22,7 @@ import { resolveDefaultWhatsAppAccountId } from "../web/accounts.js";
|
|||||||
import { createClackPrompter } from "../wizard/clack-prompter.js";
|
import { createClackPrompter } from "../wizard/clack-prompter.js";
|
||||||
import { WizardCancelledError } from "../wizard/prompts.js";
|
import { WizardCancelledError } from "../wizard/prompts.js";
|
||||||
import { applyAuthChoice, warnIfModelConfigLooksOff } from "./auth-choice.js";
|
import { applyAuthChoice, warnIfModelConfigLooksOff } from "./auth-choice.js";
|
||||||
|
import { buildAuthChoiceOptions } from "./auth-choice-options.js";
|
||||||
import { ensureWorkspaceAndSessions, moveToTrash } from "./onboard-helpers.js";
|
import { ensureWorkspaceAndSessions, moveToTrash } from "./onboard-helpers.js";
|
||||||
import { setupProviders } from "./onboard-providers.js";
|
import { setupProviders } from "./onboard-providers.js";
|
||||||
import type { AuthChoice, ProviderChoice } from "./onboard-types.js";
|
import type { AuthChoice, ProviderChoice } from "./onboard-types.js";
|
||||||
@@ -458,19 +460,13 @@ export async function agentsAddCommand(
|
|||||||
initialValue: false,
|
initialValue: false,
|
||||||
});
|
});
|
||||||
if (wantsAuth) {
|
if (wantsAuth) {
|
||||||
|
const authStore = ensureAuthProfileStore(agentDir);
|
||||||
const authChoice = (await prompter.select({
|
const authChoice = (await prompter.select({
|
||||||
message: "Model/auth choice",
|
message: "Model/auth choice",
|
||||||
options: [
|
options: buildAuthChoiceOptions({
|
||||||
{ value: "oauth", label: "Anthropic OAuth (Claude Pro/Max)" },
|
store: authStore,
|
||||||
{ value: "openai-codex", label: "OpenAI Codex (ChatGPT OAuth)" },
|
includeSkip: true,
|
||||||
{
|
}),
|
||||||
value: "antigravity",
|
|
||||||
label: "Google Antigravity (Claude Opus 4.5, Gemini 3, etc.)",
|
|
||||||
},
|
|
||||||
{ value: "apiKey", label: "Anthropic API key" },
|
|
||||||
{ value: "minimax", label: "Minimax M2.1 (LM Studio)" },
|
|
||||||
{ value: "skip", label: "Skip for now" },
|
|
||||||
],
|
|
||||||
})) as AuthChoice;
|
})) as AuthChoice;
|
||||||
|
|
||||||
const authResult = await applyAuthChoice({
|
const authResult = await applyAuthChoice({
|
||||||
|
|||||||
49
src/commands/auth-choice-options.ts
Normal file
49
src/commands/auth-choice-options.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import type { AuthProfileStore } from "../agents/auth-profiles.js";
|
||||||
|
import {
|
||||||
|
CLAUDE_CLI_PROFILE_ID,
|
||||||
|
CODEX_CLI_PROFILE_ID,
|
||||||
|
} from "../agents/auth-profiles.js";
|
||||||
|
import type { AuthChoice } from "./onboard-types.js";
|
||||||
|
|
||||||
|
export type AuthChoiceOption = { value: AuthChoice; label: string };
|
||||||
|
|
||||||
|
export function buildAuthChoiceOptions(params: {
|
||||||
|
store: AuthProfileStore;
|
||||||
|
includeSkip: boolean;
|
||||||
|
}): AuthChoiceOption[] {
|
||||||
|
const options: AuthChoiceOption[] = [];
|
||||||
|
|
||||||
|
const claudeCli = params.store.profiles[CLAUDE_CLI_PROFILE_ID];
|
||||||
|
if (claudeCli?.type === "oauth") {
|
||||||
|
options.push({
|
||||||
|
value: "claude-cli",
|
||||||
|
label: "Anthropic OAuth (Claude CLI)",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
options.push({ value: "oauth", label: "Anthropic OAuth (Claude Pro/Max)" });
|
||||||
|
|
||||||
|
const codexCli = params.store.profiles[CODEX_CLI_PROFILE_ID];
|
||||||
|
if (codexCli?.type === "oauth") {
|
||||||
|
options.push({
|
||||||
|
value: "codex-cli",
|
||||||
|
label: "OpenAI Codex OAuth (Codex CLI)",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
options.push({
|
||||||
|
value: "openai-codex",
|
||||||
|
label: "OpenAI Codex (ChatGPT OAuth)",
|
||||||
|
});
|
||||||
|
options.push({
|
||||||
|
value: "antigravity",
|
||||||
|
label: "Google Antigravity (Claude Opus 4.5, Gemini 3, etc.)",
|
||||||
|
});
|
||||||
|
options.push({ value: "apiKey", label: "Anthropic API key" });
|
||||||
|
options.push({ value: "minimax", label: "Minimax M2.1 (LM Studio)" });
|
||||||
|
if (params.includeSkip) {
|
||||||
|
options.push({ value: "skip", label: "Skip for now" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ import {
|
|||||||
} from "@mariozechner/pi-ai";
|
} from "@mariozechner/pi-ai";
|
||||||
import { resolveAgentConfig } from "../agents/agent-scope.js";
|
import { resolveAgentConfig } from "../agents/agent-scope.js";
|
||||||
import {
|
import {
|
||||||
|
CLAUDE_CLI_PROFILE_ID,
|
||||||
|
CODEX_CLI_PROFILE_ID,
|
||||||
ensureAuthProfileStore,
|
ensureAuthProfileStore,
|
||||||
listProfilesForProvider,
|
listProfilesForProvider,
|
||||||
} from "../agents/auth-profiles.js";
|
} from "../agents/auth-profiles.js";
|
||||||
@@ -165,6 +167,20 @@ export async function applyAuthChoice(params: {
|
|||||||
"OAuth help",
|
"OAuth help",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} else if (params.authChoice === "claude-cli") {
|
||||||
|
const store = ensureAuthProfileStore(params.agentDir);
|
||||||
|
if (!store.profiles[CLAUDE_CLI_PROFILE_ID]) {
|
||||||
|
await params.prompter.note(
|
||||||
|
"No Claude CLI credentials found at ~/.claude/.credentials.json.",
|
||||||
|
"Claude CLI OAuth",
|
||||||
|
);
|
||||||
|
return { config: nextConfig, agentModelOverride };
|
||||||
|
}
|
||||||
|
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||||
|
profileId: CLAUDE_CLI_PROFILE_ID,
|
||||||
|
provider: "anthropic",
|
||||||
|
mode: "oauth",
|
||||||
|
});
|
||||||
} else if (params.authChoice === "openai-codex") {
|
} else if (params.authChoice === "openai-codex") {
|
||||||
const isRemote = isRemoteEnvironment();
|
const isRemote = isRemoteEnvironment();
|
||||||
await params.prompter.note(
|
await params.prompter.note(
|
||||||
@@ -250,6 +266,33 @@ export async function applyAuthChoice(params: {
|
|||||||
"OAuth help",
|
"OAuth help",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} else if (params.authChoice === "codex-cli") {
|
||||||
|
const store = ensureAuthProfileStore(params.agentDir);
|
||||||
|
if (!store.profiles[CODEX_CLI_PROFILE_ID]) {
|
||||||
|
await params.prompter.note(
|
||||||
|
"No Codex CLI credentials found at ~/.codex/auth.json.",
|
||||||
|
"Codex CLI OAuth",
|
||||||
|
);
|
||||||
|
return { config: nextConfig, agentModelOverride };
|
||||||
|
}
|
||||||
|
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||||
|
profileId: CODEX_CLI_PROFILE_ID,
|
||||||
|
provider: "openai-codex",
|
||||||
|
mode: "oauth",
|
||||||
|
});
|
||||||
|
if (params.setDefaultModel) {
|
||||||
|
const applied = applyOpenAICodexModelDefault(nextConfig);
|
||||||
|
nextConfig = applied.next;
|
||||||
|
if (applied.changed) {
|
||||||
|
await params.prompter.note(
|
||||||
|
`Default model set to ${OPENAI_CODEX_DEFAULT_MODEL}`,
|
||||||
|
"Model configured",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
agentModelOverride = OPENAI_CODEX_DEFAULT_MODEL;
|
||||||
|
await noteAgentModel(OPENAI_CODEX_DEFAULT_MODEL);
|
||||||
|
}
|
||||||
} else if (params.authChoice === "antigravity") {
|
} else if (params.authChoice === "antigravity") {
|
||||||
const isRemote = isRemoteEnvironment();
|
const isRemote = isRemoteEnvironment();
|
||||||
await params.prompter.note(
|
await params.prompter.note(
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ import {
|
|||||||
type OAuthCredentials,
|
type OAuthCredentials,
|
||||||
type OAuthProvider,
|
type OAuthProvider,
|
||||||
} from "@mariozechner/pi-ai";
|
} from "@mariozechner/pi-ai";
|
||||||
|
import {
|
||||||
|
CLAUDE_CLI_PROFILE_ID,
|
||||||
|
CODEX_CLI_PROFILE_ID,
|
||||||
|
ensureAuthProfileStore,
|
||||||
|
} from "../agents/auth-profiles.js";
|
||||||
import type { ClawdbotConfig } from "../config/config.js";
|
import type { ClawdbotConfig } from "../config/config.js";
|
||||||
import {
|
import {
|
||||||
CONFIG_PATH_CLAWDBOT,
|
CONFIG_PATH_CLAWDBOT,
|
||||||
@@ -35,6 +40,7 @@ import {
|
|||||||
isRemoteEnvironment,
|
isRemoteEnvironment,
|
||||||
loginAntigravityVpsAware,
|
loginAntigravityVpsAware,
|
||||||
} from "./antigravity-oauth.js";
|
} from "./antigravity-oauth.js";
|
||||||
|
import { buildAuthChoiceOptions } from "./auth-choice-options.js";
|
||||||
import {
|
import {
|
||||||
DEFAULT_GATEWAY_DAEMON_RUNTIME,
|
DEFAULT_GATEWAY_DAEMON_RUNTIME,
|
||||||
GATEWAY_DAEMON_RUNTIME_OPTIONS,
|
GATEWAY_DAEMON_RUNTIME_OPTIONS,
|
||||||
@@ -254,20 +260,21 @@ async function promptAuthConfig(
|
|||||||
const authChoice = guardCancel(
|
const authChoice = guardCancel(
|
||||||
await select({
|
await select({
|
||||||
message: "Model/auth choice",
|
message: "Model/auth choice",
|
||||||
options: [
|
options: buildAuthChoiceOptions({
|
||||||
{ value: "oauth", label: "Anthropic OAuth (Claude Pro/Max)" },
|
store: ensureAuthProfileStore(),
|
||||||
{ value: "openai-codex", label: "OpenAI Codex (ChatGPT OAuth)" },
|
includeSkip: true,
|
||||||
{
|
}),
|
||||||
value: "antigravity",
|
|
||||||
label: "Google Antigravity (Claude Opus 4.5, Gemini 3, etc.)",
|
|
||||||
},
|
|
||||||
{ value: "apiKey", label: "Anthropic API key" },
|
|
||||||
{ value: "minimax", label: "Minimax M2.1 (LM Studio)" },
|
|
||||||
{ value: "skip", label: "Skip for now" },
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
runtime,
|
runtime,
|
||||||
) as "oauth" | "openai-codex" | "antigravity" | "apiKey" | "minimax" | "skip";
|
) as
|
||||||
|
| "oauth"
|
||||||
|
| "claude-cli"
|
||||||
|
| "openai-codex"
|
||||||
|
| "codex-cli"
|
||||||
|
| "antigravity"
|
||||||
|
| "apiKey"
|
||||||
|
| "minimax"
|
||||||
|
| "skip";
|
||||||
|
|
||||||
let next = cfg;
|
let next = cfg;
|
||||||
|
|
||||||
@@ -312,6 +319,12 @@ async function promptAuthConfig(
|
|||||||
runtime.error(String(err));
|
runtime.error(String(err));
|
||||||
note("Trouble with OAuth? See https://docs.clawd.bot/start/faq", "OAuth");
|
note("Trouble with OAuth? See https://docs.clawd.bot/start/faq", "OAuth");
|
||||||
}
|
}
|
||||||
|
} else if (authChoice === "claude-cli") {
|
||||||
|
next = applyAuthProfileConfig(next, {
|
||||||
|
profileId: CLAUDE_CLI_PROFILE_ID,
|
||||||
|
provider: "anthropic",
|
||||||
|
mode: "oauth",
|
||||||
|
});
|
||||||
} else if (authChoice === "openai-codex") {
|
} else if (authChoice === "openai-codex") {
|
||||||
const isRemote = isRemoteEnvironment();
|
const isRemote = isRemoteEnvironment();
|
||||||
note(
|
note(
|
||||||
@@ -386,6 +399,20 @@ async function promptAuthConfig(
|
|||||||
runtime.error(String(err));
|
runtime.error(String(err));
|
||||||
note("Trouble with OAuth? See https://docs.clawd.bot/start/faq", "OAuth");
|
note("Trouble with OAuth? See https://docs.clawd.bot/start/faq", "OAuth");
|
||||||
}
|
}
|
||||||
|
} else if (authChoice === "codex-cli") {
|
||||||
|
next = applyAuthProfileConfig(next, {
|
||||||
|
profileId: CODEX_CLI_PROFILE_ID,
|
||||||
|
provider: "openai-codex",
|
||||||
|
mode: "oauth",
|
||||||
|
});
|
||||||
|
const applied = applyOpenAICodexModelDefault(next);
|
||||||
|
next = applied.next;
|
||||||
|
if (applied.changed) {
|
||||||
|
note(
|
||||||
|
`Default model set to ${OPENAI_CODEX_DEFAULT_MODEL}`,
|
||||||
|
"Model configured",
|
||||||
|
);
|
||||||
|
}
|
||||||
} else if (authChoice === "antigravity") {
|
} else if (authChoice === "antigravity") {
|
||||||
const isRemote = isRemoteEnvironment();
|
const isRemote = isRemoteEnvironment();
|
||||||
note(
|
note(
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import {
|
||||||
|
CLAUDE_CLI_PROFILE_ID,
|
||||||
|
CODEX_CLI_PROFILE_ID,
|
||||||
|
ensureAuthProfileStore,
|
||||||
|
} from "../agents/auth-profiles.js";
|
||||||
import {
|
import {
|
||||||
type ClawdbotConfig,
|
type ClawdbotConfig,
|
||||||
CONFIG_PATH_CLAWDBOT,
|
CONFIG_PATH_CLAWDBOT,
|
||||||
@@ -30,6 +34,7 @@ import {
|
|||||||
randomToken,
|
randomToken,
|
||||||
} from "./onboard-helpers.js";
|
} from "./onboard-helpers.js";
|
||||||
import type { AuthChoice, OnboardOptions } from "./onboard-types.js";
|
import type { AuthChoice, OnboardOptions } from "./onboard-types.js";
|
||||||
|
import { applyOpenAICodexModelDefault } from "./openai-codex-model-default.js";
|
||||||
import { ensureSystemdUserLingerNonInteractive } from "./systemd-linger.js";
|
import { ensureSystemdUserLingerNonInteractive } from "./systemd-linger.js";
|
||||||
|
|
||||||
export async function runNonInteractiveOnboarding(
|
export async function runNonInteractiveOnboarding(
|
||||||
@@ -112,6 +117,33 @@ export async function runNonInteractiveOnboarding(
|
|||||||
provider: "anthropic",
|
provider: "anthropic",
|
||||||
mode: "api_key",
|
mode: "api_key",
|
||||||
});
|
});
|
||||||
|
} else if (authChoice === "claude-cli") {
|
||||||
|
const store = ensureAuthProfileStore();
|
||||||
|
if (!store.profiles[CLAUDE_CLI_PROFILE_ID]) {
|
||||||
|
runtime.error(
|
||||||
|
"No Claude CLI credentials found at ~/.claude/.credentials.json",
|
||||||
|
);
|
||||||
|
runtime.exit(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||||
|
profileId: CLAUDE_CLI_PROFILE_ID,
|
||||||
|
provider: "anthropic",
|
||||||
|
mode: "oauth",
|
||||||
|
});
|
||||||
|
} else if (authChoice === "codex-cli") {
|
||||||
|
const store = ensureAuthProfileStore();
|
||||||
|
if (!store.profiles[CODEX_CLI_PROFILE_ID]) {
|
||||||
|
runtime.error("No Codex CLI credentials found at ~/.codex/auth.json");
|
||||||
|
runtime.exit(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||||
|
profileId: CODEX_CLI_PROFILE_ID,
|
||||||
|
provider: "openai-codex",
|
||||||
|
mode: "oauth",
|
||||||
|
});
|
||||||
|
nextConfig = applyOpenAICodexModelDefault(nextConfig).next;
|
||||||
} else if (authChoice === "minimax") {
|
} else if (authChoice === "minimax") {
|
||||||
nextConfig = applyMinimaxConfig(nextConfig);
|
nextConfig = applyMinimaxConfig(nextConfig);
|
||||||
} else if (
|
} else if (
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import type { GatewayDaemonRuntime } from "./daemon-runtime.js";
|
|||||||
export type OnboardMode = "local" | "remote";
|
export type OnboardMode = "local" | "remote";
|
||||||
export type AuthChoice =
|
export type AuthChoice =
|
||||||
| "oauth"
|
| "oauth"
|
||||||
|
| "claude-cli"
|
||||||
| "openai-codex"
|
| "openai-codex"
|
||||||
|
| "codex-cli"
|
||||||
| "antigravity"
|
| "antigravity"
|
||||||
| "apiKey"
|
| "apiKey"
|
||||||
| "minimax"
|
| "minimax"
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import { ensureAuthProfileStore } from "../agents/auth-profiles.js";
|
||||||
import {
|
import {
|
||||||
applyAuthChoice,
|
applyAuthChoice,
|
||||||
warnIfModelConfigLooksOff,
|
warnIfModelConfigLooksOff,
|
||||||
} from "../commands/auth-choice.js";
|
} from "../commands/auth-choice.js";
|
||||||
|
import { buildAuthChoiceOptions } from "../commands/auth-choice-options.js";
|
||||||
import {
|
import {
|
||||||
DEFAULT_GATEWAY_DAEMON_RUNTIME,
|
DEFAULT_GATEWAY_DAEMON_RUNTIME,
|
||||||
GATEWAY_DAEMON_RUNTIME_OPTIONS,
|
GATEWAY_DAEMON_RUNTIME_OPTIONS,
|
||||||
@@ -183,19 +184,10 @@ export async function runOnboardingWizard(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const authStore = ensureAuthProfileStore();
|
||||||
const authChoice = (await prompter.select({
|
const authChoice = (await prompter.select({
|
||||||
message: "Model/auth choice",
|
message: "Model/auth choice",
|
||||||
options: [
|
options: buildAuthChoiceOptions({ store: authStore, includeSkip: true }),
|
||||||
{ value: "oauth", label: "Anthropic OAuth (Claude Pro/Max)" },
|
|
||||||
{ value: "openai-codex", label: "OpenAI Codex (ChatGPT OAuth)" },
|
|
||||||
{
|
|
||||||
value: "antigravity",
|
|
||||||
label: "Google Antigravity (Claude Opus 4.5, Gemini 3, etc.)",
|
|
||||||
},
|
|
||||||
{ value: "apiKey", label: "Anthropic API key" },
|
|
||||||
{ value: "minimax", label: "Minimax M2.1 (LM Studio)" },
|
|
||||||
{ value: "skip", label: "Skip for now" },
|
|
||||||
],
|
|
||||||
})) as AuthChoice;
|
})) as AuthChoice;
|
||||||
|
|
||||||
const authResult = await applyAuthChoice({
|
const authResult = await applyAuthChoice({
|
||||||
|
|||||||
Reference in New Issue
Block a user