sam-coder-cli 1.0.0 → 1.0.3

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/src/agentUtils.ts DELETED
@@ -1,583 +0,0 @@
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(/&lt;/g, '<')
366
- .replace(/&gt;/g, '>')
367
- .replace(/&quot;/g, '"')
368
- .replace(/&#39;/g, "'")
369
- .replace(/&amp;/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
- }