codemaxxing 0.2.1 → 0.3.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 +72 -6
- package/dist/agent.d.ts +34 -0
- package/dist/agent.js +159 -4
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +6 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.js +9 -0
- package/dist/exec.d.ts +7 -0
- package/dist/exec.js +164 -0
- package/dist/index.js +168 -4
- package/dist/utils/context.d.ts +9 -1
- package/dist/utils/context.js +31 -11
- package/dist/utils/lint.d.ts +13 -0
- package/dist/utils/lint.js +108 -0
- package/dist/utils/mcp.d.ts +55 -0
- package/dist/utils/mcp.js +251 -0
- package/package.json +2 -1
- package/src/agent.ts +179 -4
- package/src/cli.ts +5 -1
- package/src/config.ts +11 -0
- package/src/exec.ts +183 -0
- package/src/index.tsx +167 -3
- package/src/utils/context.ts +34 -12
- package/src/utils/lint.ts +116 -0
- package/src/utils/mcp.ts +307 -0
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ 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";
|
|
15
16
|
const VERSION = "0.1.9";
|
|
16
17
|
// ── Helpers ──
|
|
17
18
|
function formatTimeAgo(date) {
|
|
@@ -54,6 +55,15 @@ const SLASH_COMMANDS = [
|
|
|
54
55
|
{ cmd: "/skills search", desc: "search registry" },
|
|
55
56
|
{ cmd: "/skills on", desc: "enable skill for session" },
|
|
56
57
|
{ cmd: "/skills off", desc: "disable skill for session" },
|
|
58
|
+
{ cmd: "/architect", desc: "toggle architect mode" },
|
|
59
|
+
{ cmd: "/lint", desc: "show auto-lint status" },
|
|
60
|
+
{ cmd: "/lint on", desc: "enable auto-lint" },
|
|
61
|
+
{ cmd: "/lint off", desc: "disable auto-lint" },
|
|
62
|
+
{ cmd: "/mcp", desc: "show MCP servers" },
|
|
63
|
+
{ cmd: "/mcp tools", desc: "list MCP tools" },
|
|
64
|
+
{ cmd: "/mcp add", desc: "add MCP server" },
|
|
65
|
+
{ cmd: "/mcp remove", desc: "remove MCP server" },
|
|
66
|
+
{ cmd: "/mcp reconnect", desc: "reconnect MCP servers" },
|
|
57
67
|
{ cmd: "/quit", desc: "exit" },
|
|
58
68
|
];
|
|
59
69
|
const SPINNER_FRAMES = ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"];
|
|
@@ -236,6 +246,15 @@ function App() {
|
|
|
236
246
|
const savedStr = saved >= 1000 ? `${(saved / 1000).toFixed(1)}k` : String(saved);
|
|
237
247
|
addMsg("info", `📦 Context compressed (~${savedStr} tokens freed)`);
|
|
238
248
|
},
|
|
249
|
+
onArchitectPlan: (plan) => {
|
|
250
|
+
addMsg("info", `🏗️ Architect Plan:\n${plan}`);
|
|
251
|
+
},
|
|
252
|
+
onLintResult: (file, errors) => {
|
|
253
|
+
addMsg("info", `🔍 Lint errors in ${file}:\n${errors}`);
|
|
254
|
+
},
|
|
255
|
+
onMCPStatus: (server, status) => {
|
|
256
|
+
addMsg("info", `🔌 MCP ${server}: ${status}`);
|
|
257
|
+
},
|
|
239
258
|
contextCompressionThreshold: config.defaults.contextCompressionThreshold,
|
|
240
259
|
onToolApproval: (name, args, diff) => {
|
|
241
260
|
return new Promise((resolve) => {
|
|
@@ -246,6 +265,18 @@ function App() {
|
|
|
246
265
|
});
|
|
247
266
|
// Initialize async context (repo map)
|
|
248
267
|
await a.init();
|
|
268
|
+
// Show project rules in banner
|
|
269
|
+
const rulesSource = a.getProjectRulesSource();
|
|
270
|
+
if (rulesSource) {
|
|
271
|
+
info.push(`📋 ${rulesSource} loaded`);
|
|
272
|
+
setConnectionInfo([...info]);
|
|
273
|
+
}
|
|
274
|
+
// Show MCP server count
|
|
275
|
+
const mcpCount = a.getMCPServerCount();
|
|
276
|
+
if (mcpCount > 0) {
|
|
277
|
+
info.push(`🔌 ${mcpCount} MCP server${mcpCount > 1 ? "s" : ""} connected`);
|
|
278
|
+
setConnectionInfo([...info]);
|
|
279
|
+
}
|
|
249
280
|
setAgent(a);
|
|
250
281
|
setModelName(provider.model);
|
|
251
282
|
providerRef.current = { baseUrl: provider.baseUrl, apiKey: provider.apiKey };
|
|
@@ -287,7 +318,7 @@ function App() {
|
|
|
287
318
|
// Commands that need args (like /commit, /model) — fill input instead of executing
|
|
288
319
|
if (selected.cmd === "/commit" || selected.cmd === "/model" || selected.cmd === "/session delete" ||
|
|
289
320
|
selected.cmd === "/skills install" || selected.cmd === "/skills remove" || selected.cmd === "/skills search" ||
|
|
290
|
-
selected.cmd === "/skills on" || selected.cmd === "/skills off") {
|
|
321
|
+
selected.cmd === "/skills on" || selected.cmd === "/skills off" || selected.cmd === "/architect") {
|
|
291
322
|
setInput(selected.cmd + " ");
|
|
292
323
|
setCmdIndex(0);
|
|
293
324
|
setInputKey((k) => k + 1);
|
|
@@ -347,6 +378,15 @@ function App() {
|
|
|
347
378
|
" /git on — enable auto-commits",
|
|
348
379
|
" /git off — disable auto-commits",
|
|
349
380
|
" /skills — manage skill packs",
|
|
381
|
+
" /architect — toggle architect mode (plan then execute)",
|
|
382
|
+
" /lint — show auto-lint status & detected linter",
|
|
383
|
+
" /lint on — enable auto-lint",
|
|
384
|
+
" /lint off — disable auto-lint",
|
|
385
|
+
" /mcp — show MCP servers & status",
|
|
386
|
+
" /mcp tools — list all MCP tools",
|
|
387
|
+
" /mcp add — add MCP server to global config",
|
|
388
|
+
" /mcp remove — remove MCP server",
|
|
389
|
+
" /mcp reconnect — reconnect all MCP servers",
|
|
350
390
|
" /quit — exit",
|
|
351
391
|
].join("\n"));
|
|
352
392
|
return;
|
|
@@ -455,6 +495,130 @@ function App() {
|
|
|
455
495
|
addMsg("info", `✅ Switched to theme: ${THEMES[themeName].name}`);
|
|
456
496
|
return;
|
|
457
497
|
}
|
|
498
|
+
// ── Architect commands (work without agent) ──
|
|
499
|
+
if (trimmed === "/architect") {
|
|
500
|
+
if (!agent) {
|
|
501
|
+
addMsg("info", "🏗️ Architect mode: no agent connected. Connect first with /login or /connect.");
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
const current = agent.getArchitectModel();
|
|
505
|
+
if (current) {
|
|
506
|
+
agent.setArchitectModel(null);
|
|
507
|
+
addMsg("info", "🏗️ Architect mode OFF");
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
// Use config default or a sensible default
|
|
511
|
+
const defaultModel = loadConfig().defaults.architectModel || agent.getModel();
|
|
512
|
+
agent.setArchitectModel(defaultModel);
|
|
513
|
+
addMsg("info", `🏗️ Architect mode ON (planner: ${defaultModel})`);
|
|
514
|
+
}
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (trimmed.startsWith("/architect ")) {
|
|
518
|
+
const model = trimmed.replace("/architect ", "").trim();
|
|
519
|
+
if (!model) {
|
|
520
|
+
addMsg("info", "Usage: /architect <model> or /architect to toggle");
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
if (agent) {
|
|
524
|
+
agent.setArchitectModel(model);
|
|
525
|
+
addMsg("info", `🏗️ Architect mode ON (planner: ${model})`);
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
addMsg("info", "⚠ No agent connected. Connect first.");
|
|
529
|
+
}
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
// ── Lint commands (work without agent) ──
|
|
533
|
+
if (trimmed === "/lint") {
|
|
534
|
+
const { detectLinter } = await import("./utils/lint.js");
|
|
535
|
+
const linter = detectLinter(process.cwd());
|
|
536
|
+
const enabled = agent ? agent.isAutoLintEnabled() : true;
|
|
537
|
+
if (linter) {
|
|
538
|
+
addMsg("info", `🔍 Auto-lint: ${enabled ? "ON" : "OFF"}\n Detected: ${linter.name}\n Command: ${linter.command} <file>`);
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
addMsg("info", `🔍 Auto-lint: ${enabled ? "ON" : "OFF"}\n No linter detected in this project.`);
|
|
542
|
+
}
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (trimmed === "/lint on") {
|
|
546
|
+
if (agent)
|
|
547
|
+
agent.setAutoLint(true);
|
|
548
|
+
addMsg("info", "🔍 Auto-lint ON");
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
if (trimmed === "/lint off") {
|
|
552
|
+
if (agent)
|
|
553
|
+
agent.setAutoLint(false);
|
|
554
|
+
addMsg("info", "🔍 Auto-lint OFF");
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
// ── MCP commands (partially work without agent) ──
|
|
558
|
+
if (trimmed === "/mcp" || trimmed === "/mcp list") {
|
|
559
|
+
const servers = listServers(process.cwd());
|
|
560
|
+
if (servers.length === 0) {
|
|
561
|
+
addMsg("info", "🔌 No MCP servers configured.\n Add one: /mcp add <name> <command> [args...]");
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
const lines = servers.map((s) => {
|
|
565
|
+
const status = s.connected ? `✔ connected (${s.toolCount} tools)` : "✗ not connected";
|
|
566
|
+
return ` ${s.connected ? "●" : "○"} ${s.name} [${s.source}] — ${s.command}\n ${status}`;
|
|
567
|
+
});
|
|
568
|
+
addMsg("info", `🔌 MCP Servers:\n${lines.join("\n")}`);
|
|
569
|
+
}
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (trimmed === "/mcp tools") {
|
|
573
|
+
const servers = getConnectedServers();
|
|
574
|
+
if (servers.length === 0) {
|
|
575
|
+
addMsg("info", "🔌 No MCP servers connected.");
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
const lines = [];
|
|
579
|
+
for (const server of servers) {
|
|
580
|
+
lines.push(`${server.name} (${server.tools.length} tools):`);
|
|
581
|
+
for (const tool of server.tools) {
|
|
582
|
+
lines.push(` • ${tool.name} — ${tool.description ?? "(no description)"}`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
addMsg("info", `🔌 MCP Tools:\n${lines.join("\n")}`);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (trimmed.startsWith("/mcp add ")) {
|
|
589
|
+
const parts = trimmed.replace("/mcp add ", "").trim().split(/\s+/);
|
|
590
|
+
if (parts.length < 2) {
|
|
591
|
+
addMsg("info", "Usage: /mcp add <name> <command> [args...]\n Example: /mcp add github npx -y @modelcontextprotocol/server-github");
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
const [name, command, ...cmdArgs] = parts;
|
|
595
|
+
const result = addServer(name, { command, args: cmdArgs.length > 0 ? cmdArgs : undefined });
|
|
596
|
+
addMsg(result.ok ? "info" : "error", result.ok ? `✅ ${result.message}` : `✗ ${result.message}`);
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
if (trimmed.startsWith("/mcp remove ")) {
|
|
600
|
+
const name = trimmed.replace("/mcp remove ", "").trim();
|
|
601
|
+
if (!name) {
|
|
602
|
+
addMsg("info", "Usage: /mcp remove <name>");
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const result = removeServer(name);
|
|
606
|
+
addMsg(result.ok ? "info" : "error", result.ok ? `✅ ${result.message}` : `✗ ${result.message}`);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (trimmed === "/mcp reconnect") {
|
|
610
|
+
if (!agent) {
|
|
611
|
+
addMsg("info", "⚠ No agent connected. Connect first.");
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
addMsg("info", "🔌 Reconnecting MCP servers...");
|
|
615
|
+
await agent.reconnectMCP();
|
|
616
|
+
const count = agent.getMCPServerCount();
|
|
617
|
+
addMsg("info", count > 0
|
|
618
|
+
? `✅ ${count} MCP server${count > 1 ? "s" : ""} reconnected.`
|
|
619
|
+
: "No MCP servers connected.");
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
458
622
|
// Commands below require an active LLM connection
|
|
459
623
|
if (!agent) {
|
|
460
624
|
addMsg("info", "⚠ No LLM connected. Use /login to authenticate with a provider, or start a local server.");
|
|
@@ -641,8 +805,8 @@ function App() {
|
|
|
641
805
|
setSpinnerMsg(SPINNER_MESSAGES[Math.floor(Math.random() * SPINNER_MESSAGES.length)]);
|
|
642
806
|
try {
|
|
643
807
|
// Response is built incrementally via onToken callback
|
|
644
|
-
//
|
|
645
|
-
await agent.
|
|
808
|
+
// send() routes through architect if enabled, otherwise direct chat
|
|
809
|
+
await agent.send(trimmed);
|
|
646
810
|
}
|
|
647
811
|
catch (err) {
|
|
648
812
|
addMsg("error", `Error: ${err.message}`);
|
|
@@ -1192,7 +1356,7 @@ function App() {
|
|
|
1192
1356
|
})(), modelName ? ` · 🤖 ${modelName}` : "", (() => {
|
|
1193
1357
|
const count = getActiveSkillCount(process.cwd(), sessionDisabledSkills);
|
|
1194
1358
|
return count > 0 ? ` · 🧠 ${count} skill${count !== 1 ? "s" : ""}` : "";
|
|
1195
|
-
})()] }) }))] }));
|
|
1359
|
+
})(), agent.getArchitectModel() ? " · 🏗️ architect" : ""] }) }))] }));
|
|
1196
1360
|
}
|
|
1197
1361
|
// Clear screen before render
|
|
1198
1362
|
process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
package/dist/utils/context.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load project rules from CODEMAXXING.md, .codemaxxing/CODEMAXXING.md, or .cursorrules
|
|
3
|
+
* Returns { content, source } or null if none found
|
|
4
|
+
*/
|
|
5
|
+
export declare function loadProjectRules(cwd: string): {
|
|
6
|
+
content: string;
|
|
7
|
+
source: string;
|
|
8
|
+
} | null;
|
|
1
9
|
/**
|
|
2
10
|
* Build a project context string by scanning the working directory
|
|
3
11
|
*/
|
|
@@ -5,7 +13,7 @@ export declare function buildProjectContext(cwd: string): Promise<string>;
|
|
|
5
13
|
/**
|
|
6
14
|
* Get the system prompt for the coding agent
|
|
7
15
|
*/
|
|
8
|
-
export declare function getSystemPrompt(projectContext: string, skillPrompts?: string): Promise<string>;
|
|
16
|
+
export declare function getSystemPrompt(projectContext: string, skillPrompts?: string, projectRules?: string): Promise<string>;
|
|
9
17
|
/**
|
|
10
18
|
* Synchronous version for backwards compatibility (without repo map)
|
|
11
19
|
* @deprecated Use async buildProjectContext instead
|
package/dist/utils/context.js
CHANGED
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { buildRepoMap } from "./repomap.js";
|
|
4
|
+
/**
|
|
5
|
+
* Load project rules from CODEMAXXING.md, .codemaxxing/CODEMAXXING.md, or .cursorrules
|
|
6
|
+
* Returns { content, source } or null if none found
|
|
7
|
+
*/
|
|
8
|
+
export function loadProjectRules(cwd) {
|
|
9
|
+
const candidates = [
|
|
10
|
+
{ path: join(cwd, "CODEMAXXING.md"), source: "CODEMAXXING.md" },
|
|
11
|
+
{ path: join(cwd, ".codemaxxing", "CODEMAXXING.md"), source: ".codemaxxing/CODEMAXXING.md" },
|
|
12
|
+
{ path: join(cwd, ".cursorrules"), source: ".cursorrules" },
|
|
13
|
+
];
|
|
14
|
+
for (const { path, source } of candidates) {
|
|
15
|
+
if (existsSync(path)) {
|
|
16
|
+
try {
|
|
17
|
+
const content = readFileSync(path, "utf-8").trim();
|
|
18
|
+
if (content)
|
|
19
|
+
return { content, source };
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// skip unreadable files
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
4
28
|
/**
|
|
5
29
|
* Build a project context string by scanning the working directory
|
|
6
30
|
*/
|
|
@@ -26,14 +50,6 @@ export async function buildProjectContext(cwd) {
|
|
|
26
50
|
if (found.length > 0) {
|
|
27
51
|
lines.push(`Project files: ${found.join(", ")}`);
|
|
28
52
|
}
|
|
29
|
-
// Read PIERRE.md if it exists (like QWEN.md — project context file)
|
|
30
|
-
const contextMd = join(cwd, "CODEMAXXING.md");
|
|
31
|
-
if (existsSync(contextMd)) {
|
|
32
|
-
const content = readFileSync(contextMd, "utf-8");
|
|
33
|
-
lines.push("\n--- CODEMAXXING.md (project context) ---");
|
|
34
|
-
lines.push(content.slice(0, 4000));
|
|
35
|
-
lines.push("--- end CODEMAXXING.md ---");
|
|
36
|
-
}
|
|
37
53
|
// Read package.json for project info
|
|
38
54
|
const pkgPath = join(cwd, "package.json");
|
|
39
55
|
if (existsSync(pkgPath)) {
|
|
@@ -86,7 +102,7 @@ export async function buildProjectContext(cwd) {
|
|
|
86
102
|
/**
|
|
87
103
|
* Get the system prompt for the coding agent
|
|
88
104
|
*/
|
|
89
|
-
export async function getSystemPrompt(projectContext, skillPrompts = "") {
|
|
105
|
+
export async function getSystemPrompt(projectContext, skillPrompts = "", projectRules = "") {
|
|
90
106
|
const base = `You are CODEMAXXING, an AI coding assistant running in the terminal.
|
|
91
107
|
|
|
92
108
|
You help developers understand, write, debug, and refactor code. You have access to tools that let you read files, write files, list directories, search code, and run shell commands.
|
|
@@ -111,10 +127,14 @@ ${projectContext}
|
|
|
111
127
|
- Use code blocks with language tags
|
|
112
128
|
- Be direct and helpful
|
|
113
129
|
- If the user asks to "just do it", skip explanations and execute`;
|
|
130
|
+
let prompt = base;
|
|
131
|
+
if (projectRules) {
|
|
132
|
+
prompt += "\n\n--- Project Rules (CODEMAXXING.md) ---\n" + projectRules + "\n--- End Project Rules ---";
|
|
133
|
+
}
|
|
114
134
|
if (skillPrompts) {
|
|
115
|
-
|
|
135
|
+
prompt += "\n\n## Active Skills\n" + skillPrompts;
|
|
116
136
|
}
|
|
117
|
-
return
|
|
137
|
+
return prompt;
|
|
118
138
|
}
|
|
119
139
|
/**
|
|
120
140
|
* Synchronous version for backwards compatibility (without repo map)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
interface LinterInfo {
|
|
2
|
+
name: string;
|
|
3
|
+
command: string;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Detect the project linter based on config files in the working directory
|
|
7
|
+
*/
|
|
8
|
+
export declare function detectLinter(cwd: string): LinterInfo | null;
|
|
9
|
+
/**
|
|
10
|
+
* Run the linter on a specific file and return errors (or null if clean)
|
|
11
|
+
*/
|
|
12
|
+
export declare function runLinter(linter: LinterInfo, filePath: string, cwd: string): string | null;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { join, extname } from "path";
|
|
3
|
+
import { execSync } from "child_process";
|
|
4
|
+
/**
|
|
5
|
+
* Detect the project linter based on config files in the working directory
|
|
6
|
+
*/
|
|
7
|
+
export function detectLinter(cwd) {
|
|
8
|
+
// JavaScript/TypeScript — check for biome first (faster), then eslint
|
|
9
|
+
if (existsSync(join(cwd, "biome.json")) || existsSync(join(cwd, "biome.jsonc"))) {
|
|
10
|
+
return { name: "Biome", command: "npx biome check" };
|
|
11
|
+
}
|
|
12
|
+
if (existsSync(join(cwd, ".eslintrc")) ||
|
|
13
|
+
existsSync(join(cwd, ".eslintrc.js")) ||
|
|
14
|
+
existsSync(join(cwd, ".eslintrc.cjs")) ||
|
|
15
|
+
existsSync(join(cwd, ".eslintrc.json")) ||
|
|
16
|
+
existsSync(join(cwd, ".eslintrc.yml")) ||
|
|
17
|
+
existsSync(join(cwd, "eslint.config.js")) ||
|
|
18
|
+
existsSync(join(cwd, "eslint.config.mjs")) ||
|
|
19
|
+
existsSync(join(cwd, "eslint.config.ts"))) {
|
|
20
|
+
return { name: "ESLint", command: "npx eslint" };
|
|
21
|
+
}
|
|
22
|
+
// Check package.json for eslint dependency as fallback
|
|
23
|
+
if (existsSync(join(cwd, "package.json"))) {
|
|
24
|
+
try {
|
|
25
|
+
const pkg = JSON.parse(require("fs").readFileSync(join(cwd, "package.json"), "utf-8"));
|
|
26
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
27
|
+
if (allDeps["@biomejs/biome"]) {
|
|
28
|
+
return { name: "Biome", command: "npx biome check" };
|
|
29
|
+
}
|
|
30
|
+
if (allDeps["eslint"]) {
|
|
31
|
+
return { name: "ESLint", command: "npx eslint" };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// ignore
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Python — ruff (fast) or flake8/pylint
|
|
39
|
+
if (existsSync(join(cwd, "ruff.toml")) || existsSync(join(cwd, ".ruff.toml"))) {
|
|
40
|
+
return { name: "Ruff", command: "ruff check" };
|
|
41
|
+
}
|
|
42
|
+
if (existsSync(join(cwd, "pyproject.toml"))) {
|
|
43
|
+
try {
|
|
44
|
+
const content = require("fs").readFileSync(join(cwd, "pyproject.toml"), "utf-8");
|
|
45
|
+
if (content.includes("[tool.ruff]")) {
|
|
46
|
+
return { name: "Ruff", command: "ruff check" };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// ignore
|
|
51
|
+
}
|
|
52
|
+
return { name: "Ruff", command: "ruff check" };
|
|
53
|
+
}
|
|
54
|
+
// Rust
|
|
55
|
+
if (existsSync(join(cwd, "Cargo.toml"))) {
|
|
56
|
+
return { name: "Clippy", command: "cargo clippy --message-format=short --" };
|
|
57
|
+
}
|
|
58
|
+
// Go
|
|
59
|
+
if (existsSync(join(cwd, "go.mod"))) {
|
|
60
|
+
return { name: "golangci-lint", command: "golangci-lint run" };
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Run the linter on a specific file and return errors (or null if clean)
|
|
66
|
+
*/
|
|
67
|
+
export function runLinter(linter, filePath, cwd) {
|
|
68
|
+
// Skip files that the linter can't handle
|
|
69
|
+
const ext = extname(filePath).toLowerCase();
|
|
70
|
+
const jsExts = new Set([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".mts", ".cts"]);
|
|
71
|
+
const pyExts = new Set([".py", ".pyi"]);
|
|
72
|
+
const rsExts = new Set([".rs"]);
|
|
73
|
+
const goExts = new Set([".go"]);
|
|
74
|
+
// Only lint files matching the linter's language
|
|
75
|
+
if ((linter.name === "ESLint" || linter.name === "Biome") && !jsExts.has(ext))
|
|
76
|
+
return null;
|
|
77
|
+
if (linter.name === "Ruff" && !pyExts.has(ext))
|
|
78
|
+
return null;
|
|
79
|
+
if (linter.name === "Clippy" && !rsExts.has(ext))
|
|
80
|
+
return null;
|
|
81
|
+
if (linter.name === "golangci-lint" && !goExts.has(ext))
|
|
82
|
+
return null;
|
|
83
|
+
try {
|
|
84
|
+
// Clippy works on the whole project, not individual files
|
|
85
|
+
const command = linter.name === "Clippy"
|
|
86
|
+
? linter.command
|
|
87
|
+
: `${linter.command} ${filePath}`;
|
|
88
|
+
execSync(command, {
|
|
89
|
+
cwd,
|
|
90
|
+
encoding: "utf-8",
|
|
91
|
+
timeout: 15000,
|
|
92
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
93
|
+
});
|
|
94
|
+
return null; // No errors
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
const output = (e.stdout || "") + (e.stderr || "");
|
|
98
|
+
const trimmed = output.trim();
|
|
99
|
+
if (!trimmed)
|
|
100
|
+
return null;
|
|
101
|
+
// Limit output to avoid flooding context
|
|
102
|
+
const lines = trimmed.split("\n");
|
|
103
|
+
if (lines.length > 30) {
|
|
104
|
+
return lines.slice(0, 30).join("\n") + `\n... (${lines.length - 30} more lines)`;
|
|
105
|
+
}
|
|
106
|
+
return trimmed;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP (Model Context Protocol) client support
|
|
3
|
+
* Connects to external MCP servers and exposes their tools to the LLM agent.
|
|
4
|
+
*/
|
|
5
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
6
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
7
|
+
import type { ChatCompletionTool } from "openai/resources/chat/completions";
|
|
8
|
+
export interface MCPServerConfig {
|
|
9
|
+
command: string;
|
|
10
|
+
args?: string[];
|
|
11
|
+
env?: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
export interface MCPConfig {
|
|
14
|
+
mcpServers: Record<string, MCPServerConfig>;
|
|
15
|
+
}
|
|
16
|
+
export interface ConnectedServer {
|
|
17
|
+
name: string;
|
|
18
|
+
client: Client;
|
|
19
|
+
transport: StdioClientTransport;
|
|
20
|
+
tools: Array<{
|
|
21
|
+
name: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
inputSchema: Record<string, unknown>;
|
|
24
|
+
}>;
|
|
25
|
+
}
|
|
26
|
+
export declare function loadMCPConfig(cwd: string): MCPConfig;
|
|
27
|
+
export declare function connectToServers(config: MCPConfig, onStatus?: (name: string, status: string) => void): Promise<ConnectedServer[]>;
|
|
28
|
+
export declare function disconnectAll(): Promise<void>;
|
|
29
|
+
export declare function getConnectedServers(): ConnectedServer[];
|
|
30
|
+
export declare function getAllMCPTools(servers: ConnectedServer[]): ChatCompletionTool[];
|
|
31
|
+
/**
|
|
32
|
+
* Parse an MCP tool call name to extract server name and tool name.
|
|
33
|
+
* Format: mcp_<serverName>_<toolName>
|
|
34
|
+
* Server names can contain hyphens but not underscores (by convention).
|
|
35
|
+
*/
|
|
36
|
+
export declare function parseMCPToolName(fullName: string): {
|
|
37
|
+
serverName: string;
|
|
38
|
+
toolName: string;
|
|
39
|
+
} | null;
|
|
40
|
+
export declare function callMCPTool(serverName: string, toolName: string, args: Record<string, unknown>): Promise<string>;
|
|
41
|
+
export declare function addServer(name: string, config: MCPServerConfig): {
|
|
42
|
+
ok: boolean;
|
|
43
|
+
message: string;
|
|
44
|
+
};
|
|
45
|
+
export declare function removeServer(name: string): {
|
|
46
|
+
ok: boolean;
|
|
47
|
+
message: string;
|
|
48
|
+
};
|
|
49
|
+
export declare function listServers(cwd: string): Array<{
|
|
50
|
+
name: string;
|
|
51
|
+
source: string;
|
|
52
|
+
command: string;
|
|
53
|
+
connected: boolean;
|
|
54
|
+
toolCount: number;
|
|
55
|
+
}>;
|