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/dist/index.js 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
  const SPINNER_FRAMES = ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"];
@@ -236,6 +240,12 @@ function App() {
236
240
  const savedStr = saved >= 1000 ? `${(saved / 1000).toFixed(1)}k` : String(saved);
237
241
  addMsg("info", `📦 Context compressed (~${savedStr} tokens freed)`);
238
242
  },
243
+ onArchitectPlan: (plan) => {
244
+ addMsg("info", `🏗️ Architect Plan:\n${plan}`);
245
+ },
246
+ onLintResult: (file, errors) => {
247
+ addMsg("info", `🔍 Lint errors in ${file}:\n${errors}`);
248
+ },
239
249
  contextCompressionThreshold: config.defaults.contextCompressionThreshold,
240
250
  onToolApproval: (name, args, diff) => {
241
251
  return new Promise((resolve) => {
@@ -246,6 +256,12 @@ function App() {
246
256
  });
247
257
  // Initialize async context (repo map)
248
258
  await a.init();
259
+ // Show project rules in banner
260
+ const rulesSource = a.getProjectRulesSource();
261
+ if (rulesSource) {
262
+ info.push(`📋 ${rulesSource} loaded`);
263
+ setConnectionInfo([...info]);
264
+ }
249
265
  setAgent(a);
250
266
  setModelName(provider.model);
251
267
  providerRef.current = { baseUrl: provider.baseUrl, apiKey: provider.apiKey };
@@ -287,7 +303,7 @@ function App() {
287
303
  // Commands that need args (like /commit, /model) — fill input instead of executing
288
304
  if (selected.cmd === "/commit" || selected.cmd === "/model" || selected.cmd === "/session delete" ||
289
305
  selected.cmd === "/skills install" || selected.cmd === "/skills remove" || selected.cmd === "/skills search" ||
290
- selected.cmd === "/skills on" || selected.cmd === "/skills off") {
306
+ selected.cmd === "/skills on" || selected.cmd === "/skills off" || selected.cmd === "/architect") {
291
307
  setInput(selected.cmd + " ");
292
308
  setCmdIndex(0);
293
309
  setInputKey((k) => k + 1);
@@ -347,6 +363,10 @@ function App() {
347
363
  " /git on — enable auto-commits",
348
364
  " /git off — disable auto-commits",
349
365
  " /skills — manage skill packs",
366
+ " /architect — toggle architect mode (plan then execute)",
367
+ " /lint — show auto-lint status & detected linter",
368
+ " /lint on — enable auto-lint",
369
+ " /lint off — disable auto-lint",
350
370
  " /quit — exit",
351
371
  ].join("\n"));
352
372
  return;
@@ -455,6 +475,65 @@ function App() {
455
475
  addMsg("info", `✅ Switched to theme: ${THEMES[themeName].name}`);
456
476
  return;
457
477
  }
478
+ // ── Architect commands (work without agent) ──
479
+ if (trimmed === "/architect") {
480
+ if (!agent) {
481
+ addMsg("info", "🏗️ Architect mode: no agent connected. Connect first with /login or /connect.");
482
+ return;
483
+ }
484
+ const current = agent.getArchitectModel();
485
+ if (current) {
486
+ agent.setArchitectModel(null);
487
+ addMsg("info", "🏗️ Architect mode OFF");
488
+ }
489
+ else {
490
+ // Use config default or a sensible default
491
+ const defaultModel = loadConfig().defaults.architectModel || agent.getModel();
492
+ agent.setArchitectModel(defaultModel);
493
+ addMsg("info", `🏗️ Architect mode ON (planner: ${defaultModel})`);
494
+ }
495
+ return;
496
+ }
497
+ if (trimmed.startsWith("/architect ")) {
498
+ const model = trimmed.replace("/architect ", "").trim();
499
+ if (!model) {
500
+ addMsg("info", "Usage: /architect <model> or /architect to toggle");
501
+ return;
502
+ }
503
+ if (agent) {
504
+ agent.setArchitectModel(model);
505
+ addMsg("info", `🏗️ Architect mode ON (planner: ${model})`);
506
+ }
507
+ else {
508
+ addMsg("info", "⚠ No agent connected. Connect first.");
509
+ }
510
+ return;
511
+ }
512
+ // ── Lint commands (work without agent) ──
513
+ if (trimmed === "/lint") {
514
+ const { detectLinter } = await import("./utils/lint.js");
515
+ const linter = detectLinter(process.cwd());
516
+ const enabled = agent ? agent.isAutoLintEnabled() : true;
517
+ if (linter) {
518
+ addMsg("info", `🔍 Auto-lint: ${enabled ? "ON" : "OFF"}\n Detected: ${linter.name}\n Command: ${linter.command} <file>`);
519
+ }
520
+ else {
521
+ addMsg("info", `🔍 Auto-lint: ${enabled ? "ON" : "OFF"}\n No linter detected in this project.`);
522
+ }
523
+ return;
524
+ }
525
+ if (trimmed === "/lint on") {
526
+ if (agent)
527
+ agent.setAutoLint(true);
528
+ addMsg("info", "🔍 Auto-lint ON");
529
+ return;
530
+ }
531
+ if (trimmed === "/lint off") {
532
+ if (agent)
533
+ agent.setAutoLint(false);
534
+ addMsg("info", "🔍 Auto-lint OFF");
535
+ return;
536
+ }
458
537
  // Commands below require an active LLM connection
459
538
  if (!agent) {
460
539
  addMsg("info", "⚠ No LLM connected. Use /login to authenticate with a provider, or start a local server.");
@@ -641,8 +720,8 @@ function App() {
641
720
  setSpinnerMsg(SPINNER_MESSAGES[Math.floor(Math.random() * SPINNER_MESSAGES.length)]);
642
721
  try {
643
722
  // Response is built incrementally via onToken callback
644
- // chat() returns the final text but we don't need to add it again
645
- await agent.chat(trimmed);
723
+ // send() routes through architect if enabled, otherwise direct chat
724
+ await agent.send(trimmed);
646
725
  }
647
726
  catch (err) {
648
727
  addMsg("error", `Error: ${err.message}`);
@@ -1192,7 +1271,7 @@ function App() {
1192
1271
  })(), modelName ? ` · 🤖 ${modelName}` : "", (() => {
1193
1272
  const count = getActiveSkillCount(process.cwd(), sessionDisabledSkills);
1194
1273
  return count > 0 ? ` · 🧠 ${count} skill${count !== 1 ? "s" : ""}` : "";
1195
- })()] }) }))] }));
1274
+ })(), agent.getArchitectModel() ? " · 🏗️ architect" : ""] }) }))] }));
1196
1275
  }
1197
1276
  // Clear screen before render
1198
1277
  process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
@@ -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
@@ -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
- return base + "\n\n## Active Skills\n" + skillPrompts;
135
+ prompt += "\n\n## Active Skills\n" + skillPrompts;
116
136
  }
117
- return base;
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codemaxxing",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Open-source terminal coding agent. Connect any LLM. Max your code.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
package/src/agent.ts CHANGED
@@ -6,7 +6,8 @@ import type {
6
6
  ChatCompletionChunk,
7
7
  } from "openai/resources/chat/completions";
8
8
  import { FILE_TOOLS, executeTool, generateDiff, getExistingContent } from "./tools/files.js";
9
- import { buildProjectContext, getSystemPrompt } from "./utils/context.js";
9
+ import { detectLinter, runLinter } from "./utils/lint.js";
10
+ import { buildProjectContext, getSystemPrompt, loadProjectRules } from "./utils/context.js";
10
11
  import { isGitRepo, autoCommit } from "./utils/git.js";
11
12
  import { buildSkillPrompts, getActiveSkillCount } from "./utils/skills.js";
12
13
  import { createSession, saveMessage, updateTokenEstimate, updateSessionCost, loadMessages } from "./utils/sessions.js";
@@ -71,6 +72,8 @@ export interface AgentOptions {
71
72
  onToolApproval?: (name: string, args: Record<string, unknown>, diff?: string) => Promise<"yes" | "no" | "always">;
72
73
  onGitCommit?: (message: string) => void;
73
74
  onContextCompressed?: (oldTokens: number, newTokens: number) => void;
75
+ onArchitectPlan?: (plan: string) => void;
76
+ onLintResult?: (file: string, errors: string) => void;
74
77
  contextCompressionThreshold?: number;
75
78
  }
76
79
 
@@ -101,6 +104,10 @@ export class CodingAgent {
101
104
  private systemPrompt: string = "";
102
105
  private compressionThreshold: number;
103
106
  private sessionDisabledSkills: Set<string> = new Set();
107
+ private projectRulesSource: string | null = null;
108
+ private architectModel: string | null = null;
109
+ private autoLintEnabled: boolean = true;
110
+ private detectedLinter: { command: string; name: string } | null = null;
104
111
 
105
112
  constructor(private options: AgentOptions) {
106
113
  this.providerType = options.provider.type || "openai";
@@ -131,7 +138,12 @@ export class CodingAgent {
131
138
  async init(): Promise<void> {
132
139
  const context = await buildProjectContext(this.cwd);
133
140
  const skillPrompts = buildSkillPrompts(this.cwd, this.sessionDisabledSkills);
134
- this.systemPrompt = await getSystemPrompt(context, skillPrompts);
141
+ const rules = loadProjectRules(this.cwd);
142
+ if (rules) this.projectRulesSource = rules.source;
143
+ this.systemPrompt = await getSystemPrompt(context, skillPrompts, rules?.content ?? "");
144
+
145
+ // Detect project linter
146
+ this.detectedLinter = detectLinter(this.cwd);
135
147
 
136
148
  this.messages = [
137
149
  { role: "system", content: this.systemPrompt },
@@ -174,6 +186,16 @@ export class CodingAgent {
174
186
  return this.repoMap;
175
187
  }
176
188
 
189
+ /**
190
+ * Send a message, routing through architect model if enabled
191
+ */
192
+ async send(userMessage: string): Promise<string> {
193
+ if (this.architectModel) {
194
+ return this.architectChat(userMessage);
195
+ }
196
+ return this.chat(userMessage);
197
+ }
198
+
177
199
  /**
178
200
  * Stream a response from the model.
179
201
  * Assembles tool call chunks, emits tokens in real-time,
@@ -347,6 +369,23 @@ export class CodingAgent {
347
369
  }
348
370
  }
349
371
 
372
+ // Auto-lint after successful write_file
373
+ if (this.autoLintEnabled && this.detectedLinter && toolCall.name === "write_file" && result.startsWith("✅")) {
374
+ const filePath = String(args.path ?? "");
375
+ const lintErrors = runLinter(this.detectedLinter, filePath, this.cwd);
376
+ if (lintErrors) {
377
+ this.options.onLintResult?.(filePath, lintErrors);
378
+ const lintMsg: ChatCompletionMessageParam = {
379
+ role: "tool",
380
+ tool_call_id: toolCall.id,
381
+ content: result + `\n\nLint errors detected in ${filePath}:\n${lintErrors}\nPlease fix these issues.`,
382
+ };
383
+ this.messages.push(lintMsg);
384
+ saveMessage(this.sessionId, lintMsg);
385
+ continue; // skip the normal tool message push
386
+ }
387
+ }
388
+
350
389
  const toolMsg: ChatCompletionMessageParam = {
351
390
  role: "tool",
352
391
  tool_call_id: toolCall.id,
@@ -542,6 +581,23 @@ export class CodingAgent {
542
581
  }
543
582
  }
544
583
 
584
+ // Auto-lint after successful write_file
585
+ if (this.autoLintEnabled && this.detectedLinter && toolCall.name === "write_file" && result.startsWith("✅")) {
586
+ const filePath = String(args.path ?? "");
587
+ const lintErrors = runLinter(this.detectedLinter, filePath, this.cwd);
588
+ if (lintErrors) {
589
+ this.options.onLintResult?.(filePath, lintErrors);
590
+ const lintMsg: ChatCompletionMessageParam = {
591
+ role: "tool",
592
+ tool_call_id: toolCall.id,
593
+ content: result + `\n\nLint errors detected in ${filePath}:\n${lintErrors}\nPlease fix these issues.`,
594
+ };
595
+ this.messages.push(lintMsg);
596
+ saveMessage(this.sessionId, lintMsg);
597
+ continue;
598
+ }
599
+ }
600
+
545
601
  const toolMsg: ChatCompletionMessageParam = {
546
602
  role: "tool",
547
603
  tool_call_id: toolCall.id,
@@ -712,6 +768,72 @@ export class CodingAgent {
712
768
  return this.cwd;
713
769
  }
714
770
 
771
+ getProjectRulesSource(): string | null {
772
+ return this.projectRulesSource;
773
+ }
774
+
775
+ setArchitectModel(model: string | null): void {
776
+ this.architectModel = model;
777
+ }
778
+
779
+ getArchitectModel(): string | null {
780
+ return this.architectModel;
781
+ }
782
+
783
+ setAutoLint(enabled: boolean): void {
784
+ this.autoLintEnabled = enabled;
785
+ }
786
+
787
+ isAutoLintEnabled(): boolean {
788
+ return this.autoLintEnabled;
789
+ }
790
+
791
+ getDetectedLinter(): { command: string; name: string } | null {
792
+ return this.detectedLinter;
793
+ }
794
+
795
+ setDetectedLinter(linter: { command: string; name: string } | null): void {
796
+ this.detectedLinter = linter;
797
+ }
798
+
799
+ /**
800
+ * Run the architect model to generate a plan, then feed to editor model
801
+ */
802
+ private async architectChat(userMessage: string): Promise<string> {
803
+ const architectSystemPrompt = "You are a senior software architect. Analyze the request and create a detailed implementation plan. List exactly which files to modify, what changes to make, and in what order. Do NOT write code — just plan.";
804
+
805
+ let plan = "";
806
+
807
+ if (this.providerType === "anthropic" && this.anthropicClient) {
808
+ const response = await this.anthropicClient.messages.create({
809
+ model: this.architectModel!,
810
+ max_tokens: this.maxTokens,
811
+ system: architectSystemPrompt,
812
+ messages: [{ role: "user", content: userMessage }],
813
+ });
814
+ plan = response.content
815
+ .filter((b): b is Anthropic.TextBlock => b.type === "text")
816
+ .map((b) => b.text)
817
+ .join("");
818
+ } else {
819
+ const response = await this.client.chat.completions.create({
820
+ model: this.architectModel!,
821
+ max_tokens: this.maxTokens,
822
+ messages: [
823
+ { role: "system", content: architectSystemPrompt },
824
+ { role: "user", content: userMessage },
825
+ ],
826
+ });
827
+ plan = response.choices[0]?.message?.content ?? "(no plan generated)";
828
+ }
829
+
830
+ this.options.onArchitectPlan?.(plan);
831
+
832
+ // Feed plan + original request to the editor model
833
+ const editorPrompt = `## Architect Plan\n${plan}\n\n## Original Request\n${userMessage}\n\nExecute the plan above. Follow it step by step.`;
834
+ return this.chat(editorPrompt);
835
+ }
836
+
715
837
  reset(): void {
716
838
  const systemMsg = this.messages[0];
717
839
  this.messages = [systemMsg];
package/src/cli.ts CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  /**
4
4
  * Codemaxxing CLI entry point
5
- * Routes subcommands (login, auth) to auth-cli, everything else to the TUI
5
+ * Routes subcommands (login, auth, exec) to handlers, everything else to the TUI
6
6
  */
7
7
 
8
8
  import { spawn } from "node:child_process";
@@ -27,6 +27,10 @@ if (subcmd === "login" || subcmd === "auth") {
27
27
  });
28
28
 
29
29
  child.on("exit", (code) => process.exit(code ?? 0));
30
+ } else if (subcmd === "exec") {
31
+ // Headless/CI mode — no TUI
32
+ const { runExec } = await import("./exec.js");
33
+ await runExec(process.argv.slice(3));
30
34
  } else {
31
35
  // TUI mode — import directly (not spawn) to preserve raw stdin
32
36
  await import("./index.js");
package/src/config.ts CHANGED
@@ -22,6 +22,8 @@ export interface CodemaxxingConfig {
22
22
  contextFiles: number;
23
23
  maxTokens: number;
24
24
  contextCompressionThreshold?: number;
25
+ architectModel?: string;
26
+ autoLint?: boolean;
25
27
  };
26
28
  }
27
29
 
@@ -85,6 +87,7 @@ codemaxxing — your code. your model. no excuses.
85
87
 
86
88
  Usage:
87
89
  codemaxxing [options]
90
+ codemaxxing exec "prompt" [exec-options]
88
91
 
89
92
  Options:
90
93
  -m, --model <model> Model name to use
@@ -93,11 +96,19 @@ Options:
93
96
  -u, --base-url <url> Base URL for the provider API
94
97
  -h, --help Show this help
95
98
 
99
+ Exec options (headless/CI mode):
100
+ --auto-approve Skip tool approval prompts
101
+ --json Output JSON instead of streaming text
102
+ -m, --model <model> Model to use
103
+ -p, --provider <name> Provider profile
104
+
96
105
  Examples:
97
106
  codemaxxing # Auto-detect local LLM
98
107
  codemaxxing -m gpt-4o -u https://api.openai.com/v1 -k sk-...
99
108
  codemaxxing -p openrouter # Use saved provider profile
100
109
  codemaxxing -m qwen3.5-35b # Override model only
110
+ codemaxxing exec "fix the failing tests" # Headless mode
111
+ echo "explain this code" | codemaxxing exec # Pipe input
101
112
 
102
113
  Config: ~/.codemaxxing/settings.json
103
114
  `);