promptarchitect 0.6.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.
@@ -0,0 +1,630 @@
1
+ /**
2
+ * Agent Service
3
+ *
4
+ * Powers the AI agent capabilities within VS Code.
5
+ * Handles tool composition, workspace context, and autonomous actions.
6
+ */
7
+
8
+ import * as vscode from 'vscode';
9
+ import * as path from 'path';
10
+ import * as fs from 'fs/promises';
11
+ import { PromptArchitectAPI } from './api';
12
+ import { WorkspaceIndexer } from './workspaceIndexer';
13
+
14
+ // ============================================================================
15
+ // TYPES
16
+ // ============================================================================
17
+
18
+ export interface AgentAction {
19
+ type: 'read_file' | 'write_file' | 'execute_command' | 'search_files' |
20
+ 'insert_code' | 'open_file' | 'refine_prompt' | 'generate_prompt' |
21
+ 'run_test' | 'install_package' | 'git_status';
22
+ params: Record<string, any>;
23
+ description: string;
24
+ }
25
+
26
+ export interface AgentResponse {
27
+ message: string;
28
+ actions?: AgentAction[];
29
+ suggestions?: string[];
30
+ codeBlocks?: CodeBlock[];
31
+ workspaceAware?: boolean;
32
+ }
33
+
34
+ export interface CodeBlock {
35
+ language: string;
36
+ code: string;
37
+ filename?: string;
38
+ action?: 'insert' | 'replace' | 'create';
39
+ }
40
+
41
+ export interface WorkspaceContext {
42
+ name: string;
43
+ techStack: string[];
44
+ dependencies: Record<string, string>;
45
+ structure: string[];
46
+ currentFile?: {
47
+ path: string;
48
+ language: string;
49
+ content?: string;
50
+ selection?: string;
51
+ };
52
+ recentFiles: string[];
53
+ gitStatus?: string;
54
+ }
55
+
56
+ export interface AgentConfig {
57
+ autoInjectContext: boolean;
58
+ proactiveRefine: boolean;
59
+ suggestActions: boolean;
60
+ streamResponses: boolean;
61
+ }
62
+
63
+ // ============================================================================
64
+ // AGENT SERVICE
65
+ // ============================================================================
66
+
67
+ export class AgentService {
68
+ private context: vscode.ExtensionContext;
69
+ private api: PromptArchitectAPI;
70
+ private workspaceIndexer: WorkspaceIndexer;
71
+ private config: AgentConfig;
72
+ private recentFiles: string[] = [];
73
+ private static instance: AgentService;
74
+
75
+ private constructor(
76
+ context: vscode.ExtensionContext,
77
+ api: PromptArchitectAPI,
78
+ workspaceIndexer: WorkspaceIndexer
79
+ ) {
80
+ this.context = context;
81
+ this.api = api;
82
+ this.workspaceIndexer = workspaceIndexer;
83
+ this.config = this.loadConfig();
84
+
85
+ // Track file opens
86
+ this.setupFileTracking();
87
+ }
88
+
89
+ static getInstance(
90
+ context: vscode.ExtensionContext,
91
+ api: PromptArchitectAPI,
92
+ workspaceIndexer: WorkspaceIndexer
93
+ ): AgentService {
94
+ if (!AgentService.instance) {
95
+ AgentService.instance = new AgentService(context, api, workspaceIndexer);
96
+ }
97
+ return AgentService.instance;
98
+ }
99
+
100
+ private loadConfig(): AgentConfig {
101
+ const vsConfig = vscode.workspace.getConfiguration('promptarchitect');
102
+ return {
103
+ autoInjectContext: vsConfig.get<boolean>('autoInjectContext', true),
104
+ proactiveRefine: vsConfig.get<boolean>('proactiveRefine', false),
105
+ suggestActions: vsConfig.get<boolean>('suggestActions', true),
106
+ streamResponses: vsConfig.get<boolean>('streamResponses', true),
107
+ };
108
+ }
109
+
110
+ private setupFileTracking() {
111
+ vscode.window.onDidChangeActiveTextEditor((editor) => {
112
+ if (editor) {
113
+ const filePath = editor.document.uri.fsPath;
114
+ this.recentFiles = [
115
+ filePath,
116
+ ...this.recentFiles.filter(f => f !== filePath).slice(0, 9)
117
+ ];
118
+ }
119
+ });
120
+ }
121
+
122
+ // ============================================================================
123
+ // WORKSPACE CONTEXT
124
+ // ============================================================================
125
+
126
+ /**
127
+ * Get comprehensive workspace context for agent operations
128
+ */
129
+ async getWorkspaceContext(): Promise<WorkspaceContext> {
130
+ const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
131
+ const workspaceName = workspaceFolder?.name || 'Unknown';
132
+ const workspacePath = workspaceFolder?.uri.fsPath || '';
133
+
134
+ // Get indexed data
135
+ const index = this.workspaceIndexer.getIndex();
136
+
137
+ // Get current file context
138
+ const editor = vscode.window.activeTextEditor;
139
+ let currentFile: WorkspaceContext['currentFile'];
140
+
141
+ if (editor) {
142
+ currentFile = {
143
+ path: path.relative(workspacePath, editor.document.uri.fsPath),
144
+ language: editor.document.languageId,
145
+ };
146
+
147
+ // Include selection if any
148
+ if (!editor.selection.isEmpty) {
149
+ currentFile.selection = editor.document.getText(editor.selection);
150
+ }
151
+ }
152
+
153
+ // Get git status
154
+ let gitStatus: string | undefined;
155
+ try {
156
+ const { exec } = await import('child_process');
157
+ const { promisify } = await import('util');
158
+ const execAsync = promisify(exec);
159
+ const { stdout } = await execAsync('git status --porcelain', { cwd: workspacePath });
160
+ if (stdout.trim()) {
161
+ gitStatus = stdout.trim();
162
+ }
163
+ } catch {
164
+ // Git not available or not a git repo
165
+ }
166
+
167
+ // Get dependencies from package.json
168
+ let dependencies: Record<string, string> = {};
169
+ let devDependencies: Record<string, string> = {};
170
+ let techStack: string[] = [];
171
+
172
+ try {
173
+ const pkgPath = path.join(workspacePath, 'package.json');
174
+ const pkgContent = await fs.readFile(pkgPath, 'utf-8');
175
+ const pkg = JSON.parse(pkgContent);
176
+ dependencies = pkg.dependencies || {};
177
+ devDependencies = pkg.devDependencies || {};
178
+
179
+ // Detect tech stack
180
+ const allDeps = { ...dependencies, ...devDependencies };
181
+ if (allDeps['react']) techStack.push('React');
182
+ if (allDeps['next']) techStack.push('Next.js');
183
+ if (allDeps['vue']) techStack.push('Vue.js');
184
+ if (allDeps['express']) techStack.push('Express.js');
185
+ if (allDeps['typescript']) techStack.push('TypeScript');
186
+ if (allDeps['tailwindcss']) techStack.push('Tailwind CSS');
187
+ if (allDeps['firebase']) techStack.push('Firebase');
188
+ if (allDeps['@google/generative-ai']) techStack.push('Google Gemini');
189
+ if (allDeps['vite']) techStack.push('Vite');
190
+ } catch {
191
+ // No package.json
192
+ }
193
+
194
+ // Get structure from index or scan
195
+ const structure = index?.structure.map(s => s.path) || [];
196
+
197
+ return {
198
+ name: workspaceName,
199
+ techStack,
200
+ dependencies: { ...dependencies, ...devDependencies },
201
+ structure: structure.slice(0, 50),
202
+ currentFile,
203
+ recentFiles: this.recentFiles.map(f => path.relative(workspacePath, f)).slice(0, 5),
204
+ gitStatus,
205
+ };
206
+ }
207
+
208
+ /**
209
+ * Format workspace context for inclusion in prompts
210
+ */
211
+ formatContextForPrompt(context: WorkspaceContext): string {
212
+ const parts: string[] = [];
213
+
214
+ parts.push(`## Project: ${context.name}`);
215
+
216
+ if (context.techStack.length > 0) {
217
+ parts.push(`**Tech Stack:** ${context.techStack.join(', ')}`);
218
+ }
219
+
220
+ if (context.currentFile) {
221
+ parts.push(`\n**Current File:** ${context.currentFile.path} (${context.currentFile.language})`);
222
+ if (context.currentFile.selection) {
223
+ parts.push(`\n**Selected Code:**\n\`\`\`${context.currentFile.language}\n${context.currentFile.selection}\n\`\`\``);
224
+ }
225
+ }
226
+
227
+ if (context.recentFiles.length > 0) {
228
+ parts.push(`\n**Recent Files:** ${context.recentFiles.join(', ')}`);
229
+ }
230
+
231
+ if (context.gitStatus) {
232
+ parts.push(`\n**Git Status:** ${context.gitStatus.split('\n').length} file(s) modified`);
233
+ }
234
+
235
+ return parts.join('\n');
236
+ }
237
+
238
+ // ============================================================================
239
+ // AGENT ACTIONS
240
+ // ============================================================================
241
+
242
+ /**
243
+ * Execute an agent action
244
+ */
245
+ async executeAction(action: AgentAction): Promise<string> {
246
+ switch (action.type) {
247
+ case 'read_file':
248
+ return this.readFile(action.params.path);
249
+
250
+ case 'write_file':
251
+ return this.writeFile(action.params.path, action.params.content);
252
+
253
+ case 'execute_command':
254
+ return this.executeCommand(action.params.command);
255
+
256
+ case 'search_files':
257
+ return this.searchFiles(action.params.query, action.params.filePattern);
258
+
259
+ case 'insert_code':
260
+ return this.insertCode(action.params.code, action.params.position);
261
+
262
+ case 'open_file':
263
+ return this.openFile(action.params.path, action.params.line);
264
+
265
+ case 'run_test':
266
+ return this.runTests(action.params.file);
267
+
268
+ case 'install_package':
269
+ return this.installPackage(action.params.packages, action.params.dev);
270
+
271
+ case 'git_status':
272
+ return this.getGitStatus();
273
+
274
+ default:
275
+ return `Unknown action: ${action.type}`;
276
+ }
277
+ }
278
+
279
+ private async readFile(filePath: string): Promise<string> {
280
+ const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || '';
281
+ const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(workspacePath, filePath);
282
+
283
+ try {
284
+ const content = await fs.readFile(absolutePath, 'utf-8');
285
+ const lines = content.split('\n');
286
+ if (lines.length > 100) {
287
+ return `File: ${filePath} (${lines.length} lines, showing first 100)\n\n${lines.slice(0, 100).join('\n')}\n\n... (truncated)`;
288
+ }
289
+ return `File: ${filePath}\n\n${content}`;
290
+ } catch (error) {
291
+ return `Error reading file: ${error}`;
292
+ }
293
+ }
294
+
295
+ private async writeFile(filePath: string, content: string): Promise<string> {
296
+ const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || '';
297
+ const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(workspacePath, filePath);
298
+
299
+ try {
300
+ await fs.mkdir(path.dirname(absolutePath), { recursive: true });
301
+ await fs.writeFile(absolutePath, content, 'utf-8');
302
+
303
+ // Open the file
304
+ const doc = await vscode.workspace.openTextDocument(absolutePath);
305
+ await vscode.window.showTextDocument(doc);
306
+
307
+ return `File written: ${filePath}`;
308
+ } catch (error) {
309
+ return `Error writing file: ${error}`;
310
+ }
311
+ }
312
+
313
+ private async executeCommand(command: string): Promise<string> {
314
+ const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || '';
315
+
316
+ try {
317
+ const { exec } = await import('child_process');
318
+ const { promisify } = await import('util');
319
+ const execAsync = promisify(exec);
320
+
321
+ const { stdout, stderr } = await execAsync(command, {
322
+ cwd: workspacePath,
323
+ timeout: 30000,
324
+ });
325
+
326
+ return `Command: ${command}\n\nOutput:\n${stdout}${stderr ? `\nErrors:\n${stderr}` : ''}`;
327
+ } catch (error: any) {
328
+ return `Command failed: ${command}\n\nError: ${error.message}\n${error.stderr || ''}`;
329
+ }
330
+ }
331
+
332
+ private async searchFiles(query: string, filePattern?: string): Promise<string> {
333
+ const files = await vscode.workspace.findFiles(
334
+ filePattern || '**/*',
335
+ '**/node_modules/**'
336
+ );
337
+
338
+ const matches: string[] = [];
339
+ const regex = new RegExp(query, 'gi');
340
+
341
+ for (const file of files.slice(0, 50)) {
342
+ try {
343
+ const doc = await vscode.workspace.openTextDocument(file);
344
+ const content = doc.getText();
345
+ const lines = content.split('\n');
346
+
347
+ lines.forEach((line, index) => {
348
+ if (regex.test(line)) {
349
+ const relativePath = vscode.workspace.asRelativePath(file);
350
+ matches.push(`${relativePath}:${index + 1}: ${line.trim()}`);
351
+ }
352
+ });
353
+ } catch {
354
+ // Skip files that can't be read
355
+ }
356
+
357
+ if (matches.length >= 20) break;
358
+ }
359
+
360
+ return matches.length > 0
361
+ ? `Found ${matches.length} matches:\n\n${matches.join('\n')}`
362
+ : 'No matches found';
363
+ }
364
+
365
+ private async insertCode(code: string, position?: 'cursor' | 'end' | 'start'): Promise<string> {
366
+ const editor = vscode.window.activeTextEditor;
367
+ if (!editor) {
368
+ return 'No active editor';
369
+ }
370
+
371
+ await editor.edit((editBuilder) => {
372
+ if (position === 'start') {
373
+ editBuilder.insert(new vscode.Position(0, 0), code + '\n');
374
+ } else if (position === 'end') {
375
+ const lastLine = editor.document.lineCount - 1;
376
+ const lastChar = editor.document.lineAt(lastLine).text.length;
377
+ editBuilder.insert(new vscode.Position(lastLine, lastChar), '\n' + code);
378
+ } else {
379
+ // Default: at cursor or replace selection
380
+ if (editor.selection.isEmpty) {
381
+ editBuilder.insert(editor.selection.active, code);
382
+ } else {
383
+ editBuilder.replace(editor.selection, code);
384
+ }
385
+ }
386
+ });
387
+
388
+ return 'Code inserted successfully';
389
+ }
390
+
391
+ private async openFile(filePath: string, line?: number): Promise<string> {
392
+ const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || '';
393
+ const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(workspacePath, filePath);
394
+
395
+ try {
396
+ const doc = await vscode.workspace.openTextDocument(absolutePath);
397
+ const editor = await vscode.window.showTextDocument(doc);
398
+
399
+ if (line !== undefined) {
400
+ const position = new vscode.Position(line - 1, 0);
401
+ editor.selection = new vscode.Selection(position, position);
402
+ editor.revealRange(new vscode.Range(position, position), vscode.TextEditorRevealType.InCenter);
403
+ }
404
+
405
+ return `Opened: ${filePath}`;
406
+ } catch (error) {
407
+ return `Error opening file: ${error}`;
408
+ }
409
+ }
410
+
411
+ private async runTests(file?: string): Promise<string> {
412
+ const command = file ? `npm test -- ${file}` : 'npm test';
413
+ return this.executeCommand(command);
414
+ }
415
+
416
+ private async installPackage(packages: string[], dev: boolean = false): Promise<string> {
417
+ const flag = dev ? '--save-dev' : '';
418
+ const command = `npm install ${flag} ${packages.join(' ')}`;
419
+ return this.executeCommand(command);
420
+ }
421
+
422
+ private async getGitStatus(): Promise<string> {
423
+ return this.executeCommand('git status');
424
+ }
425
+
426
+ // ============================================================================
427
+ // INTELLIGENT SUGGESTIONS
428
+ // ============================================================================
429
+
430
+ /**
431
+ * Get smart suggestions based on current context
432
+ */
433
+ async getSuggestions(): Promise<string[]> {
434
+ const suggestions: string[] = [];
435
+ const context = await this.getWorkspaceContext();
436
+
437
+ // Suggest based on current file
438
+ if (context.currentFile) {
439
+ const ext = path.extname(context.currentFile.path);
440
+
441
+ if (['.ts', '.tsx', '.js', '.jsx'].includes(ext)) {
442
+ suggestions.push('Explain this code');
443
+ suggestions.push('Add TypeScript types');
444
+ suggestions.push('Refactor for better readability');
445
+ suggestions.push('Add error handling');
446
+ suggestions.push('Write unit tests for this');
447
+ }
448
+
449
+ if (context.currentFile.selection) {
450
+ suggestions.push('Explain selected code');
451
+ suggestions.push('Refactor selected code');
452
+ suggestions.push('Generate documentation');
453
+ }
454
+ }
455
+
456
+ // Suggest based on git status
457
+ if (context.gitStatus) {
458
+ suggestions.push('Generate commit message');
459
+ suggestions.push('Review my changes');
460
+ }
461
+
462
+ // Suggest based on tech stack
463
+ if (context.techStack.includes('React')) {
464
+ suggestions.push('Create a new React component');
465
+ suggestions.push('Add React hooks');
466
+ }
467
+
468
+ if (context.techStack.includes('TypeScript')) {
469
+ suggestions.push('Fix TypeScript errors');
470
+ suggestions.push('Improve type definitions');
471
+ }
472
+
473
+ return suggestions.slice(0, 5);
474
+ }
475
+
476
+ // ============================================================================
477
+ // ENHANCED CHAT PROCESSING
478
+ // ============================================================================
479
+
480
+ /**
481
+ * Process a chat message with full agent capabilities
482
+ */
483
+ async processAgentMessage(
484
+ message: string,
485
+ history: Array<{ role: string; content: string }>
486
+ ): Promise<AgentResponse> {
487
+ // Get workspace context if enabled
488
+ let contextString = '';
489
+ if (this.config.autoInjectContext) {
490
+ const context = await this.getWorkspaceContext();
491
+ contextString = this.formatContextForPrompt(context);
492
+ }
493
+
494
+ // Build enhanced message with context
495
+ const enhancedMessage = contextString
496
+ ? `${contextString}\n\n---\n\n**User Request:**\n${message}`
497
+ : message;
498
+
499
+ try {
500
+ // Call the API with enhanced message
501
+ const response = await this.api.chat({
502
+ message: enhancedMessage,
503
+ history,
504
+ context: contextString,
505
+ });
506
+
507
+ // Parse response for actions and code blocks
508
+ const codeBlocks = this.extractCodeBlocks(response.reply);
509
+ const actions = this.detectActions(response.reply, codeBlocks);
510
+ const suggestions = await this.getSuggestions();
511
+
512
+ return {
513
+ message: response.reply,
514
+ actions,
515
+ suggestions: this.config.suggestActions ? suggestions : undefined,
516
+ codeBlocks,
517
+ workspaceAware: !!contextString,
518
+ };
519
+ } catch (error) {
520
+ return {
521
+ message: `Error: ${error}`,
522
+ workspaceAware: false,
523
+ };
524
+ }
525
+ }
526
+
527
+ /**
528
+ * Extract code blocks from response
529
+ */
530
+ private extractCodeBlocks(text: string): CodeBlock[] {
531
+ const blocks: CodeBlock[] = [];
532
+ const regex = /```(\w*)\n([\s\S]*?)```/g;
533
+ let match;
534
+
535
+ while ((match = regex.exec(text)) !== null) {
536
+ blocks.push({
537
+ language: match[1] || 'text',
538
+ code: match[2].trim(),
539
+ });
540
+ }
541
+
542
+ return blocks;
543
+ }
544
+
545
+ /**
546
+ * Detect suggested actions from response
547
+ */
548
+ private detectActions(text: string, codeBlocks: CodeBlock[]): AgentAction[] {
549
+ const actions: AgentAction[] = [];
550
+
551
+ // If there are code blocks, suggest insert action
552
+ if (codeBlocks.length > 0) {
553
+ actions.push({
554
+ type: 'insert_code',
555
+ params: { code: codeBlocks[0].code },
556
+ description: 'Insert code at cursor',
557
+ });
558
+ }
559
+
560
+ // Detect file references
561
+ const fileMatch = text.match(/(?:file|create|write)\s+[`"]?([^\s`"]+\.[a-z]+)[`"]?/i);
562
+ if (fileMatch) {
563
+ actions.push({
564
+ type: 'open_file',
565
+ params: { path: fileMatch[1] },
566
+ description: `Open ${fileMatch[1]}`,
567
+ });
568
+ }
569
+
570
+ // Detect command suggestions
571
+ const commandMatch = text.match(/(?:run|execute)\s+[`"]([^`"]+)[`"]/i);
572
+ if (commandMatch) {
573
+ actions.push({
574
+ type: 'execute_command',
575
+ params: { command: commandMatch[1] },
576
+ description: `Run: ${commandMatch[1]}`,
577
+ });
578
+ }
579
+
580
+ // Detect npm install suggestions
581
+ const npmMatch = text.match(/npm install\s+([^\s`]+)/);
582
+ if (npmMatch) {
583
+ actions.push({
584
+ type: 'install_package',
585
+ params: { packages: [npmMatch[1]] },
586
+ description: `Install ${npmMatch[1]}`,
587
+ });
588
+ }
589
+
590
+ return actions;
591
+ }
592
+
593
+ // ============================================================================
594
+ // PROACTIVE FEATURES
595
+ // ============================================================================
596
+
597
+ /**
598
+ * Get proactive refinement for selected text
599
+ */
600
+ async getProactiveRefinement(): Promise<string | null> {
601
+ if (!this.config.proactiveRefine) return null;
602
+
603
+ const editor = vscode.window.activeTextEditor;
604
+ if (!editor || editor.selection.isEmpty) return null;
605
+
606
+ const selectedText = editor.document.getText(editor.selection);
607
+
608
+ // Check if it looks like a prompt (has some length and structure)
609
+ if (selectedText.length < 10 || selectedText.length > 2000) return null;
610
+
611
+ try {
612
+ const result = await this.api.analyzePrompt({ prompt: selectedText });
613
+
614
+ // Only suggest refinement if score is below threshold
615
+ if (result.overallScore < 70 && result.suggestions.length > 0) {
616
+ return result.suggestions[0];
617
+ }
618
+ } catch {
619
+ // Ignore analysis errors
620
+ }
621
+
622
+ return null;
623
+ }
624
+
625
+ dispose() {
626
+ // Cleanup if needed
627
+ }
628
+ }
629
+
630
+ export default AgentService;