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/src/exec.ts
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Headless/CI execution mode — runs agent without TUI
|
|
3
|
+
* Usage: codemaxxing exec "your prompt here"
|
|
4
|
+
* Flags: --auto-approve, --json, --model <model>, --provider <name>
|
|
5
|
+
* Supports stdin pipe: echo "fix the tests" | codemaxxing exec
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { CodingAgent } from "./agent.js";
|
|
9
|
+
import { loadConfig, applyOverrides, detectLocalProvider } from "./config.js";
|
|
10
|
+
import { getCredential } from "./utils/auth.js";
|
|
11
|
+
import { disconnectAll } from "./utils/mcp.js";
|
|
12
|
+
|
|
13
|
+
interface ExecArgs {
|
|
14
|
+
prompt: string;
|
|
15
|
+
autoApprove: boolean;
|
|
16
|
+
json: boolean;
|
|
17
|
+
model?: string;
|
|
18
|
+
provider?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseExecArgs(argv: string[]): ExecArgs {
|
|
22
|
+
const args: ExecArgs = {
|
|
23
|
+
prompt: "",
|
|
24
|
+
autoApprove: false,
|
|
25
|
+
json: false,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const positional: string[] = [];
|
|
29
|
+
|
|
30
|
+
for (let i = 0; i < argv.length; i++) {
|
|
31
|
+
const arg = argv[i];
|
|
32
|
+
const next = argv[i + 1];
|
|
33
|
+
|
|
34
|
+
if (arg === "--auto-approve") {
|
|
35
|
+
args.autoApprove = true;
|
|
36
|
+
} else if (arg === "--json") {
|
|
37
|
+
args.json = true;
|
|
38
|
+
} else if ((arg === "--model" || arg === "-m") && next) {
|
|
39
|
+
args.model = next;
|
|
40
|
+
i++;
|
|
41
|
+
} else if ((arg === "--provider" || arg === "-p") && next) {
|
|
42
|
+
args.provider = next;
|
|
43
|
+
i++;
|
|
44
|
+
} else if (!arg.startsWith("-")) {
|
|
45
|
+
positional.push(arg);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
args.prompt = positional.join(" ");
|
|
50
|
+
return args;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function readStdin(): Promise<string> {
|
|
54
|
+
// Check if stdin has data (piped input)
|
|
55
|
+
if (process.stdin.isTTY) return "";
|
|
56
|
+
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
let data = "";
|
|
59
|
+
process.stdin.setEncoding("utf-8");
|
|
60
|
+
process.stdin.on("data", (chunk) => { data += chunk; });
|
|
61
|
+
process.stdin.on("end", () => resolve(data.trim()));
|
|
62
|
+
// Timeout after 1s if no data arrives
|
|
63
|
+
setTimeout(() => resolve(data.trim()), 1000);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function runExec(argv: string[]): Promise<void> {
|
|
68
|
+
const args = parseExecArgs(argv);
|
|
69
|
+
|
|
70
|
+
// Read from stdin if no prompt provided
|
|
71
|
+
if (!args.prompt) {
|
|
72
|
+
args.prompt = await readStdin();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!args.prompt) {
|
|
76
|
+
process.stderr.write("Error: No prompt provided.\n");
|
|
77
|
+
process.stderr.write("Usage: codemaxxing exec \"your prompt here\"\n");
|
|
78
|
+
process.stderr.write(" echo \"fix tests\" | codemaxxing exec\n");
|
|
79
|
+
process.stderr.write("\nFlags:\n");
|
|
80
|
+
process.stderr.write(" --auto-approve Skip approval prompts\n");
|
|
81
|
+
process.stderr.write(" --json JSON output\n");
|
|
82
|
+
process.stderr.write(" -m, --model Model to use\n");
|
|
83
|
+
process.stderr.write(" -p, --provider Provider profile\n");
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Resolve provider config
|
|
88
|
+
const rawConfig = loadConfig();
|
|
89
|
+
const cliArgs = {
|
|
90
|
+
model: args.model,
|
|
91
|
+
provider: args.provider,
|
|
92
|
+
};
|
|
93
|
+
const config = applyOverrides(rawConfig, cliArgs);
|
|
94
|
+
let provider = config.provider;
|
|
95
|
+
|
|
96
|
+
// Auto-detect local provider if needed
|
|
97
|
+
if (provider.model === "auto" || (provider.baseUrl === "http://localhost:1234/v1" && !args.provider)) {
|
|
98
|
+
const detected = await detectLocalProvider();
|
|
99
|
+
if (detected) {
|
|
100
|
+
if (args.model) detected.model = args.model;
|
|
101
|
+
provider = detected;
|
|
102
|
+
} else if (!args.provider) {
|
|
103
|
+
process.stderr.write("Error: No LLM provider found. Start a local server or use --provider.\n");
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
process.stderr.write(`Provider: ${provider.baseUrl}\n`);
|
|
109
|
+
process.stderr.write(`Model: ${provider.model}\n`);
|
|
110
|
+
process.stderr.write(`Prompt: ${args.prompt.slice(0, 100)}${args.prompt.length > 100 ? "..." : ""}\n`);
|
|
111
|
+
process.stderr.write("---\n");
|
|
112
|
+
|
|
113
|
+
const cwd = process.cwd();
|
|
114
|
+
let hasChanges = false;
|
|
115
|
+
let fullResponse = "";
|
|
116
|
+
const toolResults: Array<{ tool: string; args: Record<string, unknown>; result: string }> = [];
|
|
117
|
+
|
|
118
|
+
const agent = new CodingAgent({
|
|
119
|
+
provider,
|
|
120
|
+
cwd,
|
|
121
|
+
maxTokens: config.defaults.maxTokens,
|
|
122
|
+
autoApprove: args.autoApprove,
|
|
123
|
+
onToken: (token) => {
|
|
124
|
+
if (!args.json) {
|
|
125
|
+
process.stdout.write(token);
|
|
126
|
+
}
|
|
127
|
+
fullResponse += token;
|
|
128
|
+
},
|
|
129
|
+
onToolCall: (name, toolArgs) => {
|
|
130
|
+
process.stderr.write(`Tool: ${name}(${Object.values(toolArgs).map(v => String(v).slice(0, 60)).join(", ")})\n`);
|
|
131
|
+
if (name === "write_file") hasChanges = true;
|
|
132
|
+
},
|
|
133
|
+
onToolResult: (name, result) => {
|
|
134
|
+
const lines = result.split("\n").length;
|
|
135
|
+
process.stderr.write(` └ ${lines} lines\n`);
|
|
136
|
+
toolResults.push({ tool: name, args: {}, result });
|
|
137
|
+
},
|
|
138
|
+
onToolApproval: async (name, toolArgs, diff) => {
|
|
139
|
+
if (args.autoApprove) return "yes";
|
|
140
|
+
// In non-interactive mode without auto-approve, deny dangerous tools
|
|
141
|
+
process.stderr.write(`⚠ Denied ${name} (use --auto-approve to allow)\n`);
|
|
142
|
+
return "no";
|
|
143
|
+
},
|
|
144
|
+
onMCPStatus: (server, status) => {
|
|
145
|
+
process.stderr.write(`MCP ${server}: ${status}\n`);
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
try {
|
|
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
|
+
|
|
157
|
+
await agent.send(args.prompt);
|
|
158
|
+
|
|
159
|
+
if (!args.json) {
|
|
160
|
+
// Ensure newline at end of output
|
|
161
|
+
process.stdout.write("\n");
|
|
162
|
+
} else {
|
|
163
|
+
// JSON output mode
|
|
164
|
+
const output = {
|
|
165
|
+
response: fullResponse,
|
|
166
|
+
model: provider.model,
|
|
167
|
+
tools_used: toolResults.length,
|
|
168
|
+
has_changes: hasChanges,
|
|
169
|
+
};
|
|
170
|
+
process.stdout.write(JSON.stringify(output, null, 2) + "\n");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
await disconnectAll();
|
|
174
|
+
process.exit(hasChanges ? 0 : 2);
|
|
175
|
+
} catch (err: any) {
|
|
176
|
+
await disconnectAll();
|
|
177
|
+
process.stderr.write(`Error: ${err.message}\n`);
|
|
178
|
+
if (args.json) {
|
|
179
|
+
process.stdout.write(JSON.stringify({ error: err.message }, null, 2) + "\n");
|
|
180
|
+
}
|
|
181
|
+
process.exit(1);
|
|
182
|
+
}
|
|
183
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -12,6 +12,7 @@ import { isGitRepo, getBranch, getStatus, getDiff, undoLastCommit } from "./util
|
|
|
12
12
|
import { getTheme, listThemes, THEMES, DEFAULT_THEME, type Theme } from "./themes.js";
|
|
13
13
|
import { PROVIDERS, getCredentials, openRouterOAuth, anthropicSetupToken, importCodexToken, importQwenToken, copilotDeviceFlow, saveApiKey } from "./utils/auth.js";
|
|
14
14
|
import { listInstalledSkills, installSkill, removeSkill, getRegistrySkills, searchRegistry, createSkillScaffold, getActiveSkills, getActiveSkillCount } from "./utils/skills.js";
|
|
15
|
+
import { listServers, addServer, removeServer, getAllMCPTools, getConnectedServers } from "./utils/mcp.js";
|
|
15
16
|
|
|
16
17
|
const VERSION = "0.1.9";
|
|
17
18
|
|
|
@@ -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
|
|
|
@@ -277,6 +287,15 @@ function App() {
|
|
|
277
287
|
const savedStr = saved >= 1000 ? `${(saved / 1000).toFixed(1)}k` : String(saved);
|
|
278
288
|
addMsg("info", `📦 Context compressed (~${savedStr} tokens freed)`);
|
|
279
289
|
},
|
|
290
|
+
onArchitectPlan: (plan) => {
|
|
291
|
+
addMsg("info", `🏗️ Architect Plan:\n${plan}`);
|
|
292
|
+
},
|
|
293
|
+
onLintResult: (file, errors) => {
|
|
294
|
+
addMsg("info", `🔍 Lint errors in ${file}:\n${errors}`);
|
|
295
|
+
},
|
|
296
|
+
onMCPStatus: (server, status) => {
|
|
297
|
+
addMsg("info", `🔌 MCP ${server}: ${status}`);
|
|
298
|
+
},
|
|
280
299
|
contextCompressionThreshold: config.defaults.contextCompressionThreshold,
|
|
281
300
|
onToolApproval: (name, args, diff) => {
|
|
282
301
|
return new Promise((resolve) => {
|
|
@@ -289,6 +308,20 @@ function App() {
|
|
|
289
308
|
// Initialize async context (repo map)
|
|
290
309
|
await a.init();
|
|
291
310
|
|
|
311
|
+
// Show project rules in banner
|
|
312
|
+
const rulesSource = a.getProjectRulesSource();
|
|
313
|
+
if (rulesSource) {
|
|
314
|
+
info.push(`📋 ${rulesSource} loaded`);
|
|
315
|
+
setConnectionInfo([...info]);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Show MCP server count
|
|
319
|
+
const mcpCount = a.getMCPServerCount();
|
|
320
|
+
if (mcpCount > 0) {
|
|
321
|
+
info.push(`🔌 ${mcpCount} MCP server${mcpCount > 1 ? "s" : ""} connected`);
|
|
322
|
+
setConnectionInfo([...info]);
|
|
323
|
+
}
|
|
324
|
+
|
|
292
325
|
setAgent(a);
|
|
293
326
|
setModelName(provider.model);
|
|
294
327
|
providerRef.current = { baseUrl: provider.baseUrl, apiKey: provider.apiKey };
|
|
@@ -336,7 +369,7 @@ function App() {
|
|
|
336
369
|
// Commands that need args (like /commit, /model) — fill input instead of executing
|
|
337
370
|
if (selected.cmd === "/commit" || selected.cmd === "/model" || selected.cmd === "/session delete" ||
|
|
338
371
|
selected.cmd === "/skills install" || selected.cmd === "/skills remove" || selected.cmd === "/skills search" ||
|
|
339
|
-
selected.cmd === "/skills on" || selected.cmd === "/skills off") {
|
|
372
|
+
selected.cmd === "/skills on" || selected.cmd === "/skills off" || selected.cmd === "/architect") {
|
|
340
373
|
setInput(selected.cmd + " ");
|
|
341
374
|
setCmdIndex(0);
|
|
342
375
|
setInputKey((k) => k + 1);
|
|
@@ -398,6 +431,15 @@ function App() {
|
|
|
398
431
|
" /git on — enable auto-commits",
|
|
399
432
|
" /git off — disable auto-commits",
|
|
400
433
|
" /skills — manage skill packs",
|
|
434
|
+
" /architect — toggle architect mode (plan then execute)",
|
|
435
|
+
" /lint — show auto-lint status & detected linter",
|
|
436
|
+
" /lint on — enable auto-lint",
|
|
437
|
+
" /lint off — disable auto-lint",
|
|
438
|
+
" /mcp — show MCP servers & status",
|
|
439
|
+
" /mcp tools — list all MCP tools",
|
|
440
|
+
" /mcp add — add MCP server to global config",
|
|
441
|
+
" /mcp remove — remove MCP server",
|
|
442
|
+
" /mcp reconnect — reconnect all MCP servers",
|
|
401
443
|
" /quit — exit",
|
|
402
444
|
].join("\n"));
|
|
403
445
|
return;
|
|
@@ -502,6 +544,127 @@ function App() {
|
|
|
502
544
|
addMsg("info", `✅ Switched to theme: ${THEMES[themeName].name}`);
|
|
503
545
|
return;
|
|
504
546
|
}
|
|
547
|
+
// ── Architect commands (work without agent) ──
|
|
548
|
+
if (trimmed === "/architect") {
|
|
549
|
+
if (!agent) {
|
|
550
|
+
addMsg("info", "🏗️ Architect mode: no agent connected. Connect first with /login or /connect.");
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
const current = agent.getArchitectModel();
|
|
554
|
+
if (current) {
|
|
555
|
+
agent.setArchitectModel(null);
|
|
556
|
+
addMsg("info", "🏗️ Architect mode OFF");
|
|
557
|
+
} else {
|
|
558
|
+
// Use config default or a sensible default
|
|
559
|
+
const defaultModel = loadConfig().defaults.architectModel || agent.getModel();
|
|
560
|
+
agent.setArchitectModel(defaultModel);
|
|
561
|
+
addMsg("info", `🏗️ Architect mode ON (planner: ${defaultModel})`);
|
|
562
|
+
}
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
if (trimmed.startsWith("/architect ")) {
|
|
566
|
+
const model = trimmed.replace("/architect ", "").trim();
|
|
567
|
+
if (!model) {
|
|
568
|
+
addMsg("info", "Usage: /architect <model> or /architect to toggle");
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
if (agent) {
|
|
572
|
+
agent.setArchitectModel(model);
|
|
573
|
+
addMsg("info", `🏗️ Architect mode ON (planner: ${model})`);
|
|
574
|
+
} else {
|
|
575
|
+
addMsg("info", "⚠ No agent connected. Connect first.");
|
|
576
|
+
}
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// ── Lint commands (work without agent) ──
|
|
581
|
+
if (trimmed === "/lint") {
|
|
582
|
+
const { detectLinter } = await import("./utils/lint.js");
|
|
583
|
+
const linter = detectLinter(process.cwd());
|
|
584
|
+
const enabled = agent ? agent.isAutoLintEnabled() : true;
|
|
585
|
+
if (linter) {
|
|
586
|
+
addMsg("info", `🔍 Auto-lint: ${enabled ? "ON" : "OFF"}\n Detected: ${linter.name}\n Command: ${linter.command} <file>`);
|
|
587
|
+
} else {
|
|
588
|
+
addMsg("info", `🔍 Auto-lint: ${enabled ? "ON" : "OFF"}\n No linter detected in this project.`);
|
|
589
|
+
}
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (trimmed === "/lint on") {
|
|
593
|
+
if (agent) agent.setAutoLint(true);
|
|
594
|
+
addMsg("info", "🔍 Auto-lint ON");
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
if (trimmed === "/lint off") {
|
|
598
|
+
if (agent) agent.setAutoLint(false);
|
|
599
|
+
addMsg("info", "🔍 Auto-lint OFF");
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// ── MCP commands (partially work without agent) ──
|
|
604
|
+
if (trimmed === "/mcp" || trimmed === "/mcp list") {
|
|
605
|
+
const servers = listServers(process.cwd());
|
|
606
|
+
if (servers.length === 0) {
|
|
607
|
+
addMsg("info", "🔌 No MCP servers configured.\n Add one: /mcp add <name> <command> [args...]");
|
|
608
|
+
} else {
|
|
609
|
+
const lines = servers.map((s) => {
|
|
610
|
+
const status = s.connected ? `✔ connected (${s.toolCount} tools)` : "✗ not connected";
|
|
611
|
+
return ` ${s.connected ? "●" : "○"} ${s.name} [${s.source}] — ${s.command}\n ${status}`;
|
|
612
|
+
});
|
|
613
|
+
addMsg("info", `🔌 MCP Servers:\n${lines.join("\n")}`);
|
|
614
|
+
}
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (trimmed === "/mcp tools") {
|
|
618
|
+
const servers = getConnectedServers();
|
|
619
|
+
if (servers.length === 0) {
|
|
620
|
+
addMsg("info", "🔌 No MCP servers connected.");
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
const lines: string[] = [];
|
|
624
|
+
for (const server of servers) {
|
|
625
|
+
lines.push(`${server.name} (${server.tools.length} tools):`);
|
|
626
|
+
for (const tool of server.tools) {
|
|
627
|
+
lines.push(` • ${tool.name} — ${tool.description ?? "(no description)"}`);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
addMsg("info", `🔌 MCP Tools:\n${lines.join("\n")}`);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
if (trimmed.startsWith("/mcp add ")) {
|
|
634
|
+
const parts = trimmed.replace("/mcp add ", "").trim().split(/\s+/);
|
|
635
|
+
if (parts.length < 2) {
|
|
636
|
+
addMsg("info", "Usage: /mcp add <name> <command> [args...]\n Example: /mcp add github npx -y @modelcontextprotocol/server-github");
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
const [name, command, ...cmdArgs] = parts;
|
|
640
|
+
const result = addServer(name, { command, args: cmdArgs.length > 0 ? cmdArgs : undefined });
|
|
641
|
+
addMsg(result.ok ? "info" : "error", result.ok ? `✅ ${result.message}` : `✗ ${result.message}`);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
if (trimmed.startsWith("/mcp remove ")) {
|
|
645
|
+
const name = trimmed.replace("/mcp remove ", "").trim();
|
|
646
|
+
if (!name) {
|
|
647
|
+
addMsg("info", "Usage: /mcp remove <name>");
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
const result = removeServer(name);
|
|
651
|
+
addMsg(result.ok ? "info" : "error", result.ok ? `✅ ${result.message}` : `✗ ${result.message}`);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
if (trimmed === "/mcp reconnect") {
|
|
655
|
+
if (!agent) {
|
|
656
|
+
addMsg("info", "⚠ No agent connected. Connect first.");
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
addMsg("info", "🔌 Reconnecting MCP servers...");
|
|
660
|
+
await agent.reconnectMCP();
|
|
661
|
+
const count = agent.getMCPServerCount();
|
|
662
|
+
addMsg("info", count > 0
|
|
663
|
+
? `✅ ${count} MCP server${count > 1 ? "s" : ""} reconnected.`
|
|
664
|
+
: "No MCP servers connected.");
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
|
|
505
668
|
// Commands below require an active LLM connection
|
|
506
669
|
if (!agent) {
|
|
507
670
|
addMsg("info", "⚠ No LLM connected. Use /login to authenticate with a provider, or start a local server.");
|
|
@@ -684,8 +847,8 @@ function App() {
|
|
|
684
847
|
|
|
685
848
|
try {
|
|
686
849
|
// Response is built incrementally via onToken callback
|
|
687
|
-
//
|
|
688
|
-
await agent.
|
|
850
|
+
// send() routes through architect if enabled, otherwise direct chat
|
|
851
|
+
await agent.send(trimmed);
|
|
689
852
|
} catch (err: any) {
|
|
690
853
|
addMsg("error", `Error: ${err.message}`);
|
|
691
854
|
}
|
|
@@ -1497,6 +1660,7 @@ function App() {
|
|
|
1497
1660
|
const count = getActiveSkillCount(process.cwd(), sessionDisabledSkills);
|
|
1498
1661
|
return count > 0 ? ` · 🧠 ${count} skill${count !== 1 ? "s" : ""}` : "";
|
|
1499
1662
|
})()}
|
|
1663
|
+
{agent.getArchitectModel() ? " · 🏗️ architect" : ""}
|
|
1500
1664
|
</Text>
|
|
1501
1665
|
</Box>
|
|
1502
1666
|
)}
|
package/src/utils/context.ts
CHANGED
|
@@ -3,6 +3,30 @@ import { join, extname } from "path";
|
|
|
3
3
|
import { buildRepoMap } from "./repomap.js";
|
|
4
4
|
import { buildSkillPrompts } from "./skills.js";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Load project rules from CODEMAXXING.md, .codemaxxing/CODEMAXXING.md, or .cursorrules
|
|
8
|
+
* Returns { content, source } or null if none found
|
|
9
|
+
*/
|
|
10
|
+
export function loadProjectRules(cwd: string): { content: string; source: string } | null {
|
|
11
|
+
const candidates = [
|
|
12
|
+
{ path: join(cwd, "CODEMAXXING.md"), source: "CODEMAXXING.md" },
|
|
13
|
+
{ path: join(cwd, ".codemaxxing", "CODEMAXXING.md"), source: ".codemaxxing/CODEMAXXING.md" },
|
|
14
|
+
{ path: join(cwd, ".cursorrules"), source: ".cursorrules" },
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
for (const { path, source } of candidates) {
|
|
18
|
+
if (existsSync(path)) {
|
|
19
|
+
try {
|
|
20
|
+
const content = readFileSync(path, "utf-8").trim();
|
|
21
|
+
if (content) return { content, source };
|
|
22
|
+
} catch {
|
|
23
|
+
// skip unreadable files
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
6
30
|
/**
|
|
7
31
|
* Build a project context string by scanning the working directory
|
|
8
32
|
*/
|
|
@@ -31,15 +55,6 @@ export async function buildProjectContext(cwd: string): Promise<string> {
|
|
|
31
55
|
lines.push(`Project files: ${found.join(", ")}`);
|
|
32
56
|
}
|
|
33
57
|
|
|
34
|
-
// Read PIERRE.md if it exists (like QWEN.md — project context file)
|
|
35
|
-
const contextMd = join(cwd, "CODEMAXXING.md");
|
|
36
|
-
if (existsSync(contextMd)) {
|
|
37
|
-
const content = readFileSync(contextMd, "utf-8");
|
|
38
|
-
lines.push("\n--- CODEMAXXING.md (project context) ---");
|
|
39
|
-
lines.push(content.slice(0, 4000));
|
|
40
|
-
lines.push("--- end CODEMAXXING.md ---");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
58
|
// Read package.json for project info
|
|
44
59
|
const pkgPath = join(cwd, "package.json");
|
|
45
60
|
if (existsSync(pkgPath)) {
|
|
@@ -94,7 +109,7 @@ export async function buildProjectContext(cwd: string): Promise<string> {
|
|
|
94
109
|
/**
|
|
95
110
|
* Get the system prompt for the coding agent
|
|
96
111
|
*/
|
|
97
|
-
export async function getSystemPrompt(projectContext: string, skillPrompts: string = ""): Promise<string> {
|
|
112
|
+
export async function getSystemPrompt(projectContext: string, skillPrompts: string = "", projectRules: string = ""): Promise<string> {
|
|
98
113
|
const base = `You are CODEMAXXING, an AI coding assistant running in the terminal.
|
|
99
114
|
|
|
100
115
|
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.
|
|
@@ -120,10 +135,17 @@ ${projectContext}
|
|
|
120
135
|
- Be direct and helpful
|
|
121
136
|
- If the user asks to "just do it", skip explanations and execute`;
|
|
122
137
|
|
|
138
|
+
let prompt = base;
|
|
139
|
+
|
|
140
|
+
if (projectRules) {
|
|
141
|
+
prompt += "\n\n--- Project Rules (CODEMAXXING.md) ---\n" + projectRules + "\n--- End Project Rules ---";
|
|
142
|
+
}
|
|
143
|
+
|
|
123
144
|
if (skillPrompts) {
|
|
124
|
-
|
|
145
|
+
prompt += "\n\n## Active Skills\n" + skillPrompts;
|
|
125
146
|
}
|
|
126
|
-
|
|
147
|
+
|
|
148
|
+
return prompt;
|
|
127
149
|
}
|
|
128
150
|
|
|
129
151
|
/**
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { existsSync } from "fs";
|
|
2
|
+
import { join, extname } from "path";
|
|
3
|
+
import { execSync } from "child_process";
|
|
4
|
+
|
|
5
|
+
interface LinterInfo {
|
|
6
|
+
name: string;
|
|
7
|
+
command: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Detect the project linter based on config files in the working directory
|
|
12
|
+
*/
|
|
13
|
+
export function detectLinter(cwd: string): LinterInfo | null {
|
|
14
|
+
// JavaScript/TypeScript — check for biome first (faster), then eslint
|
|
15
|
+
if (existsSync(join(cwd, "biome.json")) || existsSync(join(cwd, "biome.jsonc"))) {
|
|
16
|
+
return { name: "Biome", command: "npx biome check" };
|
|
17
|
+
}
|
|
18
|
+
if (
|
|
19
|
+
existsSync(join(cwd, ".eslintrc")) ||
|
|
20
|
+
existsSync(join(cwd, ".eslintrc.js")) ||
|
|
21
|
+
existsSync(join(cwd, ".eslintrc.cjs")) ||
|
|
22
|
+
existsSync(join(cwd, ".eslintrc.json")) ||
|
|
23
|
+
existsSync(join(cwd, ".eslintrc.yml")) ||
|
|
24
|
+
existsSync(join(cwd, "eslint.config.js")) ||
|
|
25
|
+
existsSync(join(cwd, "eslint.config.mjs")) ||
|
|
26
|
+
existsSync(join(cwd, "eslint.config.ts"))
|
|
27
|
+
) {
|
|
28
|
+
return { name: "ESLint", command: "npx eslint" };
|
|
29
|
+
}
|
|
30
|
+
// Check package.json for eslint dependency as fallback
|
|
31
|
+
if (existsSync(join(cwd, "package.json"))) {
|
|
32
|
+
try {
|
|
33
|
+
const pkg = JSON.parse(require("fs").readFileSync(join(cwd, "package.json"), "utf-8"));
|
|
34
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
35
|
+
if (allDeps["@biomejs/biome"]) {
|
|
36
|
+
return { name: "Biome", command: "npx biome check" };
|
|
37
|
+
}
|
|
38
|
+
if (allDeps["eslint"]) {
|
|
39
|
+
return { name: "ESLint", command: "npx eslint" };
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
// ignore
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Python — ruff (fast) or flake8/pylint
|
|
47
|
+
if (existsSync(join(cwd, "ruff.toml")) || existsSync(join(cwd, ".ruff.toml"))) {
|
|
48
|
+
return { name: "Ruff", command: "ruff check" };
|
|
49
|
+
}
|
|
50
|
+
if (existsSync(join(cwd, "pyproject.toml"))) {
|
|
51
|
+
try {
|
|
52
|
+
const content = require("fs").readFileSync(join(cwd, "pyproject.toml"), "utf-8");
|
|
53
|
+
if (content.includes("[tool.ruff]")) {
|
|
54
|
+
return { name: "Ruff", command: "ruff check" };
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
// ignore
|
|
58
|
+
}
|
|
59
|
+
return { name: "Ruff", command: "ruff check" };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Rust
|
|
63
|
+
if (existsSync(join(cwd, "Cargo.toml"))) {
|
|
64
|
+
return { name: "Clippy", command: "cargo clippy --message-format=short --" };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Go
|
|
68
|
+
if (existsSync(join(cwd, "go.mod"))) {
|
|
69
|
+
return { name: "golangci-lint", command: "golangci-lint run" };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Run the linter on a specific file and return errors (or null if clean)
|
|
77
|
+
*/
|
|
78
|
+
export function runLinter(linter: LinterInfo, filePath: string, cwd: string): string | null {
|
|
79
|
+
// Skip files that the linter can't handle
|
|
80
|
+
const ext = extname(filePath).toLowerCase();
|
|
81
|
+
const jsExts = new Set([".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".mts", ".cts"]);
|
|
82
|
+
const pyExts = new Set([".py", ".pyi"]);
|
|
83
|
+
const rsExts = new Set([".rs"]);
|
|
84
|
+
const goExts = new Set([".go"]);
|
|
85
|
+
|
|
86
|
+
// Only lint files matching the linter's language
|
|
87
|
+
if ((linter.name === "ESLint" || linter.name === "Biome") && !jsExts.has(ext)) return null;
|
|
88
|
+
if (linter.name === "Ruff" && !pyExts.has(ext)) return null;
|
|
89
|
+
if (linter.name === "Clippy" && !rsExts.has(ext)) return null;
|
|
90
|
+
if (linter.name === "golangci-lint" && !goExts.has(ext)) return null;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
// Clippy works on the whole project, not individual files
|
|
94
|
+
const command = linter.name === "Clippy"
|
|
95
|
+
? linter.command
|
|
96
|
+
: `${linter.command} ${filePath}`;
|
|
97
|
+
|
|
98
|
+
execSync(command, {
|
|
99
|
+
cwd,
|
|
100
|
+
encoding: "utf-8",
|
|
101
|
+
timeout: 15000,
|
|
102
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
103
|
+
});
|
|
104
|
+
return null; // No errors
|
|
105
|
+
} catch (e: any) {
|
|
106
|
+
const output = (e.stdout || "") + (e.stderr || "");
|
|
107
|
+
const trimmed = output.trim();
|
|
108
|
+
if (!trimmed) return null;
|
|
109
|
+
// Limit output to avoid flooding context
|
|
110
|
+
const lines = trimmed.split("\n");
|
|
111
|
+
if (lines.length > 30) {
|
|
112
|
+
return lines.slice(0, 30).join("\n") + `\n... (${lines.length - 30} more lines)`;
|
|
113
|
+
}
|
|
114
|
+
return trimmed;
|
|
115
|
+
}
|
|
116
|
+
}
|