@ddse/acm-aicoder 0.5.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.
Files changed (165) hide show
  1. package/.aicoder/index.json +304 -0
  2. package/AICODER_IMPLEMENTATION_PLAN_PHASE2.md +284 -0
  3. package/LICENSE +21 -0
  4. package/README.md +490 -0
  5. package/bin/interactive.tsx +232 -0
  6. package/dist/bin/interactive.d.ts +3 -0
  7. package/dist/bin/interactive.d.ts.map +1 -0
  8. package/dist/bin/interactive.js +155 -0
  9. package/dist/bin/interactive.js.map +1 -0
  10. package/dist/src/config/providers.d.ts +15 -0
  11. package/dist/src/config/providers.d.ts.map +1 -0
  12. package/dist/src/config/providers.js +142 -0
  13. package/dist/src/config/providers.js.map +1 -0
  14. package/dist/src/config/session.d.ts +25 -0
  15. package/dist/src/config/session.d.ts.map +1 -0
  16. package/dist/src/config/session.js +97 -0
  17. package/dist/src/config/session.js.map +1 -0
  18. package/dist/src/context/bm25.d.ts +68 -0
  19. package/dist/src/context/bm25.d.ts.map +1 -0
  20. package/dist/src/context/bm25.js +131 -0
  21. package/dist/src/context/bm25.js.map +1 -0
  22. package/dist/src/context/code-search.d.ts +30 -0
  23. package/dist/src/context/code-search.d.ts.map +1 -0
  24. package/dist/src/context/code-search.js +150 -0
  25. package/dist/src/context/code-search.js.map +1 -0
  26. package/dist/src/context/context-pack.d.ts +25 -0
  27. package/dist/src/context/context-pack.d.ts.map +1 -0
  28. package/dist/src/context/context-pack.js +92 -0
  29. package/dist/src/context/context-pack.js.map +1 -0
  30. package/dist/src/context/dependency-mapper.d.ts +10 -0
  31. package/dist/src/context/dependency-mapper.d.ts.map +1 -0
  32. package/dist/src/context/dependency-mapper.js +62 -0
  33. package/dist/src/context/dependency-mapper.js.map +1 -0
  34. package/dist/src/context/index.d.ts +8 -0
  35. package/dist/src/context/index.d.ts.map +1 -0
  36. package/dist/src/context/index.js +9 -0
  37. package/dist/src/context/index.js.map +1 -0
  38. package/dist/src/context/symbol-extractor.d.ts +26 -0
  39. package/dist/src/context/symbol-extractor.d.ts.map +1 -0
  40. package/dist/src/context/symbol-extractor.js +129 -0
  41. package/dist/src/context/symbol-extractor.js.map +1 -0
  42. package/dist/src/context/test-mapper.d.ts +16 -0
  43. package/dist/src/context/test-mapper.d.ts.map +1 -0
  44. package/dist/src/context/test-mapper.js +66 -0
  45. package/dist/src/context/test-mapper.js.map +1 -0
  46. package/dist/src/context/types.d.ts +61 -0
  47. package/dist/src/context/types.d.ts.map +1 -0
  48. package/dist/src/context/types.js +3 -0
  49. package/dist/src/context/types.js.map +1 -0
  50. package/dist/src/context/workspace-indexer.d.ts +39 -0
  51. package/dist/src/context/workspace-indexer.d.ts.map +1 -0
  52. package/dist/src/context/workspace-indexer.js +222 -0
  53. package/dist/src/context/workspace-indexer.js.map +1 -0
  54. package/dist/src/index.d.ts +5 -0
  55. package/dist/src/index.d.ts.map +1 -0
  56. package/dist/src/index.js +6 -0
  57. package/dist/src/index.js.map +1 -0
  58. package/dist/src/registries.d.ts +34 -0
  59. package/dist/src/registries.d.ts.map +1 -0
  60. package/dist/src/registries.js +87 -0
  61. package/dist/src/registries.js.map +1 -0
  62. package/dist/src/runtime/budget-manager.d.ts +42 -0
  63. package/dist/src/runtime/budget-manager.d.ts.map +1 -0
  64. package/dist/src/runtime/budget-manager.js +82 -0
  65. package/dist/src/runtime/budget-manager.js.map +1 -0
  66. package/dist/src/runtime/interactive-runtime.d.ts +39 -0
  67. package/dist/src/runtime/interactive-runtime.d.ts.map +1 -0
  68. package/dist/src/runtime/interactive-runtime.js +321 -0
  69. package/dist/src/runtime/interactive-runtime.js.map +1 -0
  70. package/dist/src/tasks-v2/analysis-tasks.d.ts +117 -0
  71. package/dist/src/tasks-v2/analysis-tasks.d.ts.map +1 -0
  72. package/dist/src/tasks-v2/analysis-tasks.js +209 -0
  73. package/dist/src/tasks-v2/analysis-tasks.js.map +1 -0
  74. package/dist/src/tasks-v2/developer-tasks.d.ts +226 -0
  75. package/dist/src/tasks-v2/developer-tasks.d.ts.map +1 -0
  76. package/dist/src/tasks-v2/developer-tasks.js +322 -0
  77. package/dist/src/tasks-v2/developer-tasks.js.map +1 -0
  78. package/dist/src/tasks-v2/index.d.ts +3 -0
  79. package/dist/src/tasks-v2/index.d.ts.map +1 -0
  80. package/dist/src/tasks-v2/index.js +4 -0
  81. package/dist/src/tasks-v2/index.js.map +1 -0
  82. package/dist/src/tools-v2/edit-tools.d.ts +67 -0
  83. package/dist/src/tools-v2/edit-tools.d.ts.map +1 -0
  84. package/dist/src/tools-v2/edit-tools.js +117 -0
  85. package/dist/src/tools-v2/edit-tools.js.map +1 -0
  86. package/dist/src/tools-v2/index.d.ts +6 -0
  87. package/dist/src/tools-v2/index.d.ts.map +1 -0
  88. package/dist/src/tools-v2/index.js +7 -0
  89. package/dist/src/tools-v2/index.js.map +1 -0
  90. package/dist/src/tools-v2/read-tools.d.ts +129 -0
  91. package/dist/src/tools-v2/read-tools.d.ts.map +1 -0
  92. package/dist/src/tools-v2/read-tools.js +216 -0
  93. package/dist/src/tools-v2/read-tools.js.map +1 -0
  94. package/dist/src/tools-v2/search-tools.d.ts +73 -0
  95. package/dist/src/tools-v2/search-tools.d.ts.map +1 -0
  96. package/dist/src/tools-v2/search-tools.js +132 -0
  97. package/dist/src/tools-v2/search-tools.js.map +1 -0
  98. package/dist/src/tools-v2/test-tools.d.ts +59 -0
  99. package/dist/src/tools-v2/test-tools.d.ts.map +1 -0
  100. package/dist/src/tools-v2/test-tools.js +111 -0
  101. package/dist/src/tools-v2/test-tools.js.map +1 -0
  102. package/dist/src/tools-v2/workspace-context.d.ts +65 -0
  103. package/dist/src/tools-v2/workspace-context.d.ts.map +1 -0
  104. package/dist/src/tools-v2/workspace-context.js +336 -0
  105. package/dist/src/tools-v2/workspace-context.js.map +1 -0
  106. package/dist/src/ui/App.d.ts +9 -0
  107. package/dist/src/ui/App.d.ts.map +1 -0
  108. package/dist/src/ui/App.js +257 -0
  109. package/dist/src/ui/App.js.map +1 -0
  110. package/dist/src/ui/components/ChatPane.d.ts +12 -0
  111. package/dist/src/ui/components/ChatPane.d.ts.map +1 -0
  112. package/dist/src/ui/components/ChatPane.js +41 -0
  113. package/dist/src/ui/components/ChatPane.js.map +1 -0
  114. package/dist/src/ui/components/EventsPane.d.ts +12 -0
  115. package/dist/src/ui/components/EventsPane.d.ts.map +1 -0
  116. package/dist/src/ui/components/EventsPane.js +48 -0
  117. package/dist/src/ui/components/EventsPane.js.map +1 -0
  118. package/dist/src/ui/components/GoalsTasksPane.d.ts +18 -0
  119. package/dist/src/ui/components/GoalsTasksPane.d.ts.map +1 -0
  120. package/dist/src/ui/components/GoalsTasksPane.js +83 -0
  121. package/dist/src/ui/components/GoalsTasksPane.js.map +1 -0
  122. package/dist/src/ui/store.d.ts +74 -0
  123. package/dist/src/ui/store.d.ts.map +1 -0
  124. package/dist/src/ui/store.js +260 -0
  125. package/dist/src/ui/store.js.map +1 -0
  126. package/dist/tests/integration.test.d.ts +2 -0
  127. package/dist/tests/integration.test.d.ts.map +1 -0
  128. package/dist/tests/integration.test.js +415 -0
  129. package/dist/tests/integration.test.js.map +1 -0
  130. package/dist/tsconfig.tsbuildinfo +1 -0
  131. package/docs/AICODER.png +0 -0
  132. package/docs/INTERACTIVE_CLI_GUIDE.md +201 -0
  133. package/docs/TUI_MOCKUP.md +180 -0
  134. package/package.json +52 -0
  135. package/src/config/providers.ts +174 -0
  136. package/src/config/session.ts +143 -0
  137. package/src/context/bm25.ts +173 -0
  138. package/src/context/code-search.ts +188 -0
  139. package/src/context/context-pack.ts +133 -0
  140. package/src/context/dependency-mapper.ts +72 -0
  141. package/src/context/index.ts +8 -0
  142. package/src/context/symbol-extractor.ts +149 -0
  143. package/src/context/test-mapper.ts +77 -0
  144. package/src/context/types.ts +69 -0
  145. package/src/context/workspace-indexer.ts +249 -0
  146. package/src/index.ts +5 -0
  147. package/src/registries.ts +118 -0
  148. package/src/runtime/budget-manager.ts +118 -0
  149. package/src/runtime/interactive-runtime.ts +423 -0
  150. package/src/tasks-v2/analysis-tasks.ts +311 -0
  151. package/src/tasks-v2/developer-tasks.ts +437 -0
  152. package/src/tasks-v2/index.ts +3 -0
  153. package/src/tools-v2/edit-tools.ts +153 -0
  154. package/src/tools-v2/index.ts +6 -0
  155. package/src/tools-v2/read-tools.ts +286 -0
  156. package/src/tools-v2/search-tools.ts +175 -0
  157. package/src/tools-v2/test-tools.ts +147 -0
  158. package/src/tools-v2/workspace-context.ts +428 -0
  159. package/src/ui/App.tsx +392 -0
  160. package/src/ui/components/ChatPane.tsx +84 -0
  161. package/src/ui/components/EventsPane.tsx +81 -0
  162. package/src/ui/components/GoalsTasksPane.tsx +149 -0
  163. package/src/ui/store.ts +362 -0
  164. package/tests/integration.test.ts +537 -0
  165. package/tsconfig.json +22 -0
@@ -0,0 +1,286 @@
1
+ // Enhanced Code Tools - Reading, Searching, and Comparison
2
+ import { Tool } from '@ddse/acm-sdk';
3
+ import * as fs from 'fs/promises';
4
+ import * as path from 'path';
5
+
6
+ /**
7
+ * FileStatTool - Check file existence, size, type
8
+ */
9
+ export class FileStatTool extends Tool<
10
+ { path: string },
11
+ { exists: boolean; size: number; mtime: number; isBinary: boolean }
12
+ > {
13
+ private rootPath: string;
14
+
15
+ constructor(rootPath: string = process.cwd()) {
16
+ super();
17
+ this.rootPath = path.resolve(rootPath);
18
+ }
19
+
20
+ name(): string {
21
+ return 'file_stat';
22
+ }
23
+
24
+ async call(input: { path: string }): Promise<{
25
+ exists: boolean;
26
+ size: number;
27
+ mtime: number;
28
+ isBinary: boolean;
29
+ }> {
30
+ const targetPath = this.resolvePath(input.path);
31
+
32
+ try {
33
+ const stats = await fs.stat(targetPath);
34
+ const ext = path.extname(targetPath);
35
+ const binaryExts = new Set(['.png', '.jpg', '.pdf', '.zip', '.exe', '.dll']);
36
+
37
+ return {
38
+ exists: true,
39
+ size: stats.size,
40
+ mtime: stats.mtimeMs,
41
+ isBinary: binaryExts.has(ext),
42
+ };
43
+ } catch {
44
+ return {
45
+ exists: false,
46
+ size: 0,
47
+ mtime: 0,
48
+ isBinary: false,
49
+ };
50
+ }
51
+ }
52
+
53
+ private resolvePath(filePath: string): string {
54
+ return path.isAbsolute(filePath)
55
+ ? filePath
56
+ : path.resolve(this.rootPath, filePath);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * FileReadTool - Read files with offset/limit for large files
62
+ */
63
+ export class FileReadToolV2 extends Tool<
64
+ { path: string; offset?: number; limit?: number; ranges?: Array<{ start: number; end: number }> },
65
+ { content: string; bytesRead: number; eof: boolean; lang?: string }
66
+ > {
67
+ private rootPath: string;
68
+
69
+ constructor(rootPath: string = process.cwd()) {
70
+ super();
71
+ this.rootPath = path.resolve(rootPath);
72
+ }
73
+
74
+ name(): string {
75
+ return 'file_read_v2';
76
+ }
77
+
78
+ async call(input: {
79
+ path: string;
80
+ offset?: number;
81
+ limit?: number;
82
+ ranges?: Array<{ start: number; end: number }>;
83
+ }): Promise<{ content: string; bytesRead: number; eof: boolean; lang?: string }> {
84
+ // Normalize common aliases for file path (planner may emit `filepath` or `filePath`)
85
+ const anyInput = input as any;
86
+ const normalizedPath: string | undefined = anyInput?.path ?? anyInput?.filepath ?? anyInput?.filePath;
87
+ if (!normalizedPath) {
88
+ throw new Error('FileReadToolV2: missing required path. Accepted keys: path | filepath | filePath');
89
+ }
90
+ const fullPath = this.resolvePath(normalizedPath);
91
+ const stats = await fs.stat(fullPath);
92
+
93
+ // Check if binary
94
+ const ext = path.extname(normalizedPath);
95
+ const binaryExts = new Set(['.png', '.jpg', '.pdf', '.zip', '.exe', '.dll']);
96
+ if (binaryExts.has(ext)) {
97
+ throw new Error(`File ${normalizedPath} is binary, cannot read as text`);
98
+ }
99
+
100
+ // Handle ranged reads
101
+ if (input.ranges && input.ranges.length > 0) {
102
+ const content = await fs.readFile(fullPath, 'utf-8');
103
+ const rangeContents = input.ranges.map(range =>
104
+ content.slice(range.start, range.end)
105
+ );
106
+ return {
107
+ content: rangeContents.join('\n---\n'),
108
+ bytesRead: rangeContents.reduce((sum, c) => sum + c.length, 0),
109
+ eof: true,
110
+ lang: this.detectLanguage(ext),
111
+ };
112
+ }
113
+
114
+ // Handle offset/limit reads
115
+ const offset = input.offset ?? 0;
116
+ const limit = input.limit ?? stats.size;
117
+
118
+ const content = await fs.readFile(fullPath, 'utf-8');
119
+ const slice = content.slice(offset, offset + limit);
120
+ const eof = offset + slice.length >= content.length;
121
+
122
+ return {
123
+ content: slice,
124
+ bytesRead: slice.length,
125
+ eof,
126
+ lang: this.detectLanguage(ext),
127
+ };
128
+ }
129
+
130
+ private detectLanguage(ext: string): string {
131
+ const langMap: Record<string, string> = {
132
+ '.ts': 'typescript',
133
+ '.tsx': 'typescript',
134
+ '.js': 'javascript',
135
+ '.jsx': 'javascript',
136
+ '.py': 'python',
137
+ '.go': 'go',
138
+ '.rs': 'rust',
139
+ };
140
+ return langMap[ext] || 'text';
141
+ }
142
+
143
+ private resolvePath(filePath: string): string {
144
+ return path.isAbsolute(filePath)
145
+ ? filePath
146
+ : path.resolve(this.rootPath, filePath);
147
+ }
148
+ }
149
+
150
+ /**
151
+ * FileReadLinesTool - Line-ranged reading for precision
152
+ */
153
+ export class FileReadLinesTool extends Tool<
154
+ { path: string; startLine?: number; endLine?: number; maxLines?: number },
155
+ { content: string; startLine: number; endLine: number; totalLines?: number }
156
+ > {
157
+ private rootPath: string;
158
+
159
+ constructor(rootPath: string = process.cwd()) {
160
+ super();
161
+ this.rootPath = path.resolve(rootPath);
162
+ }
163
+
164
+ name(): string {
165
+ return 'file_read_lines';
166
+ }
167
+
168
+ async call(input: {
169
+ path: string;
170
+ startLine?: number;
171
+ endLine?: number;
172
+ maxLines?: number;
173
+ }): Promise<{ content: string; startLine: number; endLine: number; totalLines?: number }> {
174
+ // Normalize common aliases for file path (planner may emit `filepath` or `filePath`)
175
+ const anyInput = input as any;
176
+ const normalizedPath: string | undefined = anyInput?.path ?? anyInput?.filepath ?? anyInput?.filePath;
177
+ if (!normalizedPath) {
178
+ throw new Error('FileReadLinesTool: missing required path. Accepted keys: path | filepath | filePath');
179
+ }
180
+ const fullPath = this.resolvePath(normalizedPath);
181
+ const content = await fs.readFile(fullPath, 'utf-8');
182
+ const lines = content.split('\n');
183
+
184
+ const startLine = Math.max(1, input.startLine ?? 1);
185
+ const maxLines = input.maxLines ?? 100;
186
+ const endLine = input.endLine
187
+ ? Math.min(lines.length, input.endLine)
188
+ : Math.min(lines.length, startLine + maxLines - 1);
189
+
190
+ // Extract lines (convert from 1-based to 0-based indexing)
191
+ const selectedLines = lines.slice(startLine - 1, endLine);
192
+
193
+ return {
194
+ content: selectedLines.join('\n'),
195
+ startLine,
196
+ endLine,
197
+ totalLines: lines.length,
198
+ };
199
+ }
200
+
201
+ private resolvePath(filePath: string): string {
202
+ return path.isAbsolute(filePath)
203
+ ? filePath
204
+ : path.resolve(this.rootPath, filePath);
205
+ }
206
+ }
207
+
208
+ /**
209
+ * DiffTool - Generate unified diffs
210
+ */
211
+ export class DiffTool extends Tool<
212
+ { aPath?: string; aContent?: string; bPath?: string; bContent?: string; context?: number },
213
+ { diff: string; stats: { filesChanged: number; additions: number; deletions: number } }
214
+ > {
215
+ private rootPath: string;
216
+
217
+ constructor(rootPath: string = process.cwd()) {
218
+ super();
219
+ this.rootPath = path.resolve(rootPath);
220
+ }
221
+
222
+ name(): string {
223
+ return 'diff';
224
+ }
225
+
226
+ async call(input: {
227
+ aPath?: string;
228
+ aContent?: string;
229
+ bPath?: string;
230
+ bContent?: string;
231
+ context?: number;
232
+ }): Promise<{ diff: string; stats: { filesChanged: number; additions: number; deletions: number } }> {
233
+ // Get content from paths or direct content
234
+ const resolvedAPath = input.aPath ? this.resolvePath(input.aPath) : undefined;
235
+ const resolvedBPath = input.bPath ? this.resolvePath(input.bPath) : undefined;
236
+
237
+ const aContent = input.aContent ?? (resolvedAPath ? await fs.readFile(resolvedAPath, 'utf-8') : '');
238
+ const bContent = input.bContent ?? (resolvedBPath ? await fs.readFile(resolvedBPath, 'utf-8') : '');
239
+
240
+ // Simple line-by-line diff
241
+ const aLines = aContent.split('\n');
242
+ const bLines = bContent.split('\n');
243
+
244
+ const diff: string[] = [];
245
+ diff.push(`--- ${resolvedAPath || 'a'}`);
246
+ diff.push(`+++ ${resolvedBPath || 'b'}`);
247
+
248
+ let additions = 0;
249
+ let deletions = 0;
250
+
251
+ // Very simple diff (not optimal, but functional)
252
+ const maxLen = Math.max(aLines.length, bLines.length);
253
+ for (let i = 0; i < maxLen; i++) {
254
+ const aLine = aLines[i];
255
+ const bLine = bLines[i];
256
+
257
+ if (aLine !== bLine) {
258
+ if (aLine !== undefined) {
259
+ diff.push(`-${aLine}`);
260
+ deletions++;
261
+ }
262
+ if (bLine !== undefined) {
263
+ diff.push(`+${bLine}`);
264
+ additions++;
265
+ }
266
+ } else if (aLine !== undefined) {
267
+ diff.push(` ${aLine}`);
268
+ }
269
+ }
270
+
271
+ return {
272
+ diff: diff.join('\n'),
273
+ stats: {
274
+ filesChanged: 1,
275
+ additions,
276
+ deletions,
277
+ },
278
+ };
279
+ }
280
+
281
+ private resolvePath(filePath: string): string {
282
+ return path.isAbsolute(filePath)
283
+ ? filePath
284
+ : path.resolve(this.rootPath, filePath);
285
+ }
286
+ }
@@ -0,0 +1,175 @@
1
+ // Search Tools - Code search and pattern matching
2
+ import { Tool } from '@ddse/acm-sdk';
3
+ import * as fs from 'fs/promises';
4
+ import * as path from 'path';
5
+ import { CodeSearch, type SearchResult as ContextSearchResult } from '../context/index.js';
6
+ import { WorkspaceIndexer } from '../context/index.js';
7
+
8
+ /**
9
+ * CodeSearchTool - BM25-based code search
10
+ */
11
+ export class CodeSearchTool extends Tool<
12
+ { query: string; k?: number; preferTypes?: string[]; includeContext?: boolean },
13
+ { results: Array<{ path: string; score: number; snippet: string; line: number }> }
14
+ > {
15
+ private search: CodeSearch | null = null;
16
+ private rootPath: string;
17
+
18
+ constructor(rootPath: string = process.cwd()) {
19
+ super();
20
+ this.rootPath = path.resolve(rootPath);
21
+ }
22
+
23
+ name(): string {
24
+ return 'code_search';
25
+ }
26
+
27
+ async call(input: {
28
+ query: string;
29
+ k?: number;
30
+ preferTypes?: string[];
31
+ includeContext?: boolean;
32
+ }): Promise<{ results: Array<{ path: string; score: number; snippet: string; line: number }> }> {
33
+ // Lazy initialize search
34
+ if (!this.search) {
35
+ this.search = new CodeSearch(this.rootPath);
36
+ const indexer = new WorkspaceIndexer(this.rootPath);
37
+ const index = await indexer.buildIndex({ useCache: true });
38
+ await this.search.indexFiles(index);
39
+ }
40
+
41
+ const results = await this.search.search(input.query, {
42
+ k: input.k ?? 10,
43
+ preferTypes: input.preferTypes,
44
+ includeContext: input.includeContext ?? true,
45
+ contextLines: 2,
46
+ });
47
+
48
+ return {
49
+ results: results.map(r => ({
50
+ path: r.path,
51
+ score: r.score,
52
+ snippet: r.snippet,
53
+ line: r.line,
54
+ })),
55
+ };
56
+ }
57
+ }
58
+
59
+ /**
60
+ * GrepTool - Pattern-based multi-file search
61
+ */
62
+ export class GrepTool extends Tool<
63
+ { pattern: string; globs?: string[]; regex?: boolean; caseInsensitive?: boolean; maxResults?: number },
64
+ { matches: Array<{ path: string; line: number; column: number; preview: string }> }
65
+ > {
66
+ private rootPath: string;
67
+
68
+ constructor(rootPath: string = process.cwd()) {
69
+ super();
70
+ this.rootPath = path.resolve(rootPath);
71
+ }
72
+
73
+ name(): string {
74
+ return 'grep';
75
+ }
76
+
77
+ async call(input: {
78
+ pattern: string;
79
+ globs?: string[];
80
+ regex?: boolean;
81
+ caseInsensitive?: boolean;
82
+ maxResults?: number;
83
+ }): Promise<{ matches: Array<{ path: string; line: number; column: number; preview: string }> }> {
84
+ const maxResults = input.maxResults ?? 100;
85
+ const matches: Array<{ path: string; line: number; column: number; preview: string }> = [];
86
+
87
+ // Build pattern
88
+ let searchPattern: RegExp;
89
+ if (input.regex) {
90
+ const flags = input.caseInsensitive ? 'gi' : 'g';
91
+ searchPattern = new RegExp(input.pattern, flags);
92
+ } else {
93
+ const escaped = input.pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
94
+ const flags = input.caseInsensitive ? 'gi' : 'g';
95
+ searchPattern = new RegExp(escaped, flags);
96
+ }
97
+
98
+ // Scan files
99
+ await this.scanDirectory(this.rootPath, searchPattern, matches, maxResults);
100
+
101
+ return { matches };
102
+ }
103
+
104
+ private async scanDirectory(
105
+ dir: string,
106
+ pattern: RegExp,
107
+ matches: Array<{ path: string; line: number; column: number; preview: string }>,
108
+ maxResults: number
109
+ ): Promise<void> {
110
+ if (matches.length >= maxResults) return;
111
+
112
+ try {
113
+ const entries = await fs.readdir(dir, { withFileTypes: true });
114
+
115
+ for (const entry of entries) {
116
+ if (matches.length >= maxResults) break;
117
+
118
+ // Skip common directories
119
+ if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === 'dist') {
120
+ continue;
121
+ }
122
+
123
+ const fullPath = path.join(dir, entry.name);
124
+
125
+ if (entry.isDirectory()) {
126
+ await this.scanDirectory(fullPath, pattern, matches, maxResults);
127
+ } else if (entry.isFile()) {
128
+ // Only search text files
129
+ const ext = path.extname(entry.name);
130
+ if (['.ts', '.tsx', '.js', '.jsx', '.md', '.json'].includes(ext)) {
131
+ await this.searchFile(fullPath, pattern, matches, maxResults);
132
+ }
133
+ }
134
+ }
135
+ } catch {
136
+ // Skip directories we can't read
137
+ }
138
+ }
139
+
140
+ private async searchFile(
141
+ filePath: string,
142
+ pattern: RegExp,
143
+ matches: Array<{ path: string; line: number; column: number; preview: string }>,
144
+ maxResults: number
145
+ ): Promise<void> {
146
+ if (matches.length >= maxResults) return;
147
+
148
+ try {
149
+ const content = await fs.readFile(filePath, 'utf-8');
150
+ const lines = content.split('\n');
151
+ const relativePath = path.relative(this.rootPath, filePath);
152
+
153
+ for (let i = 0; i < lines.length; i++) {
154
+ if (matches.length >= maxResults) break;
155
+
156
+ const line = lines[i];
157
+ const match = pattern.exec(line);
158
+
159
+ if (match) {
160
+ matches.push({
161
+ path: relativePath,
162
+ line: i + 1,
163
+ column: match.index,
164
+ preview: line.substring(0, 100), // First 100 chars
165
+ });
166
+ }
167
+
168
+ // Reset lastIndex for global regex
169
+ pattern.lastIndex = 0;
170
+ }
171
+ } catch {
172
+ // Skip files we can't read
173
+ }
174
+ }
175
+ }
@@ -0,0 +1,147 @@
1
+ // Test and Build Tools
2
+ import { Tool } from '@ddse/acm-sdk';
3
+ import * as path from 'path';
4
+
5
+ /**
6
+ * RunTestsToolV2 - Enhanced test runner with better output
7
+ */
8
+ export class RunTestsToolV2 extends Tool<
9
+ { command?: string; cwd?: string; timeout?: number },
10
+ { success: boolean; output: string; exitCode: number; duration: number }
11
+ > {
12
+ private defaultCwd: string;
13
+
14
+ constructor(defaultCwd: string = process.cwd()) {
15
+ super();
16
+ this.defaultCwd = path.resolve(defaultCwd);
17
+ }
18
+
19
+ name(): string {
20
+ return 'run_tests_v2';
21
+ }
22
+
23
+ async call(input: {
24
+ command?: string;
25
+ cwd?: string;
26
+ timeout?: number;
27
+ }): Promise<{ success: boolean; output: string; exitCode: number; duration: number }> {
28
+ const { execa } = await import('execa');
29
+ const command = input.command || 'npm test';
30
+ const timeout = input.timeout ?? 300000; // 5 minutes
31
+
32
+ const startTime = Date.now();
33
+
34
+ const resolvedCwd = this.resolveCwd(input.cwd);
35
+
36
+ try {
37
+ const result = await execa(command, {
38
+ shell: true,
39
+ cwd: resolvedCwd,
40
+ timeout,
41
+ });
42
+
43
+ return {
44
+ success: result.exitCode === 0,
45
+ output: result.stdout + '\n' + result.stderr,
46
+ exitCode: result.exitCode || 0,
47
+ duration: Date.now() - startTime,
48
+ };
49
+ } catch (error: any) {
50
+ return {
51
+ success: false,
52
+ output: (error.stdout || '') + '\n' + (error.stderr || ''),
53
+ exitCode: error.exitCode || 1,
54
+ duration: Date.now() - startTime,
55
+ };
56
+ }
57
+ }
58
+
59
+ private resolveCwd(cwd?: string): string {
60
+ if (!cwd) {
61
+ return this.defaultCwd;
62
+ }
63
+
64
+ return path.isAbsolute(cwd) ? cwd : path.resolve(this.defaultCwd, cwd);
65
+ }
66
+ }
67
+
68
+ /**
69
+ * BuildTool - Run build commands with error extraction
70
+ */
71
+ export class BuildTool extends Tool<
72
+ { command?: string; cwd?: string; timeout?: number },
73
+ { success: boolean; output: string; errors: string[]; duration: number }
74
+ > {
75
+ private defaultCwd: string;
76
+
77
+ constructor(defaultCwd: string = process.cwd()) {
78
+ super();
79
+ this.defaultCwd = path.resolve(defaultCwd);
80
+ }
81
+
82
+ name(): string {
83
+ return 'build';
84
+ }
85
+
86
+ async call(input: {
87
+ command?: string;
88
+ cwd?: string;
89
+ timeout?: number;
90
+ }): Promise<{ success: boolean; output: string; errors: string[]; duration: number }> {
91
+ const { execa } = await import('execa');
92
+ const command = input.command || 'npm run build';
93
+ const timeout = input.timeout ?? 600000; // 10 minutes
94
+
95
+ const startTime = Date.now();
96
+
97
+ const resolvedCwd = this.resolveCwd(input.cwd);
98
+
99
+ try {
100
+ const result = await execa(command, {
101
+ shell: true,
102
+ cwd: resolvedCwd,
103
+ timeout,
104
+ });
105
+
106
+ return {
107
+ success: result.exitCode === 0,
108
+ output: result.stdout + '\n' + result.stderr,
109
+ errors: [],
110
+ duration: Date.now() - startTime,
111
+ };
112
+ } catch (error: any) {
113
+ const output = (error.stdout || '') + '\n' + (error.stderr || '');
114
+
115
+ // Extract errors (simple pattern matching)
116
+ const errors = this.extractErrors(output);
117
+
118
+ return {
119
+ success: false,
120
+ output,
121
+ errors,
122
+ duration: Date.now() - startTime,
123
+ };
124
+ }
125
+ }
126
+
127
+ private extractErrors(output: string): string[] {
128
+ const errors: string[] = [];
129
+ const lines = output.split('\n');
130
+
131
+ for (const line of lines) {
132
+ if (line.includes('error') || line.includes('Error:') || line.includes('ERROR')) {
133
+ errors.push(line.trim());
134
+ }
135
+ }
136
+
137
+ return errors.slice(0, 20); // Limit to first 20 errors
138
+ }
139
+
140
+ private resolveCwd(cwd?: string): string {
141
+ if (!cwd) {
142
+ return this.defaultCwd;
143
+ }
144
+
145
+ return path.isAbsolute(cwd) ? cwd : path.resolve(this.defaultCwd, cwd);
146
+ }
147
+ }