codemaxxing 0.2.1 → 0.3.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/src/exec.ts ADDED
@@ -0,0 +1,171 @@
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
+
12
+ interface ExecArgs {
13
+ prompt: string;
14
+ autoApprove: boolean;
15
+ json: boolean;
16
+ model?: string;
17
+ provider?: string;
18
+ }
19
+
20
+ function parseExecArgs(argv: string[]): ExecArgs {
21
+ const args: ExecArgs = {
22
+ prompt: "",
23
+ autoApprove: false,
24
+ json: false,
25
+ };
26
+
27
+ const positional: string[] = [];
28
+
29
+ for (let i = 0; i < argv.length; i++) {
30
+ const arg = argv[i];
31
+ const next = argv[i + 1];
32
+
33
+ if (arg === "--auto-approve") {
34
+ args.autoApprove = true;
35
+ } else if (arg === "--json") {
36
+ args.json = true;
37
+ } else if ((arg === "--model" || arg === "-m") && next) {
38
+ args.model = next;
39
+ i++;
40
+ } else if ((arg === "--provider" || arg === "-p") && next) {
41
+ args.provider = next;
42
+ i++;
43
+ } else if (!arg.startsWith("-")) {
44
+ positional.push(arg);
45
+ }
46
+ }
47
+
48
+ args.prompt = positional.join(" ");
49
+ return args;
50
+ }
51
+
52
+ async function readStdin(): Promise<string> {
53
+ // Check if stdin has data (piped input)
54
+ if (process.stdin.isTTY) return "";
55
+
56
+ return new Promise((resolve) => {
57
+ let data = "";
58
+ process.stdin.setEncoding("utf-8");
59
+ process.stdin.on("data", (chunk) => { data += chunk; });
60
+ process.stdin.on("end", () => resolve(data.trim()));
61
+ // Timeout after 1s if no data arrives
62
+ setTimeout(() => resolve(data.trim()), 1000);
63
+ });
64
+ }
65
+
66
+ export async function runExec(argv: string[]): Promise<void> {
67
+ const args = parseExecArgs(argv);
68
+
69
+ // Read from stdin if no prompt provided
70
+ if (!args.prompt) {
71
+ args.prompt = await readStdin();
72
+ }
73
+
74
+ if (!args.prompt) {
75
+ process.stderr.write("Error: No prompt provided.\n");
76
+ process.stderr.write("Usage: codemaxxing exec \"your prompt here\"\n");
77
+ process.stderr.write(" echo \"fix tests\" | codemaxxing exec\n");
78
+ process.stderr.write("\nFlags:\n");
79
+ process.stderr.write(" --auto-approve Skip approval prompts\n");
80
+ process.stderr.write(" --json JSON output\n");
81
+ process.stderr.write(" -m, --model Model to use\n");
82
+ process.stderr.write(" -p, --provider Provider profile\n");
83
+ process.exit(1);
84
+ }
85
+
86
+ // Resolve provider config
87
+ const rawConfig = loadConfig();
88
+ const cliArgs = {
89
+ model: args.model,
90
+ provider: args.provider,
91
+ };
92
+ const config = applyOverrides(rawConfig, cliArgs);
93
+ let provider = config.provider;
94
+
95
+ // Auto-detect local provider if needed
96
+ if (provider.model === "auto" || (provider.baseUrl === "http://localhost:1234/v1" && !args.provider)) {
97
+ const detected = await detectLocalProvider();
98
+ if (detected) {
99
+ if (args.model) detected.model = args.model;
100
+ provider = detected;
101
+ } else if (!args.provider) {
102
+ process.stderr.write("Error: No LLM provider found. Start a local server or use --provider.\n");
103
+ process.exit(1);
104
+ }
105
+ }
106
+
107
+ process.stderr.write(`Provider: ${provider.baseUrl}\n`);
108
+ process.stderr.write(`Model: ${provider.model}\n`);
109
+ process.stderr.write(`Prompt: ${args.prompt.slice(0, 100)}${args.prompt.length > 100 ? "..." : ""}\n`);
110
+ process.stderr.write("---\n");
111
+
112
+ const cwd = process.cwd();
113
+ let hasChanges = false;
114
+ let fullResponse = "";
115
+ const toolResults: Array<{ tool: string; args: Record<string, unknown>; result: string }> = [];
116
+
117
+ const agent = new CodingAgent({
118
+ provider,
119
+ cwd,
120
+ maxTokens: config.defaults.maxTokens,
121
+ autoApprove: args.autoApprove,
122
+ onToken: (token) => {
123
+ if (!args.json) {
124
+ process.stdout.write(token);
125
+ }
126
+ fullResponse += token;
127
+ },
128
+ onToolCall: (name, toolArgs) => {
129
+ process.stderr.write(`Tool: ${name}(${Object.values(toolArgs).map(v => String(v).slice(0, 60)).join(", ")})\n`);
130
+ if (name === "write_file") hasChanges = true;
131
+ },
132
+ onToolResult: (name, result) => {
133
+ const lines = result.split("\n").length;
134
+ process.stderr.write(` └ ${lines} lines\n`);
135
+ toolResults.push({ tool: name, args: {}, result });
136
+ },
137
+ onToolApproval: async (name, toolArgs, diff) => {
138
+ if (args.autoApprove) return "yes";
139
+ // In non-interactive mode without auto-approve, deny dangerous tools
140
+ process.stderr.write(`⚠ Denied ${name} (use --auto-approve to allow)\n`);
141
+ return "no";
142
+ },
143
+ });
144
+
145
+ try {
146
+ await agent.init();
147
+ await agent.send(args.prompt);
148
+
149
+ if (!args.json) {
150
+ // Ensure newline at end of output
151
+ process.stdout.write("\n");
152
+ } else {
153
+ // JSON output mode
154
+ const output = {
155
+ response: fullResponse,
156
+ model: provider.model,
157
+ tools_used: toolResults.length,
158
+ has_changes: hasChanges,
159
+ };
160
+ process.stdout.write(JSON.stringify(output, null, 2) + "\n");
161
+ }
162
+
163
+ process.exit(hasChanges ? 0 : 2);
164
+ } catch (err: any) {
165
+ process.stderr.write(`Error: ${err.message}\n`);
166
+ if (args.json) {
167
+ process.stdout.write(JSON.stringify({ error: err.message }, null, 2) + "\n");
168
+ }
169
+ process.exit(1);
170
+ }
171
+ }
package/src/index.tsx CHANGED
@@ -54,6 +54,10 @@ const SLASH_COMMANDS = [
54
54
  { cmd: "/skills search", desc: "search registry" },
55
55
  { cmd: "/skills on", desc: "enable skill for session" },
56
56
  { cmd: "/skills off", desc: "disable skill for session" },
57
+ { cmd: "/architect", desc: "toggle architect mode" },
58
+ { cmd: "/lint", desc: "show auto-lint status" },
59
+ { cmd: "/lint on", desc: "enable auto-lint" },
60
+ { cmd: "/lint off", desc: "disable auto-lint" },
57
61
  { cmd: "/quit", desc: "exit" },
58
62
  ];
59
63
 
@@ -277,6 +281,12 @@ function App() {
277
281
  const savedStr = saved >= 1000 ? `${(saved / 1000).toFixed(1)}k` : String(saved);
278
282
  addMsg("info", `📦 Context compressed (~${savedStr} tokens freed)`);
279
283
  },
284
+ onArchitectPlan: (plan) => {
285
+ addMsg("info", `🏗️ Architect Plan:\n${plan}`);
286
+ },
287
+ onLintResult: (file, errors) => {
288
+ addMsg("info", `🔍 Lint errors in ${file}:\n${errors}`);
289
+ },
280
290
  contextCompressionThreshold: config.defaults.contextCompressionThreshold,
281
291
  onToolApproval: (name, args, diff) => {
282
292
  return new Promise((resolve) => {
@@ -289,6 +299,13 @@ function App() {
289
299
  // Initialize async context (repo map)
290
300
  await a.init();
291
301
 
302
+ // Show project rules in banner
303
+ const rulesSource = a.getProjectRulesSource();
304
+ if (rulesSource) {
305
+ info.push(`📋 ${rulesSource} loaded`);
306
+ setConnectionInfo([...info]);
307
+ }
308
+
292
309
  setAgent(a);
293
310
  setModelName(provider.model);
294
311
  providerRef.current = { baseUrl: provider.baseUrl, apiKey: provider.apiKey };
@@ -336,7 +353,7 @@ function App() {
336
353
  // Commands that need args (like /commit, /model) — fill input instead of executing
337
354
  if (selected.cmd === "/commit" || selected.cmd === "/model" || selected.cmd === "/session delete" ||
338
355
  selected.cmd === "/skills install" || selected.cmd === "/skills remove" || selected.cmd === "/skills search" ||
339
- selected.cmd === "/skills on" || selected.cmd === "/skills off") {
356
+ selected.cmd === "/skills on" || selected.cmd === "/skills off" || selected.cmd === "/architect") {
340
357
  setInput(selected.cmd + " ");
341
358
  setCmdIndex(0);
342
359
  setInputKey((k) => k + 1);
@@ -398,6 +415,10 @@ function App() {
398
415
  " /git on — enable auto-commits",
399
416
  " /git off — disable auto-commits",
400
417
  " /skills — manage skill packs",
418
+ " /architect — toggle architect mode (plan then execute)",
419
+ " /lint — show auto-lint status & detected linter",
420
+ " /lint on — enable auto-lint",
421
+ " /lint off — disable auto-lint",
401
422
  " /quit — exit",
402
423
  ].join("\n"));
403
424
  return;
@@ -502,6 +523,62 @@ function App() {
502
523
  addMsg("info", `✅ Switched to theme: ${THEMES[themeName].name}`);
503
524
  return;
504
525
  }
526
+ // ── Architect commands (work without agent) ──
527
+ if (trimmed === "/architect") {
528
+ if (!agent) {
529
+ addMsg("info", "🏗️ Architect mode: no agent connected. Connect first with /login or /connect.");
530
+ return;
531
+ }
532
+ const current = agent.getArchitectModel();
533
+ if (current) {
534
+ agent.setArchitectModel(null);
535
+ addMsg("info", "🏗️ Architect mode OFF");
536
+ } else {
537
+ // Use config default or a sensible default
538
+ const defaultModel = loadConfig().defaults.architectModel || agent.getModel();
539
+ agent.setArchitectModel(defaultModel);
540
+ addMsg("info", `🏗️ Architect mode ON (planner: ${defaultModel})`);
541
+ }
542
+ return;
543
+ }
544
+ if (trimmed.startsWith("/architect ")) {
545
+ const model = trimmed.replace("/architect ", "").trim();
546
+ if (!model) {
547
+ addMsg("info", "Usage: /architect <model> or /architect to toggle");
548
+ return;
549
+ }
550
+ if (agent) {
551
+ agent.setArchitectModel(model);
552
+ addMsg("info", `🏗️ Architect mode ON (planner: ${model})`);
553
+ } else {
554
+ addMsg("info", "⚠ No agent connected. Connect first.");
555
+ }
556
+ return;
557
+ }
558
+
559
+ // ── Lint commands (work without agent) ──
560
+ if (trimmed === "/lint") {
561
+ const { detectLinter } = await import("./utils/lint.js");
562
+ const linter = detectLinter(process.cwd());
563
+ const enabled = agent ? agent.isAutoLintEnabled() : true;
564
+ if (linter) {
565
+ addMsg("info", `🔍 Auto-lint: ${enabled ? "ON" : "OFF"}\n Detected: ${linter.name}\n Command: ${linter.command} <file>`);
566
+ } else {
567
+ addMsg("info", `🔍 Auto-lint: ${enabled ? "ON" : "OFF"}\n No linter detected in this project.`);
568
+ }
569
+ return;
570
+ }
571
+ if (trimmed === "/lint on") {
572
+ if (agent) agent.setAutoLint(true);
573
+ addMsg("info", "🔍 Auto-lint ON");
574
+ return;
575
+ }
576
+ if (trimmed === "/lint off") {
577
+ if (agent) agent.setAutoLint(false);
578
+ addMsg("info", "🔍 Auto-lint OFF");
579
+ return;
580
+ }
581
+
505
582
  // Commands below require an active LLM connection
506
583
  if (!agent) {
507
584
  addMsg("info", "⚠ No LLM connected. Use /login to authenticate with a provider, or start a local server.");
@@ -684,8 +761,8 @@ function App() {
684
761
 
685
762
  try {
686
763
  // Response is built incrementally via onToken callback
687
- // chat() returns the final text but we don't need to add it again
688
- await agent.chat(trimmed);
764
+ // send() routes through architect if enabled, otherwise direct chat
765
+ await agent.send(trimmed);
689
766
  } catch (err: any) {
690
767
  addMsg("error", `Error: ${err.message}`);
691
768
  }
@@ -1497,6 +1574,7 @@ function App() {
1497
1574
  const count = getActiveSkillCount(process.cwd(), sessionDisabledSkills);
1498
1575
  return count > 0 ? ` · 🧠 ${count} skill${count !== 1 ? "s" : ""}` : "";
1499
1576
  })()}
1577
+ {agent.getArchitectModel() ? " · 🏗️ architect" : ""}
1500
1578
  </Text>
1501
1579
  </Box>
1502
1580
  )}
@@ -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
- return base + "\n\n## Active Skills\n" + skillPrompts;
145
+ prompt += "\n\n## Active Skills\n" + skillPrompts;
125
146
  }
126
- return base;
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
+ }