codemaxxing 0.2.0 → 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/README.md +72 -6
- package/dist/agent.d.ts +27 -0
- package/dist/agent.js +108 -2
- 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 +154 -0
- package/dist/index.js +83 -4
- package/dist/skills/registry.js +954 -0
- 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/package.json +1 -1
- package/src/agent.ts +124 -2
- package/src/cli.ts +5 -1
- package/src/config.ts +11 -0
- package/src/exec.ts +171 -0
- package/src/index.tsx +81 -3
- package/src/skills/registry.ts +954 -0
- package/src/utils/context.ts +34 -12
- package/src/utils/lint.ts +116 -0
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
|
+
}
|
package/package.json
CHANGED
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 {
|
|
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
|
-
|
|
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
|
|
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
|
`);
|
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
|
+
}
|