prab-cli 1.0.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/LICENSE +15 -0
- package/README.md +272 -0
- package/dist/index.js +374 -0
- package/dist/lib/chat-handler.js +219 -0
- package/dist/lib/config.js +156 -0
- package/dist/lib/context.js +44 -0
- package/dist/lib/groq-models.js +53 -0
- package/dist/lib/groq.js +33 -0
- package/dist/lib/models/groq-provider.js +82 -0
- package/dist/lib/models/provider.js +10 -0
- package/dist/lib/models/registry.js +101 -0
- package/dist/lib/safety.js +109 -0
- package/dist/lib/tools/base.js +115 -0
- package/dist/lib/tools/executor.js +127 -0
- package/dist/lib/tools/file-tools.js +283 -0
- package/dist/lib/tools/git-tools.js +280 -0
- package/dist/lib/tools/shell-tools.js +73 -0
- package/dist/lib/tools/todo-tool.js +105 -0
- package/dist/lib/tracker.js +314 -0
- package/dist/lib/ui.js +578 -0
- package/dist/log-viewer.js +374 -0
- package/dist/types/index.js +5 -0
- package/package.json +75 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.GrepTool = exports.GlobTool = exports.EditFileTool = exports.WriteFileTool = exports.ReadFileTool = void 0;
|
|
7
|
+
const zod_1 = require("zod");
|
|
8
|
+
const base_1 = require("./base");
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const glob_1 = require("glob");
|
|
12
|
+
const diff_1 = require("diff");
|
|
13
|
+
/**
|
|
14
|
+
* Read a file with line numbers
|
|
15
|
+
*/
|
|
16
|
+
class ReadFileTool extends base_1.Tool {
|
|
17
|
+
constructor() {
|
|
18
|
+
super(...arguments);
|
|
19
|
+
this.name = 'read_file';
|
|
20
|
+
this.description = 'Read the contents of a file with line numbers. Supports pagination for large files.';
|
|
21
|
+
this.requiresConfirmation = false;
|
|
22
|
+
this.destructive = false;
|
|
23
|
+
this.schema = zod_1.z.object({
|
|
24
|
+
file_path: zod_1.z.string().describe('Absolute or relative path to the file to read'),
|
|
25
|
+
offset: zod_1.z.number().optional().describe('Line number to start reading from (1-indexed)'),
|
|
26
|
+
limit: zod_1.z.number().optional().describe('Maximum number of lines to read')
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
async execute(params) {
|
|
30
|
+
try {
|
|
31
|
+
const filePath = path_1.default.resolve(params.file_path);
|
|
32
|
+
if (!fs_1.default.existsSync(filePath)) {
|
|
33
|
+
return this.error(`File not found: ${params.file_path}`);
|
|
34
|
+
}
|
|
35
|
+
if (fs_1.default.statSync(filePath).isDirectory()) {
|
|
36
|
+
return this.error(`Path is a directory: ${params.file_path}`);
|
|
37
|
+
}
|
|
38
|
+
const content = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
39
|
+
const lines = content.split('\n');
|
|
40
|
+
const startLine = params.offset ? params.offset - 1 : 0;
|
|
41
|
+
const endLine = params.limit ? startLine + params.limit : lines.length;
|
|
42
|
+
const selectedLines = lines.slice(startLine, endLine);
|
|
43
|
+
// Format with line numbers (1-indexed)
|
|
44
|
+
const numberedLines = selectedLines.map((line, idx) => {
|
|
45
|
+
const lineNum = startLine + idx + 1;
|
|
46
|
+
return `${lineNum.toString().padStart(6)}→${line}`;
|
|
47
|
+
}).join('\n');
|
|
48
|
+
const totalLines = lines.length;
|
|
49
|
+
const showing = `Showing lines ${startLine + 1}-${Math.min(endLine, totalLines)} of ${totalLines}`;
|
|
50
|
+
return this.success(`${params.file_path}\n${showing}\n\n${numberedLines}`, { totalLines, startLine: startLine + 1, endLine: Math.min(endLine, totalLines) });
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
return this.error(`Failed to read file: ${error.message}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
exports.ReadFileTool = ReadFileTool;
|
|
58
|
+
/**
|
|
59
|
+
* Write content to a file (create or overwrite)
|
|
60
|
+
*/
|
|
61
|
+
class WriteFileTool extends base_1.Tool {
|
|
62
|
+
constructor() {
|
|
63
|
+
super(...arguments);
|
|
64
|
+
this.name = 'write_file';
|
|
65
|
+
this.description = 'Write content to a file. Creates the file if it doesn\'t exist, overwrites if it does. Creates parent directories as needed.';
|
|
66
|
+
this.requiresConfirmation = true;
|
|
67
|
+
this.destructive = true;
|
|
68
|
+
this.schema = zod_1.z.object({
|
|
69
|
+
file_path: zod_1.z.string().describe('Absolute or relative path to the file to write'),
|
|
70
|
+
content: zod_1.z.string().describe('Content to write to the file')
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
async execute(params) {
|
|
74
|
+
try {
|
|
75
|
+
const filePath = path_1.default.resolve(params.file_path);
|
|
76
|
+
const dir = path_1.default.dirname(filePath);
|
|
77
|
+
// Create parent directories if needed
|
|
78
|
+
if (!fs_1.default.existsSync(dir)) {
|
|
79
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
80
|
+
}
|
|
81
|
+
const fileExists = fs_1.default.existsSync(filePath);
|
|
82
|
+
fs_1.default.writeFileSync(filePath, params.content, 'utf-8');
|
|
83
|
+
const action = fileExists ? 'Updated' : 'Created';
|
|
84
|
+
const lines = params.content.split('\n').length;
|
|
85
|
+
return this.success(`${action} ${params.file_path} (${lines} lines)`, { action, path: filePath, lines });
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
return this.error(`Failed to write file: ${error.message}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
exports.WriteFileTool = WriteFileTool;
|
|
93
|
+
/**
|
|
94
|
+
* Edit a file by finding and replacing text
|
|
95
|
+
*/
|
|
96
|
+
class EditFileTool extends base_1.Tool {
|
|
97
|
+
constructor() {
|
|
98
|
+
super(...arguments);
|
|
99
|
+
this.name = 'edit_file';
|
|
100
|
+
this.description = 'Edit a file by finding and replacing text. Shows a diff preview. Supports replace all option.';
|
|
101
|
+
this.requiresConfirmation = true;
|
|
102
|
+
this.destructive = true;
|
|
103
|
+
this.schema = zod_1.z.object({
|
|
104
|
+
file_path: zod_1.z.string().describe('Absolute or relative path to the file to edit'),
|
|
105
|
+
search: zod_1.z.string().describe('Text to search for (exact match)'),
|
|
106
|
+
replace: zod_1.z.string().describe('Text to replace with'),
|
|
107
|
+
replace_all: zod_1.z.boolean().optional().describe('Replace all occurrences (default: false)')
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
async execute(params) {
|
|
111
|
+
try {
|
|
112
|
+
const filePath = path_1.default.resolve(params.file_path);
|
|
113
|
+
if (!fs_1.default.existsSync(filePath)) {
|
|
114
|
+
return this.error(`File not found: ${params.file_path}`);
|
|
115
|
+
}
|
|
116
|
+
const originalContent = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
117
|
+
// Perform replacement
|
|
118
|
+
let newContent;
|
|
119
|
+
if (params.replace_all) {
|
|
120
|
+
newContent = originalContent.split(params.search).join(params.replace);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
newContent = originalContent.replace(params.search, params.replace);
|
|
124
|
+
}
|
|
125
|
+
if (originalContent === newContent) {
|
|
126
|
+
return this.error(`Search text not found in file: "${params.search}"`);
|
|
127
|
+
}
|
|
128
|
+
// Generate diff
|
|
129
|
+
const diff = (0, diff_1.diffLines)(originalContent, newContent);
|
|
130
|
+
const diffOutput = diff.map(part => {
|
|
131
|
+
const prefix = part.added ? '+ ' : part.removed ? '- ' : ' ';
|
|
132
|
+
return part.value.split('\n').map(line => line ? prefix + line : '').join('\n');
|
|
133
|
+
}).join('');
|
|
134
|
+
// Write the changes
|
|
135
|
+
fs_1.default.writeFileSync(filePath, newContent, 'utf-8');
|
|
136
|
+
const occurrences = params.replace_all
|
|
137
|
+
? originalContent.split(params.search).length - 1
|
|
138
|
+
: 1;
|
|
139
|
+
return this.success(`Edited ${params.file_path} (${occurrences} replacement${occurrences > 1 ? 's' : ''})\n\nDiff:\n${diffOutput}`, { occurrences, path: filePath });
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
return this.error(`Failed to edit file: ${error.message}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
exports.EditFileTool = EditFileTool;
|
|
147
|
+
/**
|
|
148
|
+
* Find files matching a glob pattern
|
|
149
|
+
*/
|
|
150
|
+
class GlobTool extends base_1.Tool {
|
|
151
|
+
constructor() {
|
|
152
|
+
super(...arguments);
|
|
153
|
+
this.name = 'glob';
|
|
154
|
+
this.description = 'Find files matching a glob pattern (e.g., "**/*.ts", "src/**/*.js"). Returns paths sorted by modification time.';
|
|
155
|
+
this.requiresConfirmation = false;
|
|
156
|
+
this.destructive = false;
|
|
157
|
+
this.schema = zod_1.z.object({
|
|
158
|
+
pattern: zod_1.z.string().describe('Glob pattern to match files (e.g., "**/*.ts")'),
|
|
159
|
+
path: zod_1.z.string().optional().describe('Directory to search in (default: current directory)')
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
async execute(params) {
|
|
163
|
+
try {
|
|
164
|
+
const cwd = params.path ? path_1.default.resolve(params.path) : process.cwd();
|
|
165
|
+
const ignore = ['**/node_modules/**', '**/.git/**', '**/dist/**', '**/.env', '**/*.lock'];
|
|
166
|
+
const files = await (0, glob_1.glob)(params.pattern, {
|
|
167
|
+
cwd,
|
|
168
|
+
ignore,
|
|
169
|
+
nodir: true
|
|
170
|
+
});
|
|
171
|
+
// Sort by modification time (most recent first)
|
|
172
|
+
const filesWithStats = files.map(file => {
|
|
173
|
+
const fullPath = path_1.default.join(cwd, file);
|
|
174
|
+
const stats = fs_1.default.statSync(fullPath);
|
|
175
|
+
return { file, mtime: stats.mtime.getTime() };
|
|
176
|
+
});
|
|
177
|
+
filesWithStats.sort((a, b) => b.mtime - a.mtime);
|
|
178
|
+
const sortedFiles = filesWithStats.map(f => f.file);
|
|
179
|
+
if (sortedFiles.length === 0) {
|
|
180
|
+
return this.success(`No files found matching pattern: ${params.pattern}`);
|
|
181
|
+
}
|
|
182
|
+
const output = sortedFiles.join('\n');
|
|
183
|
+
return this.success(`Found ${sortedFiles.length} file(s) matching "${params.pattern}":\n${output}`, { count: sortedFiles.length, files: sortedFiles });
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
return this.error(`Glob search failed: ${error.message}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
exports.GlobTool = GlobTool;
|
|
191
|
+
/**
|
|
192
|
+
* Search file contents with regex
|
|
193
|
+
*/
|
|
194
|
+
class GrepTool extends base_1.Tool {
|
|
195
|
+
constructor() {
|
|
196
|
+
super(...arguments);
|
|
197
|
+
this.name = 'grep';
|
|
198
|
+
this.description = 'Search file contents using regex patterns. Supports multiple output modes and context lines.';
|
|
199
|
+
this.requiresConfirmation = false;
|
|
200
|
+
this.destructive = false;
|
|
201
|
+
this.schema = zod_1.z.object({
|
|
202
|
+
pattern: zod_1.z.string().describe('Regular expression pattern to search for'),
|
|
203
|
+
path: zod_1.z.string().optional().describe('Directory or file to search in (default: current directory)'),
|
|
204
|
+
glob_filter: zod_1.z.string().optional().describe('Glob pattern to filter files (e.g., "*.ts")'),
|
|
205
|
+
output_mode: zod_1.z.enum(['content', 'files', 'count']).optional().describe('Output mode: content (matching lines), files (file paths), count (match counts)'),
|
|
206
|
+
case_insensitive: zod_1.z.boolean().optional().describe('Case insensitive search (default: false)'),
|
|
207
|
+
context_lines: zod_1.z.number().optional().describe('Number of context lines to show before and after matches')
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
async execute(params) {
|
|
211
|
+
try {
|
|
212
|
+
const searchPath = params.path ? path_1.default.resolve(params.path) : process.cwd();
|
|
213
|
+
const outputMode = params.output_mode || 'files';
|
|
214
|
+
// Determine files to search
|
|
215
|
+
let filesToSearch;
|
|
216
|
+
if (fs_1.default.existsSync(searchPath) && fs_1.default.statSync(searchPath).isFile()) {
|
|
217
|
+
filesToSearch = [searchPath];
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
const globPattern = params.glob_filter || '**/*';
|
|
221
|
+
const ignore = ['**/node_modules/**', '**/.git/**', '**/dist/**'];
|
|
222
|
+
filesToSearch = await (0, glob_1.glob)(globPattern, {
|
|
223
|
+
cwd: searchPath,
|
|
224
|
+
ignore,
|
|
225
|
+
nodir: true,
|
|
226
|
+
absolute: true
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
// Create regex
|
|
230
|
+
const flags = params.case_insensitive ? 'gi' : 'g';
|
|
231
|
+
const regex = new RegExp(params.pattern, flags);
|
|
232
|
+
const results = [];
|
|
233
|
+
// Search files
|
|
234
|
+
for (const file of filesToSearch) {
|
|
235
|
+
try {
|
|
236
|
+
const content = fs_1.default.readFileSync(file, 'utf-8');
|
|
237
|
+
const lines = content.split('\n');
|
|
238
|
+
const matches = [];
|
|
239
|
+
lines.forEach((line, idx) => {
|
|
240
|
+
if (regex.test(line)) {
|
|
241
|
+
matches.push({ line: idx + 1, content: line });
|
|
242
|
+
}
|
|
243
|
+
// Reset regex lastIndex for global flag
|
|
244
|
+
regex.lastIndex = 0;
|
|
245
|
+
});
|
|
246
|
+
if (matches.length > 0) {
|
|
247
|
+
results.push({ file, matches });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch (e) {
|
|
251
|
+
// Skip files that can't be read (binary, permissions, etc.)
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
// Format output based on mode
|
|
256
|
+
if (outputMode === 'files') {
|
|
257
|
+
const fileList = results.map(r => r.file).join('\n');
|
|
258
|
+
return this.success(`Found matches in ${results.length} file(s):\n${fileList}`, { count: results.length, files: results.map(r => r.file) });
|
|
259
|
+
}
|
|
260
|
+
else if (outputMode === 'count') {
|
|
261
|
+
const counts = results.map(r => `${r.file}: ${r.matches.length} matches`).join('\n');
|
|
262
|
+
const total = results.reduce((sum, r) => sum + r.matches.length, 0);
|
|
263
|
+
return this.success(`Total matches: ${total}\n${counts}`, { total, breakdown: results.map(r => ({ file: r.file, count: r.matches.length })) });
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
// content mode
|
|
267
|
+
const contextLines = params.context_lines || 0;
|
|
268
|
+
let output = '';
|
|
269
|
+
for (const result of results) {
|
|
270
|
+
output += `\n${result.file}:\n`;
|
|
271
|
+
for (const match of result.matches) {
|
|
272
|
+
output += ` ${match.line}: ${match.content}\n`;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return this.success(`Found ${results.reduce((sum, r) => sum + r.matches.length, 0)} match(es) across ${results.length} file(s):${output}`, { matchCount: results.reduce((sum, r) => sum + r.matches.length, 0), fileCount: results.length });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
return this.error(`Grep search failed: ${error.message}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
exports.GrepTool = GrepTool;
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.GitPushTool = exports.GitBranchTool = exports.GitCommitTool = exports.GitLogTool = exports.GitDiffTool = exports.GitAddTool = exports.GitStatusTool = void 0;
|
|
7
|
+
const zod_1 = require("zod");
|
|
8
|
+
const base_1 = require("./base");
|
|
9
|
+
const simple_git_1 = __importDefault(require("simple-git"));
|
|
10
|
+
const git = (0, simple_git_1.default)();
|
|
11
|
+
/**
|
|
12
|
+
* Get git status
|
|
13
|
+
*/
|
|
14
|
+
class GitStatusTool extends base_1.Tool {
|
|
15
|
+
constructor() {
|
|
16
|
+
super(...arguments);
|
|
17
|
+
this.name = 'git_status';
|
|
18
|
+
this.description = 'Show the working tree status - staged, unstaged, and untracked files.';
|
|
19
|
+
this.requiresConfirmation = false;
|
|
20
|
+
this.destructive = false;
|
|
21
|
+
this.schema = zod_1.z.object({});
|
|
22
|
+
}
|
|
23
|
+
async execute(params) {
|
|
24
|
+
try {
|
|
25
|
+
const status = await git.status();
|
|
26
|
+
const output = [];
|
|
27
|
+
output.push(`Branch: ${status.current || '(detached HEAD)'}`);
|
|
28
|
+
output.push(`Ahead: ${status.ahead}, Behind: ${status.behind}`);
|
|
29
|
+
if (status.files.length === 0) {
|
|
30
|
+
output.push('\nWorking tree clean');
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
if (status.staged.length > 0) {
|
|
34
|
+
output.push('\nStaged files:');
|
|
35
|
+
status.staged.forEach(file => output.push(` + ${file}`));
|
|
36
|
+
}
|
|
37
|
+
if (status.modified.length > 0 || status.deleted.length > 0) {
|
|
38
|
+
output.push('\nUnstaged changes:');
|
|
39
|
+
status.modified.forEach(file => output.push(` M ${file}`));
|
|
40
|
+
status.deleted.forEach(file => output.push(` D ${file}`));
|
|
41
|
+
}
|
|
42
|
+
if (status.not_added.length > 0) {
|
|
43
|
+
output.push('\nUntracked files:');
|
|
44
|
+
status.not_added.forEach(file => output.push(` ? ${file}`));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return this.success(output.join('\n'), { status });
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
return this.error(`Git status failed: ${error.message}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
exports.GitStatusTool = GitStatusTool;
|
|
55
|
+
/**
|
|
56
|
+
* Stage files for commit (git add)
|
|
57
|
+
*/
|
|
58
|
+
class GitAddTool extends base_1.Tool {
|
|
59
|
+
constructor() {
|
|
60
|
+
super(...arguments);
|
|
61
|
+
this.name = 'git_add';
|
|
62
|
+
this.description = 'Stage files for commit. Use "." to stage all changes, or specify specific files.';
|
|
63
|
+
this.requiresConfirmation = false;
|
|
64
|
+
this.destructive = false;
|
|
65
|
+
this.schema = zod_1.z.object({
|
|
66
|
+
files: zod_1.z.array(zod_1.z.string()).describe('Files to stage. Use ["."] to stage all changes.')
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
async execute(params) {
|
|
70
|
+
try {
|
|
71
|
+
await git.add(params.files);
|
|
72
|
+
// Get status to show what was staged
|
|
73
|
+
const status = await git.status();
|
|
74
|
+
const stagedFiles = status.staged;
|
|
75
|
+
return this.success(`Staged ${stagedFiles.length} file(s):\n${stagedFiles.map(f => ` + ${f}`).join('\n')}`, { staged: stagedFiles });
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
return this.error(`Git add failed: ${error.message}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
exports.GitAddTool = GitAddTool;
|
|
83
|
+
/**
|
|
84
|
+
* Show git diff
|
|
85
|
+
*/
|
|
86
|
+
class GitDiffTool extends base_1.Tool {
|
|
87
|
+
constructor() {
|
|
88
|
+
super(...arguments);
|
|
89
|
+
this.name = 'git_diff';
|
|
90
|
+
this.description = 'Show changes in files. Can show staged or unstaged changes, or changes for specific files.';
|
|
91
|
+
this.requiresConfirmation = false;
|
|
92
|
+
this.destructive = false;
|
|
93
|
+
this.schema = zod_1.z.object({
|
|
94
|
+
files: zod_1.z.array(zod_1.z.string()).optional().describe('Specific files to show diff for'),
|
|
95
|
+
staged: zod_1.z.boolean().optional().describe('Show staged changes (default: false, shows unstaged)')
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
async execute(params) {
|
|
99
|
+
try {
|
|
100
|
+
const options = params.staged ? ['--cached'] : [];
|
|
101
|
+
if (params.files && params.files.length > 0) {
|
|
102
|
+
options.push('--', ...params.files);
|
|
103
|
+
}
|
|
104
|
+
const diff = await git.diff(options);
|
|
105
|
+
if (!diff || diff.trim().length === 0) {
|
|
106
|
+
return this.success('No changes to show');
|
|
107
|
+
}
|
|
108
|
+
return this.success(diff, { staged: params.staged || false });
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
return this.error(`Git diff failed: ${error.message}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
exports.GitDiffTool = GitDiffTool;
|
|
116
|
+
/**
|
|
117
|
+
* Show git log
|
|
118
|
+
*/
|
|
119
|
+
class GitLogTool extends base_1.Tool {
|
|
120
|
+
constructor() {
|
|
121
|
+
super(...arguments);
|
|
122
|
+
this.name = 'git_log';
|
|
123
|
+
this.description = 'Show commit history with messages, authors, and dates.';
|
|
124
|
+
this.requiresConfirmation = false;
|
|
125
|
+
this.destructive = false;
|
|
126
|
+
this.schema = zod_1.z.object({
|
|
127
|
+
limit: zod_1.z.number().optional().describe('Maximum number of commits to show (default: 10)'),
|
|
128
|
+
branch: zod_1.z.string().optional().describe('Branch to show log for (default: current branch)')
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
async execute(params) {
|
|
132
|
+
try {
|
|
133
|
+
const options = {
|
|
134
|
+
maxCount: params.limit || 10
|
|
135
|
+
};
|
|
136
|
+
if (params.branch) {
|
|
137
|
+
options.file = params.branch;
|
|
138
|
+
}
|
|
139
|
+
const log = await git.log(options);
|
|
140
|
+
const output = log.all.map(commit => {
|
|
141
|
+
return `${commit.hash.substring(0, 7)} ${commit.date}\n ${commit.message}`;
|
|
142
|
+
}).join('\n\n');
|
|
143
|
+
return this.success(output || 'No commits found', { count: log.all.length, commits: log.all });
|
|
144
|
+
}
|
|
145
|
+
catch (error) {
|
|
146
|
+
return this.error(`Git log failed: ${error.message}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
exports.GitLogTool = GitLogTool;
|
|
151
|
+
/**
|
|
152
|
+
* Create a git commit
|
|
153
|
+
*/
|
|
154
|
+
class GitCommitTool extends base_1.Tool {
|
|
155
|
+
constructor() {
|
|
156
|
+
super(...arguments);
|
|
157
|
+
this.name = 'git_commit';
|
|
158
|
+
this.description = 'Create a git commit with staged changes. Adds Claude attribution to commit message.';
|
|
159
|
+
this.requiresConfirmation = true;
|
|
160
|
+
this.destructive = true;
|
|
161
|
+
this.schema = zod_1.z.object({
|
|
162
|
+
message: zod_1.z.string().describe('Commit message'),
|
|
163
|
+
files: zod_1.z.array(zod_1.z.string()).optional().describe('Files to stage before committing (if not already staged)')
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
async execute(params) {
|
|
167
|
+
try {
|
|
168
|
+
// Stage files if specified
|
|
169
|
+
if (params.files && params.files.length > 0) {
|
|
170
|
+
await git.add(params.files);
|
|
171
|
+
}
|
|
172
|
+
// Add Claude attribution
|
|
173
|
+
const fullMessage = `${params.message}\n\n🤖 Generated with CLI AI Tool\n\nCo-Authored-By: AI Assistant <noreply@example.com>`;
|
|
174
|
+
// Create commit
|
|
175
|
+
const result = await git.commit(fullMessage);
|
|
176
|
+
return this.success(`Created commit: ${result.commit}\n${result.summary.changes} changes, ${result.summary.insertions} insertions, ${result.summary.deletions} deletions`, { commit: result.commit, summary: result.summary });
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
return this.error(`Git commit failed: ${error.message}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
exports.GitCommitTool = GitCommitTool;
|
|
184
|
+
/**
|
|
185
|
+
* Manage git branches
|
|
186
|
+
*/
|
|
187
|
+
class GitBranchTool extends base_1.Tool {
|
|
188
|
+
constructor() {
|
|
189
|
+
super(...arguments);
|
|
190
|
+
this.name = 'git_branch';
|
|
191
|
+
this.description = 'List, create, switch, or delete git branches.';
|
|
192
|
+
this.requiresConfirmation = true;
|
|
193
|
+
this.destructive = true;
|
|
194
|
+
this.schema = zod_1.z.object({
|
|
195
|
+
action: zod_1.z.enum(['list', 'create', 'switch', 'delete']).describe('Action to perform'),
|
|
196
|
+
name: zod_1.z.string().optional().describe('Branch name (required for create, switch, delete)')
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
async execute(params) {
|
|
200
|
+
try {
|
|
201
|
+
switch (params.action) {
|
|
202
|
+
case 'list': {
|
|
203
|
+
const branches = await git.branchLocal();
|
|
204
|
+
const output = branches.all.map(branch => {
|
|
205
|
+
return branch === branches.current ? `* ${branch}` : ` ${branch}`;
|
|
206
|
+
}).join('\n');
|
|
207
|
+
return this.success(`Branches:\n${output}`, { current: branches.current, all: branches.all });
|
|
208
|
+
}
|
|
209
|
+
case 'create': {
|
|
210
|
+
if (!params.name) {
|
|
211
|
+
return this.error('Branch name is required for create action');
|
|
212
|
+
}
|
|
213
|
+
await git.checkoutLocalBranch(params.name);
|
|
214
|
+
return this.success(`Created and switched to branch: ${params.name}`);
|
|
215
|
+
}
|
|
216
|
+
case 'switch': {
|
|
217
|
+
if (!params.name) {
|
|
218
|
+
return this.error('Branch name is required for switch action');
|
|
219
|
+
}
|
|
220
|
+
await git.checkout(params.name);
|
|
221
|
+
return this.success(`Switched to branch: ${params.name}`);
|
|
222
|
+
}
|
|
223
|
+
case 'delete': {
|
|
224
|
+
if (!params.name) {
|
|
225
|
+
return this.error('Branch name is required for delete action');
|
|
226
|
+
}
|
|
227
|
+
await git.deleteLocalBranch(params.name);
|
|
228
|
+
return this.success(`Deleted branch: ${params.name}`);
|
|
229
|
+
}
|
|
230
|
+
default:
|
|
231
|
+
return this.error(`Unknown action: ${params.action}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
return this.error(`Git branch operation failed: ${error.message}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
exports.GitBranchTool = GitBranchTool;
|
|
240
|
+
/**
|
|
241
|
+
* Push to remote
|
|
242
|
+
*/
|
|
243
|
+
class GitPushTool extends base_1.Tool {
|
|
244
|
+
constructor() {
|
|
245
|
+
super(...arguments);
|
|
246
|
+
this.name = 'git_push';
|
|
247
|
+
this.description = 'Push commits to remote repository. Use with caution, especially with force flag.';
|
|
248
|
+
this.requiresConfirmation = true;
|
|
249
|
+
this.destructive = true;
|
|
250
|
+
this.schema = zod_1.z.object({
|
|
251
|
+
branch: zod_1.z.string().optional().describe('Branch to push (default: current branch)'),
|
|
252
|
+
force: zod_1.z.boolean().optional().describe('Force push (use with extreme caution, default: false)'),
|
|
253
|
+
set_upstream: zod_1.z.boolean().optional().describe('Set upstream tracking (default: false)')
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
async execute(params) {
|
|
257
|
+
try {
|
|
258
|
+
// Warn about force push to main/master
|
|
259
|
+
if (params.force && (params.branch === 'main' || params.branch === 'master')) {
|
|
260
|
+
return this.error('Force push to main/master branch is blocked for safety. ' +
|
|
261
|
+
'Please run this manually if absolutely necessary.');
|
|
262
|
+
}
|
|
263
|
+
const options = [];
|
|
264
|
+
if (params.force) {
|
|
265
|
+
options.push('--force');
|
|
266
|
+
}
|
|
267
|
+
if (params.set_upstream) {
|
|
268
|
+
options.push('--set-upstream');
|
|
269
|
+
}
|
|
270
|
+
const remote = 'origin';
|
|
271
|
+
const branch = params.branch || (await git.branchLocal()).current;
|
|
272
|
+
await git.push(remote, branch, options);
|
|
273
|
+
return this.success(`Pushed ${branch} to ${remote}${params.force ? ' (force)' : ''}`, { remote, branch, force: params.force || false });
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
return this.error(`Git push failed: ${error.message}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
exports.GitPushTool = GitPushTool;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BashTool = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const base_1 = require("./base");
|
|
6
|
+
const child_process_1 = require("child_process");
|
|
7
|
+
const util_1 = require("util");
|
|
8
|
+
const execPromise = (0, util_1.promisify)(child_process_1.exec);
|
|
9
|
+
/**
|
|
10
|
+
* Execute bash commands
|
|
11
|
+
*/
|
|
12
|
+
class BashTool extends base_1.Tool {
|
|
13
|
+
constructor() {
|
|
14
|
+
super(...arguments);
|
|
15
|
+
this.name = 'bash';
|
|
16
|
+
this.description = 'Execute bash/shell commands with timeout. Use for running terminal commands, scripts, or system operations.';
|
|
17
|
+
this.requiresConfirmation = true;
|
|
18
|
+
this.destructive = true;
|
|
19
|
+
this.schema = zod_1.z.object({
|
|
20
|
+
command: zod_1.z.string().describe('The shell command to execute'),
|
|
21
|
+
description: zod_1.z.string().optional().describe('Brief description of what this command does'),
|
|
22
|
+
timeout: zod_1.z.number().optional().describe('Timeout in milliseconds (default: 120000ms, max: 600000ms)')
|
|
23
|
+
});
|
|
24
|
+
// Dangerous commands that always require explicit confirmation
|
|
25
|
+
this.dangerousPatterns = [
|
|
26
|
+
/rm\s+-rf\s+\//,
|
|
27
|
+
/mkfs/,
|
|
28
|
+
/dd\s+if=/,
|
|
29
|
+
/>\s*\/dev\/sd/,
|
|
30
|
+
/chmod\s+-R\s+777/,
|
|
31
|
+
/chown\s+-R/,
|
|
32
|
+
/:(){ :|:& };:/ // Fork bomb
|
|
33
|
+
];
|
|
34
|
+
}
|
|
35
|
+
isDangerous(command) {
|
|
36
|
+
return this.dangerousPatterns.some(pattern => pattern.test(command));
|
|
37
|
+
}
|
|
38
|
+
async execute(params) {
|
|
39
|
+
try {
|
|
40
|
+
// Safety check for dangerous commands
|
|
41
|
+
if (this.isDangerous(params.command)) {
|
|
42
|
+
return this.error(`Dangerous command detected and blocked: "${params.command}". ` +
|
|
43
|
+
`This command could cause system damage. Please run it manually if you're certain.`);
|
|
44
|
+
}
|
|
45
|
+
const timeout = params.timeout && params.timeout <= 600000
|
|
46
|
+
? params.timeout
|
|
47
|
+
: 120000; // Default 2 minutes
|
|
48
|
+
// Execute command
|
|
49
|
+
const { stdout, stderr } = await execPromise(params.command, {
|
|
50
|
+
timeout,
|
|
51
|
+
maxBuffer: 1024 * 1024 * 10, // 10MB buffer
|
|
52
|
+
cwd: process.cwd()
|
|
53
|
+
});
|
|
54
|
+
const output = stdout + (stderr ? `\n[stderr]\n${stderr}` : '');
|
|
55
|
+
return this.success(`Command executed: ${params.command}\n\nOutput:\n${output || '(no output)'}`, {
|
|
56
|
+
command: params.command,
|
|
57
|
+
stdout: stdout.trim(),
|
|
58
|
+
stderr: stderr.trim(),
|
|
59
|
+
hasError: stderr.length > 0
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
// Handle timeout
|
|
64
|
+
if (error.killed && error.signal === 'SIGTERM') {
|
|
65
|
+
return this.error(`Command timed out after ${params.timeout || 120000}ms: ${params.command}`);
|
|
66
|
+
}
|
|
67
|
+
// Handle command failure
|
|
68
|
+
const errorOutput = error.stdout || error.stderr || error.message;
|
|
69
|
+
return this.error(`Command failed: ${params.command}\nExit code: ${error.code || 'unknown'}\nOutput:\n${errorOutput}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
exports.BashTool = BashTool;
|