code-review-agent-cli 1.0.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 +273 -0
- package/dist/agent.js +317 -0
- package/dist/index.js +125 -0
- package/dist/prompts/prompts/system.md +16 -0
- package/dist/prompts/system.md +16 -0
- package/dist/skills/code-review.md +65 -0
- package/dist/skills/skills/code-review.md +65 -0
- package/dist/utils/display.js +125 -0
- package/dist/utils/formatting.js +46 -0
- package/package.json +27 -0
package/README.md
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# Code Review Agent
|
|
2
|
+
|
|
3
|
+
A code review and audit CLI tool powered by the Claude Agent SDK. It analyzes codebases for bugs, security issues, performance problems, and maintainability concerns — all from your terminal.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
- **Node.js** v18+
|
|
8
|
+
- An **Anthropic API key** — get one from the [Claude Console](https://platform.claude.com/)
|
|
9
|
+
|
|
10
|
+
## Setup
|
|
11
|
+
|
|
12
|
+
### From source
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
git clone <your-repo-url>
|
|
16
|
+
cd my-first-agent
|
|
17
|
+
npm install
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### As an npm package
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install -g code-review-agent
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Then run from any project directory:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
code-review-agent "Review this codebase"
|
|
30
|
+
code-review-agent --fix-recursive
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### Authentication
|
|
34
|
+
|
|
35
|
+
This tool uses the **Claude Agent SDK**, which calls the Anthropic API directly. You need an `ANTHROPIC_API_KEY` set in your environment.
|
|
36
|
+
|
|
37
|
+
**Add it to your shell profile** (recommended — works for both npm and source installs):
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Add to ~/.bashrc, ~/.zshrc, or ~/.bash_profile
|
|
41
|
+
export ANTHROPIC_API_KEY=your-api-key
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Then reload your shell (`source ~/.zshrc`) or open a new terminal.
|
|
45
|
+
|
|
46
|
+
**Or pass it inline** for a single run:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
ANTHROPIC_API_KEY=your-api-key code-review-agent "Review this codebase"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Or use a `.env` file** (when running from source only):
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
echo "ANTHROPIC_API_KEY=your-api-key" > .env
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The SDK also supports third-party API providers:
|
|
59
|
+
|
|
60
|
+
- **Amazon Bedrock**: set `CLAUDE_CODE_USE_BEDROCK=1` and configure AWS credentials
|
|
61
|
+
- **Google Vertex AI**: set `CLAUDE_CODE_USE_VERTEX=1` and configure Google Cloud credentials
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
|
|
65
|
+
### Quick start
|
|
66
|
+
|
|
67
|
+
Run with no arguments to explore and review the current directory:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npm start
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Or pass a specific prompt:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
npx tsx index.ts "Review utils.py for bugs"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
With npm start:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npm start -- "Review the authentication module"
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Fix mode
|
|
86
|
+
|
|
87
|
+
Use `--fix` to have the agent apply its recommended fixes directly to your source files:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
npx tsx index.ts --fix "Review utils.py for bugs"
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The agent will review the code, then edit the files to fix Critical and Warning issues.
|
|
94
|
+
|
|
95
|
+
### Recursive fix mode
|
|
96
|
+
|
|
97
|
+
Use `--fix-recursive` to run a review/fix loop that keeps going until there are no more critical issues:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
npx tsx index.ts --fix-recursive "Review all source files"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Each pass reviews the code, applies fixes, then re-reviews the modified files to catch any issues introduced by the fixes. The loop stops when the agent reports all clear or the pass limit is reached.
|
|
104
|
+
|
|
105
|
+
Control the maximum number of passes with `--max-passes` (default: 5):
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npx tsx index.ts --fix-recursive --max-passes 3 "Audit agent.ts"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Options
|
|
112
|
+
|
|
113
|
+
| Flag | Description | Default |
|
|
114
|
+
|------|-------------|---------|
|
|
115
|
+
| `-m, --model <model>` | Claude model to use | `claude-sonnet-4-5-20250929` |
|
|
116
|
+
| `-t, --tools <tools>` | Comma-separated list of allowed tools | `Read,Edit,Glob,Grep,Write,Bash` |
|
|
117
|
+
| `-p, --permission-mode <mode>` | `default`, `acceptEdits`, or `bypassPermissions` | `acceptEdits` |
|
|
118
|
+
| `--max-turns <n>` | Maximum number of agentic turns | unlimited |
|
|
119
|
+
| `--fix` | Apply recommended fixes to source files | off |
|
|
120
|
+
| `--fix-recursive` | Review, fix, re-review until no critical issues remain | off |
|
|
121
|
+
| `--max-passes <n>` | Max review/fix passes for `--fix-recursive` | `5` |
|
|
122
|
+
| `--cwd <dir>` | Working directory for the agent | current directory |
|
|
123
|
+
|
|
124
|
+
### Examples
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
# Review only (report issues, don't touch files)
|
|
128
|
+
npx tsx index.ts "Review src/ for security issues" -t Read,Glob,Grep
|
|
129
|
+
|
|
130
|
+
# Review and fix in one pass
|
|
131
|
+
npx tsx index.ts --fix "Find and fix bugs in utils.py"
|
|
132
|
+
|
|
133
|
+
# Recursive fix until clean
|
|
134
|
+
npx tsx index.ts --fix-recursive "Audit this codebase"
|
|
135
|
+
|
|
136
|
+
# Recursive fix with limited passes and a specific directory
|
|
137
|
+
npx tsx index.ts --fix-recursive --max-passes 3 --cwd ./src "Review all files"
|
|
138
|
+
|
|
139
|
+
# Use a different model
|
|
140
|
+
npx tsx index.ts "Audit this codebase" -m claude-sonnet-4-5-20250929
|
|
141
|
+
|
|
142
|
+
# Full permissions (use with caution)
|
|
143
|
+
npx tsx index.ts --fix "Fix all critical bugs" -p bypassPermissions
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Architecture
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
my-first-agent/
|
|
150
|
+
├── index.ts # CLI entry point — parses args, calls agent
|
|
151
|
+
├── agent.ts # Core agent — identity, tools, prompt, runs query()
|
|
152
|
+
├── prompts/
|
|
153
|
+
│ └── system.md # System prompt — agent identity and behavior
|
|
154
|
+
├── skills/
|
|
155
|
+
│ └── code-review.md # Code review methodology (loaded at startup)
|
|
156
|
+
├── utils/
|
|
157
|
+
│ ├── display.ts # Message rendering for the terminal
|
|
158
|
+
│ └── formatting.ts # String helpers (truncation, tool formatting)
|
|
159
|
+
├── marked-terminal.d.ts # Type declaration for marked-terminal
|
|
160
|
+
├── package.json
|
|
161
|
+
└── tsconfig.json
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### How it works
|
|
165
|
+
|
|
166
|
+
**`index.ts`** — Thin CLI entry point. Uses Commander to parse arguments and options, then calls `runAgent()`.
|
|
167
|
+
|
|
168
|
+
**`agent.ts`** — Core agent module. Loads the system prompt from `prompts/system.md`, configures tools, and streams output through `showMessage()`. Supports three modes:
|
|
169
|
+
|
|
170
|
+
- **Review only** (default) — runs a single review pass
|
|
171
|
+
- **`--fix`** — appends fix-mode instructions to the system prompt so the agent edits files after reviewing
|
|
172
|
+
- **`--fix-recursive`** — runs the agent in a loop: review, fix, re-review modified files, repeat until `ALL_CLEAR` or max passes reached
|
|
173
|
+
|
|
174
|
+
**`prompts/system.md`** — Defines who the agent is and how it behaves. This is what makes it a code review agent rather than a generic CLI tool.
|
|
175
|
+
|
|
176
|
+
**`skills/`** — Skill files (`.md`) loaded at startup and appended to the system prompt. Each file adds a specific capability. Drop in a new `.md` file to extend the agent — no code changes needed.
|
|
177
|
+
|
|
178
|
+
### Message streaming
|
|
179
|
+
|
|
180
|
+
The agent uses an async iterator from `query()`. Each message has a `type`:
|
|
181
|
+
|
|
182
|
+
- **`assistant`** — Claude's response. Contains text blocks and tool-use blocks.
|
|
183
|
+
- **`user`** — Tool results returned to Claude (stderr shown for Bash).
|
|
184
|
+
- **`tool_progress`** — Progress updates for long-running tools.
|
|
185
|
+
- **`result`** — Final message with stats (turns, duration, cost).
|
|
186
|
+
|
|
187
|
+
### Environment variables
|
|
188
|
+
|
|
189
|
+
| Variable | Description | Default |
|
|
190
|
+
|----------|-------------|---------|
|
|
191
|
+
| `ANTHROPIC_API_KEY` | Your Anthropic API key (required) | — |
|
|
192
|
+
| `MAX_PROMPT_LENGTH` | Maximum allowed prompt length in characters (capped at 100,000) | `50000` |
|
|
193
|
+
| `DEBUG` | Show stack traces and verbose error output | off |
|
|
194
|
+
|
|
195
|
+
## Flow Diagrams
|
|
196
|
+
|
|
197
|
+
### Normal mode (`--fix` or no flags)
|
|
198
|
+
|
|
199
|
+
```mermaid
|
|
200
|
+
flowchart TD
|
|
201
|
+
A[CLI: parse args] --> B[Validate prompt]
|
|
202
|
+
B --> C[Load system prompt]
|
|
203
|
+
C --> D{--fix flag?}
|
|
204
|
+
D -- Yes --> E[Append fix instructions]
|
|
205
|
+
D -- No --> F[Use base prompt]
|
|
206
|
+
E --> G[Build options]
|
|
207
|
+
F --> G
|
|
208
|
+
G --> H[query prompt, options]
|
|
209
|
+
|
|
210
|
+
H --> I{Stream messages}
|
|
211
|
+
I -- assistant --> J[Render text / tool calls]
|
|
212
|
+
I -- user --> K[Show stderr if any]
|
|
213
|
+
I -- tool_progress --> L[Show progress]
|
|
214
|
+
I -- result --> M[Show summary]
|
|
215
|
+
|
|
216
|
+
J --> I
|
|
217
|
+
K --> I
|
|
218
|
+
L --> I
|
|
219
|
+
M --> N[Done]
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Recursive fix mode (`--fix-recursive`)
|
|
223
|
+
|
|
224
|
+
```mermaid
|
|
225
|
+
flowchart TD
|
|
226
|
+
A[CLI: parse args] --> B[Validate prompt]
|
|
227
|
+
B --> C[Load system prompt]
|
|
228
|
+
C --> D[Append recursive fix instructions]
|
|
229
|
+
D --> E[Build options]
|
|
230
|
+
|
|
231
|
+
E --> F["Pass 1: executeQuery(user prompt)"]
|
|
232
|
+
F --> G[Agent reviews + fixes files]
|
|
233
|
+
G --> H[Capture last output text]
|
|
234
|
+
|
|
235
|
+
H --> I{Output contains\nALL_CLEAR?}
|
|
236
|
+
I -- Yes --> J["All critical issues resolved"]
|
|
237
|
+
I -- No --> K{pass < maxPasses?}
|
|
238
|
+
K -- Yes --> L["Pass N: executeQuery(re-review prompt)"]
|
|
239
|
+
L --> G
|
|
240
|
+
K -- No --> M["Reached max passes\n⚠ issues may remain"]
|
|
241
|
+
|
|
242
|
+
style J fill:#2d6,color:#fff
|
|
243
|
+
style M fill:#d93,color:#fff
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Adding Skills
|
|
247
|
+
|
|
248
|
+
Drop a `.md` file into the `skills/` directory. It will be loaded automatically at startup and appended to the system prompt.
|
|
249
|
+
|
|
250
|
+
For example, to add a dependency audit skill:
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
cat > skills/dependency-audit.md << 'EOF'
|
|
254
|
+
# Dependency Audit
|
|
255
|
+
|
|
256
|
+
When asked to audit dependencies, follow this process.
|
|
257
|
+
|
|
258
|
+
## Process
|
|
259
|
+
1. Read package.json (or requirements.txt, go.mod, etc.)
|
|
260
|
+
2. Run the appropriate audit command (npm audit, pip audit, etc.)
|
|
261
|
+
3. Check for outdated packages
|
|
262
|
+
4. Flag packages with restrictive or incompatible licenses
|
|
263
|
+
|
|
264
|
+
## Output Format
|
|
265
|
+
For each issue found:
|
|
266
|
+
- **Package**: name and version
|
|
267
|
+
- **Severity**: Critical / High / Medium / Low
|
|
268
|
+
- **Issue**: what's wrong
|
|
269
|
+
- **Fix**: recommended action
|
|
270
|
+
EOF
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Skills are loaded alphabetically by filename.
|
package/dist/agent.js
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
2
|
+
import { resolve, dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
5
|
+
import { showMessage } from "./utils/display.js";
|
|
6
|
+
import { stripAnsiCodes } from "./utils/formatting.js";
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const VALID_MODELS = new Set(["claude-sonnet-4-5-20250929", "claude-opus-4-6", "claude-opus-4"]);
|
|
9
|
+
const VALID_PERMISSION_MODES = new Set(["default", "acceptEdits", "bypassPermissions"]);
|
|
10
|
+
const MAX_PROMPT_LENGTH = (() => {
|
|
11
|
+
if (!process.env.MAX_PROMPT_LENGTH)
|
|
12
|
+
return 50000;
|
|
13
|
+
const parsed = Number.parseInt(process.env.MAX_PROMPT_LENGTH, 10);
|
|
14
|
+
if (Number.isNaN(parsed) || parsed < 1)
|
|
15
|
+
return 50000;
|
|
16
|
+
return Math.min(parsed, 100000);
|
|
17
|
+
})();
|
|
18
|
+
let cachedSystemPrompt = null;
|
|
19
|
+
async function loadSystemPrompt() {
|
|
20
|
+
if (cachedSystemPrompt !== null)
|
|
21
|
+
return cachedSystemPrompt;
|
|
22
|
+
const promptPath = resolve(__dirname, "prompts/system.md");
|
|
23
|
+
try {
|
|
24
|
+
cachedSystemPrompt = await readFile(promptPath, "utf-8");
|
|
25
|
+
return cachedSystemPrompt;
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
29
|
+
const wrapped = new Error(`Failed to load system prompt from ${promptPath}. Ensure prompts/system.md exists.\nCause: ${errorMsg}`, error instanceof Error ? { cause: error } : undefined);
|
|
30
|
+
if (error instanceof Error && error.stack) {
|
|
31
|
+
wrapped.stack = `${wrapped.stack}\n\nCaused by:\n${error.stack}`;
|
|
32
|
+
}
|
|
33
|
+
throw wrapped;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
let cachedSkills = null;
|
|
37
|
+
async function loadSkills() {
|
|
38
|
+
if (cachedSkills !== null)
|
|
39
|
+
return cachedSkills;
|
|
40
|
+
const skillsDir = resolve(__dirname, "skills");
|
|
41
|
+
let entries;
|
|
42
|
+
try {
|
|
43
|
+
entries = await readdir(skillsDir);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
const errCode = error.code;
|
|
47
|
+
if (errCode === "ENOENT" || errCode === "ENOTDIR") {
|
|
48
|
+
cachedSkills = "";
|
|
49
|
+
return cachedSkills;
|
|
50
|
+
}
|
|
51
|
+
throw new Error(`Failed to read skills directory: ${error instanceof Error ? error.message : String(error)}`);
|
|
52
|
+
}
|
|
53
|
+
const mdFiles = entries.filter(f => f.endsWith(".md")).sort((a, b) => a.localeCompare(b));
|
|
54
|
+
if (mdFiles.length === 0) {
|
|
55
|
+
cachedSkills = "";
|
|
56
|
+
return cachedSkills;
|
|
57
|
+
}
|
|
58
|
+
const contents = await Promise.all(mdFiles.map(file => readFile(join(skillsDir, file), "utf-8")));
|
|
59
|
+
const parts = contents.map(c => c.trim());
|
|
60
|
+
cachedSkills = "\n\n" + parts.join("\n\n");
|
|
61
|
+
return cachedSkills;
|
|
62
|
+
}
|
|
63
|
+
function validatePrompt(prompt) {
|
|
64
|
+
if (!prompt || prompt.trim().length === 0) {
|
|
65
|
+
throw new Error("Prompt cannot be empty");
|
|
66
|
+
}
|
|
67
|
+
if (prompt.length > MAX_PROMPT_LENGTH) {
|
|
68
|
+
throw new Error(`Prompt exceeds maximum length of ${MAX_PROMPT_LENGTH} characters`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const FIX_MODE_INSTRUCTIONS = `
|
|
72
|
+
|
|
73
|
+
## Fix Mode
|
|
74
|
+
|
|
75
|
+
Fix mode is enabled. You MUST apply fixes, not just report them.
|
|
76
|
+
|
|
77
|
+
### Process
|
|
78
|
+
1. Review the code and identify Critical and Warning issues.
|
|
79
|
+
2. For EACH issue found, immediately use the Edit tool to fix it in the source file.
|
|
80
|
+
3. After all edits are applied, output a summary listing every file you changed and what you fixed.
|
|
81
|
+
|
|
82
|
+
### Rules
|
|
83
|
+
- Fix Critical and Warning issues. Skip Suggestions unless they are trivial.
|
|
84
|
+
- Make the smallest possible change for each fix — do not refactor surrounding code.
|
|
85
|
+
- You MUST call the Edit tool for each fix. Do not just describe the fix — apply it.
|
|
86
|
+
`;
|
|
87
|
+
const FIX_RECURSIVE_INSTRUCTIONS = `
|
|
88
|
+
|
|
89
|
+
## Recursive Fix Mode
|
|
90
|
+
|
|
91
|
+
Recursive fix mode is enabled. You MUST apply fixes, not just report them.
|
|
92
|
+
|
|
93
|
+
### Process
|
|
94
|
+
1. Review the code and identify Critical and Warning issues.
|
|
95
|
+
2. For EACH issue found, immediately use the Edit tool to fix it in the source file.
|
|
96
|
+
3. After all edits are applied, output a summary in this exact format:
|
|
97
|
+
|
|
98
|
+
### Fixes Applied
|
|
99
|
+
- **file.ts:LINE** — description of what was fixed
|
|
100
|
+
|
|
101
|
+
If no fixes were needed, write: "No fixes needed."
|
|
102
|
+
|
|
103
|
+
4. On your VERY LAST line of output (after everything else), write ONLY one of these two status markers on its own line:
|
|
104
|
+
- CRITICAL_REMAINING: N (where N is the count of Critical issues you could NOT fix)
|
|
105
|
+
- ALL_CLEAR (if zero Critical issues remain after your fixes)
|
|
106
|
+
|
|
107
|
+
### Rules
|
|
108
|
+
- You MUST call the Edit tool for each fix. Do not just describe the fix — apply it.
|
|
109
|
+
- Fix Critical and Warning issues. Skip Suggestions.
|
|
110
|
+
- Make the smallest possible change — do not refactor surrounding code.
|
|
111
|
+
- The status marker must be the VERY LAST line, with nothing after it.
|
|
112
|
+
`;
|
|
113
|
+
const DEFAULT_MAX_PASSES = 5;
|
|
114
|
+
const FIX_PROMPT_PREFIX = "IMPORTANT: You are in fix mode. You MUST apply fixes using the Edit tool — do not just report issues. " +
|
|
115
|
+
"For each bug or issue you find, immediately call the Edit tool to fix it in the source file. " +
|
|
116
|
+
"Do NOT ask the user if they want fixes applied — apply them directly.\n\n";
|
|
117
|
+
function getFixInstructions(opts) {
|
|
118
|
+
if (opts.fixRecursive)
|
|
119
|
+
return FIX_RECURSIVE_INSTRUCTIONS;
|
|
120
|
+
if (opts.fix)
|
|
121
|
+
return FIX_MODE_INSTRUCTIONS;
|
|
122
|
+
return "";
|
|
123
|
+
}
|
|
124
|
+
function resolveOptions(opts, promptAppend) {
|
|
125
|
+
const model = opts.model ?? "claude-sonnet-4-5-20250929";
|
|
126
|
+
if (!VALID_MODELS.has(model)) {
|
|
127
|
+
throw new Error(`Invalid model: ${model}`);
|
|
128
|
+
}
|
|
129
|
+
const permissionMode = (opts.permissionMode ?? "acceptEdits");
|
|
130
|
+
if (!VALID_PERMISSION_MODES.has(permissionMode)) {
|
|
131
|
+
throw new Error(`Invalid permission mode: ${permissionMode}`);
|
|
132
|
+
}
|
|
133
|
+
if (permissionMode === "bypassPermissions" && !opts.bypassConfirmed) {
|
|
134
|
+
throw new Error("bypassPermissions requires confirmation via the CLI");
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
model,
|
|
138
|
+
allowedTools: opts.tools ?? ["Read", "Edit", "Glob", "Grep", "Write", "Bash"],
|
|
139
|
+
permissionMode,
|
|
140
|
+
systemPrompt: {
|
|
141
|
+
type: "preset",
|
|
142
|
+
preset: "claude_code",
|
|
143
|
+
append: promptAppend,
|
|
144
|
+
},
|
|
145
|
+
...(opts.maxTurns && { maxTurns: opts.maxTurns }),
|
|
146
|
+
...(opts.cwd && { cwd: opts.cwd }),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
// Returns the last text block from an assistant message, or null if none found.
|
|
150
|
+
function extractLastText(msg) {
|
|
151
|
+
if (msg.type !== "assistant")
|
|
152
|
+
return null;
|
|
153
|
+
const assistant = msg;
|
|
154
|
+
const content = assistant.message?.content;
|
|
155
|
+
if (!Array.isArray(content))
|
|
156
|
+
return null;
|
|
157
|
+
let text = null;
|
|
158
|
+
for (const block of content) {
|
|
159
|
+
if (typeof block === "object" && block !== null && "text" in block) {
|
|
160
|
+
const textValue = block.text;
|
|
161
|
+
if (typeof textValue === "string") {
|
|
162
|
+
text = textValue;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return text;
|
|
167
|
+
}
|
|
168
|
+
function isValidMessage(message) {
|
|
169
|
+
return !!message && typeof message === "object" && !Array.isArray(message);
|
|
170
|
+
}
|
|
171
|
+
function isFileModificationBlock(block) {
|
|
172
|
+
if (typeof block !== "object" || block === null || !("name" in block))
|
|
173
|
+
return false;
|
|
174
|
+
const name = block.name;
|
|
175
|
+
return name === "Edit" || name === "Write";
|
|
176
|
+
}
|
|
177
|
+
function getEditFilePath(block) {
|
|
178
|
+
if (typeof block.input !== "object" || block.input === null)
|
|
179
|
+
return null;
|
|
180
|
+
const input = block.input;
|
|
181
|
+
const filePath = input.file_path;
|
|
182
|
+
if (typeof filePath === "string" && filePath.length > 0)
|
|
183
|
+
return filePath;
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
function collectEdits(msg, editedFiles) {
|
|
187
|
+
if (msg.type !== "assistant")
|
|
188
|
+
return 0;
|
|
189
|
+
const assistant = msg;
|
|
190
|
+
const content = assistant.message?.content;
|
|
191
|
+
if (!Array.isArray(content))
|
|
192
|
+
return 0;
|
|
193
|
+
let count = 0;
|
|
194
|
+
for (const block of content) {
|
|
195
|
+
if (isFileModificationBlock(block)) {
|
|
196
|
+
count++;
|
|
197
|
+
const filePath = getEditFilePath(block);
|
|
198
|
+
if (filePath)
|
|
199
|
+
editedFiles.add(filePath);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return count;
|
|
203
|
+
}
|
|
204
|
+
async function executeQuery(prompt, options) {
|
|
205
|
+
let lastText = "";
|
|
206
|
+
let editCount = 0;
|
|
207
|
+
const editedFiles = new Set();
|
|
208
|
+
// Validate prompt is not empty after sanitization
|
|
209
|
+
// Strip control characters except tabs (\x09), newlines (\x0A), and carriage returns (\x0D)
|
|
210
|
+
const sanitizedPrompt = prompt.replaceAll(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "");
|
|
211
|
+
if (sanitizedPrompt.trim().length === 0) {
|
|
212
|
+
throw new Error("Prompt is empty after sanitization");
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
for await (const message of query({ prompt: sanitizedPrompt, options })) {
|
|
216
|
+
if (!isValidMessage(message))
|
|
217
|
+
continue;
|
|
218
|
+
if (typeof message.type !== "string")
|
|
219
|
+
continue;
|
|
220
|
+
editCount += collectEdits(message, editedFiles);
|
|
221
|
+
await showMessage(message);
|
|
222
|
+
lastText = extractLastText(message) ?? lastText;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
catch (error) {
|
|
226
|
+
if (error instanceof Error)
|
|
227
|
+
throw error;
|
|
228
|
+
throw new Error(`Query execution failed: ${String(error)}`);
|
|
229
|
+
}
|
|
230
|
+
return { lastText, editCount, editedFiles: Array.from(editedFiles) };
|
|
231
|
+
}
|
|
232
|
+
function getStatusFromOutput(text) {
|
|
233
|
+
const trimmed = text.trim();
|
|
234
|
+
if (trimmed.length === 0)
|
|
235
|
+
return "unknown";
|
|
236
|
+
const lines = trimmed.split("\n");
|
|
237
|
+
const lastLine = lines.length > 0 ? stripAnsiCodes(lines[lines.length - 1].trim()).toUpperCase() : "";
|
|
238
|
+
if (lastLine === "ALL_CLEAR")
|
|
239
|
+
return "all_clear";
|
|
240
|
+
const match = /^CRITICAL_REMAINING:\s*(\d+)$/i.exec(lastLine);
|
|
241
|
+
if (match && match[1]) {
|
|
242
|
+
const count = Number.parseInt(match[1], 10);
|
|
243
|
+
if (!Number.isNaN(count) && count >= 0 && count < 1000000) {
|
|
244
|
+
return count === 0 ? "all_clear" : "critical_remaining";
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return "unknown";
|
|
248
|
+
}
|
|
249
|
+
async function runRecursive(prompt, options, maxPasses) {
|
|
250
|
+
if (!Number.isInteger(maxPasses) || maxPasses < 1 || maxPasses > 100) {
|
|
251
|
+
throw new Error("maxPasses must be an integer between 1 and 100");
|
|
252
|
+
}
|
|
253
|
+
console.log(`\n\x1b[35m━━━ Pass 1/${maxPasses} ━━━\x1b[0m\n`);
|
|
254
|
+
let result = await executeQuery(FIX_PROMPT_PREFIX + prompt, options);
|
|
255
|
+
console.log(`\n\x1b[36m━━━ Pass 1 complete: ${result.editCount} file edit(s) applied ━━━\x1b[0m`);
|
|
256
|
+
for (let pass = 2; pass <= maxPasses; pass++) {
|
|
257
|
+
if (result.editCount === 0) {
|
|
258
|
+
// No edits in the last pass — check if the agent confirmed all clear
|
|
259
|
+
const status = getStatusFromOutput(result.lastText);
|
|
260
|
+
if (status === "all_clear") {
|
|
261
|
+
console.log("\n\x1b[32m━━━ All critical issues resolved ━━━\x1b[0m\n");
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
console.log("\x1b[33m⚠ No edits were made — stopping early.\x1b[0m\n");
|
|
265
|
+
}
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
// Edits were made — always re-review to verify fixes didn't introduce new issues
|
|
269
|
+
const fileList = result.editedFiles.length > 0
|
|
270
|
+
? `Re-review these modified files: ${result.editedFiles.join(", ")}.`
|
|
271
|
+
: "Re-review all source files that were just modified.";
|
|
272
|
+
const reReviewPrompt = fileList +
|
|
273
|
+
" Check if the fixes introduced new issues. Fix any remaining Critical or Warning issues.";
|
|
274
|
+
console.log(`\n\x1b[35m━━━ Pass ${pass}/${maxPasses} ━━━\x1b[0m\n`);
|
|
275
|
+
result = await executeQuery(reReviewPrompt, options);
|
|
276
|
+
console.log(`\n\x1b[36m━━━ Pass ${pass} complete: ${result.editCount} file edit(s) applied ━━━\x1b[0m`);
|
|
277
|
+
}
|
|
278
|
+
const finalStatus = getStatusFromOutput(result.lastText);
|
|
279
|
+
if (finalStatus === "all_clear") {
|
|
280
|
+
console.log("\n\x1b[32m━━━ All critical issues resolved ━━━\x1b[0m\n");
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
console.log(`\n\x1b[33m━━━ Reached max passes (${maxPasses}). Some critical issues may remain. ━━━\x1b[0m\n`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function wrapQueryError(error) {
|
|
287
|
+
if (error instanceof Error) {
|
|
288
|
+
const wrapped = new Error(`Agent query failed: ${error.message}`);
|
|
289
|
+
if (error.stack) {
|
|
290
|
+
wrapped.stack = `${wrapped.stack}\nCaused by: ${error.stack}`;
|
|
291
|
+
}
|
|
292
|
+
return wrapped;
|
|
293
|
+
}
|
|
294
|
+
return new Error(`Agent query failed: ${String(error)}`);
|
|
295
|
+
}
|
|
296
|
+
export async function runAgent(prompt, opts = {}) {
|
|
297
|
+
validatePrompt(prompt);
|
|
298
|
+
await loadSystemPrompt(); // Validate that system prompt exists
|
|
299
|
+
const skills = await loadSkills();
|
|
300
|
+
const promptAppend = skills + getFixInstructions(opts);
|
|
301
|
+
const options = resolveOptions(opts, promptAppend);
|
|
302
|
+
try {
|
|
303
|
+
if (opts.fixRecursive) {
|
|
304
|
+
await runRecursive(prompt, options, opts.maxPasses ?? DEFAULT_MAX_PASSES);
|
|
305
|
+
}
|
|
306
|
+
else if (opts.fix) {
|
|
307
|
+
const { editCount } = await executeQuery(FIX_PROMPT_PREFIX + prompt, options);
|
|
308
|
+
console.log(`\n\x1b[36m━━━ ${editCount} file edit(s) applied ━━━\x1b[0m\n`);
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
await executeQuery(prompt, options);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
catch (error) {
|
|
315
|
+
throw wrapQueryError(error);
|
|
316
|
+
}
|
|
317
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
import { createInterface } from "node:readline";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import { runAgent } from "./agent.js";
|
|
5
|
+
const DEFAULT_PROMPT = "Explore all the files in the current directory. List them and provide a brief summary of what this project is about. Then review all source files for bugs, security issues, and code quality.";
|
|
6
|
+
const VALID_TOOLS = new Set(["Read", "Edit", "Glob", "Grep", "Write", "Bash"]);
|
|
7
|
+
const VALID_PERMISSION_MODES = new Set(["default", "acceptEdits", "bypassPermissions"]);
|
|
8
|
+
function checkApiKey() {
|
|
9
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
10
|
+
if (!apiKey || apiKey.trim().length === 0) {
|
|
11
|
+
console.error("\x1b[31mError: ANTHROPIC_API_KEY is not set or empty.\x1b[0m\n");
|
|
12
|
+
console.error("Set it in your shell profile (persists across sessions):");
|
|
13
|
+
console.error(" echo 'export ANTHROPIC_API_KEY=your-api-key' >> ~/.zshrc");
|
|
14
|
+
console.error(" source ~/.zshrc\n");
|
|
15
|
+
console.error("Or pass it inline for a single run:");
|
|
16
|
+
console.error(" ANTHROPIC_API_KEY=your-api-key code-review-agent \"Review this codebase\"\n");
|
|
17
|
+
console.error("Get your API key at: https://platform.claude.com/");
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
// Basic format validation
|
|
21
|
+
const trimmed = apiKey.trim();
|
|
22
|
+
if (trimmed.length < 40 || !/^sk-ant-[a-zA-Z0-9_-]{40,}$/.test(trimmed)) {
|
|
23
|
+
console.error("\x1b[33m⚠ Warning: ANTHROPIC_API_KEY may have an invalid format.\x1b[0m");
|
|
24
|
+
console.error("\x1b[33m Expected format: sk-ant-...\x1b[0m\n");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const program = new Command();
|
|
28
|
+
program
|
|
29
|
+
.name("code-review-agent")
|
|
30
|
+
.description("Code review and audit agent powered by Claude")
|
|
31
|
+
.version("1.0.0")
|
|
32
|
+
.argument("[prompt]", "The prompt to send to the agent")
|
|
33
|
+
.option("-m, --model <model>", "Model to use", "claude-sonnet-4-5-20250929")
|
|
34
|
+
.option("-t, --tools <tools>", "Comma-separated allowed tools", "Read,Edit,Glob,Grep,Write,Bash")
|
|
35
|
+
.option("-p, --permission-mode <mode>", "Permission mode: default, acceptEdits, bypassPermissions", "acceptEdits")
|
|
36
|
+
.option("--max-turns <n>", "Max turns", (val) => {
|
|
37
|
+
const parsed = Number.parseInt(val, 10);
|
|
38
|
+
if (Number.isNaN(parsed) || parsed <= 0) {
|
|
39
|
+
throw new Error("--max-turns must be a positive integer");
|
|
40
|
+
}
|
|
41
|
+
return parsed;
|
|
42
|
+
})
|
|
43
|
+
.option("--fix", "Apply recommended fixes to source files")
|
|
44
|
+
.option("--fix-recursive", "Review, fix, and re-review until no critical issues remain")
|
|
45
|
+
.option("--max-passes <n>", "Max review passes for --fix-recursive (default: 5)", (val) => {
|
|
46
|
+
const parsed = Number.parseInt(val, 10);
|
|
47
|
+
if (Number.isNaN(parsed) || parsed <= 0) {
|
|
48
|
+
throw new Error("--max-passes must be a positive integer");
|
|
49
|
+
}
|
|
50
|
+
return parsed;
|
|
51
|
+
}, 5)
|
|
52
|
+
.option("--cwd <dir>", "Working directory")
|
|
53
|
+
.action(async (prompt, opts) => {
|
|
54
|
+
checkApiKey();
|
|
55
|
+
try {
|
|
56
|
+
const tools = opts.tools.split(",").map((t) => t.trim());
|
|
57
|
+
const invalidTools = tools.filter((t) => !VALID_TOOLS.has(t));
|
|
58
|
+
if (invalidTools.length > 0) {
|
|
59
|
+
throw new Error(`Invalid tools: ${invalidTools.join(", ")}`);
|
|
60
|
+
}
|
|
61
|
+
if (!VALID_PERMISSION_MODES.has(opts.permissionMode)) {
|
|
62
|
+
throw new Error(`Invalid permission mode: ${opts.permissionMode}. Must be one of: ${Array.from(VALID_PERMISSION_MODES).join(", ")}`);
|
|
63
|
+
}
|
|
64
|
+
if (opts.permissionMode === "bypassPermissions") {
|
|
65
|
+
if (process.env.CONFIRM_BYPASS_PERMISSIONS !== "1") {
|
|
66
|
+
throw new Error("bypassPermissions mode requires CONFIRM_BYPASS_PERMISSIONS=1 environment variable. " +
|
|
67
|
+
"Example: CONFIRM_BYPASS_PERMISSIONS=1 code-review-agent --fix -p bypassPermissions \"Review this\"");
|
|
68
|
+
}
|
|
69
|
+
if (!process.stdin.isTTY) {
|
|
70
|
+
throw new Error("bypassPermissions mode requires an interactive terminal (no piped input).");
|
|
71
|
+
}
|
|
72
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
73
|
+
console.error("\x1b[33m⚠ WARNING: Running in bypassPermissions mode.\x1b[0m");
|
|
74
|
+
console.error("\x1b[33m⚠ Files may be modified without confirmation.\x1b[0m");
|
|
75
|
+
console.error(`\x1b[33m⚠ Working directory: ${cwd}\x1b[0m`);
|
|
76
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
77
|
+
let answer;
|
|
78
|
+
try {
|
|
79
|
+
answer = await new Promise((resolve, reject) => {
|
|
80
|
+
const timeout = setTimeout(() => {
|
|
81
|
+
rl.close();
|
|
82
|
+
reject(new Error("Timeout waiting for confirmation (30s)."));
|
|
83
|
+
}, 30000);
|
|
84
|
+
rl.question("\x1b[33m⚠ Type 'yes' to confirm: \x1b[0m", (ans) => {
|
|
85
|
+
clearTimeout(timeout);
|
|
86
|
+
resolve(ans);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
rl.close();
|
|
92
|
+
}
|
|
93
|
+
if (answer.trim().toLowerCase() !== "yes") {
|
|
94
|
+
console.error("\x1b[31mAborted.\x1b[0m");
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
await runAgent(prompt ?? DEFAULT_PROMPT, {
|
|
99
|
+
model: opts.model,
|
|
100
|
+
tools,
|
|
101
|
+
permissionMode: opts.permissionMode,
|
|
102
|
+
maxTurns: opts.maxTurns,
|
|
103
|
+
fix: Boolean(opts.fix || opts.fixRecursive),
|
|
104
|
+
fixRecursive: Boolean(opts.fixRecursive),
|
|
105
|
+
maxPasses: opts.maxPasses,
|
|
106
|
+
cwd: opts.cwd,
|
|
107
|
+
bypassConfirmed: opts.permissionMode === "bypassPermissions",
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
const isValidation = error instanceof Error &&
|
|
112
|
+
(error.message.includes("Invalid") || error.message.includes("must be"));
|
|
113
|
+
if (error instanceof Error) {
|
|
114
|
+
console.error("\x1b[31mError:\x1b[0m", error.message);
|
|
115
|
+
if (process.env.DEBUG) {
|
|
116
|
+
console.error(error.stack);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
console.error("\x1b[31mError:\x1b[0m", String(error));
|
|
121
|
+
}
|
|
122
|
+
process.exit(isValidation ? 2 : 1);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Code Review Agent
|
|
2
|
+
|
|
3
|
+
You are a code review and audit agent. Your primary purpose is to analyze codebases
|
|
4
|
+
for bugs, security issues, performance problems, and maintainability concerns.
|
|
5
|
+
|
|
6
|
+
## Capabilities
|
|
7
|
+
- Read and analyze source files
|
|
8
|
+
- Search codebases with glob patterns and grep
|
|
9
|
+
- Run commands to understand project structure
|
|
10
|
+
- Provide structured, actionable feedback
|
|
11
|
+
|
|
12
|
+
## Behavior
|
|
13
|
+
- Always read files before commenting on them
|
|
14
|
+
- Be specific — reference exact file paths and line numbers
|
|
15
|
+
- Prioritize critical issues over style preferences
|
|
16
|
+
- Provide code snippets showing fixes, not just descriptions
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Code Review Agent
|
|
2
|
+
|
|
3
|
+
You are a code review and audit agent. Your primary purpose is to analyze codebases
|
|
4
|
+
for bugs, security issues, performance problems, and maintainability concerns.
|
|
5
|
+
|
|
6
|
+
## Capabilities
|
|
7
|
+
- Read and analyze source files
|
|
8
|
+
- Search codebases with glob patterns and grep
|
|
9
|
+
- Run commands to understand project structure
|
|
10
|
+
- Provide structured, actionable feedback
|
|
11
|
+
|
|
12
|
+
## Behavior
|
|
13
|
+
- Always read files before commenting on them
|
|
14
|
+
- Be specific — reference exact file paths and line numbers
|
|
15
|
+
- Prioritize critical issues over style preferences
|
|
16
|
+
- Provide code snippets showing fixes, not just descriptions
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: code-review
|
|
3
|
+
description: >
|
|
4
|
+
Perform structured, multi-pass code reviews with severity-rated findings.
|
|
5
|
+
Use when the user asks to review code, check for bugs, audit files, analyze
|
|
6
|
+
code quality, find vulnerabilities, or requests a code audit. Also trigger
|
|
7
|
+
when the user says "review this", "check this code", "find bugs", or
|
|
8
|
+
"is this code safe".
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Code Review
|
|
12
|
+
|
|
13
|
+
When asked to review code, follow this structured methodology.
|
|
14
|
+
|
|
15
|
+
## Review Process
|
|
16
|
+
|
|
17
|
+
Go through each file and check these areas in order:
|
|
18
|
+
|
|
19
|
+
### 1. Bugs & Correctness
|
|
20
|
+
- Logic errors, off-by-one mistakes, race conditions
|
|
21
|
+
- Null/undefined access, unhandled edge cases
|
|
22
|
+
- Type mismatches or incorrect assumptions about data shapes
|
|
23
|
+
- Incorrect return values or missing return statements
|
|
24
|
+
|
|
25
|
+
### 2. Security
|
|
26
|
+
- Injection vulnerabilities (SQL, command, XSS)
|
|
27
|
+
- Hardcoded secrets, exposed credentials
|
|
28
|
+
- Unsafe input handling, missing validation
|
|
29
|
+
- Insecure dependencies or configurations
|
|
30
|
+
|
|
31
|
+
### 3. Error Handling
|
|
32
|
+
- Missing try/catch around operations that can fail
|
|
33
|
+
- Swallowed errors, uninformative error messages
|
|
34
|
+
- Unhandled promise rejections or missing async error paths
|
|
35
|
+
- Missing cleanup in error paths (file handles, connections)
|
|
36
|
+
|
|
37
|
+
### 4. Performance
|
|
38
|
+
- Unnecessary loops, repeated computations
|
|
39
|
+
- Memory leaks, unclosed resources
|
|
40
|
+
- N+1 queries, blocking operations in async code
|
|
41
|
+
- Inefficient data structures or algorithms
|
|
42
|
+
|
|
43
|
+
### 5. Readability & Maintainability
|
|
44
|
+
- Confusing naming, overly complex logic
|
|
45
|
+
- Code duplication that should be extracted
|
|
46
|
+
- Functions doing too many things
|
|
47
|
+
- Missing or misleading comments on non-obvious logic
|
|
48
|
+
|
|
49
|
+
## Output Format
|
|
50
|
+
|
|
51
|
+
For each issue found, report:
|
|
52
|
+
- **File and line**: exact location
|
|
53
|
+
- **Severity**: Critical / Warning / Suggestion
|
|
54
|
+
- **Issue**: what's wrong
|
|
55
|
+
- **Fix**: how to fix it with a code snippet
|
|
56
|
+
|
|
57
|
+
End with a summary table counting issues by severity per file.
|
|
58
|
+
|
|
59
|
+
## Guidelines
|
|
60
|
+
|
|
61
|
+
- Read each file fully before reporting issues
|
|
62
|
+
- Focus on real problems, not style nitpicks
|
|
63
|
+
- Prioritize Critical issues first
|
|
64
|
+
- If no issues are found in a category, skip it
|
|
65
|
+
- Provide actionable fixes, not vague suggestions
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: code-review
|
|
3
|
+
description: >
|
|
4
|
+
Perform structured, multi-pass code reviews with severity-rated findings.
|
|
5
|
+
Use when the user asks to review code, check for bugs, audit files, analyze
|
|
6
|
+
code quality, find vulnerabilities, or requests a code audit. Also trigger
|
|
7
|
+
when the user says "review this", "check this code", "find bugs", or
|
|
8
|
+
"is this code safe".
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Code Review
|
|
12
|
+
|
|
13
|
+
When asked to review code, follow this structured methodology.
|
|
14
|
+
|
|
15
|
+
## Review Process
|
|
16
|
+
|
|
17
|
+
Go through each file and check these areas in order:
|
|
18
|
+
|
|
19
|
+
### 1. Bugs & Correctness
|
|
20
|
+
- Logic errors, off-by-one mistakes, race conditions
|
|
21
|
+
- Null/undefined access, unhandled edge cases
|
|
22
|
+
- Type mismatches or incorrect assumptions about data shapes
|
|
23
|
+
- Incorrect return values or missing return statements
|
|
24
|
+
|
|
25
|
+
### 2. Security
|
|
26
|
+
- Injection vulnerabilities (SQL, command, XSS)
|
|
27
|
+
- Hardcoded secrets, exposed credentials
|
|
28
|
+
- Unsafe input handling, missing validation
|
|
29
|
+
- Insecure dependencies or configurations
|
|
30
|
+
|
|
31
|
+
### 3. Error Handling
|
|
32
|
+
- Missing try/catch around operations that can fail
|
|
33
|
+
- Swallowed errors, uninformative error messages
|
|
34
|
+
- Unhandled promise rejections or missing async error paths
|
|
35
|
+
- Missing cleanup in error paths (file handles, connections)
|
|
36
|
+
|
|
37
|
+
### 4. Performance
|
|
38
|
+
- Unnecessary loops, repeated computations
|
|
39
|
+
- Memory leaks, unclosed resources
|
|
40
|
+
- N+1 queries, blocking operations in async code
|
|
41
|
+
- Inefficient data structures or algorithms
|
|
42
|
+
|
|
43
|
+
### 5. Readability & Maintainability
|
|
44
|
+
- Confusing naming, overly complex logic
|
|
45
|
+
- Code duplication that should be extracted
|
|
46
|
+
- Functions doing too many things
|
|
47
|
+
- Missing or misleading comments on non-obvious logic
|
|
48
|
+
|
|
49
|
+
## Output Format
|
|
50
|
+
|
|
51
|
+
For each issue found, report:
|
|
52
|
+
- **File and line**: exact location
|
|
53
|
+
- **Severity**: Critical / Warning / Suggestion
|
|
54
|
+
- **Issue**: what's wrong
|
|
55
|
+
- **Fix**: how to fix it with a code snippet
|
|
56
|
+
|
|
57
|
+
End with a summary table counting issues by severity per file.
|
|
58
|
+
|
|
59
|
+
## Guidelines
|
|
60
|
+
|
|
61
|
+
- Read each file fully before reporting issues
|
|
62
|
+
- Focus on real problems, not style nitpicks
|
|
63
|
+
- Prioritize Critical issues first
|
|
64
|
+
- If no issues are found in a category, skip it
|
|
65
|
+
- Provide actionable fixes, not vague suggestions
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { marked } from "marked";
|
|
2
|
+
import { markedTerminal } from "marked-terminal";
|
|
3
|
+
import { cleanMarkdownRemnants, formatToolCall, } from "./formatting.js";
|
|
4
|
+
marked.use(markedTerminal({ showSectionPrefix: false }));
|
|
5
|
+
const MAX_DIFF_LINES = 30;
|
|
6
|
+
function showEditDiff(input) {
|
|
7
|
+
const filePath = typeof input.file_path === "string" ? input.file_path : "unknown";
|
|
8
|
+
const oldStr = typeof input.old_string === "string" ? input.old_string : null;
|
|
9
|
+
const newStr = typeof input.new_string === "string" ? input.new_string : null;
|
|
10
|
+
if (oldStr === null || newStr === null)
|
|
11
|
+
return;
|
|
12
|
+
const oldLines = oldStr.split("\n");
|
|
13
|
+
const newLines = newStr.split("\n");
|
|
14
|
+
const totalLines = oldLines.length + newLines.length;
|
|
15
|
+
// Show short file path (last 2 segments)
|
|
16
|
+
const pathSegments = filePath.split("/");
|
|
17
|
+
const shortPath = pathSegments.length >= 2 ? pathSegments.slice(-2).join("/") : filePath;
|
|
18
|
+
console.log(`\x1b[2m ┌─ ${shortPath}\x1b[0m`);
|
|
19
|
+
const truncated = totalLines > MAX_DIFF_LINES;
|
|
20
|
+
const maxOld = truncated ? Math.floor(MAX_DIFF_LINES / 2) : oldLines.length;
|
|
21
|
+
const maxNew = truncated ? Math.floor(MAX_DIFF_LINES / 2) : newLines.length;
|
|
22
|
+
for (let i = 0; i < Math.min(oldLines.length, maxOld); i++) {
|
|
23
|
+
console.log(`\x1b[31m - ${oldLines[i]}\x1b[0m`);
|
|
24
|
+
}
|
|
25
|
+
for (let i = 0; i < Math.min(newLines.length, maxNew); i++) {
|
|
26
|
+
console.log(`\x1b[32m + ${newLines[i]}\x1b[0m`);
|
|
27
|
+
}
|
|
28
|
+
if (truncated) {
|
|
29
|
+
const remainingLines = totalLines - maxOld - maxNew;
|
|
30
|
+
console.log(`\x1b[2m ... ${remainingLines} more lines\x1b[0m`);
|
|
31
|
+
}
|
|
32
|
+
console.log(`\x1b[2m └─\x1b[0m`);
|
|
33
|
+
}
|
|
34
|
+
async function handleContentBlock(block) {
|
|
35
|
+
if (typeof block !== "object" || block === null)
|
|
36
|
+
return;
|
|
37
|
+
const b = block;
|
|
38
|
+
if (b.type === "thinking" && typeof b.thinking === "string") {
|
|
39
|
+
console.log("\n\x1b[2m💭 Thinking:\x1b[0m");
|
|
40
|
+
let thinking = b.thinking;
|
|
41
|
+
if (thinking.length > 1000) {
|
|
42
|
+
thinking = thinking.slice(0, 997) + "...";
|
|
43
|
+
}
|
|
44
|
+
console.log(`\x1b[2m${thinking}\x1b[0m\n`);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (b.type === "tool_use" && typeof b.name === "string") {
|
|
48
|
+
const input = (typeof b.input === "object" && b.input !== null)
|
|
49
|
+
? b.input
|
|
50
|
+
: {};
|
|
51
|
+
console.log(`\n\x1b[36m🔧 ${formatToolCall(b.name, input)}\x1b[0m`);
|
|
52
|
+
if (b.name === "Edit") {
|
|
53
|
+
showEditDiff(input);
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (typeof b.text === "string") {
|
|
58
|
+
const textContent = b.text;
|
|
59
|
+
try {
|
|
60
|
+
const rendered = await marked.parse(textContent);
|
|
61
|
+
process.stdout.write(cleanMarkdownRemnants(rendered));
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
65
|
+
console.error(`\x1b[33m⚠ Markdown rendering failed: ${errorMsg}\x1b[0m`);
|
|
66
|
+
console.error("\x1b[2m (Set DEBUG=1 for stack trace)\x1b[0m");
|
|
67
|
+
if (process.env.DEBUG && error instanceof Error) {
|
|
68
|
+
console.error(error.stack);
|
|
69
|
+
}
|
|
70
|
+
// Fallback: strip ANSI/control chars and output as plain text
|
|
71
|
+
const sanitized = textContent
|
|
72
|
+
.replaceAll(/\x1b\[[0-9;]*m/g, "")
|
|
73
|
+
.replaceAll(/[\x00-\x08\x0B-\x1F\x7F-\x9F]/g, "");
|
|
74
|
+
process.stdout.write(sanitized + "\n");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function handleToolResult(message) {
|
|
79
|
+
if (typeof message.tool_use_result !== "object" || message.tool_use_result === null)
|
|
80
|
+
return;
|
|
81
|
+
const result = message.tool_use_result;
|
|
82
|
+
if ("stderr" in result && typeof result.stderr === "string") {
|
|
83
|
+
const stderrTrimmed = result.stderr.trim();
|
|
84
|
+
if (stderrTrimmed.length > 0) {
|
|
85
|
+
console.log(`\x1b[31m ${stderrTrimmed}\x1b[0m`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function handleToolProgress(message) {
|
|
90
|
+
console.log(`\x1b[33m⏳ ${message.tool_name} (${message.elapsed_time_seconds}s)\x1b[0m`);
|
|
91
|
+
}
|
|
92
|
+
function handleResult(message) {
|
|
93
|
+
if (message.is_error) {
|
|
94
|
+
const errorDetail = message.errors && message.errors.length > 0 ? message.errors.join(", ") : "unknown error";
|
|
95
|
+
console.log(`\n\x1b[31m❌ Failed: ${errorDetail}\x1b[0m`);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
console.log("\n\x1b[32m✅ Done\x1b[0m");
|
|
99
|
+
}
|
|
100
|
+
console.log(` Turns: ${message.num_turns}`);
|
|
101
|
+
console.log(` Duration: ${(message.duration_ms / 1000).toFixed(1)}s`);
|
|
102
|
+
console.log(` Cost: $${message.total_cost_usd.toFixed(4)}`);
|
|
103
|
+
}
|
|
104
|
+
export async function showMessage(message) {
|
|
105
|
+
const msgType = message.type;
|
|
106
|
+
if (msgType === "assistant") {
|
|
107
|
+
const msg = message;
|
|
108
|
+
if (msg.message?.content && Array.isArray(msg.message.content)) {
|
|
109
|
+
for (const block of msg.message.content) {
|
|
110
|
+
await handleContentBlock(block);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else if (msgType === "user") {
|
|
115
|
+
if (message.tool_use_result) {
|
|
116
|
+
handleToolResult(message);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else if (msgType === "tool_progress") {
|
|
120
|
+
handleToolProgress(message);
|
|
121
|
+
}
|
|
122
|
+
else if (msgType === "result") {
|
|
123
|
+
handleResult(message);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export function truncate(value, max) {
|
|
2
|
+
if (!Number.isInteger(max) || max < 3) {
|
|
3
|
+
throw new Error("max must be an integer >= 3 to accommodate ellipsis");
|
|
4
|
+
}
|
|
5
|
+
const chars = Array.from(value);
|
|
6
|
+
if (chars.length <= max)
|
|
7
|
+
return value;
|
|
8
|
+
return chars.slice(0, max - 3).join("") + "...";
|
|
9
|
+
}
|
|
10
|
+
export function stripAnsiCodes(text) {
|
|
11
|
+
return text.replaceAll(/\x1b\[[0-9;]*m/g, "");
|
|
12
|
+
}
|
|
13
|
+
// Sanitize for single-line display: removes control chars, ANSI codes, and converts whitespace to spaces
|
|
14
|
+
function sanitize(str) {
|
|
15
|
+
return String(str)
|
|
16
|
+
.replaceAll(/[\x00-\x08\x0B-\x1F\x7F-\x9F]/g, "")
|
|
17
|
+
.replaceAll(/\x1b\[[0-9;]*m/g, "")
|
|
18
|
+
.replaceAll(/[\r\n\t]/g, " ")
|
|
19
|
+
.trim();
|
|
20
|
+
}
|
|
21
|
+
export function cleanMarkdownRemnants(text) {
|
|
22
|
+
return text
|
|
23
|
+
.replaceAll(/\*\*(.+?)\*\*/g, "\x1b[1m$1\x1b[22m") // **bold**
|
|
24
|
+
.replaceAll(/\*(.+?)\*/g, "\x1b[3m$1\x1b[23m") // *italic*
|
|
25
|
+
.replaceAll(/`([^`]+)`/g, "\x1b[36m$1\x1b[39m"); // `code`
|
|
26
|
+
}
|
|
27
|
+
const toolFormatters = {
|
|
28
|
+
Bash: (input) => `Bash > ${sanitize(input.command)}`,
|
|
29
|
+
Read: (input) => `Read > ${sanitize(input.file_path)}`,
|
|
30
|
+
Write: (input) => `Write > ${sanitize(input.file_path)}`,
|
|
31
|
+
Edit: (input) => `Edit > ${sanitize(input.file_path)}`,
|
|
32
|
+
Glob: (input) => `Glob > ${sanitize(input.pattern)}${input.path ? ` in ${sanitize(input.path)}` : ""}`,
|
|
33
|
+
Grep: (input) => `Grep > "${sanitize(input.pattern)}"${input.path ? ` in ${sanitize(input.path)}` : ""}`,
|
|
34
|
+
WebSearch: (input) => `WebSearch > "${sanitize(input.query)}"`,
|
|
35
|
+
WebFetch: (input) => `WebFetch > ${sanitize(input.url)}`,
|
|
36
|
+
};
|
|
37
|
+
const DEFAULT_TERMINAL_WIDTH = 100;
|
|
38
|
+
const TOOL_DISPLAY_PADDING = 20;
|
|
39
|
+
export function formatToolCall(name, input) {
|
|
40
|
+
const formatter = toolFormatters[name];
|
|
41
|
+
if (formatter)
|
|
42
|
+
return formatter(input);
|
|
43
|
+
const columns = process.stdout.columns ?? DEFAULT_TERMINAL_WIDTH;
|
|
44
|
+
const maxWidth = Math.max(20, columns - TOOL_DISPLAY_PADDING);
|
|
45
|
+
return `${name} > ${truncate(JSON.stringify(input), maxWidth)}`;
|
|
46
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "code-review-agent-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"code-review-agent": "./dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"start": "FORCE_COLOR=1 tsx index.ts",
|
|
13
|
+
"build": "npx tsc && cp -r prompts dist/prompts && cp -r skills dist/skills",
|
|
14
|
+
"prepublishOnly": "npm run build"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.41",
|
|
18
|
+
"commander": "^13.1.0",
|
|
19
|
+
"marked": "^15.0.12",
|
|
20
|
+
"marked-terminal": "^7.3.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^25.2.3",
|
|
24
|
+
"tsx": "^4.21.0",
|
|
25
|
+
"typescript": "^5.9.3"
|
|
26
|
+
}
|
|
27
|
+
}
|