codemaxxing 0.3.1 → 0.4.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
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import React, { useState, useEffect, useCallback } from "react";
4
4
  import { render, Box, Text, useInput, useApp, useStdout } from "ink";
5
5
  import { EventEmitter } from "events";
6
6
  import TextInput from "ink-text-input";
7
7
  import { CodingAgent } from "./agent.js";
8
- import { loadConfig, detectLocalProvider, parseCLIArgs, applyOverrides, listModels } from "./config.js";
8
+ import { loadConfig, saveConfig, detectLocalProvider, parseCLIArgs, applyOverrides, listModels } from "./config.js";
9
9
  import { listSessions, getSession, loadMessages, deleteSession } from "./utils/sessions.js";
10
10
  import { execSync } from "child_process";
11
11
  import { isGitRepo, getBranch, getStatus, getDiff, undoLastCommit } from "./utils/git.js";
@@ -13,6 +13,9 @@ import { getTheme, listThemes, THEMES, DEFAULT_THEME } from "./themes.js";
13
13
  import { PROVIDERS, getCredentials, openRouterOAuth, anthropicSetupToken, importCodexToken, importQwenToken, copilotDeviceFlow } from "./utils/auth.js";
14
14
  import { listInstalledSkills, installSkill, removeSkill, getRegistrySkills, searchRegistry, createSkillScaffold, getActiveSkills, getActiveSkillCount } from "./utils/skills.js";
15
15
  import { listServers, addServer, removeServer, getConnectedServers } from "./utils/mcp.js";
16
+ import { detectHardware, formatBytes } from "./utils/hardware.js";
17
+ import { getRecommendationsWithLlmfit, getFitIcon, isLlmfitAvailable } from "./utils/models.js";
18
+ import { isOllamaInstalled, isOllamaRunning, getOllamaInstallCommand, startOllama, stopOllama, pullModel, listInstalledModelsDetailed, deleteModel, getGPUMemoryUsage } from "./utils/ollama.js";
16
19
  const VERSION = "0.1.9";
17
20
  // ── Helpers ──
18
21
  function formatTimeAgo(date) {
@@ -64,6 +67,12 @@ const SLASH_COMMANDS = [
64
67
  { cmd: "/mcp add", desc: "add MCP server" },
65
68
  { cmd: "/mcp remove", desc: "remove MCP server" },
66
69
  { cmd: "/mcp reconnect", desc: "reconnect MCP servers" },
70
+ { cmd: "/ollama", desc: "Ollama status & models" },
71
+ { cmd: "/ollama list", desc: "list installed models" },
72
+ { cmd: "/ollama start", desc: "start Ollama server" },
73
+ { cmd: "/ollama stop", desc: "stop Ollama server" },
74
+ { cmd: "/ollama pull", desc: "download a model" },
75
+ { cmd: "/ollama delete", desc: "delete a model" },
67
76
  { cmd: "/quit", desc: "exit" },
68
77
  ];
69
78
  const SPINNER_FRAMES = ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"];
@@ -137,6 +146,17 @@ function App() {
137
146
  const [skillsPickerIndex, setSkillsPickerIndex] = useState(0);
138
147
  const [sessionDisabledSkills, setSessionDisabledSkills] = useState(new Set());
139
148
  const [approval, setApproval] = useState(null);
149
+ // ── Ollama Management State ──
150
+ const [ollamaDeleteConfirm, setOllamaDeleteConfirm] = useState(null);
151
+ const [ollamaPulling, setOllamaPulling] = useState(null);
152
+ const [ollamaExitPrompt, setOllamaExitPrompt] = useState(false);
153
+ const [wizardScreen, setWizardScreen] = useState(null);
154
+ const [wizardIndex, setWizardIndex] = useState(0);
155
+ const [wizardHardware, setWizardHardware] = useState(null);
156
+ const [wizardModels, setWizardModels] = useState([]);
157
+ const [wizardPullProgress, setWizardPullProgress] = useState(null);
158
+ const [wizardPullError, setWizardPullError] = useState(null);
159
+ const [wizardSelectedModel, setWizardSelectedModel] = useState(null);
140
160
  // Listen for paste events from stdin interceptor
141
161
  useEffect(() => {
142
162
  const handler = ({ content, lines }) => {
@@ -174,10 +194,11 @@ function App() {
174
194
  }
175
195
  else {
176
196
  info.push("✗ No local LLM server found.");
177
- info.push(" /connect — retry after starting LM Studio or Ollama");
178
- info.push(" /login — authenticate with a cloud provider");
179
197
  setConnectionInfo([...info]);
180
198
  setReady(true);
199
+ // Show the setup wizard on first run
200
+ setWizardScreen("connection");
201
+ setWizardIndex(0);
181
202
  return;
182
203
  }
183
204
  }
@@ -318,7 +339,8 @@ function App() {
318
339
  // Commands that need args (like /commit, /model) — fill input instead of executing
319
340
  if (selected.cmd === "/commit" || selected.cmd === "/model" || selected.cmd === "/session delete" ||
320
341
  selected.cmd === "/skills install" || selected.cmd === "/skills remove" || selected.cmd === "/skills search" ||
321
- selected.cmd === "/skills on" || selected.cmd === "/skills off" || selected.cmd === "/architect") {
342
+ selected.cmd === "/skills on" || selected.cmd === "/skills off" || selected.cmd === "/architect" ||
343
+ selected.cmd === "/ollama pull" || selected.cmd === "/ollama delete") {
322
344
  setInput(selected.cmd + " ");
323
345
  setCmdIndex(0);
324
346
  setInputKey((k) => k + 1);
@@ -344,7 +366,22 @@ function App() {
344
366
  return;
345
367
  addMsg("user", trimmed);
346
368
  if (trimmed === "/quit" || trimmed === "/exit") {
347
- exit();
369
+ // Check if Ollama is running and offer to stop it
370
+ const ollamaUp = await isOllamaRunning();
371
+ if (ollamaUp) {
372
+ const config = loadConfig();
373
+ if (config.defaults.stopOllamaOnExit) {
374
+ addMsg("info", "Stopping Ollama...");
375
+ await stopOllama();
376
+ exit();
377
+ }
378
+ else {
379
+ setOllamaExitPrompt(true);
380
+ }
381
+ }
382
+ else {
383
+ exit();
384
+ }
348
385
  return;
349
386
  }
350
387
  if (trimmed === "/login" || trimmed === "/auth") {
@@ -387,6 +424,12 @@ function App() {
387
424
  " /mcp add — add MCP server to global config",
388
425
  " /mcp remove — remove MCP server",
389
426
  " /mcp reconnect — reconnect all MCP servers",
427
+ " /ollama — Ollama status, models & GPU usage",
428
+ " /ollama list — list installed models with sizes",
429
+ " /ollama start — start Ollama server",
430
+ " /ollama stop — stop Ollama server (frees GPU RAM)",
431
+ " /ollama pull <model> — download a model",
432
+ " /ollama delete <model> — delete a model from disk",
390
433
  " /quit — exit",
391
434
  ].join("\n"));
392
435
  return;
@@ -554,6 +597,142 @@ function App() {
554
597
  addMsg("info", "🔍 Auto-lint OFF");
555
598
  return;
556
599
  }
600
+ // ── Ollama commands (work without agent) ──
601
+ if (trimmed === "/ollama" || trimmed === "/ollama status") {
602
+ const running = await isOllamaRunning();
603
+ const lines = [`Ollama: ${running ? "running" : "stopped"}`];
604
+ if (running) {
605
+ const models = await listInstalledModelsDetailed();
606
+ if (models.length > 0) {
607
+ lines.push(`Installed models (${models.length}):`);
608
+ for (const m of models) {
609
+ const sizeGB = (m.size / (1024 * 1024 * 1024)).toFixed(1);
610
+ lines.push(` ${m.name} (${sizeGB} GB)`);
611
+ }
612
+ }
613
+ else {
614
+ lines.push("No models installed.");
615
+ }
616
+ const gpuMem = getGPUMemoryUsage();
617
+ if (gpuMem)
618
+ lines.push(`GPU: ${gpuMem}`);
619
+ }
620
+ else {
621
+ lines.push("Start with: /ollama start");
622
+ }
623
+ addMsg("info", lines.join("\n"));
624
+ return;
625
+ }
626
+ if (trimmed === "/ollama list") {
627
+ const running = await isOllamaRunning();
628
+ if (!running) {
629
+ addMsg("info", "Ollama is not running. Start with /ollama start");
630
+ return;
631
+ }
632
+ const models = await listInstalledModelsDetailed();
633
+ if (models.length === 0) {
634
+ addMsg("info", "No models installed. Pull one with /ollama pull <model>");
635
+ }
636
+ else {
637
+ const lines = models.map((m) => {
638
+ const sizeGB = (m.size / (1024 * 1024 * 1024)).toFixed(1);
639
+ return ` ${m.name} (${sizeGB} GB)`;
640
+ });
641
+ addMsg("info", `Installed models:\n${lines.join("\n")}`);
642
+ }
643
+ return;
644
+ }
645
+ if (trimmed === "/ollama start") {
646
+ const running = await isOllamaRunning();
647
+ if (running) {
648
+ addMsg("info", "Ollama is already running.");
649
+ return;
650
+ }
651
+ if (!isOllamaInstalled()) {
652
+ addMsg("error", `Ollama is not installed. Install with: ${getOllamaInstallCommand(process.platform === "darwin" ? "macos" : process.platform === "win32" ? "windows" : "linux")}`);
653
+ return;
654
+ }
655
+ startOllama();
656
+ addMsg("info", "Starting Ollama server...");
657
+ // Wait for it to come up
658
+ for (let i = 0; i < 10; i++) {
659
+ await new Promise(r => setTimeout(r, 1000));
660
+ if (await isOllamaRunning()) {
661
+ addMsg("info", "Ollama is running.");
662
+ return;
663
+ }
664
+ }
665
+ addMsg("error", "Ollama did not start in time. Try running 'ollama serve' manually.");
666
+ return;
667
+ }
668
+ if (trimmed === "/ollama stop") {
669
+ const running = await isOllamaRunning();
670
+ if (!running) {
671
+ addMsg("info", "Ollama is not running.");
672
+ return;
673
+ }
674
+ addMsg("info", "Stopping Ollama...");
675
+ const result = await stopOllama();
676
+ addMsg(result.ok ? "info" : "error", result.ok ? `\u2705 ${result.message}` : `\u274C ${result.message}`);
677
+ return;
678
+ }
679
+ if (trimmed.startsWith("/ollama pull ")) {
680
+ const modelId = trimmed.replace("/ollama pull ", "").trim();
681
+ if (!modelId) {
682
+ addMsg("info", "Usage: /ollama pull <model>\n Example: /ollama pull qwen2.5-coder:7b");
683
+ return;
684
+ }
685
+ if (!isOllamaInstalled()) {
686
+ addMsg("error", "Ollama is not installed.");
687
+ return;
688
+ }
689
+ // Ensure ollama is running
690
+ let running = await isOllamaRunning();
691
+ if (!running) {
692
+ startOllama();
693
+ addMsg("info", "Starting Ollama server...");
694
+ for (let i = 0; i < 10; i++) {
695
+ await new Promise(r => setTimeout(r, 1000));
696
+ if (await isOllamaRunning()) {
697
+ running = true;
698
+ break;
699
+ }
700
+ }
701
+ if (!running) {
702
+ addMsg("error", "Could not start Ollama. Run 'ollama serve' manually.");
703
+ return;
704
+ }
705
+ }
706
+ setOllamaPulling({ model: modelId, progress: { status: "starting", percent: 0 } });
707
+ try {
708
+ await pullModel(modelId, (p) => {
709
+ setOllamaPulling({ model: modelId, progress: p });
710
+ });
711
+ setOllamaPulling(null);
712
+ addMsg("info", `\u2705 Downloaded ${modelId}`);
713
+ }
714
+ catch (err) {
715
+ setOllamaPulling(null);
716
+ addMsg("error", `Failed to pull ${modelId}: ${err.message}`);
717
+ }
718
+ return;
719
+ }
720
+ if (trimmed.startsWith("/ollama delete ")) {
721
+ const modelId = trimmed.replace("/ollama delete ", "").trim();
722
+ if (!modelId) {
723
+ addMsg("info", "Usage: /ollama delete <model>");
724
+ return;
725
+ }
726
+ // Look up size for confirmation
727
+ const models = await listInstalledModelsDetailed();
728
+ const found = models.find((m) => m.name === modelId || m.name.startsWith(modelId));
729
+ if (!found) {
730
+ addMsg("error", `Model "${modelId}" not found. Use /ollama list to see installed models.`);
731
+ return;
732
+ }
733
+ setOllamaDeleteConfirm({ model: found.name, size: found.size });
734
+ return;
735
+ }
557
736
  // ── MCP commands (partially work without agent) ──
558
737
  if (trimmed === "/mcp" || trimmed === "/mcp list") {
559
738
  const servers = listServers(process.cwd());
@@ -1114,6 +1293,264 @@ function App() {
1114
1293
  }
1115
1294
  return;
1116
1295
  }
1296
+ // ── Ollama delete confirmation ──
1297
+ if (ollamaDeleteConfirm) {
1298
+ if (inputChar === "y" || inputChar === "Y") {
1299
+ const model = ollamaDeleteConfirm.model;
1300
+ setOllamaDeleteConfirm(null);
1301
+ const result = deleteModel(model);
1302
+ addMsg(result.ok ? "info" : "error", result.ok ? `\u2705 ${result.message}` : `\u274C ${result.message}`);
1303
+ return;
1304
+ }
1305
+ if (inputChar === "n" || inputChar === "N" || key.escape) {
1306
+ setOllamaDeleteConfirm(null);
1307
+ addMsg("info", "Delete cancelled.");
1308
+ return;
1309
+ }
1310
+ return;
1311
+ }
1312
+ // ── Ollama exit prompt ──
1313
+ if (ollamaExitPrompt) {
1314
+ if (inputChar === "y" || inputChar === "Y") {
1315
+ setOllamaExitPrompt(false);
1316
+ stopOllama().then(() => exit());
1317
+ return;
1318
+ }
1319
+ if (inputChar === "n" || inputChar === "N") {
1320
+ setOllamaExitPrompt(false);
1321
+ exit();
1322
+ return;
1323
+ }
1324
+ if (inputChar === "a" || inputChar === "A") {
1325
+ setOllamaExitPrompt(false);
1326
+ saveConfig({ defaults: { ...loadConfig().defaults, stopOllamaOnExit: true } });
1327
+ addMsg("info", "Saved preference: always stop Ollama on exit.");
1328
+ stopOllama().then(() => exit());
1329
+ return;
1330
+ }
1331
+ if (key.escape) {
1332
+ setOllamaExitPrompt(false);
1333
+ return;
1334
+ }
1335
+ return;
1336
+ }
1337
+ // ── Setup Wizard Navigation ──
1338
+ if (wizardScreen) {
1339
+ if (wizardScreen === "connection") {
1340
+ const items = ["local", "openrouter", "apikey", "existing"];
1341
+ if (key.upArrow) {
1342
+ setWizardIndex((prev) => (prev - 1 + items.length) % items.length);
1343
+ return;
1344
+ }
1345
+ if (key.downArrow) {
1346
+ setWizardIndex((prev) => (prev + 1) % items.length);
1347
+ return;
1348
+ }
1349
+ if (key.escape) {
1350
+ setWizardScreen(null);
1351
+ return;
1352
+ }
1353
+ if (key.return) {
1354
+ const selected = items[wizardIndex];
1355
+ if (selected === "local") {
1356
+ // Scan hardware and show model picker (use llmfit if available)
1357
+ const hw = detectHardware();
1358
+ setWizardHardware(hw);
1359
+ const { models: recs } = getRecommendationsWithLlmfit(hw);
1360
+ setWizardModels(recs.filter(m => m.fit !== "skip"));
1361
+ setWizardScreen("models");
1362
+ setWizardIndex(0);
1363
+ }
1364
+ else if (selected === "openrouter") {
1365
+ setWizardScreen(null);
1366
+ addMsg("info", "Starting OpenRouter OAuth — opening browser...");
1367
+ setLoading(true);
1368
+ setSpinnerMsg("Waiting for authorization...");
1369
+ openRouterOAuth((msg) => addMsg("info", msg))
1370
+ .then(() => {
1371
+ addMsg("info", "✅ OpenRouter authenticated! Use /connect to connect.");
1372
+ setLoading(false);
1373
+ })
1374
+ .catch((err) => { addMsg("error", `OAuth failed: ${err.message}`); setLoading(false); });
1375
+ }
1376
+ else if (selected === "apikey") {
1377
+ setWizardScreen(null);
1378
+ setLoginPicker(true);
1379
+ setLoginPickerIndex(0);
1380
+ }
1381
+ else if (selected === "existing") {
1382
+ setWizardScreen(null);
1383
+ addMsg("info", "Start your LLM server, then type /connect to retry.");
1384
+ }
1385
+ return;
1386
+ }
1387
+ return;
1388
+ }
1389
+ if (wizardScreen === "models") {
1390
+ const models = wizardModels;
1391
+ if (key.upArrow) {
1392
+ setWizardIndex((prev) => (prev - 1 + models.length) % models.length);
1393
+ return;
1394
+ }
1395
+ if (key.downArrow) {
1396
+ setWizardIndex((prev) => (prev + 1) % models.length);
1397
+ return;
1398
+ }
1399
+ if (key.escape) {
1400
+ setWizardScreen("connection");
1401
+ setWizardIndex(0);
1402
+ return;
1403
+ }
1404
+ if (key.return) {
1405
+ const selected = models[wizardIndex];
1406
+ if (selected) {
1407
+ setWizardSelectedModel(selected);
1408
+ // Check if Ollama is installed
1409
+ if (!isOllamaInstalled()) {
1410
+ setWizardScreen("install-ollama");
1411
+ }
1412
+ else {
1413
+ // Start pulling the model
1414
+ setWizardScreen("pulling");
1415
+ setWizardPullProgress({ status: "starting", percent: 0 });
1416
+ setWizardPullError(null);
1417
+ (async () => {
1418
+ try {
1419
+ // Ensure ollama is running
1420
+ const running = await isOllamaRunning();
1421
+ if (!running) {
1422
+ setWizardPullProgress({ status: "Starting Ollama server...", percent: 0 });
1423
+ startOllama();
1424
+ // Wait for it to come up
1425
+ for (let i = 0; i < 15; i++) {
1426
+ await new Promise(r => setTimeout(r, 1000));
1427
+ if (await isOllamaRunning())
1428
+ break;
1429
+ }
1430
+ if (!(await isOllamaRunning())) {
1431
+ setWizardPullError("Could not start Ollama server. Run 'ollama serve' manually, then press Enter.");
1432
+ return;
1433
+ }
1434
+ }
1435
+ await pullModel(selected.ollamaId, (p) => {
1436
+ setWizardPullProgress(p);
1437
+ });
1438
+ setWizardPullProgress({ status: "success", percent: 100 });
1439
+ // Wait briefly then connect
1440
+ await new Promise(r => setTimeout(r, 500));
1441
+ setWizardScreen(null);
1442
+ setWizardPullProgress(null);
1443
+ setWizardSelectedModel(null);
1444
+ addMsg("info", `✅ ${selected.name} installed! Connecting...`);
1445
+ await connectToProvider(true);
1446
+ }
1447
+ catch (err) {
1448
+ setWizardPullError(err.message);
1449
+ }
1450
+ })();
1451
+ }
1452
+ }
1453
+ return;
1454
+ }
1455
+ return;
1456
+ }
1457
+ if (wizardScreen === "install-ollama") {
1458
+ if (key.escape) {
1459
+ setWizardScreen("models");
1460
+ setWizardIndex(0);
1461
+ return;
1462
+ }
1463
+ if (key.return) {
1464
+ // User says they installed it — check and proceed
1465
+ if (isOllamaInstalled()) {
1466
+ const selected = wizardSelectedModel;
1467
+ if (selected) {
1468
+ setWizardScreen("pulling");
1469
+ setWizardPullProgress({ status: "starting", percent: 0 });
1470
+ setWizardPullError(null);
1471
+ (async () => {
1472
+ try {
1473
+ const running = await isOllamaRunning();
1474
+ if (!running) {
1475
+ setWizardPullProgress({ status: "Starting Ollama server...", percent: 0 });
1476
+ startOllama();
1477
+ for (let i = 0; i < 15; i++) {
1478
+ await new Promise(r => setTimeout(r, 1000));
1479
+ if (await isOllamaRunning())
1480
+ break;
1481
+ }
1482
+ if (!(await isOllamaRunning())) {
1483
+ setWizardPullError("Could not start Ollama server. Run 'ollama serve' manually, then press Enter.");
1484
+ return;
1485
+ }
1486
+ }
1487
+ await pullModel(selected.ollamaId, (p) => setWizardPullProgress(p));
1488
+ setWizardPullProgress({ status: "success", percent: 100 });
1489
+ await new Promise(r => setTimeout(r, 500));
1490
+ setWizardScreen(null);
1491
+ setWizardPullProgress(null);
1492
+ setWizardSelectedModel(null);
1493
+ addMsg("info", `✅ ${selected.name} installed! Connecting...`);
1494
+ await connectToProvider(true);
1495
+ }
1496
+ catch (err) {
1497
+ setWizardPullError(err.message);
1498
+ }
1499
+ })();
1500
+ }
1501
+ }
1502
+ else {
1503
+ addMsg("info", "Ollama not found yet. Install it and press Enter again.");
1504
+ }
1505
+ return;
1506
+ }
1507
+ return;
1508
+ }
1509
+ if (wizardScreen === "pulling") {
1510
+ // Allow retry on error
1511
+ if (wizardPullError && key.return) {
1512
+ const selected = wizardSelectedModel;
1513
+ if (selected) {
1514
+ setWizardPullError(null);
1515
+ setWizardPullProgress({ status: "retrying", percent: 0 });
1516
+ (async () => {
1517
+ try {
1518
+ const running = await isOllamaRunning();
1519
+ if (!running) {
1520
+ startOllama();
1521
+ for (let i = 0; i < 15; i++) {
1522
+ await new Promise(r => setTimeout(r, 1000));
1523
+ if (await isOllamaRunning())
1524
+ break;
1525
+ }
1526
+ }
1527
+ await pullModel(selected.ollamaId, (p) => setWizardPullProgress(p));
1528
+ setWizardPullProgress({ status: "success", percent: 100 });
1529
+ await new Promise(r => setTimeout(r, 500));
1530
+ setWizardScreen(null);
1531
+ setWizardPullProgress(null);
1532
+ setWizardSelectedModel(null);
1533
+ addMsg("info", `✅ ${selected.name} installed! Connecting...`);
1534
+ await connectToProvider(true);
1535
+ }
1536
+ catch (err) {
1537
+ setWizardPullError(err.message);
1538
+ }
1539
+ })();
1540
+ }
1541
+ return;
1542
+ }
1543
+ if (wizardPullError && key.escape) {
1544
+ setWizardScreen("models");
1545
+ setWizardIndex(0);
1546
+ setWizardPullError(null);
1547
+ setWizardPullProgress(null);
1548
+ return;
1549
+ }
1550
+ return; // Ignore keys while pulling
1551
+ }
1552
+ return;
1553
+ }
1117
1554
  // Theme picker navigation
1118
1555
  if (themePicker) {
1119
1556
  const themeKeys = listThemes();
@@ -1266,7 +1703,14 @@ function App() {
1266
1703
  }
1267
1704
  if (key.ctrl && inputChar === "c") {
1268
1705
  if (ctrlCPressed) {
1269
- exit();
1706
+ // Force quit on second Ctrl+C — don't block
1707
+ const config = loadConfig();
1708
+ if (config.defaults.stopOllamaOnExit) {
1709
+ stopOllama().finally(() => exit());
1710
+ }
1711
+ else {
1712
+ exit();
1713
+ }
1270
1714
  }
1271
1715
  else {
1272
1716
  setCtrlCPressed(true);
@@ -1344,7 +1788,12 @@ function App() {
1344
1788
  })(), skillsPicker === "remove" && (() => {
1345
1789
  const installed = listInstalledSkills();
1346
1790
  return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.error, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.error, children: "Remove a skill:" }), installed.map((s, i) => (_jsxs(Text, { children: [i === skillsPickerIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsxs(Text, { color: i === skillsPickerIndex ? theme.colors.suggestion : theme.colors.muted, children: [s.name, " \u2014 ", s.description] })] }, s.name))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter remove · Esc back" })] }));
1347
- })(), 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" })] })), deleteSessionPicker && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.error, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.error, children: "Delete a session:" }), deleteSessionPicker.map((s, i) => (_jsxs(Text, { children: [i === deleteSessionPickerIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === deleteSessionPickerIndex ? theme.colors.suggestion : theme.colors.muted, children: s.display })] }, s.id))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter select · Esc cancel" })] })), deleteSessionConfirm && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.warning, paddingX: 1, marginBottom: 0, children: [_jsxs(Text, { bold: true, color: theme.colors.warning, children: ["Delete session ", deleteSessionConfirm.id, "?"] }), _jsxs(Text, { color: theme.colors.muted, children: [" ", deleteSessionConfirm.display] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.error, bold: true, children: " [y]" }), _jsx(Text, { children: "es " }), _jsx(Text, { color: theme.colors.success, bold: true, children: "[n]" }), _jsx(Text, { children: "o" })] })] })), 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 · ~", (() => {
1791
+ })(), 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" })] })), deleteSessionPicker && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.error, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.error, children: "Delete a session:" }), deleteSessionPicker.map((s, i) => (_jsxs(Text, { children: [i === deleteSessionPickerIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: "▸ " }) : _jsx(Text, { children: " " }), _jsx(Text, { color: i === deleteSessionPickerIndex ? theme.colors.suggestion : theme.colors.muted, children: s.display })] }, s.id))), _jsx(Text, { dimColor: true, children: " ↑↓ navigate · Enter select · Esc cancel" })] })), deleteSessionConfirm && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.warning, paddingX: 1, marginBottom: 0, children: [_jsxs(Text, { bold: true, color: theme.colors.warning, children: ["Delete session ", deleteSessionConfirm.id, "?"] }), _jsxs(Text, { color: theme.colors.muted, children: [" ", deleteSessionConfirm.display] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.error, bold: true, children: " [y]" }), _jsx(Text, { children: "es " }), _jsx(Text, { color: theme.colors.success, bold: true, children: "[n]" }), _jsx(Text, { children: "o" })] })] })), ollamaDeleteConfirm && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.warning, paddingX: 1, marginBottom: 0, children: [_jsxs(Text, { bold: true, color: theme.colors.warning, children: ["Delete ", ollamaDeleteConfirm.model, " (", (ollamaDeleteConfirm.size / (1024 * 1024 * 1024)).toFixed(1), " GB)?"] }), _jsxs(Text, { children: [_jsx(Text, { color: theme.colors.error, bold: true, children: " [y]" }), _jsx(Text, { children: "es " }), _jsx(Text, { color: theme.colors.success, bold: true, children: "[n]" }), _jsx(Text, { children: "o" })] })] })), ollamaPulling && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: [_jsxs(Text, { bold: true, color: theme.colors.secondary, children: [" Downloading ", ollamaPulling.model, "..."] }), ollamaPulling.progress.status === "downloading" || ollamaPulling.progress.percent > 0 ? (_jsxs(Text, { children: [" ", _jsxs(Text, { color: theme.colors.primary, children: ["\u2588".repeat(Math.floor(ollamaPulling.progress.percent / 5)), "\u2591".repeat(20 - Math.floor(ollamaPulling.progress.percent / 5))] }), " ", _jsxs(Text, { bold: true, children: [ollamaPulling.progress.percent, "%"] }), ollamaPulling.progress.completed != null && ollamaPulling.progress.total != null ? (_jsxs(Text, { color: theme.colors.muted, children: [" \u00B7 ", formatBytes(ollamaPulling.progress.completed), " / ", formatBytes(ollamaPulling.progress.total)] })) : null] })) : (_jsxs(Text, { color: theme.colors.muted, children: [" ", ollamaPulling.progress.status, "..."] }))] })), ollamaExitPrompt && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.warning, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.warning, children: "Ollama is still running." }), _jsx(Text, { color: theme.colors.muted, children: "Stop it to free GPU memory?" }), _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" })] })] })), wizardScreen === "connection" && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "No LLM detected. How do you want to connect?" }), _jsx(Text, { children: "" }), [
1792
+ { key: "local", icon: "\uD83D\uDDA5\uFE0F", label: "Set up a local model", desc: "free, runs on your machine" },
1793
+ { key: "openrouter", icon: "\uD83C\uDF10", label: "OpenRouter", desc: "200+ cloud models, browser login" },
1794
+ { key: "apikey", icon: "\uD83D\uDD11", label: "Enter API key manually", desc: "" },
1795
+ { key: "existing", icon: "\u2699\uFE0F", label: "I already have a server running", desc: "" },
1796
+ ].map((item, i) => (_jsxs(Text, { children: [i === wizardIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: " \u25B8 " }) : _jsx(Text, { children: " " }), _jsxs(Text, { color: i === wizardIndex ? theme.colors.suggestion : theme.colors.primary, bold: true, children: [item.icon, " ", item.label] }), item.desc ? _jsxs(Text, { color: theme.colors.muted, children: [" (", item.desc, ")"] }) : null] }, item.key))), _jsx(Text, { children: "" }), _jsx(Text, { dimColor: true, children: " \u2191\u2193 navigate \u00B7 Enter to select" })] })), wizardScreen === "models" && wizardHardware && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.secondary, children: "Your hardware:" }), _jsxs(Text, { color: theme.colors.muted, children: [" CPU: ", wizardHardware.cpu.name, " (", wizardHardware.cpu.cores, " cores)"] }), _jsxs(Text, { color: theme.colors.muted, children: [" RAM: ", formatBytes(wizardHardware.ram)] }), wizardHardware.gpu ? (_jsxs(Text, { color: theme.colors.muted, children: [" GPU: ", wizardHardware.gpu.name, wizardHardware.gpu.vram > 0 ? ` (${formatBytes(wizardHardware.gpu.vram)})` : ""] })) : (_jsx(Text, { color: theme.colors.muted, children: " GPU: none detected" })), !isLlmfitAvailable() && (_jsx(Text, { dimColor: true, children: " Tip: Install llmfit for smarter recommendations: brew install llmfit" })), _jsx(Text, { children: "" }), _jsx(Text, { bold: true, color: theme.colors.secondary, children: "Recommended models:" }), _jsx(Text, { children: "" }), wizardModels.map((m, i) => (_jsxs(Text, { children: [i === wizardIndex ? _jsx(Text, { color: theme.colors.suggestion, bold: true, children: " \u25B8 " }) : _jsx(Text, { children: " " }), _jsxs(Text, { children: [getFitIcon(m.fit), " "] }), _jsx(Text, { color: i === wizardIndex ? theme.colors.suggestion : theme.colors.primary, bold: true, children: m.name }), _jsxs(Text, { color: theme.colors.muted, children: [" ~", m.size, " GB \u00B7 ", m.quality === "best" ? "Best" : m.quality === "great" ? "Great" : "Good", " quality \u00B7 ", m.speed] })] }, m.ollamaId))), wizardModels.length === 0 && (_jsx(Text, { color: theme.colors.error, children: " No suitable models found for your hardware." })), _jsx(Text, { children: "" }), _jsx(Text, { dimColor: true, children: " \u2191\u2193 navigate \u00B7 Enter to install \u00B7 Esc back" })] })), wizardScreen === "install-ollama" && (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.warning, paddingX: 1, marginBottom: 0, children: [_jsx(Text, { bold: true, color: theme.colors.warning, children: "Ollama is required for local models." }), _jsx(Text, { children: "" }), _jsxs(Text, { color: theme.colors.primary, children: [" Install with: ", _jsx(Text, { bold: true, children: getOllamaInstallCommand(wizardHardware?.os ?? "linux") })] }), _jsx(Text, { children: "" }), _jsx(Text, { dimColor: true, children: " Run the command above, then press Enter to continue..." }), _jsx(Text, { dimColor: true, children: " Esc to go back" })] })), wizardScreen === "pulling" && wizardSelectedModel && (_jsx(Box, { flexDirection: "column", borderStyle: "single", borderColor: theme.colors.border, paddingX: 1, marginBottom: 0, children: wizardPullError ? (_jsxs(_Fragment, { children: [_jsxs(Text, { color: theme.colors.error, bold: true, children: [" \u274C Error: ", wizardPullError] }), _jsx(Text, { children: "" }), _jsx(Text, { dimColor: true, children: " Press Enter to retry \u00B7 Esc to go back" })] })) : wizardPullProgress ? (_jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, color: theme.colors.secondary, children: [" Downloading ", wizardSelectedModel.name, "..."] }), wizardPullProgress.status === "downloading" || wizardPullProgress.percent > 0 ? (_jsx(_Fragment, { children: _jsxs(Text, { children: [" ", _jsxs(Text, { color: theme.colors.primary, children: ["\u2588".repeat(Math.floor(wizardPullProgress.percent / 5)), "\u2591".repeat(20 - Math.floor(wizardPullProgress.percent / 5))] }), " ", _jsxs(Text, { bold: true, children: [wizardPullProgress.percent, "%"] }), wizardPullProgress.completed != null && wizardPullProgress.total != null ? (_jsxs(Text, { color: theme.colors.muted, children: [" \u00B7 ", formatBytes(wizardPullProgress.completed), " / ", formatBytes(wizardPullProgress.total)] })) : null] }) })) : (_jsxs(Text, { color: theme.colors.muted, children: [" ", wizardPullProgress.status, "..."] }))] })) : null })), 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 · ~", (() => {
1348
1797
  const tokens = agent.estimateTokens();
1349
1798
  return tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}k` : String(tokens);
1350
1799
  })(), " tokens", (() => {
@@ -0,0 +1,17 @@
1
+ export interface HardwareInfo {
2
+ cpu: {
3
+ name: string;
4
+ cores: number;
5
+ speed: number;
6
+ };
7
+ ram: number;
8
+ gpu: {
9
+ name: string;
10
+ vram: number;
11
+ } | null;
12
+ os: "macos" | "linux" | "windows";
13
+ appleSilicon: boolean;
14
+ }
15
+ export declare function detectHardware(): HardwareInfo;
16
+ /** Format bytes to human-readable string */
17
+ export declare function formatBytes(bytes: number): string;