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.
- package/README.md +6 -4
- package/dist/agent.d.ts +4 -0
- package/dist/agent.js +91 -16
- package/dist/commands/git.d.ts +2 -0
- package/dist/commands/git.js +50 -0
- package/dist/commands/ollama.d.ts +27 -0
- package/dist/commands/ollama.js +171 -0
- package/dist/commands/output.d.ts +2 -0
- package/dist/commands/output.js +18 -0
- package/dist/commands/registry.d.ts +2 -0
- package/dist/commands/registry.js +8 -0
- package/dist/commands/skills.d.ts +18 -0
- package/dist/commands/skills.js +121 -0
- package/dist/commands/types.d.ts +5 -0
- package/dist/commands/types.js +1 -0
- package/dist/commands/ui.d.ts +16 -0
- package/dist/commands/ui.js +79 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +13 -3
- package/dist/exec.js +4 -1
- package/dist/index.js +72 -388
- package/dist/tools/files.js +58 -3
- package/dist/utils/context.js +6 -0
- package/dist/utils/mcp.d.ts +7 -2
- package/dist/utils/mcp.js +34 -6
- package/package.json +8 -5
- package/src/agent.ts +0 -894
- package/src/auth-cli.ts +0 -287
- package/src/cli.ts +0 -37
- package/src/config.ts +0 -352
- package/src/exec.ts +0 -183
- package/src/index.tsx +0 -2621
- package/src/skills/registry.ts +0 -1436
- package/src/themes.ts +0 -335
- package/src/tools/files.ts +0 -374
- package/src/utils/auth.ts +0 -606
- package/src/utils/context.ts +0 -174
- package/src/utils/git.ts +0 -117
- package/src/utils/hardware.ts +0 -131
- package/src/utils/lint.ts +0 -116
- package/src/utils/mcp.ts +0 -307
- package/src/utils/models.ts +0 -218
- package/src/utils/ollama.ts +0 -352
- package/src/utils/repomap.ts +0 -220
- package/src/utils/sessions.ts +0 -254
- package/src/utils/skills.ts +0 -241
- 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
|
+
[](https://www.npmjs.com/package/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-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
"
|
|
41
|
-
"
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,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,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,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 {};
|