sam-coder-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/README.md +59 -0
- package/ai-assistant-0.0.1.vsix +0 -0
- package/bin/agi-cli.js +815 -0
- package/bin/agi-cli.js.bak +352 -0
- package/bin/agi-cli.js.new +328 -0
- package/bin/config.json +3 -0
- package/bin/ui.js +42 -0
- package/dist/agentUtils.js +539 -0
- package/dist/agentUtils.js.map +1 -0
- package/dist/aiAssistantViewProvider.js +2098 -0
- package/dist/aiAssistantViewProvider.js.map +1 -0
- package/dist/extension.js +117 -0
- package/dist/extension.js.map +1 -0
- package/dist/fetch-polyfill.js +9 -0
- package/dist/fetch-polyfill.js.map +1 -0
- package/foldersnake/snake_game.py +125 -0
- package/media/ai-icon.png +0 -0
- package/media/ai-icon.svg +5 -0
- package/media/infinity-icon.svg +4 -0
- package/out/agentUtils.d.ts +28 -0
- package/out/agentUtils.d.ts.map +1 -0
- package/out/agentUtils.js +539 -0
- package/out/agentUtils.js.map +1 -0
- package/out/aiAssistantViewProvider.d.ts +58 -0
- package/out/aiAssistantViewProvider.d.ts.map +1 -0
- package/out/aiAssistantViewProvider.js +2098 -0
- package/out/aiAssistantViewProvider.js.map +1 -0
- package/out/extension.d.ts +4 -0
- package/out/extension.d.ts.map +1 -0
- package/out/extension.js +117 -0
- package/out/extension.js.map +1 -0
- package/out/fetch-polyfill.d.ts +11 -0
- package/out/fetch-polyfill.d.ts.map +1 -0
- package/out/fetch-polyfill.js +9 -0
- package/out/fetch-polyfill.js.map +1 -0
- package/package.json +31 -0
- package/src/agentUtils.ts +583 -0
- package/src/aiAssistantViewProvider.ts +2264 -0
- package/src/cliAgentUtils.js +73 -0
- package/src/extension.ts +112 -0
- package/src/fetch-polyfill.ts +11 -0
- package/tsconfig.json +24 -0
- package/webpack.config.js +45 -0
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
import * as vscode from 'vscode';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import * as util from 'util';
|
|
5
|
+
import * as cp from 'child_process';
|
|
6
|
+
import * as os from 'os';
|
|
7
|
+
import { fetch } from './fetch-polyfill';
|
|
8
|
+
|
|
9
|
+
// Promisified versions of functions
|
|
10
|
+
const readFile = util.promisify(fs.readFile);
|
|
11
|
+
const writeFile = util.promisify(fs.writeFile);
|
|
12
|
+
const mkdir = util.promisify(fs.mkdir);
|
|
13
|
+
const exec = util.promisify(cp.exec);
|
|
14
|
+
|
|
15
|
+
export interface AgentAction {
|
|
16
|
+
type: 'read' | 'write' | 'search' | 'command' | 'execute' | 'analyze' | 'browse' | 'edit' | 'stop';
|
|
17
|
+
data: any;
|
|
18
|
+
result?: any;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class AgentUtils {
|
|
22
|
+
private _outputChannel: vscode.OutputChannel;
|
|
23
|
+
private _workspaceRoot: string | undefined;
|
|
24
|
+
|
|
25
|
+
constructor() {
|
|
26
|
+
this._outputChannel = vscode.window.createOutputChannel('AI Assistant Agent');
|
|
27
|
+
this._workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Log to output channel
|
|
31
|
+
public log(message: string): void {
|
|
32
|
+
this._outputChannel.appendLine(`[${new Date().toLocaleTimeString()}] ${message}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Show the output channel
|
|
36
|
+
public showOutputChannel(): void {
|
|
37
|
+
this._outputChannel.show();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Read a file with optional path resolution
|
|
41
|
+
public async readFile(filePath: string, maxTokens?: number): Promise<string> {
|
|
42
|
+
try {
|
|
43
|
+
// Check if we need to resolve from workspace root
|
|
44
|
+
const resolvedPath = this._resolveFilePath(filePath);
|
|
45
|
+
let content = await readFile(resolvedPath, 'utf8');
|
|
46
|
+
|
|
47
|
+
// If maxTokens is specified, ensure the content is within limits
|
|
48
|
+
if (maxTokens && maxTokens > 0) {
|
|
49
|
+
// Estimate token count (roughly 4 chars per token)
|
|
50
|
+
const estimatedTokens = Math.ceil(content.length / 4);
|
|
51
|
+
|
|
52
|
+
if (estimatedTokens > maxTokens) {
|
|
53
|
+
this.log(`File content exceeds token limit (est. ${estimatedTokens} tokens). Truncating to ~${maxTokens} tokens.`);
|
|
54
|
+
|
|
55
|
+
// Calculate how much content to keep (roughly)
|
|
56
|
+
const keepChars = maxTokens * 4;
|
|
57
|
+
const halfKeep = Math.floor(keepChars / 2);
|
|
58
|
+
|
|
59
|
+
// Keep beginning and ending portions for context, with a message in the middle
|
|
60
|
+
const firstPart = content.substring(0, halfKeep);
|
|
61
|
+
const lastPart = content.substring(content.length - halfKeep);
|
|
62
|
+
|
|
63
|
+
content = `${firstPart}\n\n[...Content truncated to fit within ${maxTokens} token limit...]\n\n${lastPart}`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return content;
|
|
68
|
+
} catch (error) {
|
|
69
|
+
this.log(`Error reading file: ${error instanceof Error ? error.message : String(error)}`);
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Write to a file with optional path resolution and directory creation
|
|
75
|
+
public async writeFile(filePath: string, content: string): Promise<void> {
|
|
76
|
+
try {
|
|
77
|
+
const resolvedPath = this._resolveFilePath(filePath);
|
|
78
|
+
const dirPath = path.dirname(resolvedPath);
|
|
79
|
+
|
|
80
|
+
// Ensure directory exists
|
|
81
|
+
if (!fs.existsSync(dirPath)) {
|
|
82
|
+
await mkdir(dirPath, { recursive: true });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await writeFile(resolvedPath, content);
|
|
86
|
+
this.log(`File written: ${resolvedPath}`);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
this.log(`Error writing file: ${error instanceof Error ? error.message : String(error)}`);
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Run a command and get output
|
|
94
|
+
public async runCommand(command: string): Promise<string> {
|
|
95
|
+
try {
|
|
96
|
+
this.log(`Running command: ${command}`);
|
|
97
|
+
const { stdout, stderr } = await exec(command, {
|
|
98
|
+
cwd: this._workspaceRoot,
|
|
99
|
+
maxBuffer: 1024 * 1024 * 5 // 5MB
|
|
100
|
+
});
|
|
101
|
+
if (stderr) {
|
|
102
|
+
this.log(`Command stderr: ${stderr}`);
|
|
103
|
+
}
|
|
104
|
+
return stdout;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
this.log(`Error running command: ${error instanceof Error ? error.message : String(error)}`);
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Search files in workspace
|
|
112
|
+
public async searchFiles(pattern: string): Promise<vscode.Uri[]> {
|
|
113
|
+
try {
|
|
114
|
+
this.log(`Searching files with pattern: ${pattern}`);
|
|
115
|
+
return await vscode.workspace.findFiles(pattern);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
this.log(`Error searching files: ${error instanceof Error ? error.message : String(error)}`);
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Search for text in files
|
|
123
|
+
public async searchText(searchText: string): Promise<vscode.Location[]> {
|
|
124
|
+
try {
|
|
125
|
+
this.log(`Searching for text: ${searchText}`);
|
|
126
|
+
// Use the built-in search API in VSCode
|
|
127
|
+
const results = await vscode.commands.executeCommand<vscode.Location[]>(
|
|
128
|
+
'vscode.executeTextSearchCommand',
|
|
129
|
+
{
|
|
130
|
+
pattern: searchText
|
|
131
|
+
}
|
|
132
|
+
);
|
|
133
|
+
return results || [];
|
|
134
|
+
} catch (error) {
|
|
135
|
+
this.log(`Error searching text: ${error instanceof Error ? error.message : String(error)}`);
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Execute a series of agent actions
|
|
141
|
+
public async executeActions(actions: AgentAction[]): Promise<AgentAction[]> {
|
|
142
|
+
const results: AgentAction[] = [];
|
|
143
|
+
|
|
144
|
+
// Get the max tokens from configuration
|
|
145
|
+
const config = vscode.workspace.getConfiguration('aiAssistant');
|
|
146
|
+
const maxTokens = config.get<number>('maxTokens') || 120000;
|
|
147
|
+
|
|
148
|
+
for (const action of actions) {
|
|
149
|
+
try {
|
|
150
|
+
let result: any;
|
|
151
|
+
|
|
152
|
+
switch (action.type) {
|
|
153
|
+
case 'read':
|
|
154
|
+
result = await this.readFile(action.data.path, maxTokens);
|
|
155
|
+
break;
|
|
156
|
+
|
|
157
|
+
case 'write':
|
|
158
|
+
await this.writeFile(action.data.path, action.data.content);
|
|
159
|
+
result = { success: true };
|
|
160
|
+
break;
|
|
161
|
+
|
|
162
|
+
case 'search':
|
|
163
|
+
if (action.data.type === 'files') {
|
|
164
|
+
result = await this.searchFiles(action.data.pattern);
|
|
165
|
+
} else if (action.data.type === 'text') {
|
|
166
|
+
result = await this.searchText(action.data.text);
|
|
167
|
+
}
|
|
168
|
+
break;
|
|
169
|
+
|
|
170
|
+
case 'command':
|
|
171
|
+
result = await this.runCommand(action.data.command);
|
|
172
|
+
break;
|
|
173
|
+
|
|
174
|
+
case 'execute':
|
|
175
|
+
result = await this.executeCode(action.data.language, action.data.code);
|
|
176
|
+
break;
|
|
177
|
+
|
|
178
|
+
case 'analyze':
|
|
179
|
+
// This is for code analysis, we'll pass it to the AI
|
|
180
|
+
result = action.data;
|
|
181
|
+
break;
|
|
182
|
+
|
|
183
|
+
case 'browse':
|
|
184
|
+
result = await this.browseWeb(action.data.query, action.data.numResults);
|
|
185
|
+
break;
|
|
186
|
+
|
|
187
|
+
case 'edit':
|
|
188
|
+
result = await this.editFile(action.data.path, action.data.edits);
|
|
189
|
+
break;
|
|
190
|
+
|
|
191
|
+
case 'stop':
|
|
192
|
+
// Just a signal to stop, no actual execution needed
|
|
193
|
+
result = { stopped: true };
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
results.push({
|
|
198
|
+
...action,
|
|
199
|
+
result
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
} catch (error) {
|
|
203
|
+
results.push({
|
|
204
|
+
...action,
|
|
205
|
+
result: { error: error instanceof Error ? error.message : String(error) }
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return results;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Execute code in different languages
|
|
214
|
+
public async executeCode(language: string, code: string): Promise<string> {
|
|
215
|
+
try {
|
|
216
|
+
this.log(`Executing ${language} code`);
|
|
217
|
+
|
|
218
|
+
// Create a temporary file to execute
|
|
219
|
+
const extension = this._getFileExtension(language);
|
|
220
|
+
if (!extension) {
|
|
221
|
+
throw new Error(`Unsupported language: ${language}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const tempDir = path.join(this._workspaceRoot || os.tmpdir(), '.ai-assistant-temp');
|
|
225
|
+
if (!fs.existsSync(tempDir)) {
|
|
226
|
+
await mkdir(tempDir, { recursive: true });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const tempFile = path.join(tempDir, `code_${Date.now()}${extension}`);
|
|
230
|
+
await writeFile(tempFile, code);
|
|
231
|
+
|
|
232
|
+
// Execute the code based on language
|
|
233
|
+
let command = '';
|
|
234
|
+
|
|
235
|
+
switch (language.toLowerCase()) {
|
|
236
|
+
case 'js':
|
|
237
|
+
case 'javascript':
|
|
238
|
+
command = `node "${tempFile}"`;
|
|
239
|
+
break;
|
|
240
|
+
case 'ts':
|
|
241
|
+
case 'typescript':
|
|
242
|
+
command = `npx ts-node "${tempFile}"`;
|
|
243
|
+
break;
|
|
244
|
+
case 'py':
|
|
245
|
+
case 'python':
|
|
246
|
+
command = `python "${tempFile}"`;
|
|
247
|
+
break;
|
|
248
|
+
case 'bash':
|
|
249
|
+
case 'sh':
|
|
250
|
+
// Make sure the file is executable on Unix systems
|
|
251
|
+
if (process.platform !== 'win32') {
|
|
252
|
+
await util.promisify(fs.chmod)(tempFile, '755');
|
|
253
|
+
}
|
|
254
|
+
command = process.platform === 'win32' ? `bash "${tempFile}"` : `"${tempFile}"`;
|
|
255
|
+
break;
|
|
256
|
+
default:
|
|
257
|
+
throw new Error(`Execution of ${language} is not supported`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Execute and capture output
|
|
261
|
+
const result = await this.runCommand(command);
|
|
262
|
+
|
|
263
|
+
// Clean up the temp file
|
|
264
|
+
try {
|
|
265
|
+
fs.unlinkSync(tempFile);
|
|
266
|
+
} catch (e) {
|
|
267
|
+
// Ignore cleanup errors
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return result;
|
|
271
|
+
} catch (error) {
|
|
272
|
+
this.log(`Error executing code: ${error instanceof Error ? error.message : String(error)}`);
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Browse the web
|
|
278
|
+
public async browseWeb(query: string, numResults: number = 5): Promise<any> {
|
|
279
|
+
try {
|
|
280
|
+
this.log(`Searching the web for: ${query}`);
|
|
281
|
+
// Using DuckDuckGo which doesn't require an API key
|
|
282
|
+
return await this.duckDuckGoSearch(query, numResults);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
this.log(`Error browsing web: ${error instanceof Error ? error.message : String(error)}`);
|
|
285
|
+
// Return an error object
|
|
286
|
+
return {
|
|
287
|
+
query,
|
|
288
|
+
error: `Search failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
289
|
+
results: []
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// DuckDuckGo search (no API key required)
|
|
295
|
+
private async duckDuckGoSearch(query: string, numResults: number = 5): Promise<any> {
|
|
296
|
+
try {
|
|
297
|
+
this.log(`Performing DuckDuckGo search for: ${query}`);
|
|
298
|
+
|
|
299
|
+
// Using DuckDuckGo HTML search which doesn't require API keys
|
|
300
|
+
const searchUrl = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`;
|
|
301
|
+
|
|
302
|
+
const response = await fetch(searchUrl, {
|
|
303
|
+
headers: {
|
|
304
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
if (!response.ok) {
|
|
309
|
+
throw new Error(`Search request failed: ${response.statusText}`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const html = await response.text();
|
|
313
|
+
|
|
314
|
+
// Extract search results from HTML
|
|
315
|
+
const results = [];
|
|
316
|
+
const titleRegex = /<a class="result__a" href="(.*?)".*?>(.*?)<\/a>/g;
|
|
317
|
+
const snippetRegex = /<a class="result__snippet".*?>(.*?)<\/a>/g;
|
|
318
|
+
|
|
319
|
+
let match;
|
|
320
|
+
let index = 0;
|
|
321
|
+
|
|
322
|
+
// Extract titles and URLs
|
|
323
|
+
const titles: { url: string, title: string }[] = [];
|
|
324
|
+
while ((match = titleRegex.exec(html)) !== null && index < numResults) {
|
|
325
|
+
titles.push({
|
|
326
|
+
url: match[1],
|
|
327
|
+
title: this._decodeHtmlEntities(match[2])
|
|
328
|
+
});
|
|
329
|
+
index++;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Extract snippets
|
|
333
|
+
const snippets: string[] = [];
|
|
334
|
+
while ((match = snippetRegex.exec(html)) !== null && snippets.length < numResults) {
|
|
335
|
+
snippets.push(this._decodeHtmlEntities(match[1]));
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Combine results
|
|
339
|
+
for (let i = 0; i < Math.min(titles.length, snippets.length); i++) {
|
|
340
|
+
results.push({
|
|
341
|
+
title: titles[i].title,
|
|
342
|
+
url: titles[i].url,
|
|
343
|
+
snippet: snippets[i]
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
query,
|
|
349
|
+
results: results.slice(0, numResults)
|
|
350
|
+
};
|
|
351
|
+
} catch (error) {
|
|
352
|
+
this.log(`Error in DuckDuckGo search: ${error instanceof Error ? error.message : String(error)}`);
|
|
353
|
+
// Return a basic error result
|
|
354
|
+
return {
|
|
355
|
+
query,
|
|
356
|
+
error: `Search failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
357
|
+
results: []
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Utility function to decode HTML entities in search results
|
|
363
|
+
private _decodeHtmlEntities(html: string): string {
|
|
364
|
+
return html
|
|
365
|
+
.replace(/</g, '<')
|
|
366
|
+
.replace(/>/g, '>')
|
|
367
|
+
.replace(/"/g, '"')
|
|
368
|
+
.replace(/'/g, "'")
|
|
369
|
+
.replace(/&/g, '&')
|
|
370
|
+
.replace(/<[^>]*>/g, ''); // Strip HTML tags
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Get recent terminal output
|
|
374
|
+
public async getTerminalOutput(maxLines: number = 20): Promise<string> {
|
|
375
|
+
try {
|
|
376
|
+
// Check if there are any terminals
|
|
377
|
+
const terminals = vscode.window.terminals;
|
|
378
|
+
if (terminals.length === 0) {
|
|
379
|
+
return "No active terminals";
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Get the active terminal or the most recently created one
|
|
383
|
+
const activeTerminal = vscode.window.activeTerminal || terminals[terminals.length - 1];
|
|
384
|
+
|
|
385
|
+
// Create a new terminal that will execute a command to retrieve the history
|
|
386
|
+
// We can't directly access terminal buffer, so we use a workaround
|
|
387
|
+
let historyCommand = '';
|
|
388
|
+
|
|
389
|
+
if (process.platform === 'win32') {
|
|
390
|
+
// Windows
|
|
391
|
+
historyCommand = 'doskey /history';
|
|
392
|
+
} else {
|
|
393
|
+
// Unix-like (Linux/macOS)
|
|
394
|
+
historyCommand = 'cat ~/.bash_history | tail -n ' + maxLines;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// Execute command and capture output
|
|
398
|
+
try {
|
|
399
|
+
const output = await this.runCommand(historyCommand);
|
|
400
|
+
return `Terminal "${activeTerminal.name}" recent history:\n${output}`;
|
|
401
|
+
} catch (error) {
|
|
402
|
+
// Fall back to just listing available terminals
|
|
403
|
+
const terminalInfo = terminals.map(t => `- ${t.name}`).join('\n');
|
|
404
|
+
return `Available terminals:\n${terminalInfo}\n(Unable to retrieve terminal history)`;
|
|
405
|
+
}
|
|
406
|
+
} catch (error) {
|
|
407
|
+
this.log(`Error getting terminal output: ${error instanceof Error ? error.message : String(error)}`);
|
|
408
|
+
return "Error retrieving terminal information";
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Get file extension for a language
|
|
413
|
+
private _getFileExtension(language: string): string | null {
|
|
414
|
+
const langMap: Record<string, string> = {
|
|
415
|
+
'js': '.js',
|
|
416
|
+
'javascript': '.js',
|
|
417
|
+
'ts': '.ts',
|
|
418
|
+
'typescript': '.ts',
|
|
419
|
+
'py': '.py',
|
|
420
|
+
'python': '.py',
|
|
421
|
+
'bash': '.sh',
|
|
422
|
+
'sh': '.sh',
|
|
423
|
+
'rb': '.rb',
|
|
424
|
+
'ruby': '.rb',
|
|
425
|
+
'ps1': '.ps1',
|
|
426
|
+
'powershell': '.ps1'
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
return langMap[language.toLowerCase()] || null;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Resolve a file path (relative to workspace root if needed)
|
|
433
|
+
private _resolveFilePath(filePath: string): string {
|
|
434
|
+
if (path.isAbsolute(filePath)) {
|
|
435
|
+
return filePath;
|
|
436
|
+
} else if (this._workspaceRoot) {
|
|
437
|
+
return path.join(this._workspaceRoot, filePath);
|
|
438
|
+
}
|
|
439
|
+
// If no workspace root is available, use the current directory
|
|
440
|
+
this.log('Warning: No workspace root available, using current directory');
|
|
441
|
+
return path.join(process.cwd(), filePath);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Edit a file at specific positions
|
|
445
|
+
public async editFile(filePath: string, edits: any): Promise<any> {
|
|
446
|
+
try {
|
|
447
|
+
this.log(`Editing file: ${filePath}`);
|
|
448
|
+
const resolvedPath = this._resolveFilePath(filePath);
|
|
449
|
+
|
|
450
|
+
// Check if file exists, if not create it with empty content
|
|
451
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
452
|
+
this.log(`File does not exist: ${filePath}, creating it`);
|
|
453
|
+
await writeFile(resolvedPath, '');
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const originalContent = await readFile(resolvedPath, 'utf8');
|
|
457
|
+
let newContent = originalContent;
|
|
458
|
+
const editResults: any[] = [];
|
|
459
|
+
|
|
460
|
+
// Apply edits (operations can be of different types)
|
|
461
|
+
if (Array.isArray(edits.operations)) {
|
|
462
|
+
for (const operation of edits.operations) {
|
|
463
|
+
switch (operation.type) {
|
|
464
|
+
case 'replace':
|
|
465
|
+
// Replace content between specific lines or by pattern
|
|
466
|
+
if (operation.startLine && operation.endLine) {
|
|
467
|
+
// Line-based replacement
|
|
468
|
+
const lines = newContent.split('\n');
|
|
469
|
+
const startIdx = Math.max(0, operation.startLine - 1); // Convert to 0-based
|
|
470
|
+
const endIdx = Math.min(lines.length, operation.endLine); // Convert to 0-based
|
|
471
|
+
|
|
472
|
+
const beforeLines = lines.slice(0, startIdx);
|
|
473
|
+
const afterLines = lines.slice(endIdx);
|
|
474
|
+
|
|
475
|
+
// Replace the specified lines with new content
|
|
476
|
+
newContent = [...beforeLines, operation.newText, ...afterLines].join('\n');
|
|
477
|
+
|
|
478
|
+
editResults.push({
|
|
479
|
+
operation: 'replace',
|
|
480
|
+
startLine: operation.startLine,
|
|
481
|
+
endLine: operation.endLine,
|
|
482
|
+
success: true
|
|
483
|
+
});
|
|
484
|
+
} else if (operation.pattern) {
|
|
485
|
+
// Pattern-based replacement
|
|
486
|
+
const regex = new RegExp(operation.pattern, operation.flags || 'g');
|
|
487
|
+
const oldContent = newContent;
|
|
488
|
+
newContent = newContent.replace(regex, operation.replacement || '');
|
|
489
|
+
|
|
490
|
+
editResults.push({
|
|
491
|
+
operation: 'replace',
|
|
492
|
+
pattern: operation.pattern,
|
|
493
|
+
occurrences: (oldContent.match(regex) || []).length,
|
|
494
|
+
success: true
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
break;
|
|
498
|
+
|
|
499
|
+
case 'insert':
|
|
500
|
+
// Insert at specific line
|
|
501
|
+
if (operation.line) {
|
|
502
|
+
const lines = newContent.split('\n');
|
|
503
|
+
const insertIdx = Math.min(lines.length, Math.max(0, operation.line - 1)); // Convert to 0-based
|
|
504
|
+
|
|
505
|
+
lines.splice(insertIdx, 0, operation.text);
|
|
506
|
+
newContent = lines.join('\n');
|
|
507
|
+
|
|
508
|
+
editResults.push({
|
|
509
|
+
operation: 'insert',
|
|
510
|
+
line: operation.line,
|
|
511
|
+
success: true
|
|
512
|
+
});
|
|
513
|
+
} else if (operation.position === 'start') {
|
|
514
|
+
// Insert at start of file
|
|
515
|
+
newContent = operation.text + newContent;
|
|
516
|
+
editResults.push({
|
|
517
|
+
operation: 'insert',
|
|
518
|
+
position: 'start',
|
|
519
|
+
success: true
|
|
520
|
+
});
|
|
521
|
+
} else if (operation.position === 'end') {
|
|
522
|
+
// Insert at end of file
|
|
523
|
+
newContent = newContent + operation.text;
|
|
524
|
+
editResults.push({
|
|
525
|
+
operation: 'insert',
|
|
526
|
+
position: 'end',
|
|
527
|
+
success: true
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
break;
|
|
531
|
+
|
|
532
|
+
case 'delete':
|
|
533
|
+
// Delete specific lines
|
|
534
|
+
if (operation.startLine && operation.endLine) {
|
|
535
|
+
const lines = newContent.split('\n');
|
|
536
|
+
const startIdx = Math.max(0, operation.startLine - 1); // Convert to 0-based
|
|
537
|
+
const endIdx = Math.min(lines.length, operation.endLine); // Convert to 0-based
|
|
538
|
+
|
|
539
|
+
const beforeLines = lines.slice(0, startIdx);
|
|
540
|
+
const afterLines = lines.slice(endIdx);
|
|
541
|
+
|
|
542
|
+
newContent = [...beforeLines, ...afterLines].join('\n');
|
|
543
|
+
|
|
544
|
+
editResults.push({
|
|
545
|
+
operation: 'delete',
|
|
546
|
+
startLine: operation.startLine,
|
|
547
|
+
endLine: operation.endLine,
|
|
548
|
+
success: true
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
break;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
} else if (typeof edits === 'string') {
|
|
555
|
+
// Simple replacement of the entire file
|
|
556
|
+
newContent = edits;
|
|
557
|
+
editResults.push({
|
|
558
|
+
operation: 'replace-all',
|
|
559
|
+
success: true
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Write the updated content back to the file
|
|
564
|
+
await writeFile(resolvedPath, newContent);
|
|
565
|
+
|
|
566
|
+
this.log(`File edited successfully: ${resolvedPath}`);
|
|
567
|
+
|
|
568
|
+
return {
|
|
569
|
+
success: true,
|
|
570
|
+
path: filePath,
|
|
571
|
+
operations: editResults,
|
|
572
|
+
diff: {
|
|
573
|
+
before: originalContent.length,
|
|
574
|
+
after: newContent.length,
|
|
575
|
+
changeSize: newContent.length - originalContent.length
|
|
576
|
+
}
|
|
577
|
+
};
|
|
578
|
+
} catch (error) {
|
|
579
|
+
this.log(`Error editing file: ${error instanceof Error ? error.message : String(error)}`);
|
|
580
|
+
throw error;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|