codemaxxing 0.1.7 → 0.1.8

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.
Files changed (3) hide show
  1. package/dist/index.js +96 -41
  2. package/package.json +1 -1
  3. package/src/index.tsx +126 -47
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import { listSessions, getSession, loadMessages } from "./utils/sessions.js";
10
10
  import { execSync } from "child_process";
11
11
  import { isGitRepo, getBranch, getStatus, getDiff, undoLastCommit } from "./utils/git.js";
12
12
  import { getTheme, listThemes, THEMES, DEFAULT_THEME } from "./themes.js";
13
- import { PROVIDERS, openRouterOAuth, anthropicSetupToken, importCodexToken, importQwenToken, copilotDeviceFlow } from "./utils/auth.js";
13
+ import { PROVIDERS, getCredentials, openRouterOAuth, anthropicSetupToken, importCodexToken, importQwenToken, copilotDeviceFlow } from "./utils/auth.js";
14
14
  const VERSION = "0.1.5";
15
15
  // ── Helpers ──
16
16
  function formatTimeAgo(date) {
@@ -108,6 +108,8 @@ function App() {
108
108
  const [themePickerIndex, setThemePickerIndex] = useState(0);
109
109
  const [loginPicker, setLoginPicker] = useState(false);
110
110
  const [loginPickerIndex, setLoginPickerIndex] = useState(0);
111
+ const [loginMethodPicker, setLoginMethodPicker] = useState(null);
112
+ const [loginMethodIndex, setLoginMethodIndex] = useState(0);
111
113
  const [approval, setApproval] = useState(null);
112
114
  // Listen for paste events from stdin interceptor
113
115
  useEffect(() => {
@@ -497,83 +499,127 @@ function App() {
497
499
  return;
498
500
  }
499
501
  }
500
- // Login picker navigation
501
- if (loginPicker) {
502
- const loginProviders = PROVIDERS.filter((p) => p.id !== "local");
502
+ // Login method picker navigation (second level — pick auth method)
503
+ if (loginMethodPicker) {
504
+ const methods = loginMethodPicker.methods;
503
505
  if (key.upArrow) {
504
- setLoginPickerIndex((prev) => (prev - 1 + loginProviders.length) % loginProviders.length);
506
+ setLoginMethodIndex((prev) => (prev - 1 + methods.length) % methods.length);
505
507
  return;
506
508
  }
507
509
  if (key.downArrow) {
508
- setLoginPickerIndex((prev) => (prev + 1) % loginProviders.length);
510
+ setLoginMethodIndex((prev) => (prev + 1) % methods.length);
511
+ return;
512
+ }
513
+ if (key.escape) {
514
+ setLoginMethodPicker(null);
515
+ setLoginPicker(true); // go back to provider picker
509
516
  return;
510
517
  }
511
518
  if (key.return) {
512
- const selected = loginProviders[loginPickerIndex];
513
- setLoginPicker(false);
514
- if (selected.id === "openrouter") {
519
+ const method = methods[loginMethodIndex];
520
+ const providerId = loginMethodPicker.provider;
521
+ setLoginMethodPicker(null);
522
+ if (method === "oauth" && providerId === "openrouter") {
515
523
  addMsg("info", "Starting OpenRouter OAuth — opening browser...");
516
524
  setLoading(true);
517
525
  setSpinnerMsg("Waiting for authorization...");
518
526
  openRouterOAuth((msg) => addMsg("info", msg))
519
- .then((cred) => {
520
- addMsg("info", `✅ OpenRouter authenticated! You now have access to 200+ models.`);
521
- addMsg("info", `Switch with: /model anthropic/claude-sonnet-4`);
527
+ .then(() => {
528
+ addMsg("info", `✅ OpenRouter authenticated! Access to 200+ models.`);
522
529
  setLoading(false);
523
530
  })
524
- .catch((err) => {
525
- addMsg("error", `OAuth failed: ${err.message}`);
526
- setLoading(false);
527
- });
531
+ .catch((err) => { addMsg("error", `OAuth failed: ${err.message}`); setLoading(false); });
528
532
  }
529
- else if (selected.id === "anthropic") {
530
- addMsg("info", "Starting Anthropic setup-token flow...");
533
+ else if (method === "setup-token") {
534
+ addMsg("info", "Starting setup-token flow — browser will open...");
531
535
  setLoading(true);
532
536
  setSpinnerMsg("Waiting for Claude Code auth...");
533
537
  anthropicSetupToken((msg) => addMsg("info", msg))
534
- .then((cred) => {
535
- addMsg("info", `✅ Anthropic authenticated! (${cred.label})`);
536
- setLoading(false);
537
- })
538
- .catch((err) => {
539
- addMsg("error", `Anthropic auth failed: ${err.message}`);
540
- setLoading(false);
541
- });
538
+ .then((cred) => { addMsg("info", `✅ Anthropic authenticated! (${cred.label})`); setLoading(false); })
539
+ .catch((err) => { addMsg("error", `Auth failed: ${err.message}`); setLoading(false); });
542
540
  }
543
- else if (selected.id === "openai") {
541
+ else if (method === "cached-token" && providerId === "openai") {
544
542
  const imported = importCodexToken((msg) => addMsg("info", msg));
545
543
  if (imported) {
546
544
  addMsg("info", `✅ Imported Codex credentials! (${imported.label})`);
547
545
  }
548
546
  else {
549
- addMsg("info", "No Codex CLI found. Run: codemaxxing auth api-key openai <your-key>");
547
+ addMsg("info", "No Codex CLI found. Install Codex CLI and sign in first.");
550
548
  }
551
549
  }
552
- else if (selected.id === "qwen") {
550
+ else if (method === "cached-token" && providerId === "qwen") {
553
551
  const imported = importQwenToken((msg) => addMsg("info", msg));
554
552
  if (imported) {
555
553
  addMsg("info", `✅ Imported Qwen credentials! (${imported.label})`);
556
554
  }
557
555
  else {
558
- addMsg("info", "No Qwen CLI found. Run: codemaxxing auth api-key qwen <your-key>");
556
+ addMsg("info", "No Qwen CLI found. Install Qwen CLI and sign in first.");
559
557
  }
560
558
  }
561
- else if (selected.id === "copilot") {
559
+ else if (method === "device-flow") {
562
560
  addMsg("info", "Starting GitHub Copilot device flow...");
563
561
  setLoading(true);
564
562
  setSpinnerMsg("Waiting for GitHub authorization...");
565
563
  copilotDeviceFlow((msg) => addMsg("info", msg))
566
- .then(() => {
567
- addMsg("info", `✅ GitHub Copilot authenticated!`);
568
- setLoading(false);
569
- })
570
- .catch((err) => {
571
- addMsg("error", `Copilot auth failed: ${err.message}`);
572
- setLoading(false);
573
- });
564
+ .then(() => { addMsg("info", `✅ GitHub Copilot authenticated!`); setLoading(false); })
565
+ .catch((err) => { addMsg("error", `Copilot auth failed: ${err.message}`); setLoading(false); });
566
+ }
567
+ else if (method === "api-key") {
568
+ const provider = PROVIDERS.find((p) => p.id === providerId);
569
+ addMsg("info", `Enter your API key via CLI:\n codemaxxing auth api-key ${providerId} <your-key>\n Get key at: ${provider?.consoleUrl ?? "your provider's dashboard"}`);
570
+ }
571
+ return;
572
+ }
573
+ return;
574
+ }
575
+ // Login picker navigation (first level — pick provider)
576
+ if (loginPicker) {
577
+ const loginProviders = PROVIDERS.filter((p) => p.id !== "local");
578
+ if (key.upArrow) {
579
+ setLoginPickerIndex((prev) => (prev - 1 + loginProviders.length) % loginProviders.length);
580
+ return;
581
+ }
582
+ if (key.downArrow) {
583
+ setLoginPickerIndex((prev) => (prev + 1) % loginProviders.length);
584
+ return;
585
+ }
586
+ if (key.return) {
587
+ const selected = loginProviders[loginPickerIndex];
588
+ setLoginPicker(false);
589
+ // Get available methods for this provider (filter out 'none')
590
+ const methods = selected.methods.filter((m) => m !== "none");
591
+ if (methods.length === 1) {
592
+ // Only one method — execute it directly
593
+ setLoginMethodPicker({ provider: selected.id, methods });
594
+ setLoginMethodIndex(0);
595
+ // Simulate Enter press on the single method
596
+ if (methods[0] === "oauth" && selected.id === "openrouter") {
597
+ setLoginMethodPicker(null);
598
+ addMsg("info", "Starting OpenRouter OAuth — opening browser...");
599
+ setLoading(true);
600
+ setSpinnerMsg("Waiting for authorization...");
601
+ openRouterOAuth((msg) => addMsg("info", msg))
602
+ .then(() => { addMsg("info", `✅ OpenRouter authenticated! Access to 200+ models.`); setLoading(false); })
603
+ .catch((err) => { addMsg("error", `OAuth failed: ${err.message}`); setLoading(false); });
604
+ }
605
+ else if (methods[0] === "device-flow") {
606
+ setLoginMethodPicker(null);
607
+ addMsg("info", "Starting GitHub Copilot device flow...");
608
+ setLoading(true);
609
+ setSpinnerMsg("Waiting for GitHub authorization...");
610
+ copilotDeviceFlow((msg) => addMsg("info", msg))
611
+ .then(() => { addMsg("info", `✅ GitHub Copilot authenticated!`); setLoading(false); })
612
+ .catch((err) => { addMsg("error", `Copilot auth failed: ${err.message}`); setLoading(false); });
613
+ }
614
+ else if (methods[0] === "api-key") {
615
+ setLoginMethodPicker(null);
616
+ addMsg("info", `Enter your API key via CLI:\n codemaxxing auth api-key ${selected.id} <your-key>\n Get key at: ${selected.consoleUrl ?? "your provider's dashboard"}`);
617
+ }
574
618
  }
575
619
  else {
576
- addMsg("info", `Run: codemaxxing auth api-key ${selected.id} <your-key>\n Get key at: ${selected.consoleUrl ?? selected.baseUrl}`);
620
+ // Multiple methods show submenu
621
+ setLoginMethodPicker({ provider: selected.id, methods });
622
+ setLoginMethodIndex(0);
577
623
  }
578
624
  return;
579
625
  }
@@ -738,7 +784,16 @@ function App() {
738
784
  default:
739
785
  return _jsx(Text, { children: msg.text }, msg.id);
740
786
  }
741
- }), loading && !approval && !streaming && _jsx(NeonSpinner, { message: spinnerMsg, colors: theme.colors }), streaming && !loading && _jsx(StreamingIndicator, { colors: theme.colors }), approval && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.warning, paddingX: 1, marginTop: 1, children: [_jsxs(Text, { bold: true, color: theme.colors.warning, children: ["\u26A0 Approve ", approval.tool, "?"] }), approval.tool === "write_file" && approval.args.path ? (_jsxs(Text, { color: theme.colors.muted, children: [" 📄 ", String(approval.args.path)] })) : null, approval.tool === "write_file" && approval.args.content ? (_jsxs(Text, { color: theme.colors.muted, children: [" ", String(approval.args.content).split("\n").length, " lines, ", String(approval.args.content).length, "B"] })) : null, approval.tool === "run_command" && approval.args.command ? (_jsxs(Text, { color: theme.colors.muted, children: [" $ ", String(approval.args.command)] })) : null, _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.success, bold: true, children: " [y]" }), _jsx(Text, { children: "es " }), _jsx(Text, { color: theme.colors.error, bold: true, children: "[n]" }), _jsx(Text, { children: "o " }), _jsx(Text, { color: theme.colors.primary, bold: true, children: "[a]" }), _jsx(Text, { children: "lways" })] })] })), themePicker && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "Choose a theme:" }), listThemes().map((key, i) => (_jsxs(Text, { children: [i === themePickerIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === themePickerIndex ? theme.colors.suggestion : theme.colors.primary, bold: true, children: key }), _jsxs(Text, { color: theme.colors.muted, children: [" — ", THEMES[key].description] }), key === theme.name.toLowerCase() ? _jsx(Text, { color: theme.colors.muted, children: " (current)" }) : null] }, key))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter select · Esc cancel" })] })), sessionPicker && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.secondary, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "Resume a session:" }), sessionPicker.map((s, i) => (_jsxs(Text, { children: [i === sessionPickerIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: " " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === sessionPickerIndex ? theme.colors.suggestion : theme.colors.muted, children: s.display })] }, s.id))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter select · Esc cancel" })] })), showSuggestions && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.muted, paddingX: 1, marginBottom: 0, children: [cmdMatches.slice(0, 6).map((c, i) => (_jsxs(Text, { children: [i === cmdIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === cmdIndex ? theme.colors.suggestion : theme.colors.primary, bold: true, children: c.cmd }), _jsxs(Text, { color: theme.colors.muted, children: [" ", c.desc] })] }, i))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Tab select" })] })), _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 ? (_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(v); setCmdIndex(0); }, onSubmit: handleSubmit }, inputKey)] })) : (_jsx(Text, { dimColor: true, children: loading ? "waiting for response..." : "initializing..." }))] }), agent && (_jsx(Box, { paddingX: 2, children: _jsxs(Text, { dimColor: true, children: ["💬 ", agent.getContextLength(), " messages · ~", (() => {
787
+ }), loading && !approval && !streaming && _jsx(NeonSpinner, { message: spinnerMsg, colors: theme.colors }), streaming && !loading && _jsx(StreamingIndicator, { colors: theme.colors }), approval && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.warning, paddingX: 1, marginTop: 1, children: [_jsxs(Text, { bold: true, color: theme.colors.warning, children: ["\u26A0 Approve ", approval.tool, "?"] }), approval.tool === "write_file" && approval.args.path ? (_jsxs(Text, { color: theme.colors.muted, children: [" 📄 ", String(approval.args.path)] })) : null, approval.tool === "write_file" && approval.args.content ? (_jsxs(Text, { color: theme.colors.muted, children: [" ", String(approval.args.content).split("\n").length, " lines, ", String(approval.args.content).length, "B"] })) : null, approval.tool === "run_command" && approval.args.command ? (_jsxs(Text, { color: theme.colors.muted, children: [" $ ", String(approval.args.command)] })) : null, _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.success, bold: true, children: " [y]" }), _jsx(Text, { children: "es " }), _jsx(Text, { color: theme.colors.error, bold: true, children: "[n]" }), _jsx(Text, { children: "o " }), _jsx(Text, { color: theme.colors.primary, bold: true, children: "[a]" }), _jsx(Text, { children: "lways" })] })] })), loginPicker && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.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: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === loginPickerIndex ? theme.colors.suggestion : theme.colors.primary, bold: true, children: p.name }), _jsxs(Text, { color: theme.colors.muted, children: [" — ", p.description] }), getCredentials().some((c) => c.provider === p.id) ? _jsx(Text, { color: theme.colors.success, children: " \u2713" }) : null] }, p.id))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter select · Esc cancel" })] })), loginMethodPicker && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "How do you want to authenticate?" }), loginMethodPicker.methods.map((method, i) => {
788
+ const labels = {
789
+ "oauth": "🌐 Browser login (OAuth)",
790
+ "setup-token": "🔑 Link subscription (via Claude Code CLI)",
791
+ "cached-token": "📦 Import from existing CLI",
792
+ "api-key": "🔒 Enter API key manually",
793
+ "device-flow": "📱 Device flow (GitHub)",
794
+ };
795
+ return (_jsxs(Text, { children: [i === loginMethodIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === loginMethodIndex ? theme.colors.suggestion : theme.colors.primary, bold: true, children: labels[method] ?? method })] }, method));
796
+ }), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter select · Esc back" })] })), themePicker && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "Choose a theme:" }), listThemes().map((key, i) => (_jsxs(Text, { children: [i === themePickerIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === themePickerIndex ? theme.colors.suggestion : theme.colors.primary, bold: true, children: key }), _jsxs(Text, { color: theme.colors.muted, children: [" — ", THEMES[key].description] }), key === theme.name.toLowerCase() ? _jsx(Text, { color: theme.colors.muted, children: " (current)" }) : null] }, key))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter select · Esc cancel" })] })), sessionPicker && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.secondary, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "Resume a session:" }), sessionPicker.map((s, i) => (_jsxs(Text, { children: [i === sessionPickerIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === sessionPickerIndex ? theme.colors.suggestion : theme.colors.muted, children: s.display })] }, s.id))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter select · Esc cancel" })] })), showSuggestions && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.muted, paddingX: 1, marginBottom: 0, children: [cmdMatches.slice(0, 6).map((c, i) => (_jsxs(Text, { children: [i === cmdIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === cmdIndex ? theme.colors.suggestion : theme.colors.primary, bold: true, children: c.cmd }), _jsxs(Text, { color: theme.colors.muted, children: [" — ", c.desc] })] }, i))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Tab select" })] })), _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 ? (_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(v); setCmdIndex(0); }, onSubmit: handleSubmit }, inputKey)] })) : (_jsx(Text, { dimColor: true, children: loading ? "waiting for response..." : "initializing..." }))] }), agent && (_jsx(Box, { paddingX: 2, children: _jsxs(Text, { dimColor: true, children: ["💬 ", agent.getContextLength(), " messages · ~", (() => {
742
797
  const tokens = agent.estimateTokens();
743
798
  return tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}k` : String(tokens);
744
799
  })(), " tokens", modelName ? ` · 🤖 ${modelName}` : ""] }) }))] }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codemaxxing",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Open-source terminal coding agent. Connect any LLM. Max your code.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/src/index.tsx CHANGED
@@ -137,6 +137,8 @@ function App() {
137
137
  const [themePickerIndex, setThemePickerIndex] = useState(0);
138
138
  const [loginPicker, setLoginPicker] = useState(false);
139
139
  const [loginPickerIndex, setLoginPickerIndex] = useState(0);
140
+ const [loginMethodPicker, setLoginMethodPicker] = useState<{ provider: string; methods: string[] } | null>(null);
141
+ const [loginMethodIndex, setLoginMethodIndex] = useState(0);
140
142
  const [approval, setApproval] = useState<{
141
143
  tool: string;
142
144
  args: Record<string, unknown>;
@@ -543,77 +545,115 @@ function App() {
543
545
  }
544
546
  }
545
547
 
546
- // Login picker navigation
547
- if (loginPicker) {
548
- const loginProviders = PROVIDERS.filter((p) => p.id !== "local");
548
+ // Login method picker navigation (second level — pick auth method)
549
+ if (loginMethodPicker) {
550
+ const methods = loginMethodPicker.methods;
549
551
  if (key.upArrow) {
550
- setLoginPickerIndex((prev: number) => (prev - 1 + loginProviders.length) % loginProviders.length);
552
+ setLoginMethodIndex((prev: number) => (prev - 1 + methods.length) % methods.length);
551
553
  return;
552
554
  }
553
555
  if (key.downArrow) {
554
- setLoginPickerIndex((prev: number) => (prev + 1) % loginProviders.length);
556
+ setLoginMethodIndex((prev: number) => (prev + 1) % methods.length);
557
+ return;
558
+ }
559
+ if (key.escape) {
560
+ setLoginMethodPicker(null);
561
+ setLoginPicker(true); // go back to provider picker
555
562
  return;
556
563
  }
557
564
  if (key.return) {
558
- const selected = loginProviders[loginPickerIndex];
559
- setLoginPicker(false);
565
+ const method = methods[loginMethodIndex];
566
+ const providerId = loginMethodPicker.provider;
567
+ setLoginMethodPicker(null);
560
568
 
561
- if (selected.id === "openrouter") {
569
+ if (method === "oauth" && providerId === "openrouter") {
562
570
  addMsg("info", "Starting OpenRouter OAuth — opening browser...");
563
571
  setLoading(true);
564
572
  setSpinnerMsg("Waiting for authorization...");
565
573
  openRouterOAuth((msg: string) => addMsg("info", msg))
566
- .then((cred) => {
567
- addMsg("info", `✅ OpenRouter authenticated! You now have access to 200+ models.`);
568
- addMsg("info", `Switch with: /model anthropic/claude-sonnet-4`);
574
+ .then(() => {
575
+ addMsg("info", `✅ OpenRouter authenticated! Access to 200+ models.`);
569
576
  setLoading(false);
570
577
  })
571
- .catch((err: any) => {
572
- addMsg("error", `OAuth failed: ${err.message}`);
573
- setLoading(false);
574
- });
575
- } else if (selected.id === "anthropic") {
576
- addMsg("info", "Starting Anthropic setup-token flow...");
578
+ .catch((err: any) => { addMsg("error", `OAuth failed: ${err.message}`); setLoading(false); });
579
+ } else if (method === "setup-token") {
580
+ addMsg("info", "Starting setup-token flow — browser will open...");
577
581
  setLoading(true);
578
582
  setSpinnerMsg("Waiting for Claude Code auth...");
579
583
  anthropicSetupToken((msg: string) => addMsg("info", msg))
580
- .then((cred) => {
581
- addMsg("info", `✅ Anthropic authenticated! (${cred.label})`);
582
- setLoading(false);
583
- })
584
- .catch((err: any) => {
585
- addMsg("error", `Anthropic auth failed: ${err.message}`);
586
- setLoading(false);
587
- });
588
- } else if (selected.id === "openai") {
584
+ .then((cred) => { addMsg("info", `✅ Anthropic authenticated! (${cred.label})`); setLoading(false); })
585
+ .catch((err: any) => { addMsg("error", `Auth failed: ${err.message}`); setLoading(false); });
586
+ } else if (method === "cached-token" && providerId === "openai") {
589
587
  const imported = importCodexToken((msg: string) => addMsg("info", msg));
590
- if (imported) {
591
- addMsg("info", `✅ Imported Codex credentials! (${imported.label})`);
592
- } else {
593
- addMsg("info", "No Codex CLI found. Run: codemaxxing auth api-key openai <your-key>");
594
- }
595
- } else if (selected.id === "qwen") {
588
+ if (imported) { addMsg("info", `✅ Imported Codex credentials! (${imported.label})`); }
589
+ else { addMsg("info", "No Codex CLI found. Install Codex CLI and sign in first."); }
590
+ } else if (method === "cached-token" && providerId === "qwen") {
596
591
  const imported = importQwenToken((msg: string) => addMsg("info", msg));
597
- if (imported) {
598
- addMsg("info", `✅ Imported Qwen credentials! (${imported.label})`);
599
- } else {
600
- addMsg("info", "No Qwen CLI found. Run: codemaxxing auth api-key qwen <your-key>");
601
- }
602
- } else if (selected.id === "copilot") {
592
+ if (imported) { addMsg("info", `✅ Imported Qwen credentials! (${imported.label})`); }
593
+ else { addMsg("info", "No Qwen CLI found. Install Qwen CLI and sign in first."); }
594
+ } else if (method === "device-flow") {
603
595
  addMsg("info", "Starting GitHub Copilot device flow...");
604
596
  setLoading(true);
605
597
  setSpinnerMsg("Waiting for GitHub authorization...");
606
598
  copilotDeviceFlow((msg: string) => addMsg("info", msg))
607
- .then(() => {
608
- addMsg("info", `✅ GitHub Copilot authenticated!`);
609
- setLoading(false);
610
- })
611
- .catch((err: any) => {
612
- addMsg("error", `Copilot auth failed: ${err.message}`);
613
- setLoading(false);
614
- });
599
+ .then(() => { addMsg("info", `✅ GitHub Copilot authenticated!`); setLoading(false); })
600
+ .catch((err: any) => { addMsg("error", `Copilot auth failed: ${err.message}`); setLoading(false); });
601
+ } else if (method === "api-key") {
602
+ const provider = PROVIDERS.find((p) => p.id === providerId);
603
+ addMsg("info", `Enter your API key via CLI:\n codemaxxing auth api-key ${providerId} <your-key>\n Get key at: ${provider?.consoleUrl ?? "your provider's dashboard"}`);
604
+ }
605
+ return;
606
+ }
607
+ return;
608
+ }
609
+
610
+ // Login picker navigation (first level — pick provider)
611
+ if (loginPicker) {
612
+ const loginProviders = PROVIDERS.filter((p) => p.id !== "local");
613
+ if (key.upArrow) {
614
+ setLoginPickerIndex((prev: number) => (prev - 1 + loginProviders.length) % loginProviders.length);
615
+ return;
616
+ }
617
+ if (key.downArrow) {
618
+ setLoginPickerIndex((prev: number) => (prev + 1) % loginProviders.length);
619
+ return;
620
+ }
621
+ if (key.return) {
622
+ const selected = loginProviders[loginPickerIndex];
623
+ setLoginPicker(false);
624
+
625
+ // Get available methods for this provider (filter out 'none')
626
+ const methods = selected.methods.filter((m) => m !== "none");
627
+
628
+ if (methods.length === 1) {
629
+ // Only one method — execute it directly
630
+ setLoginMethodPicker({ provider: selected.id, methods });
631
+ setLoginMethodIndex(0);
632
+ // Simulate Enter press on the single method
633
+ if (methods[0] === "oauth" && selected.id === "openrouter") {
634
+ setLoginMethodPicker(null);
635
+ addMsg("info", "Starting OpenRouter OAuth — opening browser...");
636
+ setLoading(true);
637
+ setSpinnerMsg("Waiting for authorization...");
638
+ openRouterOAuth((msg: string) => addMsg("info", msg))
639
+ .then(() => { addMsg("info", `✅ OpenRouter authenticated! Access to 200+ models.`); setLoading(false); })
640
+ .catch((err: any) => { addMsg("error", `OAuth failed: ${err.message}`); setLoading(false); });
641
+ } else if (methods[0] === "device-flow") {
642
+ setLoginMethodPicker(null);
643
+ addMsg("info", "Starting GitHub Copilot device flow...");
644
+ setLoading(true);
645
+ setSpinnerMsg("Waiting for GitHub authorization...");
646
+ copilotDeviceFlow((msg: string) => addMsg("info", msg))
647
+ .then(() => { addMsg("info", `✅ GitHub Copilot authenticated!`); setLoading(false); })
648
+ .catch((err: any) => { addMsg("error", `Copilot auth failed: ${err.message}`); setLoading(false); });
649
+ } else if (methods[0] === "api-key") {
650
+ setLoginMethodPicker(null);
651
+ addMsg("info", `Enter your API key via CLI:\n codemaxxing auth api-key ${selected.id} <your-key>\n Get key at: ${selected.consoleUrl ?? "your provider's dashboard"}`);
652
+ }
615
653
  } else {
616
- addMsg("info", `Run: codemaxxing auth api-key ${selected.id} <your-key>\n Get key at: ${selected.consoleUrl ?? selected.baseUrl}`);
654
+ // Multiple methods show submenu
655
+ setLoginMethodPicker({ provider: selected.id, methods });
656
+ setLoginMethodIndex(0);
617
657
  }
618
658
  return;
619
659
  }
@@ -855,6 +895,45 @@ function App() {
855
895
  </Box>
856
896
  )}
857
897
 
898
+ {/* ═══ LOGIN PICKER ═══ */}
899
+ {loginPicker && (
900
+ <Box flexDirection="column" borderStyle="single" borderColor={theme.colors.border} paddingX={1} marginBottom={0}>
901
+ <Text bold color={theme.colors.secondary}>💪 Choose a provider:</Text>
902
+ {PROVIDERS.filter((p) => p.id !== "local").map((p, i) => (
903
+ <Text key={p.id}>
904
+ {i === loginPickerIndex ? <Text color={theme.colors.suggestion} bold>{"▸ "}</Text> : <Text>{" "}</Text>}
905
+ <Text color={i === loginPickerIndex ? theme.colors.suggestion : theme.colors.primary} bold>{p.name}</Text>
906
+ <Text color={theme.colors.muted}>{" — "}{p.description}</Text>
907
+ {getCredentials().some((c) => c.provider === p.id) ? <Text color={theme.colors.success}> ✓</Text> : null}
908
+ </Text>
909
+ ))}
910
+ <Text dimColor>{" ↑↓ navigate · Enter select · Esc cancel"}</Text>
911
+ </Box>
912
+ )}
913
+
914
+ {/* ═══ LOGIN METHOD PICKER ═══ */}
915
+ {loginMethodPicker && (
916
+ <Box flexDirection="column" borderStyle="single" borderColor={theme.colors.border} paddingX={1} marginBottom={0}>
917
+ <Text bold color={theme.colors.secondary}>How do you want to authenticate?</Text>
918
+ {loginMethodPicker.methods.map((method, i) => {
919
+ const labels: Record<string, string> = {
920
+ "oauth": "🌐 Browser login (OAuth)",
921
+ "setup-token": "🔑 Link subscription (via Claude Code CLI)",
922
+ "cached-token": "📦 Import from existing CLI",
923
+ "api-key": "🔒 Enter API key manually",
924
+ "device-flow": "📱 Device flow (GitHub)",
925
+ };
926
+ return (
927
+ <Text key={method}>
928
+ {i === loginMethodIndex ? <Text color={theme.colors.suggestion} bold>{"▸ "}</Text> : <Text>{" "}</Text>}
929
+ <Text color={i === loginMethodIndex ? theme.colors.suggestion : theme.colors.primary} bold>{labels[method] ?? method}</Text>
930
+ </Text>
931
+ );
932
+ })}
933
+ <Text dimColor>{" ↑↓ navigate · Enter select · Esc back"}</Text>
934
+ </Box>
935
+ )}
936
+
858
937
  {/* ═══ THEME PICKER ═══ */}
859
938
  {themePicker && (
860
939
  <Box flexDirection="column" borderStyle="single" borderColor={theme.colors.border} paddingX={1} marginBottom={0}>