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.
@@ -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.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
- const result = await executeTool(toolCall.name, args, this.cwd);
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
- const result = await executeTool(toolCall.name, args, this.cwd);
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");