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,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
|
+
}
|