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 +16 -16
- package/dist/agent.d.ts +7 -0
- package/dist/agent.js +51 -2
- package/dist/exec.js +10 -0
- package/dist/index.js +322 -4
- package/dist/utils/hardware.d.ts +17 -0
- package/dist/utils/hardware.js +120 -0
- package/dist/utils/mcp.d.ts +55 -0
- package/dist/utils/mcp.js +251 -0
- package/dist/utils/models.d.ts +17 -0
- package/dist/utils/models.js +113 -0
- package/dist/utils/ollama.d.ts +22 -0
- package/dist/utils/ollama.js +121 -0
- package/package.json +2 -1
- package/src/agent.ts +55 -2
- package/src/exec.ts +12 -0
- package/src/index.tsx +413 -2
- package/src/utils/hardware.ts +131 -0
- package/src/utils/mcp.ts +307 -0
- package/src/utils/models.ts +137 -0
- package/src/utils/ollama.ts +137 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { execSync, spawn } from "child_process";
|
|
2
|
+
/** Check if ollama binary exists on PATH */
|
|
3
|
+
export function isOllamaInstalled() {
|
|
4
|
+
try {
|
|
5
|
+
const cmd = process.platform === "win32" ? "where ollama" : "which ollama";
|
|
6
|
+
execSync(cmd, { stdio: ["pipe", "pipe", "pipe"], timeout: 3000 });
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** Check if ollama server is responding */
|
|
14
|
+
export async function isOllamaRunning() {
|
|
15
|
+
try {
|
|
16
|
+
const controller = new AbortController();
|
|
17
|
+
const timeout = setTimeout(() => controller.abort(), 2000);
|
|
18
|
+
const res = await fetch("http://localhost:11434/api/tags", { signal: controller.signal });
|
|
19
|
+
clearTimeout(timeout);
|
|
20
|
+
return res.ok;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Get the install command for the user's OS */
|
|
27
|
+
export function getOllamaInstallCommand(os) {
|
|
28
|
+
switch (os) {
|
|
29
|
+
case "macos": return "brew install ollama";
|
|
30
|
+
case "linux": return "curl -fsSL https://ollama.com/install.sh | sh";
|
|
31
|
+
case "windows": return "winget install Ollama.Ollama";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Start ollama serve in background */
|
|
35
|
+
export function startOllama() {
|
|
36
|
+
const child = spawn("ollama", ["serve"], {
|
|
37
|
+
detached: true,
|
|
38
|
+
stdio: "ignore",
|
|
39
|
+
});
|
|
40
|
+
child.unref();
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Pull a model from Ollama registry.
|
|
44
|
+
* Calls onProgress with download updates.
|
|
45
|
+
* Returns a promise that resolves when complete.
|
|
46
|
+
*/
|
|
47
|
+
export function pullModel(modelId, onProgress) {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const child = spawn("ollama", ["pull", modelId], {
|
|
50
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
51
|
+
});
|
|
52
|
+
let lastOutput = "";
|
|
53
|
+
const parseLine = (data) => {
|
|
54
|
+
lastOutput = data;
|
|
55
|
+
// Ollama pull output looks like:
|
|
56
|
+
// pulling manifest
|
|
57
|
+
// pulling abc123... 58% ▕██████████░░░░░░░░░░▏ 2.9 GB/5.0 GB
|
|
58
|
+
// verifying sha256 digest
|
|
59
|
+
// writing manifest
|
|
60
|
+
// success
|
|
61
|
+
// Try to parse percentage
|
|
62
|
+
const pctMatch = data.match(/(\d+)%/);
|
|
63
|
+
const sizeMatch = data.match(/([\d.]+)\s*GB\s*\/\s*([\d.]+)\s*GB/);
|
|
64
|
+
if (pctMatch) {
|
|
65
|
+
const percent = parseInt(pctMatch[1]);
|
|
66
|
+
let completed;
|
|
67
|
+
let total;
|
|
68
|
+
if (sizeMatch) {
|
|
69
|
+
completed = parseFloat(sizeMatch[1]) * 1024 * 1024 * 1024;
|
|
70
|
+
total = parseFloat(sizeMatch[2]) * 1024 * 1024 * 1024;
|
|
71
|
+
}
|
|
72
|
+
onProgress?.({ status: "downloading", total, completed, percent });
|
|
73
|
+
}
|
|
74
|
+
else if (data.includes("pulling manifest")) {
|
|
75
|
+
onProgress?.({ status: "pulling manifest", percent: 0 });
|
|
76
|
+
}
|
|
77
|
+
else if (data.includes("verifying")) {
|
|
78
|
+
onProgress?.({ status: "verifying", percent: 100 });
|
|
79
|
+
}
|
|
80
|
+
else if (data.includes("writing manifest")) {
|
|
81
|
+
onProgress?.({ status: "writing manifest", percent: 100 });
|
|
82
|
+
}
|
|
83
|
+
else if (data.includes("success")) {
|
|
84
|
+
onProgress?.({ status: "success", percent: 100 });
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
child.stdout?.on("data", (data) => {
|
|
88
|
+
parseLine(data.toString().trim());
|
|
89
|
+
});
|
|
90
|
+
child.stderr?.on("data", (data) => {
|
|
91
|
+
// Ollama writes progress to stderr
|
|
92
|
+
parseLine(data.toString().trim());
|
|
93
|
+
});
|
|
94
|
+
child.on("close", (code) => {
|
|
95
|
+
if (code === 0) {
|
|
96
|
+
resolve();
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
reject(new Error(`ollama pull failed (exit ${code}): ${lastOutput}`));
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
child.on("error", (err) => {
|
|
103
|
+
reject(new Error(`Failed to run ollama pull: ${err.message}`));
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/** List models installed in Ollama */
|
|
108
|
+
export async function listInstalledModels() {
|
|
109
|
+
try {
|
|
110
|
+
const controller = new AbortController();
|
|
111
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
112
|
+
const res = await fetch("http://localhost:11434/api/tags", { signal: controller.signal });
|
|
113
|
+
clearTimeout(timeout);
|
|
114
|
+
if (res.ok) {
|
|
115
|
+
const data = (await res.json());
|
|
116
|
+
return (data.models ?? []).map((m) => m.name);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch { /* not running */ }
|
|
120
|
+
return [];
|
|
121
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codemaxxing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Open-source terminal coding agent. Connect any LLM. Max your code.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
30
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
30
31
|
"@types/react": "^19.2.14",
|
|
31
32
|
"better-sqlite3": "^12.6.2",
|
|
32
33
|
"chalk": "^5.3.0",
|
package/src/agent.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { buildProjectContext, getSystemPrompt, loadProjectRules } from "./utils/
|
|
|
11
11
|
import { isGitRepo, autoCommit } from "./utils/git.js";
|
|
12
12
|
import { buildSkillPrompts, getActiveSkillCount } from "./utils/skills.js";
|
|
13
13
|
import { createSession, saveMessage, updateTokenEstimate, updateSessionCost, loadMessages } from "./utils/sessions.js";
|
|
14
|
+
import { loadMCPConfig, connectToServers, disconnectAll, getAllMCPTools, parseMCPToolName, callMCPTool, getConnectedServers, type ConnectedServer } from "./utils/mcp.js";
|
|
14
15
|
import type { ProviderConfig } from "./config.js";
|
|
15
16
|
|
|
16
17
|
// Tools that can modify your project — require approval
|
|
@@ -74,6 +75,7 @@ export interface AgentOptions {
|
|
|
74
75
|
onContextCompressed?: (oldTokens: number, newTokens: number) => void;
|
|
75
76
|
onArchitectPlan?: (plan: string) => void;
|
|
76
77
|
onLintResult?: (file: string, errors: string) => void;
|
|
78
|
+
onMCPStatus?: (server: string, status: string) => void;
|
|
77
79
|
contextCompressionThreshold?: number;
|
|
78
80
|
}
|
|
79
81
|
|
|
@@ -108,6 +110,7 @@ export class CodingAgent {
|
|
|
108
110
|
private architectModel: string | null = null;
|
|
109
111
|
private autoLintEnabled: boolean = true;
|
|
110
112
|
private detectedLinter: { command: string; name: string } | null = null;
|
|
113
|
+
private mcpServers: ConnectedServer[] = [];
|
|
111
114
|
|
|
112
115
|
constructor(private options: AgentOptions) {
|
|
113
116
|
this.providerType = options.provider.type || "openai";
|
|
@@ -145,6 +148,16 @@ export class CodingAgent {
|
|
|
145
148
|
// Detect project linter
|
|
146
149
|
this.detectedLinter = detectLinter(this.cwd);
|
|
147
150
|
|
|
151
|
+
// Connect to MCP servers
|
|
152
|
+
const mcpConfig = loadMCPConfig(this.cwd);
|
|
153
|
+
if (Object.keys(mcpConfig.mcpServers).length > 0) {
|
|
154
|
+
this.mcpServers = await connectToServers(mcpConfig, this.options.onMCPStatus);
|
|
155
|
+
if (this.mcpServers.length > 0) {
|
|
156
|
+
const mcpTools = getAllMCPTools(this.mcpServers);
|
|
157
|
+
this.tools = [...FILE_TOOLS, ...mcpTools];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
148
161
|
this.messages = [
|
|
149
162
|
{ role: "system", content: this.systemPrompt },
|
|
150
163
|
];
|
|
@@ -357,7 +370,14 @@ export class CodingAgent {
|
|
|
357
370
|
}
|
|
358
371
|
}
|
|
359
372
|
|
|
360
|
-
|
|
373
|
+
// Route to MCP or built-in tool
|
|
374
|
+
const mcpParsed = parseMCPToolName(toolCall.name);
|
|
375
|
+
let result: string;
|
|
376
|
+
if (mcpParsed) {
|
|
377
|
+
result = await callMCPTool(mcpParsed.serverName, mcpParsed.toolName, args);
|
|
378
|
+
} else {
|
|
379
|
+
result = await executeTool(toolCall.name, args, this.cwd);
|
|
380
|
+
}
|
|
361
381
|
this.options.onToolResult?.(toolCall.name, result);
|
|
362
382
|
|
|
363
383
|
// Auto-commit after successful write_file (only if enabled)
|
|
@@ -569,7 +589,14 @@ export class CodingAgent {
|
|
|
569
589
|
}
|
|
570
590
|
}
|
|
571
591
|
|
|
572
|
-
|
|
592
|
+
// Route to MCP or built-in tool
|
|
593
|
+
const mcpParsed = parseMCPToolName(toolCall.name);
|
|
594
|
+
let result: string;
|
|
595
|
+
if (mcpParsed) {
|
|
596
|
+
result = await callMCPTool(mcpParsed.serverName, mcpParsed.toolName, args);
|
|
597
|
+
} else {
|
|
598
|
+
result = await executeTool(toolCall.name, args, this.cwd);
|
|
599
|
+
}
|
|
573
600
|
this.options.onToolResult?.(toolCall.name, result);
|
|
574
601
|
|
|
575
602
|
// Auto-commit after successful write_file
|
|
@@ -834,6 +861,32 @@ export class CodingAgent {
|
|
|
834
861
|
return this.chat(editorPrompt);
|
|
835
862
|
}
|
|
836
863
|
|
|
864
|
+
getMCPServerCount(): number {
|
|
865
|
+
return this.mcpServers.length;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
getMCPServers(): ConnectedServer[] {
|
|
869
|
+
return this.mcpServers;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
async disconnectMCP(): Promise<void> {
|
|
873
|
+
await disconnectAll();
|
|
874
|
+
this.mcpServers = [];
|
|
875
|
+
this.tools = FILE_TOOLS;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
async reconnectMCP(): Promise<void> {
|
|
879
|
+
await this.disconnectMCP();
|
|
880
|
+
const mcpConfig = loadMCPConfig(this.cwd);
|
|
881
|
+
if (Object.keys(mcpConfig.mcpServers).length > 0) {
|
|
882
|
+
this.mcpServers = await connectToServers(mcpConfig, this.options.onMCPStatus);
|
|
883
|
+
if (this.mcpServers.length > 0) {
|
|
884
|
+
const mcpTools = getAllMCPTools(this.mcpServers);
|
|
885
|
+
this.tools = [...FILE_TOOLS, ...mcpTools];
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
837
890
|
reset(): void {
|
|
838
891
|
const systemMsg = this.messages[0];
|
|
839
892
|
this.messages = [systemMsg];
|
package/src/exec.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { CodingAgent } from "./agent.js";
|
|
9
9
|
import { loadConfig, applyOverrides, detectLocalProvider } from "./config.js";
|
|
10
10
|
import { getCredential } from "./utils/auth.js";
|
|
11
|
+
import { disconnectAll } from "./utils/mcp.js";
|
|
11
12
|
|
|
12
13
|
interface ExecArgs {
|
|
13
14
|
prompt: string;
|
|
@@ -140,10 +141,19 @@ export async function runExec(argv: string[]): Promise<void> {
|
|
|
140
141
|
process.stderr.write(`⚠ Denied ${name} (use --auto-approve to allow)\n`);
|
|
141
142
|
return "no";
|
|
142
143
|
},
|
|
144
|
+
onMCPStatus: (server, status) => {
|
|
145
|
+
process.stderr.write(`MCP ${server}: ${status}\n`);
|
|
146
|
+
},
|
|
143
147
|
});
|
|
144
148
|
|
|
145
149
|
try {
|
|
146
150
|
await agent.init();
|
|
151
|
+
|
|
152
|
+
const mcpCount = agent.getMCPServerCount();
|
|
153
|
+
if (mcpCount > 0) {
|
|
154
|
+
process.stderr.write(`MCP: ${mcpCount} server${mcpCount > 1 ? "s" : ""} connected\n`);
|
|
155
|
+
}
|
|
156
|
+
|
|
147
157
|
await agent.send(args.prompt);
|
|
148
158
|
|
|
149
159
|
if (!args.json) {
|
|
@@ -160,8 +170,10 @@ export async function runExec(argv: string[]): Promise<void> {
|
|
|
160
170
|
process.stdout.write(JSON.stringify(output, null, 2) + "\n");
|
|
161
171
|
}
|
|
162
172
|
|
|
173
|
+
await disconnectAll();
|
|
163
174
|
process.exit(hasChanges ? 0 : 2);
|
|
164
175
|
} catch (err: any) {
|
|
176
|
+
await disconnectAll();
|
|
165
177
|
process.stderr.write(`Error: ${err.message}\n`);
|
|
166
178
|
if (args.json) {
|
|
167
179
|
process.stdout.write(JSON.stringify({ error: err.message }, null, 2) + "\n");
|