closed-loop-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.
Potentially problematic release.
This version of closed-loop-cli might be problematic. Click here for more details.
- package/dist/dashboard/server.js +237 -0
- package/dist/index.js +272 -0
- package/dist/orchestrator/agent-prompts.js +42 -0
- package/dist/orchestrator/autogenesis.js +973 -0
- package/dist/orchestrator/dgm-archive.js +223 -0
- package/dist/orchestrator/event-stream.js +103 -0
- package/dist/orchestrator/fitness-evaluator.js +99 -0
- package/dist/orchestrator/meta-agent.js +421 -0
- package/dist/orchestrator/microagent-registry.js +134 -0
- package/dist/orchestrator/mutation-strategies.js +174 -0
- package/dist/orchestrator/prompt-benchmark.js +102 -0
- package/dist/orchestrator/prompt-optimizer.js +169 -0
- package/dist/orchestrator/refactor-scanner.js +222 -0
- package/dist/orchestrator/research-manager.js +104 -0
- package/dist/orchestrator/rulez.js +135 -0
- package/dist/orchestrator/sahoo-gateway.js +261 -0
- package/dist/orchestrator/state-manager.js +121 -0
- package/dist/orchestrator/task-agent.js +444 -0
- package/dist/orchestrator/telegram-bot.js +374 -0
- package/dist/orchestrator/types.js +2 -0
- package/dist/tests/dynamic/dependencies.test.js +37 -0
- package/dist/tests/dynamic/dummy.test.js +7 -0
- package/dist/tests/dynamic/fuzzy-patch.test.js +68 -0
- package/dist/tests/dynamic/indexer.test.js +60 -0
- package/dist/tests/dynamic/openhands.test.js +83 -0
- package/dist/tests/dynamic/skills.test.js +88 -0
- package/dist/tests/run-tests.js +294 -0
- package/dist/tools/diff-tools.js +24 -0
- package/dist/tools/file-tools.js +191 -0
- package/dist/tools/indexer.js +301 -0
- package/dist/tools/math-helper.js +6 -0
- package/dist/tools/repo-map.js +122 -0
- package/dist/tools/search-tools.js +271 -0
- package/dist/tools/shell-tools.js +75 -0
- package/dist/tools/skills.js +122 -0
- package/dist/tools/tui-tools.js +82 -0
- package/docs/AI_Arch_Opt_Anti_Gaming.md +227 -0
- package/docs/AI_Self_Improvement_Safety.md +457 -0
- package/docs/Anthropic AI Agents_ Capabilities and Concerns.md +134 -0
- package/docs/Auto_ClosedLoop_AI_Agent.md +415 -0
- package/docs/Autonomous AI Agents_ Closing the Loop.docx +0 -0
- package/docs/Secure_AI_Sandbox_Framework.md +358 -0
- package/docs/skills/add-file-existence-check-utility.json +9 -0
- package/docs/skills/add-utility-function-for-file-existence-check.json +9 -0
- package/docs/skills/add-utility-function-to-module.json +9 -0
- package/docs/skills/extract-command-runner-utility.json +9 -0
- package/docs/skills/file-existence-check-utility.json +9 -0
- package/package.json +36 -0
- package/src/dashboard/public/index.css +1334 -0
- package/src/dashboard/public/index.html +385 -0
- package/src/dashboard/public/index.js +1059 -0
- package/src/dashboard/server.ts +209 -0
- package/src/index.ts +256 -0
- package/src/orchestrator/agent-prompts.ts +43 -0
- package/src/orchestrator/autogenesis.ts +1078 -0
- package/src/orchestrator/dgm-archive.ts +257 -0
- package/src/orchestrator/event-stream.ts +90 -0
- package/src/orchestrator/fitness-evaluator.ts +154 -0
- package/src/orchestrator/meta-agent.ts +434 -0
- package/src/orchestrator/microagent-registry.ts +115 -0
- package/src/orchestrator/microagents/git-helper.md +11 -0
- package/src/orchestrator/microagents/test-fixer.md +10 -0
- package/src/orchestrator/microagents/typescript-expert.md +11 -0
- package/src/orchestrator/mutation-strategies.ts +214 -0
- package/src/orchestrator/research-manager.ts +88 -0
- package/src/orchestrator/rulez.ts +118 -0
- package/src/orchestrator/sahoo-gateway.ts +300 -0
- package/src/orchestrator/state-manager.ts +161 -0
- package/src/orchestrator/system-prompt.txt +1 -0
- package/src/orchestrator/task-agent.ts +461 -0
- package/src/orchestrator/telegram-bot.ts +358 -0
- package/src/tests/dynamic/dependencies.test.ts +48 -0
- package/src/tests/dynamic/dummy.test.ts +4 -0
- package/src/tests/dynamic/fuzzy-patch.test.ts +42 -0
- package/src/tests/dynamic/indexer.test.ts +31 -0
- package/src/tests/dynamic/openhands.test.ts +59 -0
- package/src/tests/dynamic/skills.test.ts +63 -0
- package/src/tests/run-tests.ts +296 -0
- package/src/tools/diff-tools.ts +27 -0
- package/src/tools/file-tools.ts +187 -0
- package/src/tools/indexer.ts +325 -0
- package/src/tools/repo-map.ts +96 -0
- package/src/tools/search-tools.ts +258 -0
- package/src/tools/shell-tools.ts +90 -0
- package/src/tools/skills.ts +101 -0
- package/src/tools/tui-tools.ts +87 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as ts from 'typescript';
|
|
4
|
+
|
|
5
|
+
export interface IndexedMethod {
|
|
6
|
+
name: string;
|
|
7
|
+
isStatic: boolean;
|
|
8
|
+
isAsync: boolean;
|
|
9
|
+
parameters: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface IndexedClass {
|
|
13
|
+
name: string;
|
|
14
|
+
methods: IndexedMethod[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface IndexedInterface {
|
|
18
|
+
name: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface IndexedFunction {
|
|
22
|
+
name: string;
|
|
23
|
+
isAsync: boolean;
|
|
24
|
+
parameters: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface IndexedFile {
|
|
28
|
+
filePath: string;
|
|
29
|
+
classes: IndexedClass[];
|
|
30
|
+
interfaces: IndexedInterface[];
|
|
31
|
+
functions: IndexedFunction[];
|
|
32
|
+
imports: string[];
|
|
33
|
+
exports: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolves relative module import paths to standardized workspace-relative paths.
|
|
38
|
+
*/
|
|
39
|
+
function resolveImportPath(fromFile: string, importPath: string, workspaceRoot: string = process.cwd()): string | null {
|
|
40
|
+
const fromDir = path.dirname(path.join(workspaceRoot, fromFile));
|
|
41
|
+
const absoluteTarget = path.resolve(fromDir, importPath);
|
|
42
|
+
const relativeTarget = path.relative(workspaceRoot, absoluteTarget).replace(/\\/g, '/');
|
|
43
|
+
|
|
44
|
+
// Try standard file extensions
|
|
45
|
+
const extensions = ['.ts', '.js', '/index.ts', '/index.js'];
|
|
46
|
+
for (const ext of extensions) {
|
|
47
|
+
const targetWithExt = absoluteTarget + ext;
|
|
48
|
+
if (fs.existsSync(targetWithExt)) {
|
|
49
|
+
return (relativeTarget + ext).replace(/\/index\.(ts|js)$/, '');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// If file exists as-is
|
|
54
|
+
if (fs.existsSync(absoluteTarget)) {
|
|
55
|
+
return relativeTarget;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Fallback target path
|
|
59
|
+
return relativeTarget + '.ts';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Generates the code index database and writes it to code-index.json.
|
|
64
|
+
*/
|
|
65
|
+
export function generateCodeIndex(workspaceRoot: string = process.cwd()): IndexedFile[] {
|
|
66
|
+
const index: IndexedFile[] = [];
|
|
67
|
+
|
|
68
|
+
function walk(currentDir: string) {
|
|
69
|
+
const files = fs.readdirSync(currentDir);
|
|
70
|
+
for (const file of files) {
|
|
71
|
+
const fullPath = path.join(currentDir, file);
|
|
72
|
+
|
|
73
|
+
// Skip ignorable folders
|
|
74
|
+
if (file === 'node_modules' || file === '.git' || file === 'dist' || file === '.gemini') {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const stats = fs.statSync(fullPath);
|
|
79
|
+
if (stats.isDirectory()) {
|
|
80
|
+
walk(fullPath);
|
|
81
|
+
} else if (stats.isFile()) {
|
|
82
|
+
const ext = path.extname(file).toLowerCase();
|
|
83
|
+
if (ext === '.ts' || ext === '.js') {
|
|
84
|
+
const relativePath = path.relative(workspaceRoot, fullPath).replace(/\\/g, '/');
|
|
85
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
86
|
+
const fileIndex = parseFileAST(relativePath, content);
|
|
87
|
+
index.push(fileIndex);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
walk(workspaceRoot);
|
|
94
|
+
const indexPath = path.join(workspaceRoot, 'code-index.json');
|
|
95
|
+
fs.writeFileSync(indexPath, JSON.stringify(index, null, 2), 'utf-8');
|
|
96
|
+
return index;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Parses file AST to extract structured metadata.
|
|
101
|
+
*/
|
|
102
|
+
function parseFileAST(filePath: string, content: string): IndexedFile {
|
|
103
|
+
const sourceFile = ts.createSourceFile(
|
|
104
|
+
filePath,
|
|
105
|
+
content,
|
|
106
|
+
ts.ScriptTarget.Latest,
|
|
107
|
+
true
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const fileIndex: IndexedFile = {
|
|
111
|
+
filePath,
|
|
112
|
+
classes: [],
|
|
113
|
+
interfaces: [],
|
|
114
|
+
functions: [],
|
|
115
|
+
imports: [],
|
|
116
|
+
exports: []
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
let currentClass: IndexedClass | null = null;
|
|
120
|
+
|
|
121
|
+
function visit(node: ts.Node) {
|
|
122
|
+
const isExport = ((node as any).modifiers?.some((m: any) => m.kind === ts.SyntaxKind.ExportKeyword)) || false;
|
|
123
|
+
|
|
124
|
+
// Capture Imports
|
|
125
|
+
if (ts.isImportDeclaration(node) && node.moduleSpecifier) {
|
|
126
|
+
if (ts.isStringLiteral(node.moduleSpecifier)) {
|
|
127
|
+
const importPath = node.moduleSpecifier.text;
|
|
128
|
+
if (importPath.startsWith('.')) {
|
|
129
|
+
const resolved = resolveImportPath(filePath, importPath);
|
|
130
|
+
if (resolved) {
|
|
131
|
+
fileIndex.imports.push(resolved);
|
|
132
|
+
}
|
|
133
|
+
} else {
|
|
134
|
+
fileIndex.imports.push(importPath); // NPM library import
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Capture Named and Default Export statements
|
|
140
|
+
if (ts.isExportDeclaration(node)) {
|
|
141
|
+
if (node.exportClause && ts.isNamedExports(node.exportClause)) {
|
|
142
|
+
for (const element of node.exportClause.elements) {
|
|
143
|
+
fileIndex.exports.push(element.name.text);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
} else if (ts.isExportAssignment(node)) {
|
|
147
|
+
fileIndex.exports.push('default');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Capture Classes and Methods
|
|
151
|
+
if (ts.isClassDeclaration(node) && node.name) {
|
|
152
|
+
if (isExport) {
|
|
153
|
+
fileIndex.exports.push(node.name.text);
|
|
154
|
+
}
|
|
155
|
+
const classObj: IndexedClass = {
|
|
156
|
+
name: node.name.text,
|
|
157
|
+
methods: []
|
|
158
|
+
};
|
|
159
|
+
fileIndex.classes.push(classObj);
|
|
160
|
+
|
|
161
|
+
const oldClass = currentClass;
|
|
162
|
+
currentClass = classObj;
|
|
163
|
+
ts.forEachChild(node, visit);
|
|
164
|
+
currentClass = oldClass;
|
|
165
|
+
} else if (ts.isInterfaceDeclaration(node) && node.name) {
|
|
166
|
+
if (isExport) {
|
|
167
|
+
fileIndex.exports.push(node.name.text);
|
|
168
|
+
}
|
|
169
|
+
fileIndex.interfaces.push({ name: node.name.text });
|
|
170
|
+
} else if (ts.isFunctionDeclaration(node) && node.name) {
|
|
171
|
+
if (isExport) {
|
|
172
|
+
fileIndex.exports.push(node.name.text);
|
|
173
|
+
}
|
|
174
|
+
const isAsync = ((node as any).modifiers?.some((m: any) => m.kind === ts.SyntaxKind.AsyncKeyword)) || false;
|
|
175
|
+
const params = node.parameters.map(p => p.name.getText(sourceFile));
|
|
176
|
+
fileIndex.functions.push({
|
|
177
|
+
name: node.name.text,
|
|
178
|
+
isAsync,
|
|
179
|
+
parameters: params
|
|
180
|
+
});
|
|
181
|
+
} else if (ts.isVariableStatement(node)) {
|
|
182
|
+
if (isExport) {
|
|
183
|
+
for (const decl of node.declarationList.declarations) {
|
|
184
|
+
fileIndex.exports.push(decl.name.getText(sourceFile));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
for (const decl of node.declarationList.declarations) {
|
|
188
|
+
if (decl.initializer && (ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer))) {
|
|
189
|
+
const func = decl.initializer;
|
|
190
|
+
const isAsync = ((func as any).modifiers?.some((m: any) => m.kind === ts.SyntaxKind.AsyncKeyword)) || false;
|
|
191
|
+
const params = func.parameters.map(p => p.name.getText(sourceFile));
|
|
192
|
+
fileIndex.functions.push({
|
|
193
|
+
name: decl.name.getText(sourceFile),
|
|
194
|
+
isAsync,
|
|
195
|
+
parameters: params
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} else if (currentClass && ts.isMethodDeclaration(node) && node.name) {
|
|
200
|
+
const isAsync = ((node as any).modifiers?.some((m: any) => m.kind === ts.SyntaxKind.AsyncKeyword)) || false;
|
|
201
|
+
const isStatic = ((node as any).modifiers?.some((m: any) => m.kind === ts.SyntaxKind.StaticKeyword)) || false;
|
|
202
|
+
const params = node.parameters.map(p => p.name.getText(sourceFile));
|
|
203
|
+
currentClass.methods.push({
|
|
204
|
+
name: node.name.getText(sourceFile),
|
|
205
|
+
isStatic,
|
|
206
|
+
isAsync,
|
|
207
|
+
parameters: params
|
|
208
|
+
});
|
|
209
|
+
} else {
|
|
210
|
+
ts.forEachChild(node, visit);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
ts.forEachChild(sourceFile, visit);
|
|
215
|
+
return fileIndex;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Queries code-index.json to find all files that import a given target file.
|
|
220
|
+
*/
|
|
221
|
+
export function getDependents(targetFile: string, workspaceRoot: string = process.cwd()): string[] {
|
|
222
|
+
const indexPath = path.join(workspaceRoot, 'code-index.json');
|
|
223
|
+
if (!fs.existsSync(indexPath)) {
|
|
224
|
+
return [];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let index: IndexedFile[] = [];
|
|
228
|
+
try {
|
|
229
|
+
index = JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
|
|
230
|
+
} catch (e) {
|
|
231
|
+
return [];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const targetNormalized = targetFile.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
235
|
+
const targetBase = targetNormalized.replace(/\.(ts|js)$/, '');
|
|
236
|
+
|
|
237
|
+
const dependents: string[] = [];
|
|
238
|
+
for (const file of index) {
|
|
239
|
+
const hasImport = file.imports.some(imp => {
|
|
240
|
+
const impNormalized = imp.replace(/\.(ts|js)$/, '');
|
|
241
|
+
return impNormalized === targetBase || impNormalized === targetNormalized;
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
if (hasImport && file.filePath !== targetNormalized) {
|
|
245
|
+
dependents.push(file.filePath);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return dependents;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Queries the code-index.json database for keyword matches.
|
|
254
|
+
*/
|
|
255
|
+
export function queryCodeIndex(query: string, workspaceRoot: string = process.cwd()): string {
|
|
256
|
+
const indexPath = path.join(workspaceRoot, 'code-index.json');
|
|
257
|
+
if (!fs.existsSync(indexPath)) {
|
|
258
|
+
generateCodeIndex(workspaceRoot);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
let index: IndexedFile[] = [];
|
|
262
|
+
try {
|
|
263
|
+
index = JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
|
|
264
|
+
} catch (e) {
|
|
265
|
+
return `Error reading code-index.json: ${(e as Error).message}`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const queryLower = query.toLowerCase();
|
|
269
|
+
const matches: string[] = [];
|
|
270
|
+
|
|
271
|
+
for (const file of index) {
|
|
272
|
+
let fileHasMatch = false;
|
|
273
|
+
const fileLines: string[] = [];
|
|
274
|
+
|
|
275
|
+
// Check class names and methods
|
|
276
|
+
for (const cls of file.classes) {
|
|
277
|
+
let classMatched = cls.name.toLowerCase().includes(queryLower);
|
|
278
|
+
const matchedMethods = cls.methods.filter(m => m.name.toLowerCase().includes(queryLower));
|
|
279
|
+
|
|
280
|
+
if (classMatched || matchedMethods.length > 0) {
|
|
281
|
+
fileHasMatch = true;
|
|
282
|
+
fileLines.push(` class ${cls.name}`);
|
|
283
|
+
for (const m of cls.methods) {
|
|
284
|
+
const isMatched = m.name.toLowerCase().includes(queryLower);
|
|
285
|
+
const highlight = isMatched ? ' <-- MATCH' : '';
|
|
286
|
+
fileLines.push(` - method ${m.isStatic ? 'static ' : ''}${m.isAsync ? 'async ' : ''}${m.name}(${m.parameters.join(', ')})${highlight}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Check interfaces
|
|
292
|
+
for (const intf of file.interfaces) {
|
|
293
|
+
if (intf.name.toLowerCase().includes(queryLower)) {
|
|
294
|
+
fileHasMatch = true;
|
|
295
|
+
fileLines.push(` interface ${intf.name} <-- MATCH`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Check functions
|
|
300
|
+
for (const fn of file.functions) {
|
|
301
|
+
if (fn.name.toLowerCase().includes(queryLower)) {
|
|
302
|
+
fileHasMatch = true;
|
|
303
|
+
fileLines.push(` function ${fn.isAsync ? 'async ' : ''}${fn.name}(${fn.parameters.join(', ')}) <-- MATCH`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Check filepath match
|
|
308
|
+
if (file.filePath.toLowerCase().includes(queryLower) && !fileHasMatch) {
|
|
309
|
+
fileHasMatch = true;
|
|
310
|
+
fileLines.push(` [File matched by name]`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (fileHasMatch) {
|
|
314
|
+
matches.push(`File: ${file.filePath}`);
|
|
315
|
+
matches.push(...fileLines);
|
|
316
|
+
matches.push('');
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (matches.length === 0) {
|
|
321
|
+
return `No matches found in code index for query "${query}".`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return matches.join('\n').trim();
|
|
325
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as ts from 'typescript';
|
|
4
|
+
|
|
5
|
+
export function generateRepoMap(workspaceRoot: string = process.cwd()): string {
|
|
6
|
+
const mapLines: string[] = [];
|
|
7
|
+
|
|
8
|
+
function walk(currentDir: string) {
|
|
9
|
+
const files = fs.readdirSync(currentDir);
|
|
10
|
+
for (const file of files) {
|
|
11
|
+
const fullPath = path.join(currentDir, file);
|
|
12
|
+
|
|
13
|
+
// Skip ignorable folders
|
|
14
|
+
if (file === 'node_modules' || file === '.git' || file === 'dist' || file === '.gemini') {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const stats = fs.statSync(fullPath);
|
|
19
|
+
if (stats.isDirectory()) {
|
|
20
|
+
walk(fullPath);
|
|
21
|
+
} else if (stats.isFile()) {
|
|
22
|
+
const ext = path.extname(file).toLowerCase();
|
|
23
|
+
if (ext === '.ts' || ext === '.js') {
|
|
24
|
+
const relativePath = path.relative(workspaceRoot, fullPath).replace(/\\/g, '/');
|
|
25
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
26
|
+
const signatures = extractSignatures(relativePath, content);
|
|
27
|
+
|
|
28
|
+
if (signatures.length > 0) {
|
|
29
|
+
mapLines.push(`File: ${relativePath}`);
|
|
30
|
+
signatures.forEach(sig => mapLines.push(` - ${sig}`));
|
|
31
|
+
mapLines.push('');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
walk(workspaceRoot);
|
|
39
|
+
return mapLines.join('\n').trim();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Extracts class, interface, function, and method signatures using the TypeScript Compiler AST API.
|
|
44
|
+
*/
|
|
45
|
+
function extractSignatures(filePath: string, content: string): string[] {
|
|
46
|
+
const sourceFile = ts.createSourceFile(
|
|
47
|
+
filePath,
|
|
48
|
+
content,
|
|
49
|
+
ts.ScriptTarget.Latest,
|
|
50
|
+
true
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const signatures: string[] = [];
|
|
54
|
+
let currentClass: string | null = null;
|
|
55
|
+
|
|
56
|
+
function visit(node: ts.Node) {
|
|
57
|
+
if (ts.isClassDeclaration(node) && node.name) {
|
|
58
|
+
const className = node.name.text;
|
|
59
|
+
signatures.push(`class ${className}`);
|
|
60
|
+
|
|
61
|
+
const oldClass = currentClass;
|
|
62
|
+
currentClass = className;
|
|
63
|
+
ts.forEachChild(node, visit);
|
|
64
|
+
currentClass = oldClass;
|
|
65
|
+
} else if (ts.isInterfaceDeclaration(node) && node.name) {
|
|
66
|
+
signatures.push(`interface ${node.name.text}`);
|
|
67
|
+
} else if (ts.isFunctionDeclaration(node) && node.name) {
|
|
68
|
+
const isAsync = node.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword) ? 'async ' : '';
|
|
69
|
+
const params = node.parameters.map(p => p.name.getText(sourceFile)).join(', ');
|
|
70
|
+
signatures.push(`${isAsync}function ${node.name.text}(${params})`);
|
|
71
|
+
} else if (ts.isVariableStatement(node)) {
|
|
72
|
+
// Check if it is an exported const arrow/expression function
|
|
73
|
+
const isExport = node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
74
|
+
if (isExport) {
|
|
75
|
+
for (const decl of node.declarationList.declarations) {
|
|
76
|
+
if (decl.initializer && (ts.isArrowFunction(decl.initializer) || ts.isFunctionExpression(decl.initializer))) {
|
|
77
|
+
const func = decl.initializer;
|
|
78
|
+
const isAsync = func.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword) ? 'async ' : '';
|
|
79
|
+
const params = func.parameters.map(p => p.name.getText(sourceFile)).join(', ');
|
|
80
|
+
signatures.push(`${isAsync}const ${decl.name.getText(sourceFile)} = (${params}) =>`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} else if (currentClass && ts.isMethodDeclaration(node) && node.name) {
|
|
85
|
+
const isAsync = node.modifiers?.some(m => m.kind === ts.SyntaxKind.AsyncKeyword) ? 'async ' : '';
|
|
86
|
+
const isStatic = node.modifiers?.some(m => m.kind === ts.SyntaxKind.StaticKeyword) ? 'static ' : '';
|
|
87
|
+
const params = node.parameters.map(p => p.name.getText(sourceFile)).join(', ');
|
|
88
|
+
signatures.push(`method ${isStatic}${isAsync}${node.name.getText(sourceFile)}(${params})`);
|
|
89
|
+
} else {
|
|
90
|
+
ts.forEachChild(node, visit);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
ts.forEachChild(sourceFile, visit);
|
|
95
|
+
return signatures;
|
|
96
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import * as https from 'https';
|
|
2
|
+
|
|
3
|
+
export interface SearchResult {
|
|
4
|
+
title: string;
|
|
5
|
+
link: string;
|
|
6
|
+
snippet: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Strips HTML tags from a string.
|
|
11
|
+
*/
|
|
12
|
+
function stripHtml(text: string): string {
|
|
13
|
+
return text.replace(/<[^>]*>/g, '').trim();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Decodes common HTML entities.
|
|
18
|
+
*/
|
|
19
|
+
function decodeEntities(text: string): string {
|
|
20
|
+
return text
|
|
21
|
+
.replace(/&/g, '&')
|
|
22
|
+
.replace(/</g, '<')
|
|
23
|
+
.replace(/>/g, '>')
|
|
24
|
+
.replace(/"/g, '"')
|
|
25
|
+
.replace(/'/g, "'")
|
|
26
|
+
.replace(///g, '/')
|
|
27
|
+
.replace(/'/g, "'");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Formats results list into a readable string.
|
|
32
|
+
*/
|
|
33
|
+
function formatResults(query: string, results: SearchResult[], source: string): string {
|
|
34
|
+
let formatted = `Web search results (via ${source}) for: "${query}"\n\n`;
|
|
35
|
+
results.forEach((res, index) => {
|
|
36
|
+
formatted += `${index + 1}. ${res.title}\n`;
|
|
37
|
+
formatted += ` URL: ${res.link}\n`;
|
|
38
|
+
formatted += ` Snippet: ${res.snippet}\n\n`;
|
|
39
|
+
});
|
|
40
|
+
return formatted.trim();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Searches the web using DuckDuckGo Lite POST interface, falling back to DDG JSON and Wikipedia APIs on failure.
|
|
45
|
+
*/
|
|
46
|
+
export async function searchWeb(query: string): Promise<string> {
|
|
47
|
+
// 1. Try DuckDuckGo Lite POST
|
|
48
|
+
try {
|
|
49
|
+
const results = await fetchDuckDuckGoLite(query);
|
|
50
|
+
if (results.length > 0) {
|
|
51
|
+
return formatResults(query, results, 'DuckDuckGo Lite');
|
|
52
|
+
}
|
|
53
|
+
} catch (err) {
|
|
54
|
+
// Ignore and try next fallback
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// 2. Try DuckDuckGo Instant Answer JSON API
|
|
58
|
+
try {
|
|
59
|
+
const results = await fetchDuckDuckGoApi(query);
|
|
60
|
+
if (results.length > 0) {
|
|
61
|
+
return formatResults(query, results, 'DuckDuckGo API');
|
|
62
|
+
}
|
|
63
|
+
} catch (err) {
|
|
64
|
+
// Ignore and try next fallback
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 3. Try Wikipedia Search API
|
|
68
|
+
try {
|
|
69
|
+
const results = await fetchWikipedia(query);
|
|
70
|
+
if (results.length > 0) {
|
|
71
|
+
return formatResults(query, results, 'Wikipedia');
|
|
72
|
+
}
|
|
73
|
+
} catch (err) {
|
|
74
|
+
// Ignore
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return `No search results found for: "${query}". (Web search services are currently rate-limited or unavailable).`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* DuckDuckGo Lite Scraper
|
|
82
|
+
*/
|
|
83
|
+
function fetchDuckDuckGoLite(query: string): Promise<SearchResult[]> {
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
const encodedQuery = encodeURIComponent(query);
|
|
86
|
+
const postData = `q=${encodedQuery}`;
|
|
87
|
+
|
|
88
|
+
const options = {
|
|
89
|
+
hostname: 'lite.duckduckgo.com',
|
|
90
|
+
port: 443,
|
|
91
|
+
path: '/lite/',
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: {
|
|
94
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
95
|
+
'Content-Length': Buffer.byteLength(postData),
|
|
96
|
+
'User-Agent': 'ClosedLoopCodingAgent/1.0 (contact: akara@example.com)',
|
|
97
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
98
|
+
'Accept-Language': 'en-US,en;q=0.5'
|
|
99
|
+
},
|
|
100
|
+
timeout: 6000
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const req = https.request(options, (res) => {
|
|
104
|
+
if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
|
|
105
|
+
return reject(new Error(`Status ${res.statusCode}`));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let data = '';
|
|
109
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
110
|
+
res.on('end', () => {
|
|
111
|
+
try {
|
|
112
|
+
const results = parseDuckDuckGoLiteHTML(data);
|
|
113
|
+
resolve(results);
|
|
114
|
+
} catch (e) {
|
|
115
|
+
reject(e);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
req.on('error', (err) => reject(err));
|
|
121
|
+
req.on('timeout', () => {
|
|
122
|
+
req.destroy();
|
|
123
|
+
reject(new Error('Timeout'));
|
|
124
|
+
});
|
|
125
|
+
req.write(postData);
|
|
126
|
+
req.end();
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function parseDuckDuckGoLiteHTML(html: string): SearchResult[] {
|
|
131
|
+
const results: SearchResult[] = [];
|
|
132
|
+
const resultRegex = /<a[^>]+href="([^"]+)"[^>]+class='result-link'[^>]*>([\s\S]*?)<\/a>[\s\S]*?class='result-snippet'[^>]*>([\s\S]*?)<\/td>/gi;
|
|
133
|
+
|
|
134
|
+
let match;
|
|
135
|
+
while ((match = resultRegex.exec(html)) !== null && results.length < 8) {
|
|
136
|
+
const rawLink = match[1];
|
|
137
|
+
const title = decodeEntities(stripHtml(match[2]));
|
|
138
|
+
const snippet = decodeEntities(stripHtml(match[3]));
|
|
139
|
+
if (!title || !snippet) continue;
|
|
140
|
+
|
|
141
|
+
let link = rawLink;
|
|
142
|
+
if (link.startsWith('//')) {
|
|
143
|
+
link = 'https:' + link;
|
|
144
|
+
}
|
|
145
|
+
if (link.includes('uddg=')) {
|
|
146
|
+
const uddgMatch = link.match(/uddg=([^&]+)/);
|
|
147
|
+
if (uddgMatch) {
|
|
148
|
+
link = decodeURIComponent(uddgMatch[1]);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
results.push({ title, link, snippet });
|
|
152
|
+
}
|
|
153
|
+
return results;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* DuckDuckGo Instant Answer API
|
|
158
|
+
*/
|
|
159
|
+
function fetchDuckDuckGoApi(query: string): Promise<SearchResult[]> {
|
|
160
|
+
return new Promise((resolve, reject) => {
|
|
161
|
+
const encodedQuery = encodeURIComponent(query);
|
|
162
|
+
const url = `https://api.duckduckgo.com/?q=${encodedQuery}&format=json&no_html=1&skip_disambig=1`;
|
|
163
|
+
|
|
164
|
+
const options = {
|
|
165
|
+
headers: {
|
|
166
|
+
'User-Agent': 'ClosedLoopCodingAgent/1.0 (contact: akara@example.com)'
|
|
167
|
+
},
|
|
168
|
+
timeout: 5000
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
https.get(url, options, (res) => {
|
|
172
|
+
if (res.statusCode !== 200) {
|
|
173
|
+
return reject(new Error(`Status ${res.statusCode}`));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let data = '';
|
|
177
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
178
|
+
res.on('end', () => {
|
|
179
|
+
try {
|
|
180
|
+
const json = JSON.parse(data);
|
|
181
|
+
const results: SearchResult[] = [];
|
|
182
|
+
|
|
183
|
+
if (json.AbstractText && json.AbstractURL) {
|
|
184
|
+
results.push({
|
|
185
|
+
title: json.Heading || query,
|
|
186
|
+
link: json.AbstractURL,
|
|
187
|
+
snippet: json.AbstractText
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (json.RelatedTopics && Array.isArray(json.RelatedTopics)) {
|
|
192
|
+
for (const topic of json.RelatedTopics) {
|
|
193
|
+
if (results.length >= 5) break;
|
|
194
|
+
if (topic.Text && topic.FirstURL) {
|
|
195
|
+
results.push({
|
|
196
|
+
title: topic.Text.split(' - ')[0] || 'Topic Details',
|
|
197
|
+
link: topic.FirstURL,
|
|
198
|
+
snippet: topic.Text
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
resolve(results);
|
|
205
|
+
} catch (e) {
|
|
206
|
+
reject(e);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
}).on('error', (err) => reject(err));
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Wikipedia Search API
|
|
215
|
+
*/
|
|
216
|
+
function fetchWikipedia(query: string): Promise<SearchResult[]> {
|
|
217
|
+
return new Promise((resolve, reject) => {
|
|
218
|
+
const encodedQuery = encodeURIComponent(query);
|
|
219
|
+
const url = `https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=${encodedQuery}&format=json&origin=*`;
|
|
220
|
+
|
|
221
|
+
const options = {
|
|
222
|
+
headers: {
|
|
223
|
+
'User-Agent': 'ClosedLoopCodingAgent/1.0 (contact: akara@example.com)'
|
|
224
|
+
},
|
|
225
|
+
timeout: 5000
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
https.get(url, options, (res) => {
|
|
229
|
+
if (res.statusCode !== 200) {
|
|
230
|
+
return reject(new Error(`Status ${res.statusCode}`));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let data = '';
|
|
234
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
235
|
+
res.on('end', () => {
|
|
236
|
+
try {
|
|
237
|
+
const json = JSON.parse(data);
|
|
238
|
+
const results: SearchResult[] = [];
|
|
239
|
+
if (json.query && Array.isArray(json.query.search)) {
|
|
240
|
+
for (const item of json.query.search) {
|
|
241
|
+
if (results.length >= 8) break;
|
|
242
|
+
const link = `https://en.wikipedia.org/wiki/${encodeURIComponent(item.title.replace(/ /g, '_'))}`;
|
|
243
|
+
const snippet = stripHtml(item.snippet);
|
|
244
|
+
results.push({
|
|
245
|
+
title: item.title,
|
|
246
|
+
link,
|
|
247
|
+
snippet
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
resolve(results);
|
|
252
|
+
} catch (e) {
|
|
253
|
+
reject(e);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
}).on('error', (err) => reject(err));
|
|
257
|
+
});
|
|
258
|
+
}
|