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.

Files changed (86) hide show
  1. package/dist/dashboard/server.js +237 -0
  2. package/dist/index.js +272 -0
  3. package/dist/orchestrator/agent-prompts.js +42 -0
  4. package/dist/orchestrator/autogenesis.js +973 -0
  5. package/dist/orchestrator/dgm-archive.js +223 -0
  6. package/dist/orchestrator/event-stream.js +103 -0
  7. package/dist/orchestrator/fitness-evaluator.js +99 -0
  8. package/dist/orchestrator/meta-agent.js +421 -0
  9. package/dist/orchestrator/microagent-registry.js +134 -0
  10. package/dist/orchestrator/mutation-strategies.js +174 -0
  11. package/dist/orchestrator/prompt-benchmark.js +102 -0
  12. package/dist/orchestrator/prompt-optimizer.js +169 -0
  13. package/dist/orchestrator/refactor-scanner.js +222 -0
  14. package/dist/orchestrator/research-manager.js +104 -0
  15. package/dist/orchestrator/rulez.js +135 -0
  16. package/dist/orchestrator/sahoo-gateway.js +261 -0
  17. package/dist/orchestrator/state-manager.js +121 -0
  18. package/dist/orchestrator/task-agent.js +444 -0
  19. package/dist/orchestrator/telegram-bot.js +374 -0
  20. package/dist/orchestrator/types.js +2 -0
  21. package/dist/tests/dynamic/dependencies.test.js +37 -0
  22. package/dist/tests/dynamic/dummy.test.js +7 -0
  23. package/dist/tests/dynamic/fuzzy-patch.test.js +68 -0
  24. package/dist/tests/dynamic/indexer.test.js +60 -0
  25. package/dist/tests/dynamic/openhands.test.js +83 -0
  26. package/dist/tests/dynamic/skills.test.js +88 -0
  27. package/dist/tests/run-tests.js +294 -0
  28. package/dist/tools/diff-tools.js +24 -0
  29. package/dist/tools/file-tools.js +191 -0
  30. package/dist/tools/indexer.js +301 -0
  31. package/dist/tools/math-helper.js +6 -0
  32. package/dist/tools/repo-map.js +122 -0
  33. package/dist/tools/search-tools.js +271 -0
  34. package/dist/tools/shell-tools.js +75 -0
  35. package/dist/tools/skills.js +122 -0
  36. package/dist/tools/tui-tools.js +82 -0
  37. package/docs/AI_Arch_Opt_Anti_Gaming.md +227 -0
  38. package/docs/AI_Self_Improvement_Safety.md +457 -0
  39. package/docs/Anthropic AI Agents_ Capabilities and Concerns.md +134 -0
  40. package/docs/Auto_ClosedLoop_AI_Agent.md +415 -0
  41. package/docs/Autonomous AI Agents_ Closing the Loop.docx +0 -0
  42. package/docs/Secure_AI_Sandbox_Framework.md +358 -0
  43. package/docs/skills/add-file-existence-check-utility.json +9 -0
  44. package/docs/skills/add-utility-function-for-file-existence-check.json +9 -0
  45. package/docs/skills/add-utility-function-to-module.json +9 -0
  46. package/docs/skills/extract-command-runner-utility.json +9 -0
  47. package/docs/skills/file-existence-check-utility.json +9 -0
  48. package/package.json +36 -0
  49. package/src/dashboard/public/index.css +1334 -0
  50. package/src/dashboard/public/index.html +385 -0
  51. package/src/dashboard/public/index.js +1059 -0
  52. package/src/dashboard/server.ts +209 -0
  53. package/src/index.ts +256 -0
  54. package/src/orchestrator/agent-prompts.ts +43 -0
  55. package/src/orchestrator/autogenesis.ts +1078 -0
  56. package/src/orchestrator/dgm-archive.ts +257 -0
  57. package/src/orchestrator/event-stream.ts +90 -0
  58. package/src/orchestrator/fitness-evaluator.ts +154 -0
  59. package/src/orchestrator/meta-agent.ts +434 -0
  60. package/src/orchestrator/microagent-registry.ts +115 -0
  61. package/src/orchestrator/microagents/git-helper.md +11 -0
  62. package/src/orchestrator/microagents/test-fixer.md +10 -0
  63. package/src/orchestrator/microagents/typescript-expert.md +11 -0
  64. package/src/orchestrator/mutation-strategies.ts +214 -0
  65. package/src/orchestrator/research-manager.ts +88 -0
  66. package/src/orchestrator/rulez.ts +118 -0
  67. package/src/orchestrator/sahoo-gateway.ts +300 -0
  68. package/src/orchestrator/state-manager.ts +161 -0
  69. package/src/orchestrator/system-prompt.txt +1 -0
  70. package/src/orchestrator/task-agent.ts +461 -0
  71. package/src/orchestrator/telegram-bot.ts +358 -0
  72. package/src/tests/dynamic/dependencies.test.ts +48 -0
  73. package/src/tests/dynamic/dummy.test.ts +4 -0
  74. package/src/tests/dynamic/fuzzy-patch.test.ts +42 -0
  75. package/src/tests/dynamic/indexer.test.ts +31 -0
  76. package/src/tests/dynamic/openhands.test.ts +59 -0
  77. package/src/tests/dynamic/skills.test.ts +63 -0
  78. package/src/tests/run-tests.ts +296 -0
  79. package/src/tools/diff-tools.ts +27 -0
  80. package/src/tools/file-tools.ts +187 -0
  81. package/src/tools/indexer.ts +325 -0
  82. package/src/tools/repo-map.ts +96 -0
  83. package/src/tools/search-tools.ts +258 -0
  84. package/src/tools/shell-tools.ts +90 -0
  85. package/src/tools/skills.ts +101 -0
  86. 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(/&amp;/g, '&')
22
+ .replace(/&lt;/g, '<')
23
+ .replace(/&gt;/g, '>')
24
+ .replace(/&quot;/g, '"')
25
+ .replace(/&#x27;/g, "'")
26
+ .replace(/&#x2F;/g, '/')
27
+ .replace(/&#39;/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
+ }