11import * as readline from "node:readline" ;
22import { injectCodexConfig } from "./codex-inject" ;
33import { getDefaultConfig , saveConfig } from "./config" ;
4+ import { KEY_LOGIN_PROVIDERS , enrichProviderFromCatalog } from "./oauth/key-providers" ;
5+ import { OAUTH_PROVIDERS } from "./oauth" ;
46import type { OcxConfig , OcxProviderConfig } from "./types" ;
57
68function createPrompt ( ) : { ask ( question : string ) : Promise < string > ; close ( ) : void } {
@@ -13,94 +15,120 @@ function createPrompt(): { ask(question: string): Promise<string>; close(): void
1315 } ;
1416}
1517
16- const PRESETS : Record < string , { adapter : string ; baseUrl : string ; envKey : string ; models : string [ ] } > = {
17- "opencode-go" : {
18- adapter : "openai-chat" ,
19- baseUrl : "https://opencode.ai/zen/go/v1" ,
20- envKey : "OPENCODE_API_KEY" ,
21- models : [ "kimi-k2.5" , "kimi-k2.6" , "deepseek-v4-flash" , "qwen3.5-plus" ] ,
22- } ,
23- "anthropic" : {
24- adapter : "anthropic" ,
25- baseUrl : "https://api.anthropic.com" ,
26- envKey : "ANTHROPIC_API_KEY" ,
27- models : [ "claude-sonnet-4-20250514" , "claude-opus-4-20250916" ] ,
28- } ,
29- "openai" : {
30- adapter : "openai-responses" ,
31- baseUrl : "https://api.openai.com" ,
32- envKey : "OPENAI_API_KEY" ,
33- models : [ "gpt-5.5" , "o3-pro" ] ,
34- } ,
35- "openrouter" : {
36- adapter : "openai-chat" ,
37- baseUrl : "https://openrouter.ai/api/v1" ,
38- envKey : "OPENROUTER_API_KEY" ,
39- models : [ "anthropic/claude-sonnet-4" , "google/gemini-3-pro" ] ,
40- } ,
41- "groq" : {
42- adapter : "openai-chat" ,
43- baseUrl : "https://api.groq.com/openai/v1" ,
44- envKey : "GROQ_API_KEY" ,
45- models : [ "llama-4-scout-17b" , "llama-4-maverick-17b" ] ,
46- } ,
47- "google" : {
48- adapter : "google" ,
49- baseUrl : "https://generativelanguage.googleapis.com" ,
50- envKey : "GOOGLE_API_KEY" ,
51- models : [ "gemini-3-pro" , "gemini-3-flash" ] ,
52- } ,
53- "azure-openai" : {
54- adapter : "azure-openai" ,
55- baseUrl : "https://{your-resource}.openai.azure.com/openai/deployments/{deployment}" ,
56- envKey : "AZURE_OPENAI_API_KEY" ,
57- models : [ "gpt-5.5" ] ,
58- } ,
18+ type InitKind = "forward" | "oauth" | "key" | "local" ;
19+ export interface InitProvider {
20+ id : string ;
21+ label : string ;
22+ adapter : string ;
23+ baseUrl : string ;
24+ kind : InitKind ;
25+ dashboardUrl ?: string ;
26+ defaultModel ?: string ;
27+ }
28+
29+ const OAUTH_LABELS : Record < string , string > = {
30+ xai : "xAI (Grok)" , anthropic : "Anthropic (Claude)" , kimi : "Kimi (Moonshot)" ,
5931} ;
6032
33+ /**
34+ * The full CLI provider menu, built from the SAME registries the GUI uses (OAUTH_PROVIDERS +
35+ * KEY_LOGIN_PROVIDERS) plus the ChatGPT-forward, a few non-catalog key providers, and local servers —
36+ * so `ocx init` reaches provider parity with the GUI. Exported for verification.
37+ */
38+ export function buildInitProviders ( ) : InitProvider [ ] {
39+ const out : InitProvider [ ] = [ ] ;
40+ // ChatGPT login (no key) — the default forward provider.
41+ out . push ( { id : "openai" , label : "OpenAI — ChatGPT login (no key)" , adapter : "openai-responses" , baseUrl : "https://chatgpt.com/backend-api/codex" , kind : "forward" } ) ;
42+ // Real account logins (OAuth).
43+ for ( const id of Object . keys ( OAUTH_PROVIDERS ) ) {
44+ const pc = OAUTH_PROVIDERS [ id ] . providerConfig ;
45+ out . push ( { id, label : `${ OAUTH_LABELS [ id ] ?? id } — account login` , adapter : pc . adapter , baseUrl : pc . baseUrl , kind : "oauth" , defaultModel : pc . defaultModel } ) ;
46+ }
47+ // Key providers not in the catalog (native adapters / well-known endpoints).
48+ out . push ( { id : "openai-apikey" , label : "OpenAI (API key)" , adapter : "openai-responses" , baseUrl : "https://api.openai.com/v1" , kind : "key" , dashboardUrl : "https://platform.openai.com/api-keys" , defaultModel : "gpt-5.5" } ) ;
49+ out . push ( { id : "openrouter" , label : "OpenRouter" , adapter : "openai-chat" , baseUrl : "https://openrouter.ai/api/v1" , kind : "key" , dashboardUrl : "https://openrouter.ai/keys" } ) ;
50+ out . push ( { id : "groq" , label : "Groq" , adapter : "openai-chat" , baseUrl : "https://api.groq.com/openai/v1" , kind : "key" , dashboardUrl : "https://console.groq.com/keys" } ) ;
51+ out . push ( { id : "google" , label : "Google Gemini" , adapter : "google" , baseUrl : "https://generativelanguage.googleapis.com" , kind : "key" , dashboardUrl : "https://aistudio.google.com/apikey" , defaultModel : "gemini-3-pro" } ) ;
52+ out . push ( { id : "azure-openai" , label : "Azure OpenAI" , adapter : "azure" , baseUrl : "https://{resource}.openai.azure.com/openai/deployments/{deployment}" , kind : "key" , dashboardUrl : "https://portal.azure.com" } ) ;
53+ // The full API-key catalog (deepseek, mistral, kilo, minimax, … — same set the GUI shows).
54+ for ( const [ id , p ] of Object . entries ( KEY_LOGIN_PROVIDERS ) ) {
55+ out . push ( { id, label : p . label , adapter : p . adapter , baseUrl : p . baseUrl , kind : "key" , dashboardUrl : p . dashboardUrl , defaultModel : p . defaultModel } ) ;
56+ }
57+ // Local servers (usually no key).
58+ out . push ( { id : "ollama" , label : "Ollama (local)" , adapter : "openai-chat" , baseUrl : "http://localhost:11434/v1" , kind : "local" } ) ;
59+ out . push ( { id : "vllm" , label : "vLLM (local)" , adapter : "openai-chat" , baseUrl : "http://localhost:8000/v1" , kind : "local" } ) ;
60+ out . push ( { id : "lm-studio" , label : "LM Studio (local)" , adapter : "openai-chat" , baseUrl : "http://localhost:1234/v1" , kind : "local" } ) ;
61+ return out ;
62+ }
63+
64+ const KIND_HEADING : Record < InitKind , string > = {
65+ forward : "ChatGPT login" ,
66+ oauth : "Account login (OAuth — then run: ocx login <id>)" ,
67+ key : "API key (paste a key from the provider's dashboard)" ,
68+ local : "Local servers (usually no key)" ,
69+ } ;
70+
71+ function printMenu ( providers : InitProvider [ ] ) : void {
72+ console . log ( "Available providers:" ) ;
73+ let lastKind : InitKind | null = null ;
74+ providers . forEach ( ( p , i ) => {
75+ if ( p . kind !== lastKind ) { console . log ( `\n ${ KIND_HEADING [ p . kind ] } :` ) ; lastKind = p . kind ; }
76+ console . log ( ` ${ String ( i + 1 ) . padStart ( 2 ) } . ${ p . label } ` ) ;
77+ } ) ;
78+ console . log ( `\n ${ providers . length + 1 } . custom (enter URL manually)` ) ;
79+ }
80+
81+ const envKeyFor = ( id : string ) => `${ id . toUpperCase ( ) . replace ( / [ ^ A - Z 0 - 9 ] + / g, "_" ) } _API_KEY` ;
82+
6183export async function runInit ( ) : Promise < void > {
6284 const prompt = createPrompt ( ) ;
63-
6485 console . log ( "\n🔧 opencodex (ocx) setup\n" ) ;
6586
66- const presetNames = Object . keys ( PRESETS ) ;
67- console . log ( "Available providers:" ) ;
68- presetNames . forEach ( ( name , i ) => console . log ( ` ${ i + 1 } . ${ name } ` ) ) ;
69- console . log ( ` ${ presetNames . length + 1 } . custom (enter URL manually)` ) ;
87+ const providers = buildInitProviders ( ) ;
88+ printMenu ( providers ) ;
7089
7190 const choice = await prompt . ask ( "\nSelect provider (number): " ) ;
7291 const idx = parseInt ( choice , 10 ) - 1 ;
7392
7493 let providerName : string ;
7594 let providerConfig : OcxProviderConfig ;
76-
77- if ( idx >= 0 && idx < presetNames . length ) {
78- providerName = presetNames [ idx ] ;
79- const preset = PRESETS [ providerName ] ;
80-
81- console . log ( `\n📡 ${ providerName } selected` ) ;
82- console . log ( ` Base URL: ${ preset . baseUrl } ` ) ;
83- console . log ( ` Models: ${ preset . models . join ( ", " ) } ` ) ;
84-
85- const apiKey = await prompt . ask ( `\nAPI key (or env var ${ preset . envKey } ): ` ) ;
86- const resolvedKey = apiKey . trim ( ) || `\${${ preset . envKey } }` ;
87-
88- const modelChoice = await prompt . ask ( `Default model [${ preset . models [ 0 ] } ]: ` ) ;
89- const defaultModel = modelChoice . trim ( ) || preset . models [ 0 ] ;
90-
91- providerConfig = {
92- adapter : preset . adapter ,
93- baseUrl : preset . baseUrl ,
94- apiKey : resolvedKey ,
95- defaultModel,
96- } ;
95+ let oauthHint = false ;
96+
97+ if ( idx >= 0 && idx < providers . length ) {
98+ const p = providers [ idx ] ;
99+ providerName = p . id ;
100+ console . log ( `\n📡 ${ p . label } ` ) ;
101+ console . log ( ` Base URL: ${ p . baseUrl } ` ) ;
102+
103+ if ( p . kind === "forward" ) {
104+ providerConfig = { adapter : p . adapter , baseUrl : p . baseUrl , authMode : "forward" } ;
105+ console . log ( " No API key needed — forwards your existing `codex login`." ) ;
106+ } else if ( p . kind === "oauth" ) {
107+ providerConfig = { adapter : p . adapter , baseUrl : p . baseUrl , authMode : "oauth" , ...( p . defaultModel ? { defaultModel : p . defaultModel } : { } ) } ;
108+ oauthHint = true ;
109+ } else {
110+ // key + local: collect a key (local usually blank).
111+ if ( p . dashboardUrl ) console . log ( ` 🔑 Get your key: ${ p . dashboardUrl } ` ) ;
112+ const env = envKeyFor ( p . id ) ;
113+ const hint = p . kind === "local" ? "API key (usually blank — press Enter): " : `API key (paste, or env var $${ env } ): ` ;
114+ const apiKey = ( await prompt . ask ( `\n${ hint } ` ) ) . trim ( ) ;
115+ const modelChoice = ( await prompt . ask ( `Default model${ p . defaultModel ? ` [${ p . defaultModel } ]` : " (optional)" } : ` ) ) . trim ( ) ;
116+ const defaultModel = modelChoice || p . defaultModel ;
117+ providerConfig = {
118+ adapter : p . adapter ,
119+ baseUrl : p . baseUrl ,
120+ ...( p . kind === "key" ? { apiKey : apiKey || `\${${ env } }` } : apiKey ? { apiKey } : { } ) ,
121+ ...( defaultModel ? { defaultModel } : { } ) ,
122+ } ;
123+ // Apply the catalog's models / vision classification (same enrichment as the GUI).
124+ enrichProviderFromCatalog ( p . id , providerConfig ) ;
125+ }
97126 } else {
98127 providerName = await prompt . ask ( "Provider name: " ) ;
99128 const baseUrl = await prompt . ask ( "Base URL (e.g. http://localhost:11434/v1): " ) ;
100129 const adapter = await prompt . ask ( "Adapter [openai-chat]: " ) || "openai-chat" ;
101130 const apiKey = await prompt . ask ( "API key (optional): " ) ;
102131 const defaultModel = await prompt . ask ( "Default model: " ) ;
103-
104132 providerConfig = {
105133 adapter : adapter . trim ( ) ,
106134 baseUrl : baseUrl . trim ( ) ,
@@ -109,7 +137,7 @@ export async function runInit(): Promise<void> {
109137 } ;
110138 }
111139
112- const portStr = await prompt . ask ( "Proxy port [10100]: " ) ;
140+ const portStr = await prompt . ask ( "\nProxy port [10100]: " ) ;
113141 const port = parseInt ( portStr , 10 ) || 10100 ;
114142
115143 const config : OcxConfig = {
@@ -121,6 +149,7 @@ export async function runInit(): Promise<void> {
121149
122150 saveConfig ( config ) ;
123151 console . log ( `\n✅ Config saved to ~/.opencodex/config.json` ) ;
152+ if ( oauthHint ) console . log ( `🔐 Authenticate this provider with: ocx login ${ providerName } ` ) ;
124153
125154 const injectAnswer = await prompt . ask ( "Inject into Codex config.toml? [Y/n]: " ) ;
126155 if ( injectAnswer . trim ( ) . toLowerCase ( ) !== "n" ) {
0 commit comments