codemaxxing 1.0.17 → 1.1.1

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/index.js CHANGED
@@ -16,10 +16,11 @@ import { listServers, addServer, removeServer, getConnectedServers } from "./uti
16
16
  import { tryHandleSkillsCommand } from "./commands/skills.js";
17
17
  import { isOllamaRunning, stopOllama, listInstalledModelsDetailed } from "./utils/ollama.js";
18
18
  import { routeKeyPress } from "./ui/input-router.js";
19
+ import { getCredential } from "./utils/auth.js";
19
20
  import { Banner, ConnectionInfo } from "./ui/banner.js";
20
21
  import { StatusBar } from "./ui/status-bar.js";
21
22
  import { refreshConnectionBanner as refreshConnectionBannerImpl, connectToProvider as connectToProviderImpl, } from "./ui/connection.js";
22
- import { CommandSuggestions, LoginPicker, LoginMethodPickerUI, SkillsMenu, SkillsBrowse, SkillsInstalled, SkillsRemove, ThemePickerUI, SessionPicker, DeleteSessionPicker, DeleteSessionConfirm, ModelPicker, OllamaDeletePicker, OllamaPullPicker, OllamaDeleteConfirm, OllamaPullProgress, OllamaExitPrompt, ApprovalPrompt, WizardConnection, WizardModels, WizardInstallOllama, WizardPulling, } from "./ui/pickers.js";
23
+ import { CommandSuggestions, LoginPicker, LoginMethodPickerUI, SkillsMenu, SkillsBrowse, SkillsInstalled, SkillsRemove, ThemePickerUI, SessionPicker, DeleteSessionPicker, DeleteSessionConfirm, ProviderPicker, ModelPicker, OllamaDeletePicker, OllamaPullPicker, OllamaDeleteConfirm, OllamaPullProgress, OllamaExitPrompt, ApprovalPrompt, WizardConnection, WizardModels, WizardInstallOllama, WizardPulling, } from "./ui/pickers.js";
23
24
  import { createRequire } from "module";
24
25
  const _require = createRequire(import.meta.url);
25
26
  const VERSION = _require("../package.json").version;
@@ -51,9 +52,8 @@ const SLASH_COMMANDS = [
51
52
  { cmd: "/push", desc: "push to remote" },
52
53
  { cmd: "/git on", desc: "enable auto-commits" },
53
54
  { cmd: "/git off", desc: "disable auto-commits" },
54
- { cmd: "/models", desc: "list available models" },
55
+ { cmd: "/models", desc: "switch model" },
55
56
  { cmd: "/theme", desc: "switch color theme" },
56
- { cmd: "/model", desc: "switch model mid-session" },
57
57
  { cmd: "/sessions", desc: "list past sessions" },
58
58
  { cmd: "/session delete", desc: "delete a session" },
59
59
  { cmd: "/resume", desc: "resume a past session" },
@@ -182,8 +182,12 @@ function App() {
182
182
  const [ollamaDeletePickerIndex, setOllamaDeletePickerIndex] = useState(0);
183
183
  const [ollamaPullPicker, setOllamaPullPicker] = useState(false);
184
184
  const [ollamaPullPickerIndex, setOllamaPullPickerIndex] = useState(0);
185
- const [modelPicker, setModelPicker] = useState(null);
185
+ const [modelPickerGroups, setModelPickerGroups] = useState(null);
186
186
  const [modelPickerIndex, setModelPickerIndex] = useState(0);
187
+ const [flatModelList, setFlatModelList] = useState([]);
188
+ const [providerPicker, setProviderPicker] = useState(null);
189
+ const [providerPickerIndex, setProviderPickerIndex] = useState(0);
190
+ const [selectedProvider, setSelectedProvider] = useState(null);
187
191
  // ── Setup Wizard State ──
188
192
  const [wizardScreen, setWizardScreen] = useState(null);
189
193
  const [wizardIndex, setWizardIndex] = useState(0);
@@ -259,7 +263,7 @@ function App() {
259
263
  const selected = matches[idx];
260
264
  if (selected) {
261
265
  // Commands that need args (like /commit, /model) — fill input instead of executing
262
- if (selected.cmd === "/commit" || selected.cmd === "/model" || selected.cmd === "/session delete" ||
266
+ if (selected.cmd === "/commit" || selected.cmd === "/session delete" ||
263
267
  selected.cmd === "/architect") {
264
268
  setInput(selected.cmd + " ");
265
269
  setCmdIndex(0);
@@ -320,8 +324,7 @@ function App() {
320
324
  " /help — show this",
321
325
  " /connect — retry LLM connection",
322
326
  " /login — authentication setup (run codemaxxing login in terminal)",
323
- " /model — switch model mid-session",
324
- " /models — list available models",
327
+ " /models — switch model",
325
328
  " /map — show repository map",
326
329
  " /sessions — list past sessions",
327
330
  " /session delete — delete a session",
@@ -471,54 +474,140 @@ function App() {
471
474
  addMsg("info", `Messages in context: ${agent.getContextLength()}`);
472
475
  return;
473
476
  }
474
- if (trimmed === "/models") {
477
+ if (trimmed === "/models" || trimmed === "/model") {
475
478
  addMsg("info", "Fetching available models...");
476
- const { baseUrl, apiKey } = providerRef.current;
477
- const models = await listModels(baseUrl, apiKey);
478
- if (models.length === 0) {
479
- addMsg("info", "No models found or couldn't reach provider.");
480
- }
481
- else {
482
- addMsg("info", "Available models:\n" + models.map(m => ` ${m}`).join("\n"));
479
+ const groups = {};
480
+ const providerEntries = [];
481
+ // Local LLM (Ollama/LM Studio) — always show, auto-detect
482
+ let localFound = false;
483
+ // Check common local LLM endpoints
484
+ const localEndpoints = [
485
+ { name: "LM Studio", port: 1234 },
486
+ { name: "Ollama", port: 11434 },
487
+ { name: "vLLM", port: 8000 },
488
+ { name: "LocalAI", port: 8080 },
489
+ ];
490
+ for (const endpoint of localEndpoints) {
491
+ if (localFound)
492
+ break;
493
+ try {
494
+ const url = `http://localhost:${endpoint.port}/v1`;
495
+ const models = await listModels(url, "local");
496
+ if (models.length > 0) {
497
+ groups["Local LLM"] = models.map(m => ({
498
+ name: m,
499
+ baseUrl: url,
500
+ apiKey: "local",
501
+ providerType: "openai",
502
+ }));
503
+ localFound = true;
504
+ }
505
+ }
506
+ catch { /* not running */ }
483
507
  }
484
- return;
485
- }
486
- if (trimmed === "/model") {
487
- // Show picker of available models
488
- addMsg("info", "Fetching available models...");
489
- try {
490
- const ollamaModels = await listInstalledModelsDetailed();
491
- if (ollamaModels.length > 0) {
492
- setModelPicker(ollamaModels.map(m => m.name));
493
- setModelPickerIndex(0);
494
- return;
508
+ // Also check Ollama native API
509
+ if (!localFound) {
510
+ try {
511
+ const ollamaModels = await listInstalledModelsDetailed();
512
+ if (ollamaModels.length > 0) {
513
+ groups["Local LLM"] = ollamaModels.map(m => ({
514
+ name: m.name,
515
+ baseUrl: "http://localhost:11434/v1",
516
+ apiKey: "ollama",
517
+ providerType: "openai",
518
+ }));
519
+ localFound = true;
520
+ }
495
521
  }
522
+ catch { /* Ollama not running */ }
523
+ }
524
+ if (localFound) {
525
+ providerEntries.push({ name: "Local LLM", description: "No auth needed — auto-detected", authed: true });
496
526
  }
497
- catch (err) {
498
- // Ollama not available or failed, try provider
527
+ // Anthropic
528
+ const anthropicCred = getCredential("anthropic");
529
+ const claudeModels = ["claude-sonnet-4-6", "claude-opus-4-6", "claude-haiku-4-5-20251001"];
530
+ if (anthropicCred) {
531
+ groups["Anthropic (Claude)"] = claudeModels.map(m => ({
532
+ name: m,
533
+ baseUrl: "https://api.anthropic.com",
534
+ apiKey: anthropicCred.apiKey,
535
+ providerType: "anthropic",
536
+ }));
499
537
  }
500
- // Fallback: try provider's model list
501
- if (providerRef.current?.baseUrl && providerRef.current.baseUrl !== "auto") {
538
+ providerEntries.push({ name: "Anthropic (Claude)", description: "Claude Opus, Sonnet, Haiku — use your subscription or API key", authed: !!anthropicCred });
539
+ // OpenAI
540
+ const openaiCred = getCredential("openai");
541
+ const openaiModels = ["gpt-5.4", "gpt-5.4-pro", "gpt-5", "gpt-5-mini", "gpt-4.1", "gpt-4.1-mini", "o3", "o4-mini", "gpt-4o"];
542
+ if (openaiCred) {
543
+ // OAuth tokens (non sk- keys) must use ChatGPT backend, not api.openai.com
544
+ const isOAuthToken = openaiCred.method === "oauth" || openaiCred.method === "cached-token" ||
545
+ (!openaiCred.apiKey.startsWith("sk-") && !openaiCred.apiKey.startsWith("sess-"));
546
+ const baseUrl = isOAuthToken
547
+ ? "https://chatgpt.com/backend-api"
548
+ : (openaiCred.baseUrl || "https://api.openai.com/v1");
549
+ groups["OpenAI (ChatGPT)"] = openaiModels.map(m => ({
550
+ name: m,
551
+ baseUrl,
552
+ apiKey: openaiCred.apiKey,
553
+ providerType: "openai",
554
+ }));
555
+ }
556
+ providerEntries.push({ name: "OpenAI (ChatGPT)", description: "GPT-5, GPT-4.1, o3 — use your ChatGPT subscription or API key", authed: !!openaiCred });
557
+ // OpenRouter
558
+ const openrouterCred = getCredential("openrouter");
559
+ if (openrouterCred) {
502
560
  try {
503
- const providerModels = await listModels(providerRef.current.baseUrl, providerRef.current.apiKey || "");
504
- if (providerModels.length > 0) {
505
- setModelPicker(providerModels);
506
- setModelPickerIndex(0);
507
- return;
561
+ const orModels = await listModels(openrouterCred.baseUrl || "https://openrouter.ai/api/v1", openrouterCred.apiKey);
562
+ if (orModels.length > 0) {
563
+ groups["OpenRouter"] = orModels.slice(0, 20).map(m => ({
564
+ name: m,
565
+ baseUrl: openrouterCred.baseUrl || "https://openrouter.ai/api/v1",
566
+ apiKey: openrouterCred.apiKey,
567
+ providerType: "openai",
568
+ }));
508
569
  }
509
570
  }
510
- catch (err) {
511
- // Provider fetch failed
512
- }
571
+ catch { /* skip */ }
572
+ }
573
+ providerEntries.push({ name: "OpenRouter", description: "200+ models (Claude, GPT, Gemini, Llama, etc.) — one login", authed: !!openrouterCred });
574
+ // Qwen
575
+ const qwenCred = getCredential("qwen");
576
+ if (qwenCred) {
577
+ groups["Qwen"] = ["qwen-max", "qwen-plus", "qwen-turbo"].map(m => ({
578
+ name: m,
579
+ baseUrl: qwenCred.baseUrl || "https://dashscope.aliyuncs.com/compatible-mode/v1",
580
+ apiKey: qwenCred.apiKey,
581
+ providerType: "openai",
582
+ }));
583
+ }
584
+ providerEntries.push({ name: "Qwen", description: "Qwen 3.5, Qwen Coder — use your Qwen CLI login or API key", authed: !!qwenCred });
585
+ // GitHub Copilot
586
+ const copilotCred = getCredential("copilot");
587
+ if (copilotCred) {
588
+ groups["GitHub Copilot"] = ["gpt-4o", "claude-3.5-sonnet"].map(m => ({
589
+ name: m,
590
+ baseUrl: copilotCred.baseUrl || "https://api.githubcopilot.com",
591
+ apiKey: copilotCred.apiKey,
592
+ providerType: "openai",
593
+ }));
594
+ }
595
+ providerEntries.push({ name: "GitHub Copilot", description: "Use your GitHub Copilot subscription", authed: !!copilotCred });
596
+ // Show provider picker (step 1)
597
+ if (providerEntries.length > 0) {
598
+ setModelPickerGroups(groups);
599
+ setProviderPicker(providerEntries);
600
+ setProviderPickerIndex(0);
601
+ setSelectedProvider(null);
602
+ return;
513
603
  }
514
- // No models found anywhere
515
604
  addMsg("error", "No models available. Download one with /ollama pull or configure a provider.");
516
605
  return;
517
606
  }
518
607
  if (trimmed.startsWith("/model ")) {
519
608
  const newModel = trimmed.replace("/model ", "").trim();
520
609
  if (!newModel) {
521
- addMsg("info", `Current model: ${modelName}\n Usage: /model <model-name>`);
610
+ addMsg("info", `Current model: ${modelName}\n Usage: /models`);
522
611
  return;
523
612
  }
524
613
  agent.switchModel(newModel);
@@ -670,10 +759,18 @@ function App() {
670
759
  setSkillsPicker,
671
760
  sessionDisabledSkills,
672
761
  setSessionDisabledSkills,
673
- modelPicker,
762
+ modelPickerGroups,
674
763
  modelPickerIndex,
675
764
  setModelPickerIndex,
676
- setModelPicker,
765
+ setModelPickerGroups,
766
+ flatModelList,
767
+ setFlatModelList,
768
+ providerPicker,
769
+ providerPickerIndex,
770
+ setProviderPickerIndex,
771
+ setProviderPicker,
772
+ selectedProvider,
773
+ setSelectedProvider,
677
774
  ollamaDeletePicker,
678
775
  ollamaDeletePickerIndex,
679
776
  setOllamaDeletePickerIndex,
@@ -754,7 +851,7 @@ function App() {
754
851
  default:
755
852
  return _jsx(Text, { children: msg.text }, msg.id);
756
853
  }
757
- }), loading && !approval && !streaming && _jsx(NeonSpinner, { message: spinnerMsg, colors: theme.colors }), streaming && !loading && _jsx(StreamingIndicator, { colors: theme.colors }), approval && (_jsx(ApprovalPrompt, { approval: approval, colors: theme.colors })), loginPicker && (_jsx(LoginPicker, { loginPickerIndex: loginPickerIndex, colors: theme.colors })), loginMethodPicker && (_jsx(LoginMethodPickerUI, { loginMethodPicker: loginMethodPicker, loginMethodIndex: loginMethodIndex, colors: theme.colors })), skillsPicker === "menu" && (_jsx(SkillsMenu, { skillsPickerIndex: skillsPickerIndex, colors: theme.colors })), skillsPicker === "browse" && (_jsx(SkillsBrowse, { skillsPickerIndex: skillsPickerIndex, colors: theme.colors })), skillsPicker === "installed" && (_jsx(SkillsInstalled, { skillsPickerIndex: skillsPickerIndex, sessionDisabledSkills: sessionDisabledSkills, colors: theme.colors })), skillsPicker === "remove" && (_jsx(SkillsRemove, { skillsPickerIndex: skillsPickerIndex, colors: theme.colors })), themePicker && (_jsx(ThemePickerUI, { themePickerIndex: themePickerIndex, theme: theme })), sessionPicker && (_jsx(SessionPicker, { sessions: sessionPicker, selectedIndex: sessionPickerIndex, colors: theme.colors })), deleteSessionPicker && (_jsx(DeleteSessionPicker, { sessions: deleteSessionPicker, selectedIndex: deleteSessionPickerIndex, colors: theme.colors })), deleteSessionConfirm && (_jsx(DeleteSessionConfirm, { session: deleteSessionConfirm, colors: theme.colors })), modelPicker && (_jsx(ModelPicker, { models: modelPicker, selectedIndex: modelPickerIndex, activeModel: modelName, colors: theme.colors })), ollamaDeletePicker && (_jsx(OllamaDeletePicker, { models: ollamaDeletePicker.models, selectedIndex: ollamaDeletePickerIndex, colors: theme.colors })), ollamaPullPicker && (_jsx(OllamaPullPicker, { selectedIndex: ollamaPullPickerIndex, colors: theme.colors })), ollamaDeleteConfirm && (_jsx(OllamaDeleteConfirm, { model: ollamaDeleteConfirm.model, size: ollamaDeleteConfirm.size, colors: theme.colors })), ollamaPulling && (_jsx(OllamaPullProgress, { model: ollamaPulling.model, progress: ollamaPulling.progress, colors: theme.colors })), ollamaExitPrompt && (_jsx(OllamaExitPrompt, { colors: theme.colors })), wizardScreen === "connection" && (_jsx(WizardConnection, { wizardIndex: wizardIndex, colors: theme.colors })), wizardScreen === "models" && wizardHardware && (_jsx(WizardModels, { wizardIndex: wizardIndex, wizardHardware: wizardHardware, wizardModels: wizardModels, colors: theme.colors })), wizardScreen === "install-ollama" && (_jsx(WizardInstallOllama, { wizardHardware: wizardHardware, colors: theme.colors })), wizardScreen === "pulling" && (wizardSelectedModel || wizardPullProgress) && (_jsx(WizardPulling, { wizardSelectedModel: wizardSelectedModel, wizardPullProgress: wizardPullProgress, wizardPullError: wizardPullError, colors: theme.colors })), showSuggestions && (_jsx(CommandSuggestions, { cmdMatches: cmdMatches, cmdIndex: cmdIndex, colors: theme.colors })), _jsxs(Box, { borderStyle: "single", borderColor: approval ? theme.colors.warning : theme.colors.border, paddingX: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: "> " }), approval ? (_jsx(Text, { color: theme.colors.warning, children: "waiting for approval..." })) : ready && !loading && !wizardScreen ? (_jsxs(Box, { children: [pastedChunks.map((p) => (_jsxs(Text, { color: theme.colors.muted, children: ["[Pasted text #", p.id, " +", p.lines, " lines]"] }, p.id))), _jsx(TextInput, { value: input, onChange: (v) => { setInput(sanitizeInputArtifacts(v)); setCmdIndex(0); }, onSubmit: handleSubmit }, inputKey)] })) : (_jsx(Text, { dimColor: true, children: loading ? "waiting for response..." : "initializing..." }))] }), agent && (_jsx(StatusBar, { agent: agent, modelName: modelName, sessionDisabledSkills: sessionDisabledSkills }))] }));
854
+ }), loading && !approval && !streaming && _jsx(NeonSpinner, { message: spinnerMsg, colors: theme.colors }), streaming && !loading && _jsx(StreamingIndicator, { colors: theme.colors }), approval && (_jsx(ApprovalPrompt, { approval: approval, colors: theme.colors })), loginPicker && (_jsx(LoginPicker, { loginPickerIndex: loginPickerIndex, colors: theme.colors })), loginMethodPicker && (_jsx(LoginMethodPickerUI, { loginMethodPicker: loginMethodPicker, loginMethodIndex: loginMethodIndex, colors: theme.colors })), skillsPicker === "menu" && (_jsx(SkillsMenu, { skillsPickerIndex: skillsPickerIndex, colors: theme.colors })), skillsPicker === "browse" && (_jsx(SkillsBrowse, { skillsPickerIndex: skillsPickerIndex, colors: theme.colors })), skillsPicker === "installed" && (_jsx(SkillsInstalled, { skillsPickerIndex: skillsPickerIndex, sessionDisabledSkills: sessionDisabledSkills, colors: theme.colors })), skillsPicker === "remove" && (_jsx(SkillsRemove, { skillsPickerIndex: skillsPickerIndex, colors: theme.colors })), themePicker && (_jsx(ThemePickerUI, { themePickerIndex: themePickerIndex, theme: theme })), sessionPicker && (_jsx(SessionPicker, { sessions: sessionPicker, selectedIndex: sessionPickerIndex, colors: theme.colors })), deleteSessionPicker && (_jsx(DeleteSessionPicker, { sessions: deleteSessionPicker, selectedIndex: deleteSessionPickerIndex, colors: theme.colors })), deleteSessionConfirm && (_jsx(DeleteSessionConfirm, { session: deleteSessionConfirm, colors: theme.colors })), providerPicker && !selectedProvider && (_jsx(ProviderPicker, { providers: providerPicker, selectedIndex: providerPickerIndex, colors: theme.colors })), selectedProvider && modelPickerGroups && modelPickerGroups[selectedProvider] && (_jsx(ModelPicker, { providerName: selectedProvider, models: modelPickerGroups[selectedProvider], selectedIndex: modelPickerIndex, activeModel: modelName, colors: theme.colors })), ollamaDeletePicker && (_jsx(OllamaDeletePicker, { models: ollamaDeletePicker.models, selectedIndex: ollamaDeletePickerIndex, colors: theme.colors })), ollamaPullPicker && (_jsx(OllamaPullPicker, { selectedIndex: ollamaPullPickerIndex, colors: theme.colors })), ollamaDeleteConfirm && (_jsx(OllamaDeleteConfirm, { model: ollamaDeleteConfirm.model, size: ollamaDeleteConfirm.size, colors: theme.colors })), ollamaPulling && (_jsx(OllamaPullProgress, { model: ollamaPulling.model, progress: ollamaPulling.progress, colors: theme.colors })), ollamaExitPrompt && (_jsx(OllamaExitPrompt, { colors: theme.colors })), wizardScreen === "connection" && (_jsx(WizardConnection, { wizardIndex: wizardIndex, colors: theme.colors })), wizardScreen === "models" && wizardHardware && (_jsx(WizardModels, { wizardIndex: wizardIndex, wizardHardware: wizardHardware, wizardModels: wizardModels, colors: theme.colors })), wizardScreen === "install-ollama" && (_jsx(WizardInstallOllama, { wizardHardware: wizardHardware, colors: theme.colors })), wizardScreen === "pulling" && (wizardSelectedModel || wizardPullProgress) && (_jsx(WizardPulling, { wizardSelectedModel: wizardSelectedModel, wizardPullProgress: wizardPullProgress, wizardPullError: wizardPullError, colors: theme.colors })), showSuggestions && (_jsx(CommandSuggestions, { cmdMatches: cmdMatches, cmdIndex: cmdIndex, colors: theme.colors })), _jsxs(Box, { borderStyle: "single", borderColor: approval ? theme.colors.warning : theme.colors.border, paddingX: 1, children: [_jsx(Text, { color: theme.colors.secondary, bold: true, children: "> " }), approval ? (_jsx(Text, { color: theme.colors.warning, children: "waiting for approval..." })) : ready && !loading && !wizardScreen ? (_jsxs(Box, { children: [pastedChunks.map((p) => (_jsxs(Text, { color: theme.colors.muted, children: ["[Pasted text #", p.id, " +", p.lines, " lines]"] }, p.id))), _jsx(TextInput, { value: input, onChange: (v) => { setInput(sanitizeInputArtifacts(v)); setCmdIndex(0); }, onSubmit: handleSubmit }, inputKey)] })) : (_jsx(Text, { dimColor: true, children: loading ? "waiting for response..." : "initializing..." }))] }), agent && (_jsx(StatusBar, { agent: agent, modelName: modelName, sessionDisabledSkills: sessionDisabledSkills }))] }));
758
855
  }
759
856
  // Clear screen before render
760
857
  process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
@@ -36,10 +36,50 @@ export interface InputRouterContext extends WizardContext {
36
36
  setSkillsPicker: (val: "menu" | "browse" | "installed" | "remove" | null) => void;
37
37
  sessionDisabledSkills: Set<string>;
38
38
  setSessionDisabledSkills: (fn: (prev: Set<string>) => Set<string>) => void;
39
- modelPicker: string[] | null;
39
+ providerPicker: Array<{
40
+ name: string;
41
+ description: string;
42
+ authed: boolean;
43
+ }> | null;
44
+ providerPickerIndex: number;
45
+ setProviderPickerIndex: (fn: (prev: number) => number) => void;
46
+ setProviderPicker: (val: Array<{
47
+ name: string;
48
+ description: string;
49
+ authed: boolean;
50
+ }> | null) => void;
51
+ selectedProvider: string | null;
52
+ setSelectedProvider: (val: string | null) => void;
53
+ modelPickerGroups: {
54
+ [providerName: string]: Array<{
55
+ name: string;
56
+ baseUrl: string;
57
+ apiKey: string;
58
+ providerType: "openai" | "anthropic";
59
+ }>;
60
+ } | null;
40
61
  modelPickerIndex: number;
41
62
  setModelPickerIndex: (fn: (prev: number) => number) => void;
42
- setModelPicker: (val: string[] | null) => void;
63
+ setModelPickerGroups: (val: {
64
+ [providerName: string]: Array<{
65
+ name: string;
66
+ baseUrl: string;
67
+ apiKey: string;
68
+ providerType: "openai" | "anthropic";
69
+ }>;
70
+ } | null) => void;
71
+ flatModelList: Array<{
72
+ name: string;
73
+ baseUrl: string;
74
+ apiKey: string;
75
+ providerType: "openai" | "anthropic";
76
+ }>;
77
+ setFlatModelList: (val: Array<{
78
+ name: string;
79
+ baseUrl: string;
80
+ apiKey: string;
81
+ providerType: "openai" | "anthropic";
82
+ }>) => void;
43
83
  ollamaDeletePicker: {
44
84
  models: {
45
85
  name: string;
@@ -1,4 +1,6 @@
1
- import { PROVIDERS, openRouterOAuth, anthropicSetupToken, importCodexToken, importQwenToken, copilotDeviceFlow } from "../utils/auth.js";
1
+ import { PROVIDERS, openRouterOAuth, importCodexToken, importQwenToken, copilotDeviceFlow } from "../utils/auth.js";
2
+ import { loginOpenAICodexOAuth } from "../utils/openai-oauth.js";
3
+ import { loginAnthropicOAuth } from "../utils/anthropic-oauth.js";
2
4
  import { listInstalledSkills, installSkill, removeSkill, getRegistrySkills } from "../utils/skills.js";
3
5
  import { listThemes, getTheme, THEMES } from "../themes.js";
4
6
  import { getSession, loadMessages, deleteSession } from "../utils/sessions.js";
@@ -15,6 +17,8 @@ export function routeKeyPress(inputChar, key, ctx) {
15
17
  return true;
16
18
  if (handleSkillsPicker(inputChar, key, ctx))
17
19
  return true;
20
+ if (handleProviderPicker(inputChar, key, ctx))
21
+ return true;
18
22
  if (handleModelPicker(inputChar, key, ctx))
19
23
  return true;
20
24
  if (handleOllamaDeletePicker(inputChar, key, ctx))
@@ -99,21 +103,37 @@ function handleLoginMethodPicker(inputChar, key, ctx) {
99
103
  })
100
104
  .catch((err) => { ctx.addMsg("error", `OAuth failed: ${err.message}`); ctx.setLoading(false); });
101
105
  }
102
- else if (method === "setup-token") {
103
- ctx.addMsg("info", "Starting setup-token flow browser will open...");
106
+ else if (method === "oauth" && providerId === "anthropic") {
107
+ ctx.addMsg("info", "Opening browser for Claude login...");
104
108
  ctx.setLoading(true);
105
- ctx.setSpinnerMsg("Waiting for Claude Code auth...");
106
- anthropicSetupToken((msg) => ctx.addMsg("info", msg))
109
+ ctx.setSpinnerMsg("Waiting for Anthropic authorization...");
110
+ loginAnthropicOAuth((msg) => ctx.addMsg("info", msg))
107
111
  .then((cred) => { ctx.addMsg("info", `✅ Anthropic authenticated! (${cred.label})`); ctx.setLoading(false); })
108
- .catch((err) => { ctx.addMsg("error", `Auth failed: ${err.message}`); ctx.setLoading(false); });
112
+ .catch((err) => {
113
+ ctx.addMsg("error", `OAuth failed: ${err.message}\n Fallback: set your key via CLI: codemaxxing auth api-key anthropic <your-key>\n Or set ANTHROPIC_API_KEY env var and restart.\n Get key at: console.anthropic.com/settings/keys`);
114
+ ctx.setLoading(false);
115
+ });
109
116
  }
110
- else if (method === "cached-token" && providerId === "openai") {
117
+ else if (method === "oauth" && providerId === "openai") {
118
+ // Try cached Codex token first as a quick path
111
119
  const imported = importCodexToken((msg) => ctx.addMsg("info", msg));
112
120
  if (imported) {
113
121
  ctx.addMsg("info", `✅ Imported Codex credentials! (${imported.label})`);
114
122
  }
115
123
  else {
116
- ctx.addMsg("info", "No Codex CLI found. Install Codex CLI and sign in first.");
124
+ // Primary flow: browser OAuth
125
+ ctx.addMsg("info", "Opening browser for ChatGPT login...");
126
+ ctx.setLoading(true);
127
+ ctx.setSpinnerMsg("Waiting for OpenAI authorization...");
128
+ loginOpenAICodexOAuth((msg) => ctx.addMsg("info", msg))
129
+ .then((cred) => {
130
+ ctx.addMsg("info", `✅ OpenAI authenticated! (${cred.label})`);
131
+ ctx.setLoading(false);
132
+ })
133
+ .catch((err) => {
134
+ ctx.addMsg("error", `OAuth failed: ${err.message}\n Fallback: set your key via CLI: codemaxxing auth api-key openai <your-key>\n Or set OPENAI_API_KEY env var and restart.\n Get key at: platform.openai.com/api-keys`);
135
+ ctx.setLoading(false);
136
+ });
117
137
  }
118
138
  }
119
139
  else if (method === "cached-token" && providerId === "qwen") {
@@ -343,30 +363,68 @@ function handleSkillsPicker(_inputChar, key, ctx) {
343
363
  }
344
364
  return true; // absorb all keys when skills picker is active
345
365
  }
366
+ function handleProviderPicker(_inputChar, key, ctx) {
367
+ if (!ctx.providerPicker || ctx.selectedProvider)
368
+ return false;
369
+ const len = ctx.providerPicker.length;
370
+ if (key.upArrow) {
371
+ ctx.setProviderPickerIndex((prev) => (prev - 1 + len) % len);
372
+ return true;
373
+ }
374
+ if (key.downArrow) {
375
+ ctx.setProviderPickerIndex((prev) => (prev + 1) % len);
376
+ return true;
377
+ }
378
+ if (key.escape) {
379
+ ctx.setProviderPicker(null);
380
+ return true;
381
+ }
382
+ if (key.return) {
383
+ const selected = ctx.providerPicker[ctx.providerPickerIndex];
384
+ ctx.setSelectedProvider(selected.name);
385
+ ctx.setModelPickerIndex(() => 0);
386
+ return true;
387
+ }
388
+ return true;
389
+ }
346
390
  function handleModelPicker(_inputChar, key, ctx) {
347
- if (!ctx.modelPicker)
391
+ if (!ctx.selectedProvider || !ctx.modelPickerGroups)
348
392
  return false;
393
+ const models = ctx.modelPickerGroups[ctx.selectedProvider];
394
+ if (!models)
395
+ return false;
396
+ const len = models.length;
349
397
  if (key.upArrow) {
350
- ctx.setModelPickerIndex((prev) => (prev - 1 + ctx.modelPicker.length) % ctx.modelPicker.length);
398
+ ctx.setModelPickerIndex((prev) => (prev - 1 + len) % len);
351
399
  return true;
352
400
  }
353
401
  if (key.downArrow) {
354
- ctx.setModelPickerIndex((prev) => (prev + 1) % ctx.modelPicker.length);
402
+ ctx.setModelPickerIndex((prev) => (prev + 1) % len);
355
403
  return true;
356
404
  }
357
405
  if (key.escape) {
358
- ctx.setModelPicker(null);
406
+ ctx.setSelectedProvider(null);
407
+ ctx.setModelPickerIndex(() => 0);
359
408
  return true;
360
409
  }
361
410
  if (key.return) {
362
- const selected = ctx.modelPicker[ctx.modelPickerIndex];
411
+ const selected = models[ctx.modelPickerIndex];
363
412
  if (selected && ctx.agent) {
364
- ctx.agent.switchModel(selected);
365
- ctx.setModelName(selected);
366
- ctx.addMsg("info", `✅ Switched to: ${selected}`);
413
+ ctx.agent.switchModel(selected.name, selected.baseUrl, selected.apiKey, selected.providerType);
414
+ ctx.setModelName(selected.name);
415
+ ctx.addMsg("info", `✅ Switched to: ${selected.name}`);
367
416
  ctx.refreshConnectionBanner();
368
417
  }
369
- ctx.setModelPicker(null);
418
+ else if (selected && !ctx.agent) {
419
+ // First-time: trigger reconnect which will create the agent
420
+ ctx.addMsg("info", `Initializing with ${selected.name}...`);
421
+ ctx.connectToProvider?.(false);
422
+ }
423
+ ctx.setModelPickerGroups(null);
424
+ ctx.setProviderPicker(null);
425
+ ctx.setSelectedProvider(null);
426
+ ctx.setModelPickerIndex(() => 0);
427
+ ctx.setProviderPickerIndex(() => 0);
370
428
  return true;
371
429
  }
372
430
  return true;
@@ -77,13 +77,34 @@ interface DeleteSessionConfirmProps {
77
77
  colors: Theme["colors"];
78
78
  }
79
79
  export declare function DeleteSessionConfirm({ session, colors }: DeleteSessionConfirmProps): import("react/jsx-runtime").JSX.Element;
80
+ export interface ModelEntry {
81
+ name: string;
82
+ baseUrl: string;
83
+ apiKey: string;
84
+ providerType: "openai" | "anthropic";
85
+ }
86
+ export interface GroupedModels {
87
+ [providerName: string]: ModelEntry[];
88
+ }
89
+ export interface ProviderPickerEntry {
90
+ name: string;
91
+ description: string;
92
+ authed: boolean;
93
+ }
94
+ interface ProviderPickerProps {
95
+ providers: ProviderPickerEntry[];
96
+ selectedIndex: number;
97
+ colors: Theme["colors"];
98
+ }
99
+ export declare function ProviderPicker({ providers, selectedIndex, colors }: ProviderPickerProps): import("react/jsx-runtime").JSX.Element;
80
100
  interface ModelPickerProps {
81
- models: string[];
101
+ providerName: string;
102
+ models: ModelEntry[];
82
103
  selectedIndex: number;
83
104
  activeModel: string;
84
105
  colors: Theme["colors"];
85
106
  }
86
- export declare function ModelPicker({ models, selectedIndex, activeModel, colors }: ModelPickerProps): import("react/jsx-runtime").JSX.Element;
107
+ export declare function ModelPicker({ providerName, models, selectedIndex, activeModel, colors }: ModelPickerProps): import("react/jsx-runtime").JSX.Element;
87
108
  interface OllamaDeletePickerProps {
88
109
  models: Array<{
89
110
  name: string;
@@ -13,6 +13,11 @@ export function LoginPicker({ loginPickerIndex, colors }) {
13
13
  return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: colors.secondary, children: "\uD83D\uDCAA Choose a provider:" }), PROVIDERS.filter((p) => p.id !== "local").map((p, i) => (_jsxs(Text, { children: [i === loginPickerIndex ? _jsx(Text, { color: colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === loginPickerIndex ? colors.suggestion : colors.primary, bold: true, children: p.name }), _jsxs(Text, { color: colors.muted, children: [" — ", p.description] }), getCredentials().some((c) => c.provider === p.id) ? _jsx(Text, { color: colors.success, children: " \u2713" }) : null] }, p.id))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter select · Esc cancel" })] }));
14
14
  }
15
15
  export function LoginMethodPickerUI({ loginMethodPicker, loginMethodIndex, colors }) {
16
+ // Provider-specific label overrides
17
+ const providerLabels = {
18
+ anthropic: { "oauth": "🔐 Login with Claude Pro/Max (browser)" },
19
+ openai: { "oauth": "🔐 Login with ChatGPT (browser)" },
20
+ };
16
21
  const labels = {
17
22
  "oauth": "🌐 Browser login (OAuth)",
18
23
  "setup-token": "🔑 Link subscription (via Claude Code CLI)",
@@ -20,7 +25,8 @@ export function LoginMethodPickerUI({ loginMethodPicker, loginMethodIndex, color
20
25
  "api-key": "🔒 Enter API key manually",
21
26
  "device-flow": "📱 Device flow (GitHub)",
22
27
  };
23
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: colors.secondary, children: "How do you want to authenticate?" }), loginMethodPicker.methods.map((method, i) => (_jsxs(Text, { children: [i === loginMethodIndex ? _jsx(Text, { color: colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === loginMethodIndex ? colors.suggestion : colors.primary, bold: true, children: labels[method] ?? method })] }, method))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter select · Esc back" })] }));
28
+ const getLabel = (method) => providerLabels[loginMethodPicker.provider]?.[method] ?? labels[method] ?? method;
29
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: colors.secondary, children: "How do you want to authenticate?" }), loginMethodPicker.methods.map((method, i) => (_jsxs(Text, { children: [i === loginMethodIndex ? _jsx(Text, { color: colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === loginMethodIndex ? colors.suggestion : colors.primary, bold: true, children: getLabel(method) })] }, method))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter select · Esc back" })] }));
24
30
  }
25
31
  export function SkillsMenu({ skillsPickerIndex, colors }) {
26
32
  const items = [
@@ -57,8 +63,11 @@ export function DeleteSessionPicker({ sessions, selectedIndex, colors }) {
57
63
  export function DeleteSessionConfirm({ session, colors }) {
58
64
  return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: colors.warning, paddingX: 1, marginBottom: 0, children: [_jsxs(Text, { bold: true, color: colors.warning, children: ["Delete session ", session.id, "?"] }), _jsxs(Text, { color: colors.muted, children: [" ", session.display] }), _jsxs(Text, { children: [_jsx(Text, { color: colors.error, bold: true, children: " [y]" }), _jsx(Text, { children: "es " }), _jsx(Text, { color: colors.success, bold: true, children: "[n]" }), _jsx(Text, { children: "o" })] })] }));
59
65
  }
60
- export function ModelPicker({ models, selectedIndex, activeModel, colors }) {
61
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: colors.secondary, children: "Switch model:" }), _jsx(Text, { children: "" }), models.map((m, i) => (_jsxs(Text, { children: [" ", i === selectedIndex ? _jsx(Text, { color: colors.primary, bold: true, children: "▸ " }) : " ", _jsx(Text, { color: i === selectedIndex ? colors.primary : undefined, children: m }), m === activeModel ? _jsx(Text, { color: colors.success, children: " (active)" }) : null] }, m))), _jsx(Text, { children: "" }), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter to switch · Esc cancel" })] }));
66
+ export function ProviderPicker({ providers, selectedIndex, colors }) {
67
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: colors.secondary, children: "Choose a provider:" }), _jsx(Text, { children: "" }), providers.map((p, i) => (_jsxs(Text, { children: [i === selectedIndex ? _jsx(Text, { color: colors.primary, bold: true, children: "▸ " }) : " ", _jsx(Text, { color: i === selectedIndex ? colors.primary : undefined, bold: true, children: p.name }), _jsxs(Text, { dimColor: true, children: [" — ", p.description] }), p.authed ? _jsx(Text, { color: colors.success, children: " " }) : null] }, p.name))), _jsx(Text, { children: "" }), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter select · Esc cancel" })] }));
68
+ }
69
+ export function ModelPicker({ providerName, models, selectedIndex, activeModel, colors }) {
70
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: colors.border, paddingX: 1, marginBottom: 0, children: [_jsxs(Text, { bold: true, color: colors.secondary, children: ["── ", providerName, " ──"] }), _jsx(Text, { children: "" }), models.map((entry, i) => (_jsxs(Text, { children: [" ", i === selectedIndex ? _jsx(Text, { color: colors.primary, bold: true, children: "▸ " }) : " ", _jsx(Text, { color: i === selectedIndex ? colors.primary : undefined, children: entry.name }), entry.name === activeModel ? _jsx(Text, { color: colors.success, children: " (active)" }) : null] }, entry.name))), _jsx(Text, { children: "" }), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter switch · Esc back" })] }));
62
71
  }
63
72
  export function OllamaDeletePicker({ models, selectedIndex, colors }) {
64
73
  return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: colors.secondary, children: "Delete which model?" }), _jsx(Text, { children: "" }), models.map((m, i) => (_jsxs(Text, { children: [" ", i === selectedIndex ? _jsx(Text, { color: colors.primary, bold: true, children: "▸ " }) : " ", _jsx(Text, { color: i === selectedIndex ? colors.primary : undefined, children: m.name }), _jsxs(Text, { color: colors.muted, children: [" (", (m.size / (1024 * 1024 * 1024)).toFixed(1), " GB)"] })] }, m.name))), _jsx(Text, { children: "" }), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter to delete · Esc cancel" })] }));
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Anthropic OAuth PKCE flow
3
+ *
4
+ * Lets users log in with their Claude Pro/Max subscription (no API key needed).
5
+ * Uses the same OAuth flow as Claude Code CLI.
6
+ */
7
+ import { type AuthCredential } from "./auth.js";
8
+ export declare function refreshAnthropicOAuthToken(refreshToken: string): Promise<{
9
+ access: string;
10
+ refresh: string;
11
+ expires: number;
12
+ }>;
13
+ export declare function loginAnthropicOAuth(onStatus?: (msg: string) => void): Promise<AuthCredential>;