codebot-ai 1.2.3 → 1.4.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.
Files changed (42) hide show
  1. package/README.md +135 -116
  2. package/dist/agent.js +51 -27
  3. package/dist/cli.js +18 -2
  4. package/dist/providers/anthropic.js +38 -18
  5. package/dist/providers/openai.js +35 -14
  6. package/dist/retry.d.ts +22 -0
  7. package/dist/retry.js +59 -0
  8. package/dist/scheduler.d.ts +2 -0
  9. package/dist/scheduler.js +25 -17
  10. package/dist/tools/code-analysis.d.ts +33 -0
  11. package/dist/tools/code-analysis.js +232 -0
  12. package/dist/tools/code-review.d.ts +32 -0
  13. package/dist/tools/code-review.js +228 -0
  14. package/dist/tools/database.d.ts +35 -0
  15. package/dist/tools/database.js +129 -0
  16. package/dist/tools/diff-viewer.d.ts +39 -0
  17. package/dist/tools/diff-viewer.js +145 -0
  18. package/dist/tools/docker.d.ts +26 -0
  19. package/dist/tools/docker.js +101 -0
  20. package/dist/tools/git.d.ts +26 -0
  21. package/dist/tools/git.js +58 -0
  22. package/dist/tools/http-client.d.ts +39 -0
  23. package/dist/tools/http-client.js +114 -0
  24. package/dist/tools/image-info.d.ts +23 -0
  25. package/dist/tools/image-info.js +170 -0
  26. package/dist/tools/index.js +34 -0
  27. package/dist/tools/multi-search.d.ts +28 -0
  28. package/dist/tools/multi-search.js +153 -0
  29. package/dist/tools/notification.d.ts +38 -0
  30. package/dist/tools/notification.js +96 -0
  31. package/dist/tools/package-manager.d.ts +31 -0
  32. package/dist/tools/package-manager.js +161 -0
  33. package/dist/tools/pdf-extract.d.ts +33 -0
  34. package/dist/tools/pdf-extract.js +178 -0
  35. package/dist/tools/ssh-remote.d.ts +39 -0
  36. package/dist/tools/ssh-remote.js +84 -0
  37. package/dist/tools/task-planner.d.ts +42 -0
  38. package/dist/tools/task-planner.js +161 -0
  39. package/dist/tools/test-runner.d.ts +36 -0
  40. package/dist/tools/test-runner.js +193 -0
  41. package/dist/tools/web-fetch.js +11 -2
  42. package/package.json +16 -8
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Retry utilities for resilient network operations.
3
+ * Exponential backoff with jitter, Retry-After header support.
4
+ * Zero dependencies.
5
+ */
6
+ export interface RetryOptions {
7
+ maxRetries?: number;
8
+ baseDelayMs?: number;
9
+ maxDelayMs?: number;
10
+ retryableStatuses?: number[];
11
+ }
12
+ declare const DEFAULTS: Required<RetryOptions>;
13
+ /** Returns true if the error/status is retryable (network error or retryable HTTP status). */
14
+ export declare function isRetryable(error: unknown, status?: number, opts?: RetryOptions): boolean;
15
+ /**
16
+ * Calculate delay with exponential backoff + jitter.
17
+ * For 429 responses, respects Retry-After header.
18
+ */
19
+ export declare function getRetryDelay(attempt: number, retryAfterHeader?: string | null, opts?: RetryOptions): number;
20
+ export declare function sleep(ms: number): Promise<void>;
21
+ export { DEFAULTS as RETRY_DEFAULTS };
22
+ //# sourceMappingURL=retry.d.ts.map
package/dist/retry.js ADDED
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ /**
3
+ * Retry utilities for resilient network operations.
4
+ * Exponential backoff with jitter, Retry-After header support.
5
+ * Zero dependencies.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.RETRY_DEFAULTS = void 0;
9
+ exports.isRetryable = isRetryable;
10
+ exports.getRetryDelay = getRetryDelay;
11
+ exports.sleep = sleep;
12
+ const DEFAULTS = {
13
+ maxRetries: 3,
14
+ baseDelayMs: 1000,
15
+ maxDelayMs: 30000,
16
+ retryableStatuses: [429, 500, 502, 503, 504],
17
+ };
18
+ exports.RETRY_DEFAULTS = DEFAULTS;
19
+ /** Returns true if the error/status is retryable (network error or retryable HTTP status). */
20
+ function isRetryable(error, status, opts) {
21
+ const statuses = opts?.retryableStatuses ?? DEFAULTS.retryableStatuses;
22
+ if (status && statuses.includes(status))
23
+ return true;
24
+ if (error instanceof TypeError)
25
+ return true; // fetch network errors
26
+ if (error instanceof Error) {
27
+ const msg = error.message.toLowerCase();
28
+ if (msg.includes('fetch failed') || msg.includes('econnreset') ||
29
+ msg.includes('econnrefused') || msg.includes('etimedout') ||
30
+ msg.includes('socket hang up') || msg.includes('network') ||
31
+ msg.includes('abort')) {
32
+ return true;
33
+ }
34
+ }
35
+ return false;
36
+ }
37
+ /**
38
+ * Calculate delay with exponential backoff + jitter.
39
+ * For 429 responses, respects Retry-After header.
40
+ */
41
+ function getRetryDelay(attempt, retryAfterHeader, opts) {
42
+ const base = opts?.baseDelayMs ?? DEFAULTS.baseDelayMs;
43
+ const max = opts?.maxDelayMs ?? DEFAULTS.maxDelayMs;
44
+ // Respect Retry-After header (in seconds)
45
+ if (retryAfterHeader) {
46
+ const seconds = parseInt(retryAfterHeader, 10);
47
+ if (!isNaN(seconds) && seconds > 0) {
48
+ return Math.min(seconds * 1000, max);
49
+ }
50
+ }
51
+ // Exponential backoff with jitter: base * 2^attempt * (0.5..1.5)
52
+ const exponential = base * Math.pow(2, attempt);
53
+ const jitter = 0.5 + Math.random();
54
+ return Math.min(exponential * jitter, max);
55
+ }
56
+ function sleep(ms) {
57
+ return new Promise(resolve => setTimeout(resolve, ms));
58
+ }
59
+ //# sourceMappingURL=retry.js.map
@@ -12,6 +12,8 @@ export declare class Scheduler {
12
12
  /** Check if any routines need to run right now */
13
13
  private tick;
14
14
  private executeRoutine;
15
+ /** Run the agent loop for a routine — separated so it can be wrapped in Promise.race */
16
+ private runRoutineAgent;
15
17
  private loadRoutines;
16
18
  private saveRoutines;
17
19
  }
package/dist/scheduler.js CHANGED
@@ -90,25 +90,14 @@ class Scheduler {
90
90
  }
91
91
  async executeRoutine(routine, allRoutines) {
92
92
  this.running = true;
93
+ const ROUTINE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes max per routine
93
94
  try {
94
95
  this.onOutput?.(`\n⏰ Running routine: ${routine.name}\n Task: ${routine.prompt}\n`);
95
- // Run the agent with the routine's prompt
96
- for await (const event of this.agent.run(routine.prompt)) {
97
- switch (event.type) {
98
- case 'text':
99
- this.onOutput?.(event.text || '');
100
- break;
101
- case 'tool_call':
102
- this.onOutput?.(`\n⚡ ${event.toolCall?.name}(${Object.entries(event.toolCall?.args || {}).map(([k, v]) => `${k}: ${typeof v === 'string' ? v.substring(0, 40) : v}`).join(', ')})\n`);
103
- break;
104
- case 'tool_result':
105
- this.onOutput?.(` ✓ ${event.toolResult?.result?.substring(0, 100) || ''}\n`);
106
- break;
107
- case 'error':
108
- this.onOutput?.(` ✗ Error: ${event.error}\n`);
109
- break;
110
- }
111
- }
96
+ // Race against a timeout so a hanging routine doesn't block the scheduler forever
97
+ await Promise.race([
98
+ this.runRoutineAgent(routine),
99
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`Routine timed out after ${ROUTINE_TIMEOUT_MS / 1000}s`)), ROUTINE_TIMEOUT_MS)),
100
+ ]);
112
101
  // Update last run time
113
102
  routine.lastRun = new Date().toISOString();
114
103
  this.saveRoutines(allRoutines);
@@ -122,6 +111,25 @@ class Scheduler {
122
111
  this.running = false;
123
112
  }
124
113
  }
114
+ /** Run the agent loop for a routine — separated so it can be wrapped in Promise.race */
115
+ async runRoutineAgent(routine) {
116
+ for await (const event of this.agent.run(routine.prompt)) {
117
+ switch (event.type) {
118
+ case 'text':
119
+ this.onOutput?.(event.text || '');
120
+ break;
121
+ case 'tool_call':
122
+ this.onOutput?.(`\n⚡ ${event.toolCall?.name}(${Object.entries(event.toolCall?.args || {}).map(([k, v]) => `${k}: ${typeof v === 'string' ? v.substring(0, 40) : v}`).join(', ')})\n`);
123
+ break;
124
+ case 'tool_result':
125
+ this.onOutput?.(` ✓ ${event.toolResult?.result?.substring(0, 100) || ''}\n`);
126
+ break;
127
+ case 'error':
128
+ this.onOutput?.(` ✗ Error: ${event.error}\n`);
129
+ break;
130
+ }
131
+ }
132
+ }
125
133
  loadRoutines() {
126
134
  try {
127
135
  if (fs.existsSync(ROUTINES_FILE)) {
@@ -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
@@ -0,0 +1,228 @@
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.CodeReviewTool = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const SECURITY_PATTERNS = [
40
+ { pattern: /\beval\s*\(/, rule: 'no-eval', message: 'eval() is a security risk — allows arbitrary code execution', severity: 'error' },
41
+ { pattern: /new\s+Function\s*\(/, rule: 'no-new-function', message: 'new Function() is equivalent to eval()', severity: 'error' },
42
+ { pattern: /child_process.*exec(?!Sync)/, rule: 'unsafe-exec', message: 'exec() can be vulnerable to command injection — prefer execFile()', severity: 'warning' },
43
+ { pattern: /innerHTML\s*=/, rule: 'no-innerhtml', message: 'innerHTML is vulnerable to XSS attacks', severity: 'warning' },
44
+ { pattern: /document\.write\s*\(/, rule: 'no-document-write', message: 'document.write() is a security and performance issue', severity: 'warning' },
45
+ { pattern: /(?:password|secret|api.?key|token)\s*[:=]\s*['"][^'"]{8,}['"]/i, rule: 'hardcoded-secret', message: 'Possible hardcoded secret/credential', severity: 'error' },
46
+ { pattern: /\bsqlite3?\s.*\+\s*(?:req\.|args\.|input)/i, rule: 'sql-injection', message: 'Possible SQL injection — use parameterized queries', severity: 'error' },
47
+ { pattern: /https?:\/\/[^'"]*['"]\s*\+/, rule: 'url-injection', message: 'String concatenation in URL — possible injection', severity: 'warning' },
48
+ { pattern: /console\.(log|debug|info)\(/, rule: 'no-console', message: 'Console statement (consider removing for production)', severity: 'info' },
49
+ { pattern: /TODO|FIXME|HACK|XXX/i, rule: 'todo-comment', message: 'TODO/FIXME comment found', severity: 'info' },
50
+ ];
51
+ class CodeReviewTool {
52
+ name = 'code_review';
53
+ description = 'Review code for security issues, complexity, and code smells. Actions: security, complexity, review (full).';
54
+ permission = 'auto';
55
+ parameters = {
56
+ type: 'object',
57
+ properties: {
58
+ action: { type: 'string', description: 'Action: security (scan for vulnerabilities), complexity (function/nesting analysis), review (full review)' },
59
+ path: { type: 'string', description: 'File or directory to review' },
60
+ severity: { type: 'string', description: 'Minimum severity to report: error, warning, info (default: warning)' },
61
+ },
62
+ required: ['action', 'path'],
63
+ };
64
+ async execute(args) {
65
+ const action = args.action;
66
+ const targetPath = args.path;
67
+ if (!action)
68
+ return 'Error: action is required';
69
+ if (!targetPath)
70
+ return 'Error: path is required';
71
+ if (!fs.existsSync(targetPath))
72
+ return `Error: path not found: ${targetPath}`;
73
+ const minSeverity = args.severity || 'warning';
74
+ switch (action) {
75
+ case 'security': return this.securityScan(targetPath, minSeverity);
76
+ case 'complexity': return this.complexityAnalysis(targetPath);
77
+ case 'review': {
78
+ const sec = this.securityScan(targetPath, minSeverity);
79
+ const comp = this.complexityAnalysis(targetPath);
80
+ return `=== Security Review ===\n${sec}\n\n=== Complexity Analysis ===\n${comp}`;
81
+ }
82
+ default: return `Error: unknown action "${action}". Use: security, complexity, review`;
83
+ }
84
+ }
85
+ securityScan(targetPath, minSeverity) {
86
+ const issues = [];
87
+ const sevOrder = { error: 3, warning: 2, info: 1 };
88
+ const minLevel = sevOrder[minSeverity] || 2;
89
+ const stat = fs.statSync(targetPath);
90
+ if (stat.isFile()) {
91
+ this.scanFile(targetPath, issues);
92
+ }
93
+ else {
94
+ this.scanDir(targetPath, issues);
95
+ }
96
+ // Filter by severity
97
+ const filtered = issues.filter(i => (sevOrder[i.severity] || 0) >= minLevel);
98
+ if (filtered.length === 0)
99
+ return 'No security issues found.';
100
+ const errors = filtered.filter(i => i.severity === 'error').length;
101
+ const warnings = filtered.filter(i => i.severity === 'warning').length;
102
+ const infos = filtered.filter(i => i.severity === 'info').length;
103
+ const icons = { error: 'X', warning: '!', info: 'i' };
104
+ const lines = filtered.slice(0, 50).map(i => ` [${icons[i.severity]}] ${i.file}:${i.line} ${i.rule} — ${i.message}`);
105
+ return `Found ${filtered.length} issue(s): ${errors} errors, ${warnings} warnings, ${infos} info\n${lines.join('\n')}`;
106
+ }
107
+ complexityAnalysis(targetPath) {
108
+ const stat = fs.statSync(targetPath);
109
+ const results = [];
110
+ if (stat.isFile()) {
111
+ this.analyzeFileComplexity(targetPath, results);
112
+ }
113
+ else {
114
+ this.analyzeDir(targetPath, results);
115
+ }
116
+ if (results.length === 0)
117
+ return 'No complexity issues found.';
118
+ return results.join('\n');
119
+ }
120
+ scanFile(filePath, issues) {
121
+ const ext = path.extname(filePath);
122
+ if (!['.ts', '.js', '.tsx', '.jsx', '.py', '.go', '.rb', '.java'].includes(ext))
123
+ return;
124
+ try {
125
+ const content = fs.readFileSync(filePath, 'utf-8');
126
+ const lines = content.split('\n');
127
+ for (let i = 0; i < lines.length; i++) {
128
+ for (const check of SECURITY_PATTERNS) {
129
+ if (check.pattern.test(lines[i])) {
130
+ issues.push({
131
+ file: filePath, line: i + 1,
132
+ severity: check.severity, rule: check.rule, message: check.message,
133
+ });
134
+ }
135
+ }
136
+ }
137
+ }
138
+ catch { /* skip */ }
139
+ }
140
+ scanDir(dir, issues) {
141
+ const skip = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '__pycache__']);
142
+ let entries;
143
+ try {
144
+ entries = fs.readdirSync(dir, { withFileTypes: true });
145
+ }
146
+ catch {
147
+ return;
148
+ }
149
+ for (const entry of entries) {
150
+ if (entry.name.startsWith('.') || skip.has(entry.name))
151
+ continue;
152
+ const full = path.join(dir, entry.name);
153
+ if (entry.isDirectory()) {
154
+ this.scanDir(full, issues);
155
+ }
156
+ else {
157
+ this.scanFile(full, issues);
158
+ }
159
+ }
160
+ }
161
+ analyzeFileComplexity(filePath, results) {
162
+ const ext = path.extname(filePath);
163
+ if (!['.ts', '.js', '.tsx', '.jsx', '.py', '.go'].includes(ext))
164
+ return;
165
+ try {
166
+ const content = fs.readFileSync(filePath, 'utf-8');
167
+ const lines = content.split('\n');
168
+ let currentFunc = '';
169
+ let funcStart = 0;
170
+ let maxNesting = 0;
171
+ let currentNesting = 0;
172
+ for (let i = 0; i < lines.length; i++) {
173
+ const line = lines[i];
174
+ // Detect function starts
175
+ const funcMatch = line.match(/(?:function|async function|def|fn|func)\s+(\w+)|(\w+)\s*[:=]\s*(?:async\s+)?\(/);
176
+ if (funcMatch) {
177
+ // Report previous function if long
178
+ if (currentFunc && (i - funcStart) > 50) {
179
+ results.push(` [!] ${filePath}:${funcStart + 1} "${currentFunc}" is ${i - funcStart} lines long (consider breaking up)`);
180
+ }
181
+ currentFunc = funcMatch[1] || funcMatch[2] || '';
182
+ funcStart = i;
183
+ maxNesting = 0;
184
+ }
185
+ // Track nesting
186
+ const opens = (line.match(/[{(]/g) || []).length;
187
+ const closes = (line.match(/[})]/g) || []).length;
188
+ currentNesting += opens - closes;
189
+ if (currentNesting > maxNesting)
190
+ maxNesting = currentNesting;
191
+ if (maxNesting > 5 && currentFunc) {
192
+ results.push(` [!] ${filePath}:${i + 1} deep nesting (${maxNesting} levels) in "${currentFunc}"`);
193
+ maxNesting = 0; // Don't re-report
194
+ }
195
+ }
196
+ // Check last function
197
+ if (currentFunc && (lines.length - funcStart) > 50) {
198
+ results.push(` [!] ${filePath}:${funcStart + 1} "${currentFunc}" is ${lines.length - funcStart} lines long`);
199
+ }
200
+ // File-level checks
201
+ if (lines.length > 500) {
202
+ results.push(` [i] ${filePath}: ${lines.length} lines — consider splitting into modules`);
203
+ }
204
+ }
205
+ catch { /* skip */ }
206
+ }
207
+ analyzeDir(dir, results) {
208
+ const skip = new Set(['node_modules', '.git', 'dist', 'build', 'coverage']);
209
+ let entries;
210
+ try {
211
+ entries = fs.readdirSync(dir, { withFileTypes: true });
212
+ }
213
+ catch {
214
+ return;
215
+ }
216
+ for (const entry of entries) {
217
+ if (entry.name.startsWith('.') || skip.has(entry.name))
218
+ continue;
219
+ const full = path.join(dir, entry.name);
220
+ if (entry.isDirectory())
221
+ this.analyzeDir(full, results);
222
+ else
223
+ this.analyzeFileComplexity(full, results);
224
+ }
225
+ }
226
+ }
227
+ exports.CodeReviewTool = CodeReviewTool;
228
+ //# sourceMappingURL=code-review.js.map