codemaxxing 0.3.0 → 0.4.0

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/README.md CHANGED
@@ -12,34 +12,34 @@ Open-source terminal coding agent. Connect **any** LLM — local or remote — a
12
12
 
13
13
  Every coding agent locks you into their API. Codemaxxing doesn't. Run it with LM Studio, Ollama, OpenRouter, OpenAI, or any OpenAI-compatible endpoint. Your machine, your model, your rules.
14
14
 
15
- ## Quick Install (Recommended)
15
+ ## Install
16
16
 
17
- **Linux / macOS:**
17
+ **If you have Node.js:**
18
18
  ```bash
19
- bash -c "$(curl -fsSL https://raw.githubusercontent.com/MarcosV6/codemaxxing/main/install.sh)"
19
+ npm install -g codemaxxing
20
20
  ```
21
21
 
22
- **Windows (PowerShell as Administrator):**
23
- ```powershell
24
- curl -fsSL -o $env:TEMP\install-codemaxxing.bat https://raw.githubusercontent.com/MarcosV6/codemaxxing/main/install.bat; & $env:TEMP\install-codemaxxing.bat
22
+ **If you don't have Node.js:**
23
+
24
+ The one-line installers below will install Node.js first, then codemaxxing.
25
+
26
+ *Linux / macOS:*
27
+ ```bash
28
+ bash -c "$(curl -fsSL https://raw.githubusercontent.com/MarcosV6/codemaxxing/main/install.sh)"
25
29
  ```
26
30
 
27
- **Windows (CMD as Administrator):**
31
+ *Windows (CMD as Administrator):*
28
32
  ```
29
33
  curl -fsSL -o %TEMP%\install-codemaxxing.bat https://raw.githubusercontent.com/MarcosV6/codemaxxing/main/install.bat && %TEMP%\install-codemaxxing.bat
30
34
  ```
31
35
 
32
- > **Note:** Restart your terminal after installation to ensure everything works.
33
-
34
- ## Manual Installation
35
-
36
- **Prerequisites:** [Node.js](https://nodejs.org) 20 or later.
37
-
38
- **NPM:**
39
- ```bash
40
- npm install -g codemaxxing
36
+ *Windows (PowerShell as Administrator):*
37
+ ```powershell
38
+ curl -fsSL -o $env:TEMP\install-codemaxxing.bat https://raw.githubusercontent.com/MarcosV6/codemaxxing/main/install.bat; & $env:TEMP\install-codemaxxing.bat
41
39
  ```
42
40
 
41
+ > **Windows note:** If Node.js was just installed, you may need to close and reopen your terminal, then run `npm install -g codemaxxing` manually. This is a Windows PATH limitation.
42
+
43
43
  ## Updating
44
44
 
45
45
  ```bash
package/dist/agent.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type ConnectedServer } from "./utils/mcp.js";
1
2
  import type { ProviderConfig } from "./config.js";
2
3
  export interface AgentOptions {
3
4
  provider: ProviderConfig;
@@ -13,6 +14,7 @@ export interface AgentOptions {
13
14
  onContextCompressed?: (oldTokens: number, newTokens: number) => void;
14
15
  onArchitectPlan?: (plan: string) => void;
15
16
  onLintResult?: (file: string, errors: string) => void;
17
+ onMCPStatus?: (server: string, status: string) => void;
16
18
  contextCompressionThreshold?: number;
17
19
  }
18
20
  export declare class CodingAgent {
@@ -41,6 +43,7 @@ export declare class CodingAgent {
41
43
  private architectModel;
42
44
  private autoLintEnabled;
43
45
  private detectedLinter;
46
+ private mcpServers;
44
47
  constructor(options: AgentOptions);
45
48
  /**
46
49
  * Initialize the agent — call this after constructor to build async context
@@ -124,5 +127,9 @@ export declare class CodingAgent {
124
127
  * Run the architect model to generate a plan, then feed to editor model
125
128
  */
126
129
  private architectChat;
130
+ getMCPServerCount(): number;
131
+ getMCPServers(): ConnectedServer[];
132
+ disconnectMCP(): Promise<void>;
133
+ reconnectMCP(): Promise<void>;
127
134
  reset(): void;
128
135
  }
package/dist/agent.js CHANGED
@@ -6,6 +6,7 @@ import { buildProjectContext, getSystemPrompt, loadProjectRules } from "./utils/
6
6
  import { isGitRepo, autoCommit } from "./utils/git.js";
7
7
  import { buildSkillPrompts, getActiveSkillCount } from "./utils/skills.js";
8
8
  import { createSession, saveMessage, updateTokenEstimate, updateSessionCost, loadMessages } from "./utils/sessions.js";
9
+ import { loadMCPConfig, connectToServers, disconnectAll, getAllMCPTools, parseMCPToolName, callMCPTool } from "./utils/mcp.js";
9
10
  // Tools that can modify your project — require approval
10
11
  const DANGEROUS_TOOLS = new Set(["write_file", "run_command"]);
11
12
  // Cost per 1M tokens (input/output) for common models
@@ -78,6 +79,7 @@ export class CodingAgent {
78
79
  architectModel = null;
79
80
  autoLintEnabled = true;
80
81
  detectedLinter = null;
82
+ mcpServers = [];
81
83
  constructor(options) {
82
84
  this.options = options;
83
85
  this.providerType = options.provider.type || "openai";
@@ -113,6 +115,15 @@ export class CodingAgent {
113
115
  this.systemPrompt = await getSystemPrompt(context, skillPrompts, rules?.content ?? "");
114
116
  // Detect project linter
115
117
  this.detectedLinter = detectLinter(this.cwd);
118
+ // Connect to MCP servers
119
+ const mcpConfig = loadMCPConfig(this.cwd);
120
+ if (Object.keys(mcpConfig.mcpServers).length > 0) {
121
+ this.mcpServers = await connectToServers(mcpConfig, this.options.onMCPStatus);
122
+ if (this.mcpServers.length > 0) {
123
+ const mcpTools = getAllMCPTools(this.mcpServers);
124
+ this.tools = [...FILE_TOOLS, ...mcpTools];
125
+ }
126
+ }
116
127
  this.messages = [
117
128
  { role: "system", content: this.systemPrompt },
118
129
  ];
@@ -305,7 +316,15 @@ export class CodingAgent {
305
316
  }
306
317
  }
307
318
  }
308
- const result = await executeTool(toolCall.name, args, this.cwd);
319
+ // Route to MCP or built-in tool
320
+ const mcpParsed = parseMCPToolName(toolCall.name);
321
+ let result;
322
+ if (mcpParsed) {
323
+ result = await callMCPTool(mcpParsed.serverName, mcpParsed.toolName, args);
324
+ }
325
+ else {
326
+ result = await executeTool(toolCall.name, args, this.cwd);
327
+ }
309
328
  this.options.onToolResult?.(toolCall.name, result);
310
329
  // Auto-commit after successful write_file (only if enabled)
311
330
  if (this.gitEnabled && this.autoCommitEnabled && toolCall.name === "write_file" && result.startsWith("✅")) {
@@ -502,7 +521,15 @@ export class CodingAgent {
502
521
  }
503
522
  }
504
523
  }
505
- const result = await executeTool(toolCall.name, args, this.cwd);
524
+ // Route to MCP or built-in tool
525
+ const mcpParsed = parseMCPToolName(toolCall.name);
526
+ let result;
527
+ if (mcpParsed) {
528
+ result = await callMCPTool(mcpParsed.serverName, mcpParsed.toolName, args);
529
+ }
530
+ else {
531
+ result = await executeTool(toolCall.name, args, this.cwd);
532
+ }
506
533
  this.options.onToolResult?.(toolCall.name, result);
507
534
  // Auto-commit after successful write_file
508
535
  if (this.gitEnabled && this.autoCommitEnabled && toolCall.name === "write_file" && result.startsWith("✅")) {
@@ -738,6 +765,28 @@ export class CodingAgent {
738
765
  const editorPrompt = `## Architect Plan\n${plan}\n\n## Original Request\n${userMessage}\n\nExecute the plan above. Follow it step by step.`;
739
766
  return this.chat(editorPrompt);
740
767
  }
768
+ getMCPServerCount() {
769
+ return this.mcpServers.length;
770
+ }
771
+ getMCPServers() {
772
+ return this.mcpServers;
773
+ }
774
+ async disconnectMCP() {
775
+ await disconnectAll();
776
+ this.mcpServers = [];
777
+ this.tools = FILE_TOOLS;
778
+ }
779
+ async reconnectMCP() {
780
+ await this.disconnectMCP();
781
+ const mcpConfig = loadMCPConfig(this.cwd);
782
+ if (Object.keys(mcpConfig.mcpServers).length > 0) {
783
+ this.mcpServers = await connectToServers(mcpConfig, this.options.onMCPStatus);
784
+ if (this.mcpServers.length > 0) {
785
+ const mcpTools = getAllMCPTools(this.mcpServers);
786
+ this.tools = [...FILE_TOOLS, ...mcpTools];
787
+ }
788
+ }
789
+ }
741
790
  reset() {
742
791
  const systemMsg = this.messages[0];
743
792
  this.messages = [systemMsg];
package/dist/exec.js CHANGED
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { CodingAgent } from "./agent.js";
8
8
  import { loadConfig, applyOverrides, detectLocalProvider } from "./config.js";
9
+ import { disconnectAll } from "./utils/mcp.js";
9
10
  function parseExecArgs(argv) {
10
11
  const args = {
11
12
  prompt: "",
@@ -124,9 +125,16 @@ export async function runExec(argv) {
124
125
  process.stderr.write(`⚠ Denied ${name} (use --auto-approve to allow)\n`);
125
126
  return "no";
126
127
  },
128
+ onMCPStatus: (server, status) => {
129
+ process.stderr.write(`MCP ${server}: ${status}\n`);
130
+ },
127
131
  });
128
132
  try {
129
133
  await agent.init();
134
+ const mcpCount = agent.getMCPServerCount();
135
+ if (mcpCount > 0) {
136
+ process.stderr.write(`MCP: ${mcpCount} server${mcpCount > 1 ? "s" : ""} connected\n`);
137
+ }
130
138
  await agent.send(args.prompt);
131
139
  if (!args.json) {
132
140
  // Ensure newline at end of output
@@ -142,9 +150,11 @@ export async function runExec(argv) {
142
150
  };
143
151
  process.stdout.write(JSON.stringify(output, null, 2) + "\n");
144
152
  }
153
+ await disconnectAll();
145
154
  process.exit(hasChanges ? 0 : 2);
146
155
  }
147
156
  catch (err) {
157
+ await disconnectAll();
148
158
  process.stderr.write(`Error: ${err.message}\n`);
149
159
  if (args.json) {
150
160
  process.stdout.write(JSON.stringify({ error: err.message }, null, 2) + "\n");
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
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";
@@ -12,6 +12,10 @@ import { isGitRepo, getBranch, getStatus, getDiff, undoLastCommit } from "./util
12
12
  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
+ import { listServers, addServer, removeServer, getConnectedServers } from "./utils/mcp.js";
16
+ import { detectHardware, formatBytes } from "./utils/hardware.js";
17
+ import { getRecommendations, getFitIcon } from "./utils/models.js";
18
+ import { isOllamaInstalled, isOllamaRunning, getOllamaInstallCommand, startOllama, pullModel } from "./utils/ollama.js";
15
19
  const VERSION = "0.1.9";
16
20
  // ── Helpers ──
17
21
  function formatTimeAgo(date) {
@@ -58,6 +62,11 @@ const SLASH_COMMANDS = [
58
62
  { cmd: "/lint", desc: "show auto-lint status" },
59
63
  { cmd: "/lint on", desc: "enable auto-lint" },
60
64
  { cmd: "/lint off", desc: "disable auto-lint" },
65
+ { cmd: "/mcp", desc: "show MCP servers" },
66
+ { cmd: "/mcp tools", desc: "list MCP tools" },
67
+ { cmd: "/mcp add", desc: "add MCP server" },
68
+ { cmd: "/mcp remove", desc: "remove MCP server" },
69
+ { cmd: "/mcp reconnect", desc: "reconnect MCP servers" },
61
70
  { cmd: "/quit", desc: "exit" },
62
71
  ];
63
72
  const SPINNER_FRAMES = ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"];
@@ -131,6 +140,13 @@ function App() {
131
140
  const [skillsPickerIndex, setSkillsPickerIndex] = useState(0);
132
141
  const [sessionDisabledSkills, setSessionDisabledSkills] = useState(new Set());
133
142
  const [approval, setApproval] = useState(null);
143
+ const [wizardScreen, setWizardScreen] = useState(null);
144
+ const [wizardIndex, setWizardIndex] = useState(0);
145
+ const [wizardHardware, setWizardHardware] = useState(null);
146
+ const [wizardModels, setWizardModels] = useState([]);
147
+ const [wizardPullProgress, setWizardPullProgress] = useState(null);
148
+ const [wizardPullError, setWizardPullError] = useState(null);
149
+ const [wizardSelectedModel, setWizardSelectedModel] = useState(null);
134
150
  // Listen for paste events from stdin interceptor
135
151
  useEffect(() => {
136
152
  const handler = ({ content, lines }) => {
@@ -168,10 +184,11 @@ function App() {
168
184
  }
169
185
  else {
170
186
  info.push("✗ No local LLM server found.");
171
- info.push(" /connect — retry after starting LM Studio or Ollama");
172
- info.push(" /login — authenticate with a cloud provider");
173
187
  setConnectionInfo([...info]);
174
188
  setReady(true);
189
+ // Show the setup wizard on first run
190
+ setWizardScreen("connection");
191
+ setWizardIndex(0);
175
192
  return;
176
193
  }
177
194
  }
@@ -246,6 +263,9 @@ function App() {
246
263
  onLintResult: (file, errors) => {
247
264
  addMsg("info", `🔍 Lint errors in ${file}:\n${errors}`);
248
265
  },
266
+ onMCPStatus: (server, status) => {
267
+ addMsg("info", `🔌 MCP ${server}: ${status}`);
268
+ },
249
269
  contextCompressionThreshold: config.defaults.contextCompressionThreshold,
250
270
  onToolApproval: (name, args, diff) => {
251
271
  return new Promise((resolve) => {
@@ -262,6 +282,12 @@ function App() {
262
282
  info.push(`📋 ${rulesSource} loaded`);
263
283
  setConnectionInfo([...info]);
264
284
  }
285
+ // Show MCP server count
286
+ const mcpCount = a.getMCPServerCount();
287
+ if (mcpCount > 0) {
288
+ info.push(`🔌 ${mcpCount} MCP server${mcpCount > 1 ? "s" : ""} connected`);
289
+ setConnectionInfo([...info]);
290
+ }
265
291
  setAgent(a);
266
292
  setModelName(provider.model);
267
293
  providerRef.current = { baseUrl: provider.baseUrl, apiKey: provider.apiKey };
@@ -367,6 +393,11 @@ function App() {
367
393
  " /lint — show auto-lint status & detected linter",
368
394
  " /lint on — enable auto-lint",
369
395
  " /lint off — disable auto-lint",
396
+ " /mcp — show MCP servers & status",
397
+ " /mcp tools — list all MCP tools",
398
+ " /mcp add — add MCP server to global config",
399
+ " /mcp remove — remove MCP server",
400
+ " /mcp reconnect — reconnect all MCP servers",
370
401
  " /quit — exit",
371
402
  ].join("\n"));
372
403
  return;
@@ -534,6 +565,71 @@ function App() {
534
565
  addMsg("info", "🔍 Auto-lint OFF");
535
566
  return;
536
567
  }
568
+ // ── MCP commands (partially work without agent) ──
569
+ if (trimmed === "/mcp" || trimmed === "/mcp list") {
570
+ const servers = listServers(process.cwd());
571
+ if (servers.length === 0) {
572
+ addMsg("info", "🔌 No MCP servers configured.\n Add one: /mcp add <name> <command> [args...]");
573
+ }
574
+ else {
575
+ const lines = servers.map((s) => {
576
+ const status = s.connected ? `✔ connected (${s.toolCount} tools)` : "✗ not connected";
577
+ return ` ${s.connected ? "●" : "○"} ${s.name} [${s.source}] — ${s.command}\n ${status}`;
578
+ });
579
+ addMsg("info", `🔌 MCP Servers:\n${lines.join("\n")}`);
580
+ }
581
+ return;
582
+ }
583
+ if (trimmed === "/mcp tools") {
584
+ const servers = getConnectedServers();
585
+ if (servers.length === 0) {
586
+ addMsg("info", "🔌 No MCP servers connected.");
587
+ return;
588
+ }
589
+ const lines = [];
590
+ for (const server of servers) {
591
+ lines.push(`${server.name} (${server.tools.length} tools):`);
592
+ for (const tool of server.tools) {
593
+ lines.push(` • ${tool.name} — ${tool.description ?? "(no description)"}`);
594
+ }
595
+ }
596
+ addMsg("info", `🔌 MCP Tools:\n${lines.join("\n")}`);
597
+ return;
598
+ }
599
+ if (trimmed.startsWith("/mcp add ")) {
600
+ const parts = trimmed.replace("/mcp add ", "").trim().split(/\s+/);
601
+ if (parts.length < 2) {
602
+ addMsg("info", "Usage: /mcp add <name> <command> [args...]\n Example: /mcp add github npx -y @modelcontextprotocol/server-github");
603
+ return;
604
+ }
605
+ const [name, command, ...cmdArgs] = parts;
606
+ const result = addServer(name, { command, args: cmdArgs.length > 0 ? cmdArgs : undefined });
607
+ addMsg(result.ok ? "info" : "error", result.ok ? `✅ ${result.message}` : `✗ ${result.message}`);
608
+ return;
609
+ }
610
+ if (trimmed.startsWith("/mcp remove ")) {
611
+ const name = trimmed.replace("/mcp remove ", "").trim();
612
+ if (!name) {
613
+ addMsg("info", "Usage: /mcp remove <name>");
614
+ return;
615
+ }
616
+ const result = removeServer(name);
617
+ addMsg(result.ok ? "info" : "error", result.ok ? `✅ ${result.message}` : `✗ ${result.message}`);
618
+ return;
619
+ }
620
+ if (trimmed === "/mcp reconnect") {
621
+ if (!agent) {
622
+ addMsg("info", "⚠ No agent connected. Connect first.");
623
+ return;
624
+ }
625
+ addMsg("info", "🔌 Reconnecting MCP servers...");
626
+ await agent.reconnectMCP();
627
+ const count = agent.getMCPServerCount();
628
+ addMsg("info", count > 0
629
+ ? `✅ ${count} MCP server${count > 1 ? "s" : ""} reconnected.`
630
+ : "No MCP servers connected.");
631
+ return;
632
+ }
537
633
  // Commands below require an active LLM connection
538
634
  if (!agent) {
539
635
  addMsg("info", "⚠ No LLM connected. Use /login to authenticate with a provider, or start a local server.");
@@ -1029,6 +1125,223 @@ function App() {
1029
1125
  }
1030
1126
  return;
1031
1127
  }
1128
+ // ── Setup Wizard Navigation ──
1129
+ if (wizardScreen) {
1130
+ if (wizardScreen === "connection") {
1131
+ const items = ["local", "openrouter", "apikey", "existing"];
1132
+ if (key.upArrow) {
1133
+ setWizardIndex((prev) => (prev - 1 + items.length) % items.length);
1134
+ return;
1135
+ }
1136
+ if (key.downArrow) {
1137
+ setWizardIndex((prev) => (prev + 1) % items.length);
1138
+ return;
1139
+ }
1140
+ if (key.escape) {
1141
+ setWizardScreen(null);
1142
+ return;
1143
+ }
1144
+ if (key.return) {
1145
+ const selected = items[wizardIndex];
1146
+ if (selected === "local") {
1147
+ // Scan hardware and show model picker
1148
+ const hw = detectHardware();
1149
+ setWizardHardware(hw);
1150
+ const recs = getRecommendations(hw).filter(m => m.fit !== "skip");
1151
+ setWizardModels(recs);
1152
+ setWizardScreen("models");
1153
+ setWizardIndex(0);
1154
+ }
1155
+ else if (selected === "openrouter") {
1156
+ setWizardScreen(null);
1157
+ addMsg("info", "Starting OpenRouter OAuth — opening browser...");
1158
+ setLoading(true);
1159
+ setSpinnerMsg("Waiting for authorization...");
1160
+ openRouterOAuth((msg) => addMsg("info", msg))
1161
+ .then(() => {
1162
+ addMsg("info", "✅ OpenRouter authenticated! Use /connect to connect.");
1163
+ setLoading(false);
1164
+ })
1165
+ .catch((err) => { addMsg("error", `OAuth failed: ${err.message}`); setLoading(false); });
1166
+ }
1167
+ else if (selected === "apikey") {
1168
+ setWizardScreen(null);
1169
+ setLoginPicker(true);
1170
+ setLoginPickerIndex(0);
1171
+ }
1172
+ else if (selected === "existing") {
1173
+ setWizardScreen(null);
1174
+ addMsg("info", "Start your LLM server, then type /connect to retry.");
1175
+ }
1176
+ return;
1177
+ }
1178
+ return;
1179
+ }
1180
+ if (wizardScreen === "models") {
1181
+ const models = wizardModels;
1182
+ if (key.upArrow) {
1183
+ setWizardIndex((prev) => (prev - 1 + models.length) % models.length);
1184
+ return;
1185
+ }
1186
+ if (key.downArrow) {
1187
+ setWizardIndex((prev) => (prev + 1) % models.length);
1188
+ return;
1189
+ }
1190
+ if (key.escape) {
1191
+ setWizardScreen("connection");
1192
+ setWizardIndex(0);
1193
+ return;
1194
+ }
1195
+ if (key.return) {
1196
+ const selected = models[wizardIndex];
1197
+ if (selected) {
1198
+ setWizardSelectedModel(selected);
1199
+ // Check if Ollama is installed
1200
+ if (!isOllamaInstalled()) {
1201
+ setWizardScreen("install-ollama");
1202
+ }
1203
+ else {
1204
+ // Start pulling the model
1205
+ setWizardScreen("pulling");
1206
+ setWizardPullProgress({ status: "starting", percent: 0 });
1207
+ setWizardPullError(null);
1208
+ (async () => {
1209
+ try {
1210
+ // Ensure ollama is running
1211
+ const running = await isOllamaRunning();
1212
+ if (!running) {
1213
+ setWizardPullProgress({ status: "Starting Ollama server...", percent: 0 });
1214
+ startOllama();
1215
+ // Wait for it to come up
1216
+ for (let i = 0; i < 15; i++) {
1217
+ await new Promise(r => setTimeout(r, 1000));
1218
+ if (await isOllamaRunning())
1219
+ break;
1220
+ }
1221
+ if (!(await isOllamaRunning())) {
1222
+ setWizardPullError("Could not start Ollama server. Run 'ollama serve' manually, then press Enter.");
1223
+ return;
1224
+ }
1225
+ }
1226
+ await pullModel(selected.ollamaId, (p) => {
1227
+ setWizardPullProgress(p);
1228
+ });
1229
+ setWizardPullProgress({ status: "success", percent: 100 });
1230
+ // Wait briefly then connect
1231
+ await new Promise(r => setTimeout(r, 500));
1232
+ setWizardScreen(null);
1233
+ setWizardPullProgress(null);
1234
+ setWizardSelectedModel(null);
1235
+ addMsg("info", `✅ ${selected.name} installed! Connecting...`);
1236
+ await connectToProvider(true);
1237
+ }
1238
+ catch (err) {
1239
+ setWizardPullError(err.message);
1240
+ }
1241
+ })();
1242
+ }
1243
+ }
1244
+ return;
1245
+ }
1246
+ return;
1247
+ }
1248
+ if (wizardScreen === "install-ollama") {
1249
+ if (key.escape) {
1250
+ setWizardScreen("models");
1251
+ setWizardIndex(0);
1252
+ return;
1253
+ }
1254
+ if (key.return) {
1255
+ // User says they installed it — check and proceed
1256
+ if (isOllamaInstalled()) {
1257
+ const selected = wizardSelectedModel;
1258
+ if (selected) {
1259
+ setWizardScreen("pulling");
1260
+ setWizardPullProgress({ status: "starting", percent: 0 });
1261
+ setWizardPullError(null);
1262
+ (async () => {
1263
+ try {
1264
+ const running = await isOllamaRunning();
1265
+ if (!running) {
1266
+ setWizardPullProgress({ status: "Starting Ollama server...", percent: 0 });
1267
+ startOllama();
1268
+ for (let i = 0; i < 15; i++) {
1269
+ await new Promise(r => setTimeout(r, 1000));
1270
+ if (await isOllamaRunning())
1271
+ break;
1272
+ }
1273
+ if (!(await isOllamaRunning())) {
1274
+ setWizardPullError("Could not start Ollama server. Run 'ollama serve' manually, then press Enter.");
1275
+ return;
1276
+ }
1277
+ }
1278
+ await pullModel(selected.ollamaId, (p) => setWizardPullProgress(p));
1279
+ setWizardPullProgress({ status: "success", percent: 100 });
1280
+ await new Promise(r => setTimeout(r, 500));
1281
+ setWizardScreen(null);
1282
+ setWizardPullProgress(null);
1283
+ setWizardSelectedModel(null);
1284
+ addMsg("info", `✅ ${selected.name} installed! Connecting...`);
1285
+ await connectToProvider(true);
1286
+ }
1287
+ catch (err) {
1288
+ setWizardPullError(err.message);
1289
+ }
1290
+ })();
1291
+ }
1292
+ }
1293
+ else {
1294
+ addMsg("info", "Ollama not found yet. Install it and press Enter again.");
1295
+ }
1296
+ return;
1297
+ }
1298
+ return;
1299
+ }
1300
+ if (wizardScreen === "pulling") {
1301
+ // Allow retry on error
1302
+ if (wizardPullError && key.return) {
1303
+ const selected = wizardSelectedModel;
1304
+ if (selected) {
1305
+ setWizardPullError(null);
1306
+ setWizardPullProgress({ status: "retrying", percent: 0 });
1307
+ (async () => {
1308
+ try {
1309
+ const running = await isOllamaRunning();
1310
+ if (!running) {
1311
+ startOllama();
1312
+ for (let i = 0; i < 15; i++) {
1313
+ await new Promise(r => setTimeout(r, 1000));
1314
+ if (await isOllamaRunning())
1315
+ break;
1316
+ }
1317
+ }
1318
+ await pullModel(selected.ollamaId, (p) => setWizardPullProgress(p));
1319
+ setWizardPullProgress({ status: "success", percent: 100 });
1320
+ await new Promise(r => setTimeout(r, 500));
1321
+ setWizardScreen(null);
1322
+ setWizardPullProgress(null);
1323
+ setWizardSelectedModel(null);
1324
+ addMsg("info", `✅ ${selected.name} installed! Connecting...`);
1325
+ await connectToProvider(true);
1326
+ }
1327
+ catch (err) {
1328
+ setWizardPullError(err.message);
1329
+ }
1330
+ })();
1331
+ }
1332
+ return;
1333
+ }
1334
+ if (wizardPullError && key.escape) {
1335
+ setWizardScreen("models");
1336
+ setWizardIndex(0);
1337
+ setWizardPullError(null);
1338
+ setWizardPullProgress(null);
1339
+ return;
1340
+ }
1341
+ return; // Ignore keys while pulling
1342
+ }
1343
+ return;
1344
+ }
1032
1345
  // Theme picker navigation
1033
1346
  if (themePicker) {
1034
1347
  const themeKeys = listThemes();
@@ -1259,7 +1572,12 @@ function App() {
1259
1572
  })(), skillsPicker === "remove" && (() => {
1260
1573
  const installed = listInstalledSkills();
1261
1574
  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" })] }));
1262
- })(), 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 · ~", (() => {
1575
+ })(), 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" })] })] })), 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: "" }), [
1576
+ { key: "local", icon: "\uD83D\uDDA5\uFE0F", label: "Set up a local model", desc: "free, runs on your machine" },
1577
+ { key: "openrouter", icon: "\uD83C\uDF10", label: "OpenRouter", desc: "200+ cloud models, browser login" },
1578
+ { key: "apikey", icon: "\uD83D\uDD11", label: "Enter API key manually", desc: "" },
1579
+ { key: "existing", icon: "\u2699\uFE0F", label: "I already have a server running", desc: "" },
1580
+ ].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" })), _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 · ~", (() => {
1263
1581
  const tokens = agent.estimateTokens();
1264
1582
  return tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}k` : String(tokens);
1265
1583
  })(), " 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;