Skip to content

Commit 744c7e8

Browse files
Ascendralclaude
andcommitted
v1.4.0: 15 new tools — git, code analysis, docker, database, SSH, and more
Takes CodeBot from 13 to 28 built-in tools. All zero-dependency. Tier 1 (Intelligence): - git: status, diff, log, commit, branch, checkout, stash, push, pull, merge, blame, tag - code_analysis: symbol extraction, find references, imports, outline - multi_search: fuzzy search across filenames, content, and symbols - task_planner: hierarchical task tracking with priorities - diff_viewer: file comparison and git diffs Tier 2 (Development): - docker: container management (ps, run, build, compose) - database: SQLite queries with destructive SQL blocking - test_runner: auto-detect framework (jest, vitest, pytest, go, cargo) - http_client: HTTP requests with auth, headers, body - image_info: PNG/JPEG/GIF/SVG dimensions from binary headers Tier 3 (Infrastructure): - ssh_remote: remote exec and file transfer with injection protection - notification: Slack/Discord/webhook with severity formatting - pdf_extract: text extraction and metadata from PDFs - package_manager: auto-detect npm/yarn/pip/cargo/go - code_review: security scanning and complexity analysis 131 tests passing (up from 99). All tools have security guards (SQL injection blocking, SSH injection protection, SSRF blocking, etc.) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent f9da30d commit 744c7e8

20 files changed

Lines changed: 2270 additions & 14 deletions

README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ echo "explain this error" | codebot # Pipe mode
107107

108108
## Tools
109109

110-
CodeBot has 13 built-in tools:
110+
CodeBot has 28 built-in tools:
111111

112112
| Tool | Description | Permission |
113113
|------|-------------|-----------|
@@ -124,6 +124,21 @@ CodeBot has 13 built-in tools:
124124
| `web_search` | Internet search with result summaries | prompt |
125125
| `browser` | Chrome automation via CDP | prompt |
126126
| `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 |
127142

128143
### Permission Levels
129144

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codebot-ai",
3-
"version": "1.3.0",
3+
"version": "1.4.0",
44
"description": "Zero-dependency autonomous AI agent. Code, browse, search, automate. Works with any LLM — Ollama, Claude, GPT, Gemini, DeepSeek, Groq, Mistral, Grok.",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",

src/cli.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { banner, randomGreeting, compactBanner } from './banner';
1010
import { EditFileTool } from './tools';
1111
import { Scheduler } from './scheduler';
1212

13-
const VERSION = '1.3.0';
13+
const VERSION = '1.4.0';
1414

1515
// Session-wide token tracking
1616
let sessionTokens = { input: 0, output: 0, total: 0 };

src/tools/code-analysis.ts

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import * as fs from 'fs';
2+
import * as path from 'path';
3+
import { Tool } from '../types';
4+
5+
export class CodeAnalysisTool implements Tool {
6+
name = 'code_analysis';
7+
description = 'Analyze code structure. Actions: symbols (list classes/functions/exports), imports (list imports), outline (file structure), references (find where a symbol is used).';
8+
permission: Tool['permission'] = 'auto';
9+
parameters = {
10+
type: 'object',
11+
properties: {
12+
action: { type: 'string', description: 'Action: symbols, imports, outline, references' },
13+
path: { type: 'string', description: 'File or directory to analyze' },
14+
symbol: { type: 'string', description: 'Symbol name to find references for (required for "references" action)' },
15+
},
16+
required: ['action', 'path'],
17+
};
18+
19+
async execute(args: Record<string, unknown>): Promise<string> {
20+
const action = args.action as string;
21+
const targetPath = args.path as string;
22+
23+
if (!action) return 'Error: action is required';
24+
if (!targetPath) return 'Error: path is required';
25+
26+
if (!fs.existsSync(targetPath)) {
27+
return `Error: path not found: ${targetPath}`;
28+
}
29+
30+
switch (action) {
31+
case 'symbols': return this.extractSymbols(targetPath);
32+
case 'imports': return this.extractImports(targetPath);
33+
case 'outline': return this.buildOutline(targetPath);
34+
case 'references': {
35+
const symbol = args.symbol as string;
36+
if (!symbol) return 'Error: symbol is required for references action';
37+
return this.findReferences(targetPath, symbol);
38+
}
39+
default:
40+
return `Error: unknown action "${action}". Use: symbols, imports, outline, references`;
41+
}
42+
}
43+
44+
private extractSymbols(filePath: string): string {
45+
const content = this.readFile(filePath);
46+
if (!content) return 'Error: could not read file';
47+
48+
const symbols: string[] = [];
49+
const lines = content.split('\n');
50+
51+
for (let i = 0; i < lines.length; i++) {
52+
const line = lines[i];
53+
const lineNum = i + 1;
54+
55+
// Classes
56+
const classMatch = line.match(/^(?:export\s+)?(?:abstract\s+)?class\s+(\w+)/);
57+
if (classMatch) symbols.push(` class ${classMatch[1]} (line ${lineNum})`);
58+
59+
// Functions
60+
const funcMatch = line.match(/^(?:export\s+)?(?:async\s+)?function\s+(\w+)/);
61+
if (funcMatch) symbols.push(` function ${funcMatch[1]} (line ${lineNum})`);
62+
63+
// Arrow function exports
64+
const arrowMatch = line.match(/^(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*(?:async\s+)?\(/);
65+
if (arrowMatch) symbols.push(` const ${arrowMatch[1]} (line ${lineNum})`);
66+
67+
// Interfaces & Types
68+
const ifaceMatch = line.match(/^(?:export\s+)?interface\s+(\w+)/);
69+
if (ifaceMatch) symbols.push(` interface ${ifaceMatch[1]} (line ${lineNum})`);
70+
71+
const typeMatch = line.match(/^(?:export\s+)?type\s+(\w+)/);
72+
if (typeMatch) symbols.push(` type ${typeMatch[1]} (line ${lineNum})`);
73+
74+
// Methods inside classes
75+
const methodMatch = line.match(/^\s+(?:async\s+)?(?:private\s+|public\s+|protected\s+)?(\w+)\s*\([^)]*\)\s*[:{]/);
76+
if (methodMatch && !['if', 'for', 'while', 'switch', 'catch', 'constructor'].includes(methodMatch[1])) {
77+
symbols.push(` method ${methodMatch[1]} (line ${lineNum})`);
78+
}
79+
}
80+
81+
if (symbols.length === 0) return 'No symbols found.';
82+
return `Symbols in ${path.basename(filePath)}:\n${symbols.join('\n')}`;
83+
}
84+
85+
private extractImports(filePath: string): string {
86+
const content = this.readFile(filePath);
87+
if (!content) return 'Error: could not read file';
88+
89+
const imports: string[] = [];
90+
const lines = content.split('\n');
91+
92+
for (const line of lines) {
93+
// ES imports
94+
const esMatch = line.match(/^import\s+.*from\s+['"]([^'"]+)['"]/);
95+
if (esMatch) { imports.push(` ${esMatch[1]}`); continue; }
96+
97+
// Require
98+
const reqMatch = line.match(/require\s*\(\s*['"]([^'"]+)['"]\s*\)/);
99+
if (reqMatch) { imports.push(` ${reqMatch[1]}`); continue; }
100+
101+
// Python imports
102+
const pyMatch = line.match(/^(?:from\s+(\S+)\s+)?import\s+(\S+)/);
103+
if (pyMatch && !line.includes('{')) {
104+
imports.push(` ${pyMatch[1] || pyMatch[2]}`);
105+
}
106+
}
107+
108+
if (imports.length === 0) return 'No imports found.';
109+
return `Imports in ${path.basename(filePath)}:\n${imports.join('\n')}`;
110+
}
111+
112+
private buildOutline(targetPath: string): string {
113+
const stat = fs.statSync(targetPath);
114+
115+
if (stat.isFile()) {
116+
return this.extractSymbols(targetPath);
117+
}
118+
119+
// Directory outline
120+
const lines: string[] = [`Outline of ${path.basename(targetPath)}/`];
121+
this.walkDir(targetPath, '', lines, 0, 3);
122+
return lines.join('\n');
123+
}
124+
125+
private walkDir(dir: string, prefix: string, lines: string[], depth: number, maxDepth: number): void {
126+
if (depth >= maxDepth) return;
127+
const skip = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '__pycache__', '.next']);
128+
129+
let entries: fs.Dirent[];
130+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
131+
132+
const dirs = entries.filter(e => e.isDirectory() && !e.name.startsWith('.') && !skip.has(e.name));
133+
const files = entries.filter(e => e.isFile() && !e.name.startsWith('.'));
134+
135+
for (const d of dirs.sort((a, b) => a.name.localeCompare(b.name))) {
136+
lines.push(`${prefix}${d.name}/`);
137+
this.walkDir(path.join(dir, d.name), prefix + ' ', lines, depth + 1, maxDepth);
138+
}
139+
for (const f of files.sort((a, b) => a.name.localeCompare(b.name))) {
140+
lines.push(`${prefix}${f.name}`);
141+
}
142+
}
143+
144+
private findReferences(targetPath: string, symbol: string): string {
145+
const stat = fs.statSync(targetPath);
146+
const dir = stat.isFile() ? path.dirname(targetPath) : targetPath;
147+
const results: string[] = [];
148+
const regex = new RegExp(`\\b${symbol.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g');
149+
150+
this.searchRefs(dir, regex, results, 50);
151+
152+
if (results.length === 0) return `No references to "${symbol}" found.`;
153+
return `References to "${symbol}":\n${results.join('\n')}`;
154+
}
155+
156+
private searchRefs(dir: string, regex: RegExp, results: string[], max: number): void {
157+
if (results.length >= max) return;
158+
const skip = new Set(['node_modules', '.git', 'dist', 'build', 'coverage']);
159+
160+
let entries: fs.Dirent[];
161+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
162+
163+
for (const entry of entries) {
164+
if (results.length >= max) break;
165+
if (entry.name.startsWith('.') || skip.has(entry.name)) continue;
166+
167+
const full = path.join(dir, entry.name);
168+
if (entry.isDirectory()) {
169+
this.searchRefs(full, regex, results, max);
170+
} else {
171+
const ext = path.extname(entry.name);
172+
if (!['.ts', '.js', '.tsx', '.jsx', '.py', '.go', '.rs', '.java', '.rb', '.c', '.cpp', '.h'].includes(ext)) continue;
173+
try {
174+
const content = fs.readFileSync(full, 'utf-8');
175+
const lines = content.split('\n');
176+
for (let i = 0; i < lines.length && results.length < max; i++) {
177+
regex.lastIndex = 0;
178+
if (regex.test(lines[i])) {
179+
results.push(` ${full}:${i + 1}: ${lines[i].trimEnd()}`);
180+
}
181+
}
182+
} catch { /* skip */ }
183+
}
184+
}
185+
}
186+
187+
private readFile(filePath: string): string | null {
188+
try {
189+
return fs.readFileSync(filePath, 'utf-8');
190+
} catch { return null; }
191+
}
192+
}

0 commit comments

Comments
 (0)