codebot-ai 1.3.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -1
- package/dist/agent.js +23 -1
- package/dist/cli.js +1 -1
- package/dist/providers/anthropic.js +27 -1
- package/dist/providers/openai.d.ts +4 -0
- package/dist/providers/openai.js +54 -2
- package/dist/retry.d.ts +5 -0
- package/dist/retry.js +24 -0
- package/dist/tools/code-analysis.d.ts +33 -0
- package/dist/tools/code-analysis.js +232 -0
- package/dist/tools/code-review.d.ts +32 -0
- package/dist/tools/code-review.js +228 -0
- package/dist/tools/database.d.ts +35 -0
- package/dist/tools/database.js +129 -0
- package/dist/tools/diff-viewer.d.ts +39 -0
- package/dist/tools/diff-viewer.js +145 -0
- package/dist/tools/docker.d.ts +26 -0
- package/dist/tools/docker.js +101 -0
- package/dist/tools/git.d.ts +26 -0
- package/dist/tools/git.js +58 -0
- package/dist/tools/http-client.d.ts +39 -0
- package/dist/tools/http-client.js +114 -0
- package/dist/tools/image-info.d.ts +23 -0
- package/dist/tools/image-info.js +170 -0
- package/dist/tools/index.js +34 -0
- package/dist/tools/multi-search.d.ts +28 -0
- package/dist/tools/multi-search.js +153 -0
- package/dist/tools/notification.d.ts +38 -0
- package/dist/tools/notification.js +96 -0
- package/dist/tools/package-manager.d.ts +31 -0
- package/dist/tools/package-manager.js +161 -0
- package/dist/tools/pdf-extract.d.ts +33 -0
- package/dist/tools/pdf-extract.js +178 -0
- package/dist/tools/ssh-remote.d.ts +39 -0
- package/dist/tools/ssh-remote.js +84 -0
- package/dist/tools/task-planner.d.ts +42 -0
- package/dist/tools/task-planner.js +161 -0
- package/dist/tools/test-runner.d.ts +36 -0
- package/dist/tools/test-runner.js +193 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -107,7 +107,7 @@ echo "explain this error" | codebot # Pipe mode
|
|
|
107
107
|
|
|
108
108
|
## Tools
|
|
109
109
|
|
|
110
|
-
CodeBot has
|
|
110
|
+
CodeBot has 28 built-in tools:
|
|
111
111
|
|
|
112
112
|
| Tool | Description | Permission |
|
|
113
113
|
|------|-------------|-----------|
|
|
@@ -124,6 +124,21 @@ CodeBot has 13 built-in tools:
|
|
|
124
124
|
| `web_search` | Internet search with result summaries | prompt |
|
|
125
125
|
| `browser` | Chrome automation via CDP | prompt |
|
|
126
126
|
| `routine` | Schedule recurring tasks with cron | prompt |
|
|
127
|
+
| `git` | Git operations (status, diff, log, commit, branch, etc.) | prompt |
|
|
128
|
+
| `code_analysis` | Symbol extraction, find references, imports, outline | auto |
|
|
129
|
+
| `multi_search` | Fuzzy search across filenames, content, and symbols | auto |
|
|
130
|
+
| `task_planner` | Hierarchical task tracking with priorities | auto |
|
|
131
|
+
| `diff_viewer` | File comparison and git diffs | auto |
|
|
132
|
+
| `docker` | Container management (ps, run, build, compose) | prompt |
|
|
133
|
+
| `database` | Query SQLite databases (blocks destructive SQL) | prompt |
|
|
134
|
+
| `test_runner` | Auto-detect and run tests (jest, vitest, pytest, go, cargo) | prompt |
|
|
135
|
+
| `http_client` | Advanced HTTP requests with auth and headers | prompt |
|
|
136
|
+
| `image_info` | Image dimensions and metadata (PNG, JPEG, GIF, SVG) | auto |
|
|
137
|
+
| `ssh_remote` | Remote command execution and file transfer via SSH | always-ask |
|
|
138
|
+
| `notification` | Webhook notifications (Slack, Discord, generic) | prompt |
|
|
139
|
+
| `pdf_extract` | Extract text and metadata from PDF files | auto |
|
|
140
|
+
| `package_manager` | Dependency management (npm, yarn, pip, cargo, go) | prompt |
|
|
141
|
+
| `code_review` | Security scanning and complexity analysis | auto |
|
|
127
142
|
|
|
128
143
|
### Permission Levels
|
|
129
144
|
|
package/dist/agent.js
CHANGED
|
@@ -38,6 +38,7 @@ const readline = __importStar(require("readline"));
|
|
|
38
38
|
const tools_1 = require("./tools");
|
|
39
39
|
const parser_1 = require("./parser");
|
|
40
40
|
const manager_1 = require("./context/manager");
|
|
41
|
+
const retry_1 = require("./retry");
|
|
41
42
|
const repo_map_1 = require("./context/repo-map");
|
|
42
43
|
const memory_1 = require("./memory");
|
|
43
44
|
const registry_1 = require("./providers/registry");
|
|
@@ -98,6 +99,9 @@ class Agent {
|
|
|
98
99
|
yield { type: 'compaction', text: 'Context compacted (summary unavailable).' };
|
|
99
100
|
}
|
|
100
101
|
}
|
|
102
|
+
// Circuit breaker: track consecutive identical errors
|
|
103
|
+
let consecutiveErrors = 0;
|
|
104
|
+
let lastErrorMsg = '';
|
|
101
105
|
for (let i = 0; i < this.maxIterations; i++) {
|
|
102
106
|
// Validate message integrity: ensure every tool_call has a matching tool response
|
|
103
107
|
// This prevents cascading 400 errors from OpenAI when a previous call failed
|
|
@@ -136,11 +140,29 @@ class Agent {
|
|
|
136
140
|
const msg = err instanceof Error ? err.message : String(err);
|
|
137
141
|
streamError = `Stream error: ${msg}`;
|
|
138
142
|
}
|
|
139
|
-
// On error: yield it to the UI but DON'T return — continue to next iteration
|
|
140
143
|
if (streamError) {
|
|
141
144
|
yield { type: 'error', error: streamError };
|
|
145
|
+
// Fatal errors (missing API key, auth failure, billing, etc.) — stop immediately
|
|
146
|
+
if ((0, retry_1.isFatalError)(streamError)) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
// Circuit breaker: stop after 3 consecutive identical errors
|
|
150
|
+
if (streamError === lastErrorMsg) {
|
|
151
|
+
consecutiveErrors++;
|
|
152
|
+
if (consecutiveErrors >= 3) {
|
|
153
|
+
yield { type: 'error', error: `Same error repeated ${consecutiveErrors} times — stopping. Fix the issue and try again.` };
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
consecutiveErrors = 1;
|
|
159
|
+
lastErrorMsg = streamError;
|
|
160
|
+
}
|
|
142
161
|
continue;
|
|
143
162
|
}
|
|
163
|
+
// Reset error tracking on success
|
|
164
|
+
consecutiveErrors = 0;
|
|
165
|
+
lastErrorMsg = '';
|
|
144
166
|
// If no native tool calls, try parsing from text
|
|
145
167
|
if (toolCalls.length === 0 && fullText) {
|
|
146
168
|
toolCalls = (0, parser_1.parseToolCalls)(fullText);
|
package/dist/cli.js
CHANGED
|
@@ -44,7 +44,7 @@ const setup_1 = require("./setup");
|
|
|
44
44
|
const banner_1 = require("./banner");
|
|
45
45
|
const tools_1 = require("./tools");
|
|
46
46
|
const scheduler_1 = require("./scheduler");
|
|
47
|
-
const VERSION = '1.
|
|
47
|
+
const VERSION = '1.4.1';
|
|
48
48
|
// Session-wide token tracking
|
|
49
49
|
let sessionTokens = { input: 0, output: 0, total: 0 };
|
|
50
50
|
const C = {
|
|
@@ -10,6 +10,11 @@ class AnthropicProvider {
|
|
|
10
10
|
this.name = config.model;
|
|
11
11
|
}
|
|
12
12
|
async *chat(messages, tools) {
|
|
13
|
+
// Early check: Anthropic always requires an API key
|
|
14
|
+
if (!this.config.apiKey) {
|
|
15
|
+
yield { type: 'error', error: `No API key configured for ${this.config.model}. Set ANTHROPIC_API_KEY or run: codebot --setup` };
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
13
18
|
const { systemPrompt, apiMessages } = this.convertMessages(messages);
|
|
14
19
|
const body = {
|
|
15
20
|
model: this.config.model,
|
|
@@ -66,7 +71,28 @@ class AnthropicProvider {
|
|
|
66
71
|
}
|
|
67
72
|
if (!response || !response.ok) {
|
|
68
73
|
const text = response ? await response.text().catch(() => '') : '';
|
|
69
|
-
|
|
74
|
+
// Extract readable error message from JSON response
|
|
75
|
+
let errorMessage = '';
|
|
76
|
+
try {
|
|
77
|
+
const json = JSON.parse(text);
|
|
78
|
+
errorMessage = json?.error?.message || json?.message || '';
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
errorMessage = text.substring(0, 200);
|
|
82
|
+
}
|
|
83
|
+
const status = response?.status;
|
|
84
|
+
if (status === 401 || (errorMessage && errorMessage.toLowerCase().includes('api key'))) {
|
|
85
|
+
yield { type: 'error', error: `Authentication failed (${status}): ${errorMessage || 'Invalid API key'}. Set ANTHROPIC_API_KEY or run: codebot --setup` };
|
|
86
|
+
}
|
|
87
|
+
else if (status === 403) {
|
|
88
|
+
yield { type: 'error', error: `Access denied (403): ${errorMessage || 'Permission denied'}. Check your API key permissions.` };
|
|
89
|
+
}
|
|
90
|
+
else if (status === 404) {
|
|
91
|
+
yield { type: 'error', error: `Model not found (404): ${errorMessage || `"${this.config.model}" may not be available`}.` };
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
yield { type: 'error', error: `Anthropic error (${status || 'unknown'}): ${errorMessage || lastError || 'Unknown error'}` };
|
|
95
|
+
}
|
|
70
96
|
return;
|
|
71
97
|
}
|
|
72
98
|
if (!response.body) {
|
|
@@ -6,6 +6,10 @@ export declare class OpenAIProvider implements LLMProvider {
|
|
|
6
6
|
constructor(config: ProviderConfig);
|
|
7
7
|
chat(messages: Message[], tools?: ToolSchema[]): AsyncGenerator<StreamEvent>;
|
|
8
8
|
listModels(): Promise<string[]>;
|
|
9
|
+
/** Get a helpful hint about which env var to set for the current provider */
|
|
10
|
+
private getApiKeyHint;
|
|
11
|
+
/** Format API error responses into readable messages (not raw JSON) */
|
|
12
|
+
private formatApiError;
|
|
9
13
|
private formatMessage;
|
|
10
14
|
}
|
|
11
15
|
//# sourceMappingURL=openai.d.ts.map
|
package/dist/providers/openai.js
CHANGED
|
@@ -13,6 +13,13 @@ class OpenAIProvider {
|
|
|
13
13
|
this.supportsTools = (0, registry_1.getModelInfo)(config.model).supportsToolCalling;
|
|
14
14
|
}
|
|
15
15
|
async *chat(messages, tools) {
|
|
16
|
+
const isLocal = this.config.baseUrl.includes('localhost') || this.config.baseUrl.includes('127.0.0.1');
|
|
17
|
+
// Early check: cloud providers require an API key
|
|
18
|
+
if (!isLocal && !this.config.apiKey) {
|
|
19
|
+
const hint = this.getApiKeyHint();
|
|
20
|
+
yield { type: 'error', error: `No API key configured for ${this.config.model}. ${hint}` };
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
16
23
|
const body = {
|
|
17
24
|
model: this.config.model,
|
|
18
25
|
messages: messages.map(m => this.formatMessage(m)),
|
|
@@ -22,7 +29,6 @@ class OpenAIProvider {
|
|
|
22
29
|
body.tools = tools;
|
|
23
30
|
}
|
|
24
31
|
// Ollama/local provider optimizations: set context window and keep model loaded
|
|
25
|
-
const isLocal = this.config.baseUrl.includes('localhost') || this.config.baseUrl.includes('127.0.0.1');
|
|
26
32
|
if (isLocal) {
|
|
27
33
|
const modelInfo = (0, registry_1.getModelInfo)(this.config.model);
|
|
28
34
|
body.options = { num_ctx: modelInfo.contextWindow };
|
|
@@ -69,7 +75,8 @@ class OpenAIProvider {
|
|
|
69
75
|
}
|
|
70
76
|
if (!response || !response.ok) {
|
|
71
77
|
const text = response ? await response.text().catch(() => '') : '';
|
|
72
|
-
|
|
78
|
+
const friendlyError = this.formatApiError(response?.status, text, lastError);
|
|
79
|
+
yield { type: 'error', error: friendlyError };
|
|
73
80
|
return;
|
|
74
81
|
}
|
|
75
82
|
if (!response.body) {
|
|
@@ -240,6 +247,51 @@ class OpenAIProvider {
|
|
|
240
247
|
return [];
|
|
241
248
|
}
|
|
242
249
|
}
|
|
250
|
+
/** Get a helpful hint about which env var to set for the current provider */
|
|
251
|
+
getApiKeyHint() {
|
|
252
|
+
const url = this.config.baseUrl.toLowerCase();
|
|
253
|
+
if (url.includes('openai.com'))
|
|
254
|
+
return 'Set OPENAI_API_KEY or run: codebot --setup';
|
|
255
|
+
if (url.includes('deepseek'))
|
|
256
|
+
return 'Set DEEPSEEK_API_KEY or run: codebot --setup';
|
|
257
|
+
if (url.includes('groq'))
|
|
258
|
+
return 'Set GROQ_API_KEY or run: codebot --setup';
|
|
259
|
+
if (url.includes('mistral'))
|
|
260
|
+
return 'Set MISTRAL_API_KEY or run: codebot --setup';
|
|
261
|
+
if (url.includes('generativelanguage.googleapis') || url.includes('gemini'))
|
|
262
|
+
return 'Set GEMINI_API_KEY or run: codebot --setup';
|
|
263
|
+
if (url.includes('x.ai') || url.includes('grok'))
|
|
264
|
+
return 'Set XAI_API_KEY or run: codebot --setup';
|
|
265
|
+
return 'Set your API key or run: codebot --setup';
|
|
266
|
+
}
|
|
267
|
+
/** Format API error responses into readable messages (not raw JSON) */
|
|
268
|
+
formatApiError(status, responseText, lastError) {
|
|
269
|
+
// Try to extract a useful message from JSON error response
|
|
270
|
+
let errorMessage = '';
|
|
271
|
+
try {
|
|
272
|
+
const json = JSON.parse(responseText);
|
|
273
|
+
errorMessage = json?.error?.message || json?.message || json?.error || '';
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
errorMessage = responseText.substring(0, 200);
|
|
277
|
+
}
|
|
278
|
+
const hint = this.getApiKeyHint();
|
|
279
|
+
if (status === 401 || (errorMessage && errorMessage.toLowerCase().includes('api key'))) {
|
|
280
|
+
return `Authentication failed (${status || 'no status'}): ${errorMessage || 'Invalid or missing API key'}. ${hint}`;
|
|
281
|
+
}
|
|
282
|
+
if (status === 403) {
|
|
283
|
+
return `Access denied (403): ${errorMessage || 'Permission denied'}. Check your API key permissions.`;
|
|
284
|
+
}
|
|
285
|
+
if (status === 404) {
|
|
286
|
+
return `Model not found (404): ${errorMessage || `"${this.config.model}" may not be available`}. Check the model name.`;
|
|
287
|
+
}
|
|
288
|
+
if (status === 429) {
|
|
289
|
+
return `Rate limited (429): ${errorMessage || 'Too many requests'}. Wait a moment and try again.`;
|
|
290
|
+
}
|
|
291
|
+
// Generic fallback — still clean, not raw JSON
|
|
292
|
+
const statusStr = status ? `(${status})` : '';
|
|
293
|
+
return `LLM error ${statusStr}: ${errorMessage || lastError || 'Unknown error'}`;
|
|
294
|
+
}
|
|
243
295
|
formatMessage(msg) {
|
|
244
296
|
const formatted = { role: msg.role, content: msg.content };
|
|
245
297
|
if (msg.tool_calls) {
|
package/dist/retry.d.ts
CHANGED
|
@@ -18,5 +18,10 @@ export declare function isRetryable(error: unknown, status?: number, opts?: Retr
|
|
|
18
18
|
*/
|
|
19
19
|
export declare function getRetryDelay(attempt: number, retryAfterHeader?: string | null, opts?: RetryOptions): number;
|
|
20
20
|
export declare function sleep(ms: number): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* Returns true if the error message indicates a fatal/permanent failure
|
|
23
|
+
* that will never succeed on retry (missing API key, auth failure, billing, etc.).
|
|
24
|
+
*/
|
|
25
|
+
export declare function isFatalError(errorMsg: string): boolean;
|
|
21
26
|
export { DEFAULTS as RETRY_DEFAULTS };
|
|
22
27
|
//# sourceMappingURL=retry.d.ts.map
|
package/dist/retry.js
CHANGED
|
@@ -9,6 +9,7 @@ exports.RETRY_DEFAULTS = void 0;
|
|
|
9
9
|
exports.isRetryable = isRetryable;
|
|
10
10
|
exports.getRetryDelay = getRetryDelay;
|
|
11
11
|
exports.sleep = sleep;
|
|
12
|
+
exports.isFatalError = isFatalError;
|
|
12
13
|
const DEFAULTS = {
|
|
13
14
|
maxRetries: 3,
|
|
14
15
|
baseDelayMs: 1000,
|
|
@@ -56,4 +57,27 @@ function getRetryDelay(attempt, retryAfterHeader, opts) {
|
|
|
56
57
|
function sleep(ms) {
|
|
57
58
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
58
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Returns true if the error message indicates a fatal/permanent failure
|
|
62
|
+
* that will never succeed on retry (missing API key, auth failure, billing, etc.).
|
|
63
|
+
*/
|
|
64
|
+
function isFatalError(errorMsg) {
|
|
65
|
+
const lower = errorMsg.toLowerCase();
|
|
66
|
+
return (lower.includes('api key') ||
|
|
67
|
+
lower.includes('api_key') ||
|
|
68
|
+
lower.includes('apikey') ||
|
|
69
|
+
lower.includes('authentication') ||
|
|
70
|
+
lower.includes('unauthorized') ||
|
|
71
|
+
lower.includes('invalid_request_error') ||
|
|
72
|
+
lower.includes('invalid request') ||
|
|
73
|
+
lower.includes('permission denied') ||
|
|
74
|
+
lower.includes('account deactivated') ||
|
|
75
|
+
lower.includes('account suspended') ||
|
|
76
|
+
lower.includes('billing') ||
|
|
77
|
+
(lower.includes('quota') && lower.includes('exceeded')) ||
|
|
78
|
+
lower.includes('insufficient_quota') ||
|
|
79
|
+
lower.includes('model not found') ||
|
|
80
|
+
lower.includes('does not exist') ||
|
|
81
|
+
lower.includes('access denied'));
|
|
82
|
+
}
|
|
59
83
|
//# sourceMappingURL=retry.js.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Tool } from '../types';
|
|
2
|
+
export declare class CodeAnalysisTool implements Tool {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
permission: Tool['permission'];
|
|
6
|
+
parameters: {
|
|
7
|
+
type: string;
|
|
8
|
+
properties: {
|
|
9
|
+
action: {
|
|
10
|
+
type: string;
|
|
11
|
+
description: string;
|
|
12
|
+
};
|
|
13
|
+
path: {
|
|
14
|
+
type: string;
|
|
15
|
+
description: string;
|
|
16
|
+
};
|
|
17
|
+
symbol: {
|
|
18
|
+
type: string;
|
|
19
|
+
description: string;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
required: string[];
|
|
23
|
+
};
|
|
24
|
+
execute(args: Record<string, unknown>): Promise<string>;
|
|
25
|
+
private extractSymbols;
|
|
26
|
+
private extractImports;
|
|
27
|
+
private buildOutline;
|
|
28
|
+
private walkDir;
|
|
29
|
+
private findReferences;
|
|
30
|
+
private searchRefs;
|
|
31
|
+
private readFile;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=code-analysis.d.ts.map
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.CodeAnalysisTool = void 0;
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
class CodeAnalysisTool {
|
|
40
|
+
name = 'code_analysis';
|
|
41
|
+
description = 'Analyze code structure. Actions: symbols (list classes/functions/exports), imports (list imports), outline (file structure), references (find where a symbol is used).';
|
|
42
|
+
permission = 'auto';
|
|
43
|
+
parameters = {
|
|
44
|
+
type: 'object',
|
|
45
|
+
properties: {
|
|
46
|
+
action: { type: 'string', description: 'Action: symbols, imports, outline, references' },
|
|
47
|
+
path: { type: 'string', description: 'File or directory to analyze' },
|
|
48
|
+
symbol: { type: 'string', description: 'Symbol name to find references for (required for "references" action)' },
|
|
49
|
+
},
|
|
50
|
+
required: ['action', 'path'],
|
|
51
|
+
};
|
|
52
|
+
async execute(args) {
|
|
53
|
+
const action = args.action;
|
|
54
|
+
const targetPath = args.path;
|
|
55
|
+
if (!action)
|
|
56
|
+
return 'Error: action is required';
|
|
57
|
+
if (!targetPath)
|
|
58
|
+
return 'Error: path is required';
|
|
59
|
+
if (!fs.existsSync(targetPath)) {
|
|
60
|
+
return `Error: path not found: ${targetPath}`;
|
|
61
|
+
}
|
|
62
|
+
switch (action) {
|
|
63
|
+
case 'symbols': return this.extractSymbols(targetPath);
|
|
64
|
+
case 'imports': return this.extractImports(targetPath);
|
|
65
|
+
case 'outline': return this.buildOutline(targetPath);
|
|
66
|
+
case 'references': {
|
|
67
|
+
const symbol = args.symbol;
|
|
68
|
+
if (!symbol)
|
|
69
|
+
return 'Error: symbol is required for references action';
|
|
70
|
+
return this.findReferences(targetPath, symbol);
|
|
71
|
+
}
|
|
72
|
+
default:
|
|
73
|
+
return `Error: unknown action "${action}". Use: symbols, imports, outline, references`;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
extractSymbols(filePath) {
|
|
77
|
+
const content = this.readFile(filePath);
|
|
78
|
+
if (!content)
|
|
79
|
+
return 'Error: could not read file';
|
|
80
|
+
const symbols = [];
|
|
81
|
+
const lines = content.split('\n');
|
|
82
|
+
for (let i = 0; i < lines.length; i++) {
|
|
83
|
+
const line = lines[i];
|
|
84
|
+
const lineNum = i + 1;
|
|
85
|
+
// Classes
|
|
86
|
+
const classMatch = line.match(/^(?:export\s+)?(?:abstract\s+)?class\s+(\w+)/);
|
|
87
|
+
if (classMatch)
|
|
88
|
+
symbols.push(` class ${classMatch[1]} (line ${lineNum})`);
|
|
89
|
+
// Functions
|
|
90
|
+
const funcMatch = line.match(/^(?:export\s+)?(?:async\s+)?function\s+(\w+)/);
|
|
91
|
+
if (funcMatch)
|
|
92
|
+
symbols.push(` function ${funcMatch[1]} (line ${lineNum})`);
|
|
93
|
+
// Arrow function exports
|
|
94
|
+
const arrowMatch = line.match(/^(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*(?:async\s+)?\(/);
|
|
95
|
+
if (arrowMatch)
|
|
96
|
+
symbols.push(` const ${arrowMatch[1]} (line ${lineNum})`);
|
|
97
|
+
// Interfaces & Types
|
|
98
|
+
const ifaceMatch = line.match(/^(?:export\s+)?interface\s+(\w+)/);
|
|
99
|
+
if (ifaceMatch)
|
|
100
|
+
symbols.push(` interface ${ifaceMatch[1]} (line ${lineNum})`);
|
|
101
|
+
const typeMatch = line.match(/^(?:export\s+)?type\s+(\w+)/);
|
|
102
|
+
if (typeMatch)
|
|
103
|
+
symbols.push(` type ${typeMatch[1]} (line ${lineNum})`);
|
|
104
|
+
// Methods inside classes
|
|
105
|
+
const methodMatch = line.match(/^\s+(?:async\s+)?(?:private\s+|public\s+|protected\s+)?(\w+)\s*\([^)]*\)\s*[:{]/);
|
|
106
|
+
if (methodMatch && !['if', 'for', 'while', 'switch', 'catch', 'constructor'].includes(methodMatch[1])) {
|
|
107
|
+
symbols.push(` method ${methodMatch[1]} (line ${lineNum})`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (symbols.length === 0)
|
|
111
|
+
return 'No symbols found.';
|
|
112
|
+
return `Symbols in ${path.basename(filePath)}:\n${symbols.join('\n')}`;
|
|
113
|
+
}
|
|
114
|
+
extractImports(filePath) {
|
|
115
|
+
const content = this.readFile(filePath);
|
|
116
|
+
if (!content)
|
|
117
|
+
return 'Error: could not read file';
|
|
118
|
+
const imports = [];
|
|
119
|
+
const lines = content.split('\n');
|
|
120
|
+
for (const line of lines) {
|
|
121
|
+
// ES imports
|
|
122
|
+
const esMatch = line.match(/^import\s+.*from\s+['"]([^'"]+)['"]/);
|
|
123
|
+
if (esMatch) {
|
|
124
|
+
imports.push(` ${esMatch[1]}`);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
// Require
|
|
128
|
+
const reqMatch = line.match(/require\s*\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
129
|
+
if (reqMatch) {
|
|
130
|
+
imports.push(` ${reqMatch[1]}`);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
// Python imports
|
|
134
|
+
const pyMatch = line.match(/^(?:from\s+(\S+)\s+)?import\s+(\S+)/);
|
|
135
|
+
if (pyMatch && !line.includes('{')) {
|
|
136
|
+
imports.push(` ${pyMatch[1] || pyMatch[2]}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (imports.length === 0)
|
|
140
|
+
return 'No imports found.';
|
|
141
|
+
return `Imports in ${path.basename(filePath)}:\n${imports.join('\n')}`;
|
|
142
|
+
}
|
|
143
|
+
buildOutline(targetPath) {
|
|
144
|
+
const stat = fs.statSync(targetPath);
|
|
145
|
+
if (stat.isFile()) {
|
|
146
|
+
return this.extractSymbols(targetPath);
|
|
147
|
+
}
|
|
148
|
+
// Directory outline
|
|
149
|
+
const lines = [`Outline of ${path.basename(targetPath)}/`];
|
|
150
|
+
this.walkDir(targetPath, '', lines, 0, 3);
|
|
151
|
+
return lines.join('\n');
|
|
152
|
+
}
|
|
153
|
+
walkDir(dir, prefix, lines, depth, maxDepth) {
|
|
154
|
+
if (depth >= maxDepth)
|
|
155
|
+
return;
|
|
156
|
+
const skip = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '__pycache__', '.next']);
|
|
157
|
+
let entries;
|
|
158
|
+
try {
|
|
159
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const dirs = entries.filter(e => e.isDirectory() && !e.name.startsWith('.') && !skip.has(e.name));
|
|
165
|
+
const files = entries.filter(e => e.isFile() && !e.name.startsWith('.'));
|
|
166
|
+
for (const d of dirs.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
167
|
+
lines.push(`${prefix}${d.name}/`);
|
|
168
|
+
this.walkDir(path.join(dir, d.name), prefix + ' ', lines, depth + 1, maxDepth);
|
|
169
|
+
}
|
|
170
|
+
for (const f of files.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
171
|
+
lines.push(`${prefix}${f.name}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
findReferences(targetPath, symbol) {
|
|
175
|
+
const stat = fs.statSync(targetPath);
|
|
176
|
+
const dir = stat.isFile() ? path.dirname(targetPath) : targetPath;
|
|
177
|
+
const results = [];
|
|
178
|
+
const regex = new RegExp(`\\b${symbol.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g');
|
|
179
|
+
this.searchRefs(dir, regex, results, 50);
|
|
180
|
+
if (results.length === 0)
|
|
181
|
+
return `No references to "${symbol}" found.`;
|
|
182
|
+
return `References to "${symbol}":\n${results.join('\n')}`;
|
|
183
|
+
}
|
|
184
|
+
searchRefs(dir, regex, results, max) {
|
|
185
|
+
if (results.length >= max)
|
|
186
|
+
return;
|
|
187
|
+
const skip = new Set(['node_modules', '.git', 'dist', 'build', 'coverage']);
|
|
188
|
+
let entries;
|
|
189
|
+
try {
|
|
190
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
for (const entry of entries) {
|
|
196
|
+
if (results.length >= max)
|
|
197
|
+
break;
|
|
198
|
+
if (entry.name.startsWith('.') || skip.has(entry.name))
|
|
199
|
+
continue;
|
|
200
|
+
const full = path.join(dir, entry.name);
|
|
201
|
+
if (entry.isDirectory()) {
|
|
202
|
+
this.searchRefs(full, regex, results, max);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
const ext = path.extname(entry.name);
|
|
206
|
+
if (!['.ts', '.js', '.tsx', '.jsx', '.py', '.go', '.rs', '.java', '.rb', '.c', '.cpp', '.h'].includes(ext))
|
|
207
|
+
continue;
|
|
208
|
+
try {
|
|
209
|
+
const content = fs.readFileSync(full, 'utf-8');
|
|
210
|
+
const lines = content.split('\n');
|
|
211
|
+
for (let i = 0; i < lines.length && results.length < max; i++) {
|
|
212
|
+
regex.lastIndex = 0;
|
|
213
|
+
if (regex.test(lines[i])) {
|
|
214
|
+
results.push(` ${full}:${i + 1}: ${lines[i].trimEnd()}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
catch { /* skip */ }
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
readFile(filePath) {
|
|
223
|
+
try {
|
|
224
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
exports.CodeAnalysisTool = CodeAnalysisTool;
|
|
232
|
+
//# sourceMappingURL=code-analysis.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Tool } from '../types';
|
|
2
|
+
export declare class CodeReviewTool implements Tool {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
permission: Tool['permission'];
|
|
6
|
+
parameters: {
|
|
7
|
+
type: string;
|
|
8
|
+
properties: {
|
|
9
|
+
action: {
|
|
10
|
+
type: string;
|
|
11
|
+
description: string;
|
|
12
|
+
};
|
|
13
|
+
path: {
|
|
14
|
+
type: string;
|
|
15
|
+
description: string;
|
|
16
|
+
};
|
|
17
|
+
severity: {
|
|
18
|
+
type: string;
|
|
19
|
+
description: string;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
required: string[];
|
|
23
|
+
};
|
|
24
|
+
execute(args: Record<string, unknown>): Promise<string>;
|
|
25
|
+
private securityScan;
|
|
26
|
+
private complexityAnalysis;
|
|
27
|
+
private scanFile;
|
|
28
|
+
private scanDir;
|
|
29
|
+
private analyzeFileComplexity;
|
|
30
|
+
private analyzeDir;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=code-review.d.ts.map
|