codemaxxing 0.4.17 → 1.0.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.
Files changed (47) hide show
  1. package/README.md +6 -4
  2. package/dist/agent.d.ts +4 -0
  3. package/dist/agent.js +91 -16
  4. package/dist/commands/git.d.ts +2 -0
  5. package/dist/commands/git.js +50 -0
  6. package/dist/commands/ollama.d.ts +27 -0
  7. package/dist/commands/ollama.js +171 -0
  8. package/dist/commands/output.d.ts +2 -0
  9. package/dist/commands/output.js +18 -0
  10. package/dist/commands/registry.d.ts +2 -0
  11. package/dist/commands/registry.js +8 -0
  12. package/dist/commands/skills.d.ts +18 -0
  13. package/dist/commands/skills.js +121 -0
  14. package/dist/commands/types.d.ts +5 -0
  15. package/dist/commands/types.js +1 -0
  16. package/dist/commands/ui.d.ts +16 -0
  17. package/dist/commands/ui.js +79 -0
  18. package/dist/config.d.ts +9 -0
  19. package/dist/config.js +13 -3
  20. package/dist/exec.js +4 -1
  21. package/dist/index.js +72 -388
  22. package/dist/tools/files.js +58 -3
  23. package/dist/utils/context.js +6 -0
  24. package/dist/utils/mcp.d.ts +7 -2
  25. package/dist/utils/mcp.js +34 -6
  26. package/package.json +8 -5
  27. package/src/agent.ts +0 -894
  28. package/src/auth-cli.ts +0 -287
  29. package/src/cli.ts +0 -37
  30. package/src/config.ts +0 -352
  31. package/src/exec.ts +0 -183
  32. package/src/index.tsx +0 -2621
  33. package/src/skills/registry.ts +0 -1436
  34. package/src/themes.ts +0 -335
  35. package/src/tools/files.ts +0 -374
  36. package/src/utils/auth.ts +0 -606
  37. package/src/utils/context.ts +0 -174
  38. package/src/utils/git.ts +0 -117
  39. package/src/utils/hardware.ts +0 -131
  40. package/src/utils/lint.ts +0 -116
  41. package/src/utils/mcp.ts +0 -307
  42. package/src/utils/models.ts +0 -218
  43. package/src/utils/ollama.ts +0 -352
  44. package/src/utils/repomap.ts +0 -220
  45. package/src/utils/sessions.ts +0 -254
  46. package/src/utils/skills.ts +0 -241
  47. package/tsconfig.json +0 -16
package/README.md CHANGED
@@ -6,6 +6,8 @@
6
6
  <img src="assets/screenshot.jpg" alt="codemaxxing terminal UI" width="700">
7
7
  </p>
8
8
 
9
+ [![npm version](https://img.shields.io/npm/v/codemaxxing)](https://www.npmjs.com/package/codemaxxing) [![license](https://img.shields.io/npm/l/codemaxxing)](LICENSE)
10
+
9
11
  Open-source terminal coding agent. Connect **any** LLM — local or remote — and start building. Like Claude Code, but you bring your own model.
10
12
 
11
13
  ## Why?
@@ -110,7 +112,7 @@ Credentials stored securely in `~/.codemaxxing/auth.json` (owner-only permission
110
112
  **With a remote provider (OpenAI, OpenRouter, etc.):**
111
113
 
112
114
  ```bash
113
- codemaxxing --base-url https://api.openai.com/v1 --api-key sk-... --model gpt-4o
115
+ codemaxxing --base-url https://api.openai.com/v1 --api-key sk-... --model gpt-5
114
116
  ```
115
117
 
116
118
  **With a saved provider profile:**
@@ -219,7 +221,7 @@ Full Ollama control from inside codemaxxing:
219
221
  ### 🔄 Multi-Provider
220
222
  Switch models mid-session with an interactive picker:
221
223
  - `/model` — browse and switch models
222
- - `/model gpt-4o` — switch directly by name
224
+ - `/model gpt-5` — switch directly by name
223
225
  - Native Anthropic API support (not just OpenAI-compatible)
224
226
 
225
227
  ### 🎨 14 Themes
@@ -306,13 +308,13 @@ Settings are stored in `~/.codemaxxing/settings.json`:
306
308
  "name": "OpenRouter",
307
309
  "baseUrl": "https://openrouter.ai/api/v1",
308
310
  "apiKey": "sk-or-...",
309
- "model": "anthropic/claude-sonnet-4"
311
+ "model": "anthropic/claude-sonnet-4-6"
310
312
  },
311
313
  "openai": {
312
314
  "name": "OpenAI",
313
315
  "baseUrl": "https://api.openai.com/v1",
314
316
  "apiKey": "sk-...",
315
- "model": "gpt-4o"
317
+ "model": "gpt-5"
316
318
  }
317
319
  },
318
320
  "defaults": {
package/dist/agent.d.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  import { type ConnectedServer } from "./utils/mcp.js";
2
2
  import type { ProviderConfig } from "./config.js";
3
+ export declare function getModelCost(model: string): {
4
+ input: number;
5
+ output: number;
6
+ };
3
7
  export interface AgentOptions {
4
8
  provider: ProviderConfig;
5
9
  cwd: string;
package/dist/agent.js CHANGED
@@ -8,10 +8,11 @@ import { buildSkillPrompts, getActiveSkillCount } from "./utils/skills.js";
8
8
  import { createSession, saveMessage, updateTokenEstimate, updateSessionCost, loadMessages } from "./utils/sessions.js";
9
9
  import { loadMCPConfig, connectToServers, disconnectAll, getAllMCPTools, parseMCPToolName, callMCPTool } from "./utils/mcp.js";
10
10
  // Tools that can modify your project — require approval
11
- const DANGEROUS_TOOLS = new Set(["write_file", "run_command"]);
11
+ const DANGEROUS_TOOLS = new Set(["write_file", "edit_file", "run_command"]);
12
12
  // Cost per 1M tokens (input/output) for common models
13
+ // Prices as of mid-2025; update when providers change pricing.
13
14
  const MODEL_COSTS = {
14
- // OpenAI
15
+ // ── OpenAI ──────────────────────────────────────────────────────────────
15
16
  "gpt-4o": { input: 2.5, output: 10 },
16
17
  "gpt-4o-mini": { input: 0.15, output: 0.6 },
17
18
  "gpt-4-turbo": { input: 10, output: 30 },
@@ -19,28 +20,82 @@ const MODEL_COSTS = {
19
20
  "gpt-3.5-turbo": { input: 0.5, output: 1.5 },
20
21
  "o1": { input: 15, output: 60 },
21
22
  "o1-mini": { input: 3, output: 12 },
23
+ "o1-pro": { input: 150, output: 600 },
24
+ "o3": { input: 10, output: 40 },
22
25
  "o3-mini": { input: 1.1, output: 4.4 },
23
- // Anthropic
26
+ "o4-mini": { input: 1.1, output: 4.4 },
27
+ // Provider-prefixed variants (OpenRouter / LM Studio style)
28
+ "openai/gpt-4o": { input: 2.5, output: 10 },
29
+ "openai/gpt-4o-mini": { input: 0.15, output: 0.6 },
30
+ "openai/o3-mini": { input: 1.1, output: 4.4 },
31
+ "openai/o4-mini": { input: 1.1, output: 4.4 },
32
+ "openai/o1": { input: 15, output: 60 },
33
+ "openai/o3": { input: 10, output: 40 },
34
+ // ── Anthropic ────────────────────────────────────────────────────────────
35
+ // Claude 3.5 family
24
36
  "claude-3-5-sonnet-20241022": { input: 3, output: 15 },
25
37
  "claude-3-5-sonnet": { input: 3, output: 15 },
26
- "claude-sonnet-4-20250514": { input: 3, output: 15 },
27
38
  "claude-3-5-haiku-20241022": { input: 0.8, output: 4 },
39
+ "claude-3-5-haiku": { input: 0.8, output: 4 },
40
+ // Claude 3 family
28
41
  "claude-3-opus-20240229": { input: 15, output: 75 },
42
+ "claude-3-opus": { input: 15, output: 75 },
43
+ "claude-3-sonnet-20240229": { input: 3, output: 15 },
29
44
  "claude-3-haiku-20240307": { input: 0.25, output: 1.25 },
30
- // Qwen (typically free/cheap on local, but OpenRouter pricing)
45
+ "claude-3-haiku": { input: 0.25, output: 1.25 },
46
+ // Claude 4 family (2025)
47
+ "claude-sonnet-4-20250514": { input: 3, output: 15 },
48
+ "claude-sonnet-4": { input: 3, output: 15 },
49
+ "claude-opus-4-20250514": { input: 15, output: 75 },
50
+ "claude-opus-4": { input: 15, output: 75 },
51
+ "claude-haiku-4": { input: 0.8, output: 4 },
52
+ // Provider-prefixed (OpenRouter)
53
+ "anthropic/claude-3-5-sonnet": { input: 3, output: 15 },
54
+ "anthropic/claude-3-5-haiku": { input: 0.8, output: 4 },
55
+ "anthropic/claude-3-opus": { input: 15, output: 75 },
56
+ "anthropic/claude-sonnet-4-20250514": { input: 3, output: 15 },
57
+ "anthropic/claude-opus-4-20250514": { input: 15, output: 75 },
58
+ // ── Google Gemini ────────────────────────────────────────────────────────
59
+ // Gemini 1.5
60
+ "gemini-1.5-pro": { input: 1.25, output: 5 },
61
+ "gemini-1.5-flash": { input: 0.075, output: 0.3 },
62
+ "gemini-1.5-flash-8b": { input: 0.0375, output: 0.15 },
63
+ // Gemini 2.0
64
+ "gemini-2.0-flash": { input: 0.1, output: 0.4 },
65
+ "gemini-2.0-flash-lite": { input: 0.075, output: 0.3 },
66
+ "gemini-2.0-pro": { input: 1.25, output: 10 },
67
+ // Gemini 2.5
68
+ "gemini-2.5-pro": { input: 1.25, output: 10 },
69
+ "gemini-2.5-flash": { input: 0.15, output: 0.6 },
70
+ // Provider-prefixed (OpenRouter / LM Studio)
71
+ "google/gemini-pro-1.5": { input: 1.25, output: 5 },
72
+ "google/gemini-flash-1.5": { input: 0.075, output: 0.3 },
73
+ "google/gemini-2.0-flash": { input: 0.1, output: 0.4 },
74
+ "google/gemini-2.5-pro": { input: 1.25, output: 10 },
75
+ "google/gemini-2.5-flash": { input: 0.15, output: 0.6 },
76
+ // ── Qwen ─────────────────────────────────────────────────────────────────
31
77
  "qwen/qwen-2.5-coder-32b-instruct": { input: 0.2, output: 0.2 },
32
78
  "qwen/qwen-2.5-72b-instruct": { input: 0.35, output: 0.4 },
33
- // DeepSeek
79
+ "qwen/qwq-32b": { input: 0.15, output: 0.2 },
80
+ "qwen/qwen3-235b-a22b": { input: 0.2, output: 0.4 },
81
+ // ── DeepSeek ─────────────────────────────────────────────────────────────
34
82
  "deepseek/deepseek-chat": { input: 0.14, output: 0.28 },
35
83
  "deepseek/deepseek-coder": { input: 0.14, output: 0.28 },
36
- // Llama
84
+ "deepseek/deepseek-r1": { input: 0.55, output: 2.19 },
85
+ "deepseek-chat": { input: 0.14, output: 0.28 },
86
+ "deepseek-reasoner": { input: 0.55, output: 2.19 },
87
+ // ── Meta Llama ───────────────────────────────────────────────────────────
37
88
  "meta-llama/llama-3.1-70b-instruct": { input: 0.52, output: 0.75 },
38
89
  "meta-llama/llama-3.1-8b-instruct": { input: 0.055, output: 0.055 },
39
- // Google
40
- "google/gemini-pro-1.5": { input: 1.25, output: 5 },
41
- "google/gemini-flash-1.5": { input: 0.075, output: 0.3 },
90
+ "meta-llama/llama-3.3-70b-instruct": { input: 0.12, output: 0.3 },
91
+ "meta-llama/llama-4-scout": { input: 0.11, output: 0.34 },
92
+ "meta-llama/llama-4-maverick": { input: 0.22, output: 0.88 },
93
+ // ── Mistral ──────────────────────────────────────────────────────────────
94
+ "mistral/mistral-large": { input: 2, output: 6 },
95
+ "mistral/mistral-small": { input: 0.1, output: 0.3 },
96
+ "mistral/codestral": { input: 0.3, output: 0.9 },
42
97
  };
43
- function getModelCost(model) {
98
+ export function getModelCost(model) {
44
99
  // Direct match
45
100
  if (MODEL_COSTS[model])
46
101
  return MODEL_COSTS[model];
@@ -290,7 +345,7 @@ export class CodingAgent {
290
345
  // Check approval for dangerous tools
291
346
  if (DANGEROUS_TOOLS.has(toolCall.name) && !this.autoApprove && !this.alwaysApproved.has(toolCall.name)) {
292
347
  if (this.options.onToolApproval) {
293
- // Generate diff for write_file if file already exists
348
+ // Generate diff preview for file-modifying tools
294
349
  let diff;
295
350
  if (toolCall.name === "write_file" && args.path && args.content) {
296
351
  const existing = getExistingContent(String(args.path), this.cwd);
@@ -298,6 +353,16 @@ export class CodingAgent {
298
353
  diff = generateDiff(existing, String(args.content), String(args.path));
299
354
  }
300
355
  }
356
+ if (toolCall.name === "edit_file" && args.path && args.oldText !== undefined && args.newText !== undefined) {
357
+ const existing = getExistingContent(String(args.path), this.cwd);
358
+ if (existing !== null) {
359
+ const oldText = String(args.oldText);
360
+ const newText = String(args.newText);
361
+ const replaceAll = Boolean(args.replaceAll);
362
+ const next = replaceAll ? existing.split(oldText).join(newText) : existing.replace(oldText, newText);
363
+ diff = generateDiff(existing, next, String(args.path));
364
+ }
365
+ }
301
366
  const decision = await this.options.onToolApproval(toolCall.name, args, diff);
302
367
  if (decision === "no") {
303
368
  const denied = `Tool call "${toolCall.name}" was denied by the user.`;
@@ -327,7 +392,7 @@ export class CodingAgent {
327
392
  }
328
393
  this.options.onToolResult?.(toolCall.name, result);
329
394
  // Auto-commit after successful write_file (only if enabled)
330
- if (this.gitEnabled && this.autoCommitEnabled && toolCall.name === "write_file" && result.startsWith("✅")) {
395
+ if (this.gitEnabled && this.autoCommitEnabled && ["write_file", "edit_file"].includes(toolCall.name) && result.startsWith("✅")) {
331
396
  const path = String(args.path ?? "unknown");
332
397
  const committed = autoCommit(this.cwd, path, "write");
333
398
  if (committed) {
@@ -335,7 +400,7 @@ export class CodingAgent {
335
400
  }
336
401
  }
337
402
  // Auto-lint after successful write_file
338
- if (this.autoLintEnabled && this.detectedLinter && toolCall.name === "write_file" && result.startsWith("✅")) {
403
+ if (this.autoLintEnabled && this.detectedLinter && ["write_file", "edit_file"].includes(toolCall.name) && result.startsWith("✅")) {
339
404
  const filePath = String(args.path ?? "");
340
405
  const lintErrors = runLinter(this.detectedLinter, filePath, this.cwd);
341
406
  if (lintErrors) {
@@ -503,6 +568,16 @@ export class CodingAgent {
503
568
  diff = generateDiff(existing, String(args.content), String(args.path));
504
569
  }
505
570
  }
571
+ if (toolCall.name === "edit_file" && args.path && args.oldText !== undefined && args.newText !== undefined) {
572
+ const existing = getExistingContent(String(args.path), this.cwd);
573
+ if (existing !== null) {
574
+ const oldText = String(args.oldText);
575
+ const newText = String(args.newText);
576
+ const replaceAll = Boolean(args.replaceAll);
577
+ const next = replaceAll ? existing.split(oldText).join(newText) : existing.replace(oldText, newText);
578
+ diff = generateDiff(existing, next, String(args.path));
579
+ }
580
+ }
506
581
  const decision = await this.options.onToolApproval(toolCall.name, args, diff);
507
582
  if (decision === "no") {
508
583
  const denied = `Tool call "${toolCall.name}" was denied by the user.`;
@@ -532,7 +607,7 @@ export class CodingAgent {
532
607
  }
533
608
  this.options.onToolResult?.(toolCall.name, result);
534
609
  // Auto-commit after successful write_file
535
- if (this.gitEnabled && this.autoCommitEnabled && toolCall.name === "write_file" && result.startsWith("✅")) {
610
+ if (this.gitEnabled && this.autoCommitEnabled && ["write_file", "edit_file"].includes(toolCall.name) && result.startsWith("✅")) {
536
611
  const path = String(args.path ?? "unknown");
537
612
  const committed = autoCommit(this.cwd, path, "write");
538
613
  if (committed) {
@@ -540,7 +615,7 @@ export class CodingAgent {
540
615
  }
541
616
  }
542
617
  // Auto-lint after successful write_file
543
- if (this.autoLintEnabled && this.detectedLinter && toolCall.name === "write_file" && result.startsWith("✅")) {
618
+ if (this.autoLintEnabled && this.detectedLinter && ["write_file", "edit_file"].includes(toolCall.name) && result.startsWith("✅")) {
544
619
  const filePath = String(args.path ?? "");
545
620
  const lintErrors = runLinter(this.detectedLinter, filePath, this.cwd);
546
621
  if (lintErrors) {
@@ -0,0 +1,2 @@
1
+ import type { AddMsg } from "./types.js";
2
+ export declare function tryHandleGitCommand(trimmed: string, cwd: string, addMsg: AddMsg): boolean;
@@ -0,0 +1,50 @@
1
+ import { exec as execAsync } from "child_process";
2
+ import { promisify } from "util";
3
+ import { getDiff, undoLastCommit } from "../utils/git.js";
4
+ import { compactCommandOutput, getCommandErrorMessage } from "./output.js";
5
+ const execPromise = promisify(execAsync);
6
+ export function tryHandleGitCommand(trimmed, cwd, addMsg) {
7
+ if (trimmed === "/diff") {
8
+ const diff = getDiff(cwd);
9
+ addMsg("info", diff);
10
+ return true;
11
+ }
12
+ if (trimmed === "/undo") {
13
+ const result = undoLastCommit(cwd);
14
+ addMsg("info", result.success ? `✅ ${result.message}` : `✗ ${result.message}`);
15
+ return true;
16
+ }
17
+ if (trimmed === "/push") {
18
+ addMsg("info", "⏳ Pushing to remote...");
19
+ execPromise("git push", { cwd })
20
+ .then(({ stdout, stderr }) => {
21
+ const out = compactCommandOutput(stdout + stderr);
22
+ addMsg("info", `✅ Pushed to remote${out ? ` — ${out}` : ""}`);
23
+ })
24
+ .catch((e) => {
25
+ const message = getCommandErrorMessage(e);
26
+ addMsg("error", `Push failed${message ? ` — ${message}` : ""}`);
27
+ });
28
+ return true;
29
+ }
30
+ if (trimmed.startsWith("/commit")) {
31
+ const msg = trimmed.replace("/commit", "").trim();
32
+ if (!msg) {
33
+ addMsg("info", "Usage: /commit your commit message here");
34
+ return true;
35
+ }
36
+ addMsg("info", "⏳ Committing...");
37
+ execPromise("git add -A", { cwd })
38
+ .then(() => execPromise(`git commit -m ${JSON.stringify(msg)}`, { cwd }))
39
+ .then(({ stdout, stderr }) => {
40
+ const out = compactCommandOutput(stdout + stderr);
41
+ addMsg("info", `✅ Committed: ${msg}${out ? ` — ${out}` : ""}`);
42
+ })
43
+ .catch((e) => {
44
+ const message = getCommandErrorMessage(e);
45
+ addMsg("error", `Commit failed${message ? ` — ${message}` : ""}`);
46
+ });
47
+ return true;
48
+ }
49
+ return false;
50
+ }
@@ -0,0 +1,27 @@
1
+ import { type PullProgress } from "../utils/ollama.js";
2
+ import type { AddMsg } from "./types.js";
3
+ type SetState<T> = (value: T) => void;
4
+ interface HandleOllamaCommandOptions {
5
+ trimmed: string;
6
+ addMsg: AddMsg;
7
+ refreshConnectionBanner: () => Promise<void>;
8
+ setOllamaPullPicker: SetState<boolean>;
9
+ setOllamaPullPickerIndex: SetState<number>;
10
+ setOllamaPulling: SetState<{
11
+ model: string;
12
+ progress: PullProgress;
13
+ } | null>;
14
+ setOllamaDeletePicker: SetState<{
15
+ models: {
16
+ name: string;
17
+ size: number;
18
+ }[];
19
+ } | null>;
20
+ setOllamaDeletePickerIndex: SetState<number>;
21
+ setOllamaDeleteConfirm: SetState<{
22
+ model: string;
23
+ size: number;
24
+ } | null>;
25
+ }
26
+ export declare function tryHandleOllamaCommand(options: HandleOllamaCommandOptions): Promise<boolean>;
27
+ export {};
@@ -0,0 +1,171 @@
1
+ import { isOllamaInstalled, isOllamaRunning, getOllamaInstallCommand, startOllama, stopOllama, pullModel, listInstalledModelsDetailed, getGPUMemoryUsage, } from "../utils/ollama.js";
2
+ async function ensureOllamaRunning(addMsg, startMessage, failMessage) {
3
+ let running = await isOllamaRunning();
4
+ if (running)
5
+ return true;
6
+ addMsg("info", startMessage);
7
+ startOllama();
8
+ for (let i = 0; i < 10; i++) {
9
+ await new Promise((resolve) => setTimeout(resolve, 1000));
10
+ if (await isOllamaRunning()) {
11
+ running = true;
12
+ break;
13
+ }
14
+ }
15
+ if (!running) {
16
+ addMsg("error", failMessage);
17
+ return false;
18
+ }
19
+ return true;
20
+ }
21
+ export async function tryHandleOllamaCommand(options) {
22
+ const { trimmed, addMsg, refreshConnectionBanner, setOllamaPullPicker, setOllamaPullPickerIndex, setOllamaPulling, setOllamaDeletePicker, setOllamaDeletePickerIndex, setOllamaDeleteConfirm, } = options;
23
+ if (trimmed === "/ollama" || trimmed === "/ollama status") {
24
+ const running = await isOllamaRunning();
25
+ const lines = [`Ollama: ${running ? "running" : "stopped"}`];
26
+ if (running) {
27
+ const models = await listInstalledModelsDetailed();
28
+ if (models.length > 0) {
29
+ lines.push(`Installed models (${models.length}):`);
30
+ for (const model of models) {
31
+ const sizeGB = (model.size / (1024 * 1024 * 1024)).toFixed(1);
32
+ lines.push(` ${model.name} (${sizeGB} GB)`);
33
+ }
34
+ }
35
+ else {
36
+ lines.push("No models installed.");
37
+ }
38
+ const gpuMem = getGPUMemoryUsage();
39
+ if (gpuMem)
40
+ lines.push(`GPU: ${gpuMem}`);
41
+ }
42
+ else {
43
+ lines.push("Start with: /ollama start");
44
+ }
45
+ addMsg("info", lines.join("\n"));
46
+ return true;
47
+ }
48
+ if (trimmed === "/ollama list") {
49
+ const running = await isOllamaRunning();
50
+ if (!running) {
51
+ addMsg("info", "Ollama is not running. Start with /ollama start");
52
+ return true;
53
+ }
54
+ const models = await listInstalledModelsDetailed();
55
+ if (models.length === 0) {
56
+ addMsg("info", "No models installed. Pull one with /ollama pull <model>");
57
+ }
58
+ else {
59
+ const lines = models.map((model) => {
60
+ const sizeGB = (model.size / (1024 * 1024 * 1024)).toFixed(1);
61
+ return ` ${model.name} (${sizeGB} GB)`;
62
+ });
63
+ addMsg("info", `Installed models:\n${lines.join("\n")}`);
64
+ }
65
+ return true;
66
+ }
67
+ if (trimmed === "/ollama start") {
68
+ const running = await isOllamaRunning();
69
+ if (running) {
70
+ addMsg("info", "Ollama is already running.");
71
+ return true;
72
+ }
73
+ if (!isOllamaInstalled()) {
74
+ addMsg("error", `Ollama is not installed. Install with: ${getOllamaInstallCommand(process.platform === "darwin" ? "macos" : process.platform === "win32" ? "windows" : "linux")}`);
75
+ return true;
76
+ }
77
+ startOllama();
78
+ addMsg("info", "Starting Ollama server...");
79
+ for (let i = 0; i < 10; i++) {
80
+ await new Promise((resolve) => setTimeout(resolve, 1000));
81
+ if (await isOllamaRunning()) {
82
+ addMsg("info", "Ollama is running.");
83
+ await refreshConnectionBanner();
84
+ return true;
85
+ }
86
+ }
87
+ addMsg("error", "Ollama did not start in time. Try running 'ollama serve' manually.");
88
+ return true;
89
+ }
90
+ if (trimmed === "/ollama stop") {
91
+ const running = await isOllamaRunning();
92
+ if (!running) {
93
+ addMsg("info", "Ollama is not running.");
94
+ return true;
95
+ }
96
+ addMsg("info", "Stopping Ollama...");
97
+ const result = await stopOllama();
98
+ addMsg(result.ok ? "info" : "error", result.ok ? `✅ ${result.message}` : `❌ ${result.message}`);
99
+ if (result.ok)
100
+ await refreshConnectionBanner();
101
+ return true;
102
+ }
103
+ if (trimmed === "/ollama pull") {
104
+ setOllamaPullPicker(true);
105
+ setOllamaPullPickerIndex(0);
106
+ return true;
107
+ }
108
+ if (trimmed.startsWith("/ollama pull ")) {
109
+ const modelId = trimmed.replace("/ollama pull ", "").trim();
110
+ if (!modelId) {
111
+ setOllamaPullPicker(true);
112
+ setOllamaPullPickerIndex(0);
113
+ return true;
114
+ }
115
+ if (!isOllamaInstalled()) {
116
+ addMsg("error", "Ollama is not installed.");
117
+ return true;
118
+ }
119
+ const running = await ensureOllamaRunning(addMsg, "Starting Ollama server...", "Could not start Ollama. Run 'ollama serve' manually.");
120
+ if (!running)
121
+ return true;
122
+ setOllamaPulling({ model: modelId, progress: { status: "starting", percent: 0 } });
123
+ try {
124
+ await pullModel(modelId, (progress) => {
125
+ setOllamaPulling({ model: modelId, progress });
126
+ });
127
+ setOllamaPulling(null);
128
+ addMsg("info", `✅ Downloaded ${modelId}`);
129
+ }
130
+ catch (err) {
131
+ setOllamaPulling(null);
132
+ addMsg("error", `Failed to pull ${modelId}: ${err.message}`);
133
+ }
134
+ return true;
135
+ }
136
+ if (trimmed === "/ollama delete") {
137
+ const running = await ensureOllamaRunning(addMsg, "Starting Ollama to list models...", "Could not start Ollama. Start it manually first.");
138
+ if (!running)
139
+ return true;
140
+ const models = await listInstalledModelsDetailed();
141
+ if (models.length === 0) {
142
+ addMsg("info", "No models installed.");
143
+ return true;
144
+ }
145
+ setOllamaDeletePicker({ models: models.map((model) => ({ name: model.name, size: model.size })) });
146
+ setOllamaDeletePickerIndex(0);
147
+ return true;
148
+ }
149
+ if (trimmed.startsWith("/ollama delete ")) {
150
+ const modelId = trimmed.replace("/ollama delete ", "").trim();
151
+ if (!modelId) {
152
+ const models = await listInstalledModelsDetailed();
153
+ if (models.length === 0) {
154
+ addMsg("info", "No models installed.");
155
+ return true;
156
+ }
157
+ setOllamaDeletePicker({ models: models.map((model) => ({ name: model.name, size: model.size })) });
158
+ setOllamaDeletePickerIndex(0);
159
+ return true;
160
+ }
161
+ const models = await listInstalledModelsDetailed();
162
+ const found = models.find((model) => model.name === modelId || model.name.startsWith(modelId));
163
+ if (!found) {
164
+ addMsg("error", `Model "${modelId}" not found. Use /ollama list to see installed models.`);
165
+ return true;
166
+ }
167
+ setOllamaDeleteConfirm({ model: found.name, size: found.size });
168
+ return true;
169
+ }
170
+ return false;
171
+ }
@@ -0,0 +1,2 @@
1
+ export declare function compactCommandOutput(value: unknown, maxLen?: number): string;
2
+ export declare function getCommandErrorMessage(error: any): string;
@@ -0,0 +1,18 @@
1
+ export function compactCommandOutput(value, maxLen = 160) {
2
+ const text = String(value ?? "")
3
+ .replace(/\r/g, "")
4
+ .split("\n")
5
+ .map((line) => line.trim())
6
+ .filter(Boolean)
7
+ .join(" | ")
8
+ .replace(/\s+/g, " ")
9
+ .trim();
10
+ if (!text)
11
+ return "";
12
+ if (text.length <= maxLen)
13
+ return text;
14
+ return text.slice(0, maxLen - 1) + "…";
15
+ }
16
+ export function getCommandErrorMessage(error) {
17
+ return compactCommandOutput(error?.stderr || error?.stdout || error?.message || error);
18
+ }
@@ -0,0 +1,2 @@
1
+ import type { CommandRegistryContext, CommandRegistryHandler } from "./types.js";
2
+ export declare function dispatchRegisteredCommands<TContext extends CommandRegistryContext>(handlers: Array<CommandRegistryHandler<TContext>>, context: TContext): Promise<boolean>;
@@ -0,0 +1,8 @@
1
+ export async function dispatchRegisteredCommands(handlers, context) {
2
+ for (const handler of handlers) {
3
+ if (await handler(context)) {
4
+ return true;
5
+ }
6
+ }
7
+ return false;
8
+ }
@@ -0,0 +1,18 @@
1
+ import { type Dispatch, type SetStateAction } from "react";
2
+ import type { CodingAgent } from "../agent.js";
3
+ import type { AddMsg } from "./types.js";
4
+ type SkillsPickerMode = "menu" | "browse" | "installed" | "remove" | null;
5
+ interface HandleSkillsCommandOptions {
6
+ trimmed: string;
7
+ cwd: string;
8
+ addMsg: AddMsg;
9
+ agent: CodingAgent | null;
10
+ sessionDisabledSkills: Set<string>;
11
+ setSkillsPicker: Dispatch<SetStateAction<SkillsPickerMode>>;
12
+ setSkillsPickerIndex: Dispatch<SetStateAction<number>>;
13
+ setSessionDisabledSkills: Dispatch<SetStateAction<Set<string>>>;
14
+ setInput: Dispatch<SetStateAction<string>>;
15
+ setInputKey: Dispatch<SetStateAction<number>>;
16
+ }
17
+ export declare function tryHandleSkillsCommand(options: HandleSkillsCommandOptions): boolean;
18
+ export {};