codebot-ai 1.3.0 → 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 +16 -1
- package/dist/cli.js +1 -1
- 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/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.0';
|
|
48
48
|
// Session-wide token tracking
|
|
49
49
|
let sessionTokens = { input: 0, output: 0, total: 0 };
|
|
50
50
|
const C = {
|
|
@@ -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
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Tool } from '../types';
|
|
2
|
+
export declare class DatabaseTool 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
|
+
db: {
|
|
14
|
+
type: string;
|
|
15
|
+
description: string;
|
|
16
|
+
};
|
|
17
|
+
sql: {
|
|
18
|
+
type: string;
|
|
19
|
+
description: string;
|
|
20
|
+
};
|
|
21
|
+
table: {
|
|
22
|
+
type: string;
|
|
23
|
+
description: string;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
required: string[];
|
|
27
|
+
};
|
|
28
|
+
execute(args: Record<string, unknown>): Promise<string>;
|
|
29
|
+
private runQuery;
|
|
30
|
+
private listTables;
|
|
31
|
+
private showSchema;
|
|
32
|
+
private dbInfo;
|
|
33
|
+
private sqlite;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=database.d.ts.map
|