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.
- package/README.md +135 -116
- package/dist/agent.js +51 -27
- package/dist/cli.js +18 -2
- package/dist/providers/anthropic.js +38 -18
- package/dist/providers/openai.js +35 -14
- package/dist/retry.d.ts +22 -0
- package/dist/retry.js +59 -0
- package/dist/scheduler.d.ts +2 -0
- package/dist/scheduler.js +25 -17
- 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/dist/tools/web-fetch.js +11 -2
- package/package.json +16 -8
package/dist/retry.d.ts
ADDED
|
@@ -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
|
package/dist/scheduler.d.ts
CHANGED
|
@@ -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
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|