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,296 @@
1
+ import * as path from 'path';
2
+ import * as fs from 'fs';
3
+ import { runCommand } from '../tools/shell-tools';
4
+ import * as fileTools from '../tools/file-tools';
5
+ import * as rulez from '../orchestrator/rulez';
6
+ import * as sahoo from '../orchestrator/sahoo-gateway';
7
+ import { generateRepoMap } from '../tools/repo-map';
8
+ import { searchWeb } from '../tools/search-tools';
9
+ import { AutogenesisEngine } from '../orchestrator/autogenesis';
10
+
11
+ async function runTest(name: string, fn: () => Promise<void> | void) {
12
+ console.log(`\x1b[36m[Test]\x1b[0m Running: ${name}...`);
13
+ try {
14
+ await fn();
15
+ console.log(`\x1b[32m[Pass]\x1b[0m ${name}\n`);
16
+ } catch (err: any) {
17
+ console.error(`\x1b[31m[Fail]\x1b[0m ${name}`);
18
+ console.error(err.stack || err.message);
19
+ console.error('');
20
+ process.exit(1);
21
+ }
22
+ }
23
+
24
+ async function main() {
25
+ console.log('\x1b[35m=== RUNNING BINARY ASSERTION TESTS ===\x1b[0m\n');
26
+
27
+ // Test 1: TypeScript Compilation
28
+ await runTest('TypeScript Compilation Check', async () => {
29
+ console.log('Running tsc compilation...');
30
+ const result = await runCommand('npx tsc --noEmit');
31
+ if (result.exitCode !== 0) {
32
+ throw new Error(`TypeScript compilation failed!\nStdout:\n${result.stdout}\nStderr:\n${result.stderr}`);
33
+ }
34
+ });
35
+
36
+ // Test 2: File Tools Read/Write/Edit
37
+ await runTest('File Tools Functionality', () => {
38
+ const tempFile = 'temp-test-file.txt';
39
+ const content = 'Line 1\nTarget content here\nLine 3';
40
+
41
+ // Test Write
42
+ fileTools.writeFile(tempFile, content);
43
+
44
+ // Test Read
45
+ const readBack = fileTools.readFile(tempFile);
46
+ if (readBack !== content) {
47
+ fs.unlinkSync(tempFile);
48
+ throw new Error(`Read content did not match written content.`);
49
+ }
50
+
51
+ // Test Edit (Precise replacement)
52
+ fileTools.editFile(tempFile, 'Target content here', 'Replaced content');
53
+ const readAfterEdit = fileTools.readFile(tempFile);
54
+ const expectedContent = 'Line 1\nReplaced content\nLine 3';
55
+
56
+ // Clean up
57
+ fs.unlinkSync(tempFile);
58
+
59
+ if (readAfterEdit !== expectedContent) {
60
+ throw new Error(`Edit did not replace target content correctly. Got:\n${readAfterEdit}`);
61
+ }
62
+ });
63
+
64
+ // Test 3: List Directory
65
+ await runTest('List Directory Tool', () => {
66
+ const items = fileTools.listDir('.');
67
+ const hasSrc = items.some(item => item.name === 'src' && item.isDir);
68
+ const hasPackageJson = items.some(item => item.name === 'package.json' && !item.isDir);
69
+
70
+ if (!hasSrc || !hasPackageJson) {
71
+ throw new Error(`Directory listing did not return expected workspace files.`);
72
+ }
73
+ });
74
+
75
+ // Test 4: Grep Search Tool
76
+ await runTest('Grep Search Tool', () => {
77
+ const results = fileTools.grepSearch('AutogenesisEngine', 'src');
78
+ if (results.length === 0) {
79
+ throw new Error(`Grep search could not find occurrences of 'AutogenesisEngine' in src directory.`);
80
+ }
81
+ const hasIndexMatch = results.some(res => res.filePath.includes('index.ts'));
82
+ if (!hasIndexMatch) {
83
+ throw new Error(`Grep search missed match in src/index.ts`);
84
+ }
85
+ });
86
+
87
+ // Test 5: RuleZ Policy Engine
88
+ await runTest('RuleZ Policy Block and Allow Checks', () => {
89
+ // Unsafe commands
90
+ const blockCmd = rulez.checkCommand('rm -rf /');
91
+ if (blockCmd.allowed || blockCmd.exitCode !== 2) {
92
+ throw new Error(`RuleZ failed to block destructive command 'rm -rf /'.`);
93
+ }
94
+
95
+ const blockPush = rulez.checkCommand('git push --force');
96
+ if (blockPush.allowed) {
97
+ throw new Error('RuleZ failed to block git push --force');
98
+ }
99
+
100
+ // Safe command
101
+ const allowCmd = rulez.checkCommand('npm run build');
102
+ if (!allowCmd.allowed || allowCmd.exitCode !== 0) {
103
+ throw new Error('RuleZ blocked a safe command npm run build');
104
+ }
105
+
106
+ // Unsafe file edit (eval)
107
+ const blockEval = rulez.checkFileEdit('src/test.ts', 'const x = eval("1+1");');
108
+ if (blockEval.allowed || blockEval.exitCode !== 2) {
109
+ throw new Error('RuleZ failed to block file edit containing eval().');
110
+ }
111
+
112
+ // Unsafe file edit (secrets)
113
+ const blockSecret = rulez.checkFileEdit('src/secrets.ts', 'const token = "tp-sxbionmkvlslw9t9vnjmuwey6a47cwwnfi6jtd8zcq65bnv1";');
114
+ if (blockSecret.allowed || blockSecret.exitCode !== 2) {
115
+ throw new Error('RuleZ failed to block file edit containing a MiMo token.');
116
+ }
117
+
118
+ // Sandbox Path Isolation checks
119
+ const blockRelativeOutside = rulez.checkFileEdit('../outside.txt', 'hello');
120
+ if (blockRelativeOutside.allowed || blockRelativeOutside.exitCode !== 2) {
121
+ throw new Error('RuleZ failed to block file edit outside the workspace (relative path).');
122
+ }
123
+
124
+ const blockAbsoluteOutside = rulez.checkFileEdit('C:\\Users\\akara\\outside.txt', 'hello');
125
+ if (blockAbsoluteOutside.allowed || blockAbsoluteOutside.exitCode !== 2) {
126
+ throw new Error('RuleZ failed to block file edit outside the workspace (absolute path).');
127
+ }
128
+
129
+ const allowInside = rulez.checkFileEdit('src/tools/math-helper.ts', 'const x = 1;');
130
+ if (!allowInside.allowed || allowInside.exitCode !== 0) {
131
+ throw new Error('RuleZ blocked a safe file edit inside the workspace.');
132
+ }
133
+ });
134
+
135
+ // Test 6: SAHOO Safeguarding Gateway Checks
136
+ await runTest('SAHOO Gateway Drifts and Constraints', () => {
137
+ const beforeCode = 'function calculateSum(a: number, b: number) {\n return a + b;\n}';
138
+ const afterCode = 'function calculateSum(a: number, b: number) {\n return a + b;\n}';
139
+
140
+ // Test identical code drift is 0
141
+ const semDriftZero = sahoo.calculateSemanticDrift(beforeCode, afterCode);
142
+ if (semDriftZero !== 0) {
143
+ throw new Error(`Semantic drift for identical strings should be 0, got ${semDriftZero}`);
144
+ }
145
+
146
+ // Test different code drift
147
+ const veryDifferent = 'const value = "hello world";\nconsole.log(value);';
148
+ const semDriftDiff = sahoo.calculateSemanticDrift(beforeCode, veryDifferent);
149
+ if (semDriftDiff === 0) {
150
+ throw new Error('Semantic drift for different strings should be non-zero.');
151
+ }
152
+
153
+ // Test CPS halts on compilation failure
154
+ const evalFail = sahoo.evaluateSahoo('src/temp.ts', beforeCode, afterCode, false, true);
155
+ if (evalFail.passed) {
156
+ throw new Error('SAHOO should reject changes if compilation failed.');
157
+ }
158
+
159
+ // Test CPS halts on eval injection
160
+ const evalWithViolation = sahoo.evaluateSahoo('src/temp.ts', beforeCode, 'const x = eval("5");', true, true);
161
+ if (evalWithViolation.passed) {
162
+ throw new Error('SAHOO should reject changes containing eval dynamic code.');
163
+ }
164
+ });
165
+
166
+ // Test 7: Repository Map Generation
167
+ await runTest('Repository Map Generation Check', () => {
168
+ const repoMap = generateRepoMap();
169
+ if (!repoMap) {
170
+ throw new Error('Repository map returned empty output.');
171
+ }
172
+ if (!repoMap.includes('File: src/orchestrator/meta-agent.ts') && !repoMap.includes('File: src/orchestrator/meta-agent')) {
173
+ throw new Error('Repository map is missing expected source files.');
174
+ }
175
+ if (!repoMap.includes('runSelfImprovingTask')) {
176
+ throw new Error('Repository map is missing expected signatures like runSelfImprovingTask.');
177
+ }
178
+ });
179
+
180
+ // Test 8: Web Search Tool Integration
181
+ await runTest('Web Search Tool Integration Check', async () => {
182
+ const results = await searchWeb('javascript nodejs');
183
+ if (!results) {
184
+ throw new Error('Web search returned empty output.');
185
+ }
186
+ // Check if it got actual results or a readable connection error
187
+ if (!results.includes('results') && !results.includes('Error')) {
188
+ throw new Error(`Web search output format unexpected: ${results}`);
189
+ }
190
+ console.log(`Web search test output snippet: ${results.substring(0, 150)}...`);
191
+ });
192
+
193
+ // Test 9: Autonomous Refactor Proposal Generation Check
194
+ await runTest('Autonomous Refactor Proposal Scan Check', async () => {
195
+ const engine = new AutogenesisEngine();
196
+ const proposal = await engine.getAutonomousRefactorProposal();
197
+ if (!proposal.targetFile) {
198
+ throw new Error('Refactoring proposal targetFile is missing.');
199
+ }
200
+ if (!proposal.refactorGoal) {
201
+ throw new Error('Refactoring proposal refactorGoal is missing.');
202
+ }
203
+ console.log(`Scan test proposal generated - Target: ${proposal.targetFile} | Goal: ${proposal.refactorGoal}`);
204
+ });
205
+
206
+ // Test 10: Prompt Benchmarking Score Calculation Check
207
+ await runTest('Prompt Benchmarking Score Calculation Check', () => {
208
+ const engine = new AutogenesisEngine();
209
+
210
+ // Score with zero parameters should be 0
211
+ const zeroScore = engine.calculatePromptScore(0, 0);
212
+ if (zeroScore !== 0) {
213
+ throw new Error(`Expected zero score for 0 time/tokens, got ${zeroScore}`);
214
+ }
215
+
216
+ // Score with valid parameters
217
+ const scoreA = engine.calculatePromptScore(10, 100); // 10*0.4 + 100*0.6 = 64 => 100000/64 = 1562.5
218
+ const scoreB = engine.calculatePromptScore(20, 200); // 20*0.4 + 200*0.6 = 128 => 100000/128 = 781.25
219
+
220
+ if (Math.abs(scoreA - 1562.5) > 0.01) {
221
+ throw new Error(`Score A calculation incorrect: got ${scoreA}, expected 1562.5`);
222
+ }
223
+ if (Math.abs(scoreB - 781.25) > 0.01) {
224
+ throw new Error(`Score B calculation incorrect: got ${scoreB}, expected 781.25`);
225
+ }
226
+
227
+ // A better performance (less time, less tokens) must produce a higher score
228
+ if (scoreA <= scoreB) {
229
+ throw new Error(`Expected scoreA (${scoreA}) to be higher than scoreB (${scoreB})`);
230
+ }
231
+ });
232
+
233
+ // Test 11: Campbell Regime Write-Protection Policy Check
234
+ await runTest('Campbell Regime Write-Protection Policy Check', () => {
235
+ // Attempt edit of rulez.ts
236
+ const blockRulez = rulez.checkFileEdit('src/orchestrator/rulez.ts', 'const debug = true;');
237
+ if (blockRulez.allowed || blockRulez.exitCode !== 2) {
238
+ throw new Error('RuleZ failed to block modifications to rulez.ts safety checker.');
239
+ }
240
+
241
+ // Attempt edit of sahoo-gateway.ts
242
+ const blockSahoo = rulez.checkFileEdit('src/orchestrator/sahoo-gateway.ts', 'const bypass = true;');
243
+ if (blockSahoo.allowed || blockSahoo.exitCode !== 2) {
244
+ throw new Error('RuleZ failed to block modifications to sahoo-gateway.ts safety gate.');
245
+ }
246
+
247
+ // Attempt edit of run-tests.ts
248
+ const blockTests = rulez.checkFileEdit('src/tests/run-tests.ts', 'const pass = true;');
249
+ if (blockTests.allowed || blockTests.exitCode !== 2) {
250
+ throw new Error('RuleZ failed to block modifications to run-tests.ts test suite.');
251
+ }
252
+
253
+ // Attempt edit of package.json
254
+ const blockPackage = rulez.checkFileEdit('package.json', '{}');
255
+ if (blockPackage.allowed || blockPackage.exitCode !== 2) {
256
+ throw new Error('RuleZ failed to block modifications to package.json.');
257
+ }
258
+
259
+ // Attempt edit of .env
260
+ const blockEnv = rulez.checkFileEdit('.env', 'API_KEY=xxx');
261
+ if (blockEnv.allowed || blockEnv.exitCode !== 2) {
262
+ throw new Error('RuleZ failed to block modifications to .env.');
263
+ }
264
+ });
265
+
266
+ // Dynamic User-Created Tests Loading
267
+ const dynamicTestsDir = path.join(__dirname, 'dynamic');
268
+ if (fs.existsSync(dynamicTestsDir)) {
269
+ const files = fs.readdirSync(dynamicTestsDir).filter(f => f.endsWith('.js'));
270
+ if (files.length > 0) {
271
+ console.log('\n\x1b[35m=== RUNNING DYNAMIC USER-GENERATED TESTS ===\x1b[0m\n');
272
+ for (const file of files) {
273
+ const filePath = path.join(dynamicTestsDir, file);
274
+ await runTest(`Dynamic Test File: ${file}`, async () => {
275
+ const testModule = require(filePath);
276
+ if (typeof testModule === 'function') {
277
+ await testModule();
278
+ } else if (testModule && typeof testModule.run === 'function') {
279
+ await testModule.run();
280
+ } else if (testModule && typeof testModule.default === 'function') {
281
+ await testModule.default();
282
+ } else {
283
+ throw new Error(`Dynamic test file ${file} does not export a default function, run function, or standard module.exports function.`);
284
+ }
285
+ });
286
+ }
287
+ }
288
+ }
289
+
290
+ console.log('\x1b[32;1m=== ALL BINARY ASSERTION TESTS PASSED ===\x1b[0m');
291
+ }
292
+
293
+ main().catch(err => {
294
+ console.error('Test execution failed:', err);
295
+ process.exit(1);
296
+ });
@@ -0,0 +1,27 @@
1
+ import { runCommand } from './shell-tools';
2
+
3
+ /**
4
+ * Generates and formats a colorized Git diff for a specific file or the entire workspace.
5
+ */
6
+ export async function getFormattedDiff(filePath?: string): Promise<string> {
7
+ const target = filePath ? `"${filePath}"` : '';
8
+
9
+ // Use git's native diffing with forced ANSI color codes
10
+ const cmd = `git diff --color=always ${target}`;
11
+ const result = await runCommand(cmd);
12
+
13
+ if (result.exitCode !== 0) {
14
+ return `\x1b[31mError generating diff: ${result.stderr || 'unknown error'}\x1b[0m`;
15
+ }
16
+
17
+ const output = result.stdout.trim();
18
+ if (!output) {
19
+ return '\x1b[90m(No changes detected)\x1b[0m';
20
+ }
21
+
22
+ // Wrap the diff output inside a visual container box
23
+ const border = '\x1b[36m┌────────────────────────────────────────────────────────────┐\x1b[0m';
24
+ const footer = '\x1b[36m└────────────────────────────────────────────────────────────┘\x1b[0m';
25
+
26
+ return `${border}\n\x1b[36m│ CODE CHANGES VISUALIZATION │\x1b[0m\n${border}\n${output}\n${footer}`;
27
+ }
@@ -0,0 +1,187 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+
4
+ /**
5
+ * Resolves a path relative to the workspace root and ensures it remains inside the workspace.
6
+ */
7
+ export function resolveWorkspacePath(filePath: string, workspaceRoot: string = process.cwd()): string {
8
+ const absolutePath = path.isAbsolute(filePath)
9
+ ? filePath
10
+ : path.resolve(workspaceRoot, filePath);
11
+
12
+ // Security check: ensure path does not escape workspace (e.g. using ../..)
13
+ const relative = path.relative(workspaceRoot, absolutePath);
14
+ if (relative.startsWith('..') || path.isAbsolute(relative)) {
15
+ throw new Error(`Access denied: Path '${filePath}' is outside the workspace root.`);
16
+ }
17
+ return absolutePath;
18
+ }
19
+
20
+ /**
21
+ * Reads the content of a file.
22
+ */
23
+ export function readFile(filePath: string, workspaceRoot?: string): string {
24
+ const resolved = resolveWorkspacePath(filePath, workspaceRoot);
25
+ if (!fs.existsSync(resolved)) {
26
+ throw new Error(`File not found: ${filePath}`);
27
+ }
28
+ return fs.readFileSync(resolved, 'utf-8');
29
+ }
30
+
31
+ /**
32
+ * Checks if a file exists at the given path.
33
+ */
34
+ export function fileExists(filePath: string, workspaceRoot?: string): boolean {
35
+ const resolved = resolveWorkspacePath(filePath, workspaceRoot);
36
+ return fs.existsSync(resolved);
37
+ }
38
+
39
+ /**
40
+ * Creates or overwrites a file with content.
41
+ */
42
+ export function writeFile(filePath: string, content: string, workspaceRoot?: string): void {
43
+ const resolved = resolveWorkspacePath(filePath, workspaceRoot);
44
+ const dir = path.dirname(resolved);
45
+ if (!fs.existsSync(dir)) {
46
+ fs.mkdirSync(dir, { recursive: true });
47
+ }
48
+ fs.writeFileSync(resolved, content, 'utf-8');
49
+ }
50
+
51
+ function escapeRegExp(str: string): string {
52
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
53
+ }
54
+
55
+ /**
56
+ * Replaces a target string with replacement content inside a file.
57
+ * Falls back to fuzzy whitespace-insensitive matching if exact match is not found.
58
+ */
59
+ export function editFile(filePath: string, targetContent: string, replacementContent: string, workspaceRoot?: string): void {
60
+ const resolved = resolveWorkspacePath(filePath, workspaceRoot);
61
+ const content = readFile(resolved, workspaceRoot);
62
+
63
+ // Detect if file primarily uses CRLF
64
+ const isCrlf = content.includes('\r\n');
65
+
66
+ // Normalize both content and target contents to LF (\n)
67
+ const normalizedContent = content.replace(/\r\n/g, '\n');
68
+ const normalizedTarget = targetContent.replace(/\r\n/g, '\n');
69
+ const normalizedReplacement = replacementContent.replace(/\r\n/g, '\n');
70
+
71
+ const occurrences = normalizedContent.split(normalizedTarget).length - 1;
72
+ if (occurrences === 1) {
73
+ let newContent = normalizedContent.replace(normalizedTarget, normalizedReplacement);
74
+ if (isCrlf) {
75
+ newContent = newContent.replace(/\r\n/g, '\n').replace(/\n/g, '\r\n');
76
+ }
77
+ fs.writeFileSync(resolved, newContent, 'utf-8');
78
+ return;
79
+ }
80
+
81
+ // Exact match failed or is ambiguous. Try fuzzy whitespace regex matching.
82
+ const tokens = targetContent.trim().split(/\s+/);
83
+ if (tokens.length === 0 || tokens.join('') === '') {
84
+ throw new Error(`Target content to edit is empty or whitespace only.`);
85
+ }
86
+
87
+ const regexStr = tokens.map(tok => escapeRegExp(tok)).join('\\s+');
88
+ const regex = new RegExp(regexStr, 'g');
89
+
90
+ const matches = content.match(regex);
91
+ const matchCount = matches ? matches.length : 0;
92
+
93
+ if (matchCount === 0) {
94
+ throw new Error(`Target content to edit was not found in file: ${filePath} (tried exact and fuzzy whitespace matching).`);
95
+ } else if (matchCount > 1) {
96
+ throw new Error(`Target content to edit is ambiguous (found ${matchCount} occurrences fuzzy-matching) in file: ${filePath}. Please provide more context.`);
97
+ }
98
+
99
+ // Perform the single replace
100
+ const newContent = content.replace(regex, replacementContent);
101
+ fs.writeFileSync(resolved, newContent, 'utf-8');
102
+ }
103
+
104
+ /**
105
+ * Lists the contents of a directory (files and subdirectories).
106
+ */
107
+ export interface FileItem {
108
+ name: string;
109
+ isDir: boolean;
110
+ sizeBytes?: number;
111
+ }
112
+
113
+ export function listDir(dirPath: string = '.', workspaceRoot?: string): FileItem[] {
114
+ const resolved = resolveWorkspacePath(dirPath, workspaceRoot);
115
+ if (!fs.existsSync(resolved)) {
116
+ throw new Error(`Directory not found: ${dirPath}`);
117
+ }
118
+ const stats = fs.statSync(resolved);
119
+ if (!stats.isDirectory()) {
120
+ throw new Error(`Path is not a directory: ${dirPath}`);
121
+ }
122
+
123
+ const items = fs.readdirSync(resolved);
124
+ return items.map(name => {
125
+ const itemPath = path.join(resolved, name);
126
+ const itemStats = fs.statSync(itemPath);
127
+ const isDir = itemStats.isDirectory();
128
+ return {
129
+ name,
130
+ isDir,
131
+ sizeBytes: isDir ? undefined : itemStats.size
132
+ };
133
+ });
134
+ }
135
+
136
+ /**
137
+ * Simple grep search implementation to find matches in text files inside a directory.
138
+ */
139
+ export interface SearchResult {
140
+ filePath: string;
141
+ lineNumber: number;
142
+ lineContent: string;
143
+ }
144
+
145
+ export function grepSearch(pattern: string, searchDir: string = '.', workspaceRoot: string = process.cwd()): SearchResult[] {
146
+ const resolvedDir = resolveWorkspacePath(searchDir, workspaceRoot);
147
+ const results: SearchResult[] = [];
148
+ const regex = new RegExp(pattern, 'i'); // Case-insensitive match
149
+
150
+ function walk(currentDir: string) {
151
+ const files = fs.readdirSync(currentDir);
152
+ for (const file of files) {
153
+ const fullPath = path.join(currentDir, file);
154
+
155
+ // Skip node_modules, .git, dist
156
+ if (file === 'node_modules' || file === '.git' || file === 'dist' || file === '.gemini') {
157
+ continue;
158
+ }
159
+
160
+ const stats = fs.statSync(fullPath);
161
+ if (stats.isDirectory()) {
162
+ walk(fullPath);
163
+ } else if (stats.isFile()) {
164
+ // Only search text-like files
165
+ const ext = path.extname(file).toLowerCase();
166
+ if (['.ts', '.js', '.json', '.md', '.txt', '.env', '.html', '.css'].includes(ext)) {
167
+ const content = fs.readFileSync(fullPath, 'utf-8');
168
+ const lines = content.split('\n');
169
+ lines.forEach((line, index) => {
170
+ if (regex.test(line)) {
171
+ results.push({
172
+ filePath: path.relative(workspaceRoot, fullPath).replace(/\\/g, '/'),
173
+ lineNumber: index + 1,
174
+ lineContent: line.trim()
175
+ });
176
+ }
177
+ });
178
+ }
179
+ }
180
+ }
181
+ }
182
+
183
+ if (fs.existsSync(resolvedDir)) {
184
+ walk(resolvedDir);
185
+ }
186
+ return results.slice(0, 50); // Cap at 50 results
187
+ }