@axplusb/kepler 1.0.5 → 1.0.10
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.
- package/KEPLER-README.md +34 -0
- package/package.json +4 -4
- package/src/core/headless.mjs +68 -24
- package/src/core/pricing.mjs +23 -1
- package/src/core/project-artifacts.mjs +37 -0
- package/src/core/tool-executor.mjs +192 -57
- package/src/skills/installer.mjs +188 -0
- package/src/skills/loader.mjs +217 -112
- package/src/terminal/ansi.mjs +3 -5
- package/src/terminal/main.mjs +18 -0
- package/src/terminal/repl.mjs +38 -60
- package/src/terminal/skills.mjs +54 -0
- package/src/tools/bash.mjs +5 -2
- package/src/tools/project-overview.mjs +418 -0
- package/src/tools/registry.mjs +0 -16
|
@@ -5,15 +5,16 @@
|
|
|
5
5
|
* This bridge translates those into OCC tool calls and wraps the results.
|
|
6
6
|
*
|
|
7
7
|
* Safety guardrails integrated — prevents destructive operations on source code.
|
|
8
|
-
*
|
|
8
|
+
* Tools are mapped across file, search, shell, validation, and Git operations.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { createToolRegistry } from '../tools/registry.mjs';
|
|
12
12
|
import { filterOutput } from './output-filter.mjs';
|
|
13
13
|
import { validatePath, validateDelete, validateShellCommand, validateWrite } from './safety.mjs';
|
|
14
14
|
import { classifyCommand, isExitCodeError } from '../permissions/command-classifier.mjs';
|
|
15
|
-
import { ContextRetriever } from '../context/retriever.mjs';
|
|
16
15
|
import { analyzeCode } from '../context/ast-parser.mjs';
|
|
16
|
+
import { ProjectRegistry } from '../tools/project-overview.mjs';
|
|
17
|
+
import { SkillsLoader } from '../skills/loader.mjs';
|
|
17
18
|
import * as fs from 'node:fs';
|
|
18
19
|
import * as path from 'node:path';
|
|
19
20
|
import { execSync } from 'node:child_process';
|
|
@@ -21,29 +22,36 @@ import { execSync } from 'node:child_process';
|
|
|
21
22
|
/**
|
|
22
23
|
* Create a tool executor that bridges Tarang tool names to OCC tools.
|
|
23
24
|
* @param {Object} [options]
|
|
24
|
-
* @param {
|
|
25
|
+
* @param {ProjectRegistry} [options.projectRegistry] - session-owned project registry
|
|
25
26
|
* @returns {{ execute(name, args): Promise<Object>, listTools(): string[] }}
|
|
26
27
|
*/
|
|
27
|
-
export function createToolExecutor({
|
|
28
|
+
export function createToolExecutor({
|
|
29
|
+
projectRegistry = new ProjectRegistry(),
|
|
30
|
+
skillsLoader = new SkillsLoader().load(process.cwd()),
|
|
31
|
+
} = {}) {
|
|
28
32
|
const occRegistry = createToolRegistry();
|
|
33
|
+
const skillTool = occRegistry.get('Skill');
|
|
34
|
+
if (skillTool) skillTool._skillsLoader = skillsLoader;
|
|
29
35
|
let _searchCodeUsed = false; // tracks if search_code was called (for read_file nudge)
|
|
30
36
|
|
|
31
|
-
|
|
32
|
-
|
|
37
|
+
function resolvePath(p, args = {}, options = {}) {
|
|
38
|
+
return projectRegistry.resolvePath(p, args.project_id, options);
|
|
39
|
+
}
|
|
33
40
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
41
|
+
function projectRootFor(filePath) {
|
|
42
|
+
const project = projectRegistry.projectForPath(filePath);
|
|
43
|
+
if (!project) throw new Error(`No registered project contains path: ${filePath}`);
|
|
44
|
+
return project.resource.root;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function commandCwd(args = {}) {
|
|
48
|
+
return resolvePath(args.cwd || null, args);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function updateProjectIndex(filePath) {
|
|
52
|
+
try {
|
|
53
|
+
projectRegistry.projectForPath(filePath)?.retriever.updateFile(filePath);
|
|
54
|
+
} catch { /* best effort */ }
|
|
47
55
|
}
|
|
48
56
|
|
|
49
57
|
/**
|
|
@@ -105,6 +113,25 @@ export function createToolExecutor({ retriever } = {}) {
|
|
|
105
113
|
}
|
|
106
114
|
}
|
|
107
115
|
|
|
116
|
+
// ── Post-edit verification hint ──────────────────────────────
|
|
117
|
+
// Appended to edit_file/write_file results so the model knows
|
|
118
|
+
// exactly how to verify. Uses detected project commands.
|
|
119
|
+
|
|
120
|
+
function verificationHint(filePath) {
|
|
121
|
+
const project = projectRegistry.projectForPath(filePath);
|
|
122
|
+
const commands = project?.resource?.commands || {};
|
|
123
|
+
const parts = [];
|
|
124
|
+
if (commands.test) {
|
|
125
|
+
parts.push(`Run tests: ${commands.test}`);
|
|
126
|
+
}
|
|
127
|
+
if (parts.length === 0) {
|
|
128
|
+
const ext = path.extname(filePath);
|
|
129
|
+
if (ext === '.py') parts.push('Run tests: python -m pytest');
|
|
130
|
+
else if (['.js', '.ts', '.tsx', '.mjs'].includes(ext)) parts.push('Run tests: npm test');
|
|
131
|
+
}
|
|
132
|
+
return parts.length ? `\n--- Verify ---\n${parts.join('\n')}` : '';
|
|
133
|
+
}
|
|
134
|
+
|
|
108
135
|
// ── Tool mapping table ──────────────────────────────────────
|
|
109
136
|
|
|
110
137
|
const toolMap = {
|
|
@@ -115,7 +142,7 @@ export function createToolExecutor({ retriever } = {}) {
|
|
|
115
142
|
if (!shellCheck.safe) {
|
|
116
143
|
return {
|
|
117
144
|
success: false,
|
|
118
|
-
output: `BLOCKED: ${shellCheck.reason}.
|
|
145
|
+
output: `BLOCKED: ${shellCheck.reason}. Work only inside a registered project root.`,
|
|
119
146
|
_tool: 'shell', _blocked: true,
|
|
120
147
|
};
|
|
121
148
|
}
|
|
@@ -136,13 +163,14 @@ export function createToolExecutor({ retriever } = {}) {
|
|
|
136
163
|
args._riskReason = classification.reason || shellCheck.reason;
|
|
137
164
|
}
|
|
138
165
|
args._classification = classification.classification; // 'safe' or 'contained'
|
|
166
|
+
const cwd = commandCwd(args);
|
|
139
167
|
|
|
140
168
|
// Pre-check: if command is rm/unlink, verify targets exist first
|
|
141
169
|
const rmMatch = (args.command || '').match(/^rm\s+(?:-\w+\s+)*(.+)$/);
|
|
142
170
|
if (rmMatch) {
|
|
143
171
|
const targets = rmMatch[1].split(/\s+/).filter(t => !t.startsWith('-'));
|
|
144
172
|
const missing = targets.filter(t => {
|
|
145
|
-
try { return !fs.existsSync(path.resolve(
|
|
173
|
+
try { return !fs.existsSync(path.resolve(cwd, t)); } catch { return true; }
|
|
146
174
|
});
|
|
147
175
|
if (missing.length > 0 && missing.length === targets.length) {
|
|
148
176
|
return {
|
|
@@ -159,6 +187,7 @@ export function createToolExecutor({ retriever } = {}) {
|
|
|
159
187
|
command: args.command,
|
|
160
188
|
timeout: args.timeout,
|
|
161
189
|
description: args.description || `Run: ${(args.command || '').slice(0, 50)}`,
|
|
190
|
+
cwd,
|
|
162
191
|
});
|
|
163
192
|
const rawOutput = typeof result === 'string' ? result : String(result);
|
|
164
193
|
const exitMatch = rawOutput.match(/Exit code: (\d+)/);
|
|
@@ -182,7 +211,7 @@ export function createToolExecutor({ retriever } = {}) {
|
|
|
182
211
|
|
|
183
212
|
// 2. read_file → Read (with smart truncation for large files)
|
|
184
213
|
read_file: async (args) => {
|
|
185
|
-
const filePath = resolvePath(args.file_path || args.path);
|
|
214
|
+
const filePath = resolvePath(args.file_path || args.path, args);
|
|
186
215
|
const hasLineRange = args.start_line || args.end_line || args.offset || args.limit;
|
|
187
216
|
|
|
188
217
|
// Nudge: if reading shallow overview files, remind agent to search deeper
|
|
@@ -242,10 +271,10 @@ export function createToolExecutor({ retriever } = {}) {
|
|
|
242
271
|
write_file: async (args) => {
|
|
243
272
|
const rawPath = args.file_path || args.path;
|
|
244
273
|
if (!rawPath || rawPath === 'file' || rawPath.length < 3) {
|
|
245
|
-
return { success: false, output: `Error: Invalid file path "${rawPath || ''}".
|
|
274
|
+
return { success: false, output: `Error: Invalid file path "${rawPath || ''}". Register the project, then use an absolute path.`, _tool: 'write_file' };
|
|
246
275
|
}
|
|
247
|
-
const filePath = resolvePath(rawPath);
|
|
248
|
-
const writeCheck = validateWrite(filePath, args.content);
|
|
276
|
+
const filePath = resolvePath(rawPath, args, { allowMissing: true });
|
|
277
|
+
const writeCheck = validateWrite(filePath, args.content, projectRootFor(filePath));
|
|
249
278
|
if (!writeCheck.safe) {
|
|
250
279
|
return { success: false, output: `🛡️ BLOCKED: ${writeCheck.reason}`, _tool: 'write_file', _blocked: true };
|
|
251
280
|
}
|
|
@@ -260,14 +289,19 @@ export function createToolExecutor({ retriever } = {}) {
|
|
|
260
289
|
content: args.content,
|
|
261
290
|
});
|
|
262
291
|
const wrapped = wrapResult(result, 'write_file');
|
|
292
|
+
updateProjectIndex(filePath);
|
|
263
293
|
|
|
264
294
|
// Auto-lint the written file
|
|
265
295
|
const lintOutput = autoLint(filePath);
|
|
266
296
|
if (lintOutput) {
|
|
267
|
-
wrapped.output += `\n\n--- Lint
|
|
297
|
+
wrapped.output += `\n\n--- Lint ---\n${lintOutput}`;
|
|
268
298
|
wrapped.lint = lintOutput;
|
|
269
299
|
}
|
|
270
300
|
|
|
301
|
+
// Nudge: tell the model how to verify
|
|
302
|
+
const hint = verificationHint(filePath);
|
|
303
|
+
if (hint) wrapped.output += hint;
|
|
304
|
+
|
|
271
305
|
return wrapped;
|
|
272
306
|
},
|
|
273
307
|
|
|
@@ -287,10 +321,10 @@ export function createToolExecutor({ retriever } = {}) {
|
|
|
287
321
|
errors.push('Missing path in file entry');
|
|
288
322
|
continue;
|
|
289
323
|
}
|
|
290
|
-
const filePath = resolvePath(rawPath);
|
|
324
|
+
const filePath = resolvePath(rawPath, file, { allowMissing: true });
|
|
291
325
|
const content = file.content || '';
|
|
292
326
|
|
|
293
|
-
const writeCheck = validateWrite(filePath, content);
|
|
327
|
+
const writeCheck = validateWrite(filePath, content, projectRootFor(filePath));
|
|
294
328
|
if (!writeCheck.safe) {
|
|
295
329
|
errors.push(`${rawPath}: BLOCKED — ${writeCheck.reason}`);
|
|
296
330
|
continue;
|
|
@@ -309,6 +343,7 @@ export function createToolExecutor({ retriever } = {}) {
|
|
|
309
343
|
} catch { /* file may not exist yet */ }
|
|
310
344
|
|
|
311
345
|
await occRegistry.call('Write', { file_path: filePath, content });
|
|
346
|
+
updateProjectIndex(filePath);
|
|
312
347
|
results.push(rawPath);
|
|
313
348
|
} catch (err) {
|
|
314
349
|
errors.push(`${rawPath}: ${err.message}`);
|
|
@@ -335,7 +370,11 @@ export function createToolExecutor({ retriever } = {}) {
|
|
|
335
370
|
// 4. edit_file → Edit + auto-lint + auto-fallback to sed
|
|
336
371
|
edit_file: async (args) => {
|
|
337
372
|
const rawPath = args.file_path || args.path;
|
|
338
|
-
const filePath = resolvePath(rawPath);
|
|
373
|
+
const filePath = resolvePath(rawPath, args);
|
|
374
|
+
const writeCheck = validateWrite(filePath, args.replace, projectRootFor(filePath));
|
|
375
|
+
if (!writeCheck.safe) {
|
|
376
|
+
return { success: false, output: `BLOCKED: ${writeCheck.reason}`, _tool: 'edit_file', _blocked: true };
|
|
377
|
+
}
|
|
339
378
|
// OCC Edit requires Read first
|
|
340
379
|
try {
|
|
341
380
|
await occRegistry.call('Read', { file_path: filePath, limit: 1 });
|
|
@@ -366,7 +405,11 @@ content = content.replace(old, new, 1)
|
|
|
366
405
|
with open('${filePath}', 'w') as f: f.write(content)
|
|
367
406
|
print('OK: replaced')
|
|
368
407
|
"`;
|
|
369
|
-
const fallbackResult = execSync(pyCmd, {
|
|
408
|
+
const fallbackResult = execSync(pyCmd, {
|
|
409
|
+
encoding: 'utf-8',
|
|
410
|
+
timeout: 5000,
|
|
411
|
+
cwd: projectRootFor(filePath),
|
|
412
|
+
});
|
|
370
413
|
result = `Edited ${filePath} (via fallback): ${fallbackResult.trim()}`;
|
|
371
414
|
} catch (sedErr) {
|
|
372
415
|
return { success: false, output: `edit_file failed: ${editErr?.message || 'unknown'}. Fallback also failed: ${sedErr?.message || 'unknown'}. Try shell(sed) manually.`, _tool: 'edit_file' };
|
|
@@ -374,14 +417,19 @@ print('OK: replaced')
|
|
|
374
417
|
}
|
|
375
418
|
|
|
376
419
|
const wrapped = wrapResult(result, 'edit_file');
|
|
420
|
+
updateProjectIndex(filePath);
|
|
377
421
|
|
|
378
422
|
// Auto-lint the edited file
|
|
379
423
|
const lintOutput = autoLint(filePath);
|
|
380
424
|
if (lintOutput) {
|
|
381
|
-
wrapped.output += `\n\n--- Lint
|
|
425
|
+
wrapped.output += `\n\n--- Lint ---\n${lintOutput}`;
|
|
382
426
|
wrapped.lint = lintOutput;
|
|
383
427
|
}
|
|
384
428
|
|
|
429
|
+
// Nudge: tell the model how to verify
|
|
430
|
+
const hint = verificationHint(filePath);
|
|
431
|
+
if (hint) wrapped.output += hint;
|
|
432
|
+
|
|
385
433
|
return wrapped;
|
|
386
434
|
},
|
|
387
435
|
|
|
@@ -389,7 +437,7 @@ print('OK: replaced')
|
|
|
389
437
|
list_files: async (args) => {
|
|
390
438
|
const result = await occRegistry.call('Glob', {
|
|
391
439
|
pattern: args.pattern || '**/*',
|
|
392
|
-
path:
|
|
440
|
+
path: resolvePath(args.path || null, args),
|
|
393
441
|
});
|
|
394
442
|
const output = typeof result === 'string' ? result : String(result);
|
|
395
443
|
const files = output.split('\n').filter(Boolean);
|
|
@@ -407,7 +455,24 @@ print('OK: replaced')
|
|
|
407
455
|
const query = args.query || args.pattern;
|
|
408
456
|
if (!query) return { success: false, output: 'query required', _tool: 'search_code' };
|
|
409
457
|
|
|
410
|
-
|
|
458
|
+
let project;
|
|
459
|
+
if (args.project_id) {
|
|
460
|
+
project = projectRegistry.get(args.project_id);
|
|
461
|
+
if (!project) {
|
|
462
|
+
return { success: false, output: `Unknown project_id: ${args.project_id}`, _tool: 'search_code' };
|
|
463
|
+
}
|
|
464
|
+
} else if (args.path) {
|
|
465
|
+
project = projectRegistry.projectForPath(resolvePath(args.path, args));
|
|
466
|
+
} else if (projectRegistry.resources().length === 1) {
|
|
467
|
+
project = projectRegistry.get(projectRegistry.resources()[0].project_id);
|
|
468
|
+
} else {
|
|
469
|
+
return {
|
|
470
|
+
success: false,
|
|
471
|
+
output: 'search_code requires project_id when multiple or no projects are registered',
|
|
472
|
+
_tool: 'search_code',
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
const searchPath = args.path ? resolvePath(args.path, args) : project.resource.root;
|
|
411
476
|
const parts = [];
|
|
412
477
|
|
|
413
478
|
// Layer 1: ripgrep — exact text matches with context
|
|
@@ -420,9 +485,9 @@ print('OK: replaced')
|
|
|
420
485
|
} catch { /* rg not found or no results */ }
|
|
421
486
|
|
|
422
487
|
// Layer 2: BM25 — semantic relevance (finds related code even without exact match)
|
|
423
|
-
if (retriever) {
|
|
424
|
-
if (!retriever.index) retriever.loadIndex();
|
|
425
|
-
const chunks = retriever.retrieve(query, 5);
|
|
488
|
+
if (project?.retriever) {
|
|
489
|
+
if (!project.retriever.index) project.retriever.loadIndex();
|
|
490
|
+
const chunks = project.retriever.retrieve(query, 5);
|
|
426
491
|
if (chunks.length > 0) {
|
|
427
492
|
const bm25Output = chunks.map(c => {
|
|
428
493
|
const score = c.score?.toFixed(2) || '?';
|
|
@@ -461,7 +526,7 @@ print('OK: replaced')
|
|
|
461
526
|
if (query.includes('*') || query.includes('?')) {
|
|
462
527
|
const result = await occRegistry.call('Glob', {
|
|
463
528
|
pattern: query,
|
|
464
|
-
path:
|
|
529
|
+
path: resolvePath(args.path || null, args),
|
|
465
530
|
});
|
|
466
531
|
const output = typeof result === 'string' ? result : String(result);
|
|
467
532
|
return {
|
|
@@ -475,7 +540,7 @@ print('OK: replaced')
|
|
|
475
540
|
// For text patterns: grep with context lines (like grep -n -C 3)
|
|
476
541
|
const result = await occRegistry.call('Grep', {
|
|
477
542
|
pattern: query,
|
|
478
|
-
path:
|
|
543
|
+
path: resolvePath(args.path || null, args),
|
|
479
544
|
output_mode: 'content',
|
|
480
545
|
'-n': true,
|
|
481
546
|
'-C': 3,
|
|
@@ -495,12 +560,12 @@ print('OK: replaced')
|
|
|
495
560
|
const pattern = args.pattern;
|
|
496
561
|
if (!pattern) return { success: false, output: 'pattern required', _tool: 'grep' };
|
|
497
562
|
|
|
498
|
-
const searchPath = args.path
|
|
563
|
+
const searchPath = resolvePath(args.path || null, args);
|
|
499
564
|
const includeFlag = args.include ? `--glob "${args.include}"` : '';
|
|
500
565
|
|
|
501
566
|
try {
|
|
502
567
|
const cmd = `rg -n -C 2 --max-count 10 --max-filesize 500K ${includeFlag} -e ${JSON.stringify(pattern)} ${JSON.stringify(searchPath)} 2>/dev/null | head -80`;
|
|
503
|
-
const output = execSync(cmd, { encoding: 'utf-8', timeout: 15000, cwd:
|
|
568
|
+
const output = execSync(cmd, { encoding: 'utf-8', timeout: 15000, cwd: searchPath }).trim();
|
|
504
569
|
if (output) {
|
|
505
570
|
return { success: true, output, _tool: 'grep' };
|
|
506
571
|
}
|
|
@@ -521,7 +586,7 @@ print('OK: replaced')
|
|
|
521
586
|
const results = [];
|
|
522
587
|
for (const p of paths) {
|
|
523
588
|
try {
|
|
524
|
-
const filePath = resolvePath(p);
|
|
589
|
+
const filePath = resolvePath(p, args);
|
|
525
590
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
526
591
|
const lines = content.split('\n').length;
|
|
527
592
|
|
|
@@ -547,12 +612,13 @@ print('OK: replaced')
|
|
|
547
612
|
// 9. delete_file + safety check
|
|
548
613
|
delete_file: async (args) => {
|
|
549
614
|
try {
|
|
550
|
-
const filePath = resolvePath(args.file_path || args.path);
|
|
551
|
-
const delCheck = validateDelete(filePath);
|
|
615
|
+
const filePath = resolvePath(args.file_path || args.path, args);
|
|
616
|
+
const delCheck = validateDelete(filePath, projectRootFor(filePath));
|
|
552
617
|
if (!delCheck.safe) {
|
|
553
618
|
return { success: false, output: `🛡️ BLOCKED: ${delCheck.reason}`, _tool: 'delete_file', _blocked: true };
|
|
554
619
|
}
|
|
555
620
|
fs.unlinkSync(filePath);
|
|
621
|
+
updateProjectIndex(filePath);
|
|
556
622
|
return { success: true, message: `Deleted ${args.path}`, _tool: 'delete_file' };
|
|
557
623
|
} catch (err) {
|
|
558
624
|
return { success: false, output: `Error: ${err.message}`, _tool: 'delete_file' };
|
|
@@ -562,7 +628,7 @@ print('OK: replaced')
|
|
|
562
628
|
// 10. get_file_info
|
|
563
629
|
get_file_info: async (args) => {
|
|
564
630
|
try {
|
|
565
|
-
const filePath = resolvePath(args.file_path || args.path);
|
|
631
|
+
const filePath = resolvePath(args.file_path || args.path, args);
|
|
566
632
|
const stat = fs.statSync(filePath);
|
|
567
633
|
return {
|
|
568
634
|
success: true,
|
|
@@ -580,14 +646,14 @@ print('OK: replaced')
|
|
|
580
646
|
// 11. validate_file (syntax check)
|
|
581
647
|
validate_file: async (args) => {
|
|
582
648
|
try {
|
|
583
|
-
const filePath = resolvePath(args.path);
|
|
649
|
+
const filePath = resolvePath(args.path, args);
|
|
584
650
|
const ext = path.extname(filePath);
|
|
585
651
|
let cmd;
|
|
586
652
|
if (ext === '.py') cmd = `python3 -m py_compile "${filePath}"`;
|
|
587
653
|
else if (ext === '.js' || ext === '.mjs') cmd = `node --check "${filePath}"`;
|
|
588
654
|
else return { success: true, valid: true, message: 'No validator for this file type', _tool: 'validate_file' };
|
|
589
655
|
|
|
590
|
-
execSync(cmd, { stdio: 'pipe' });
|
|
656
|
+
execSync(cmd, { stdio: 'pipe', cwd: projectRootFor(filePath) });
|
|
591
657
|
return { success: true, valid: true, _tool: 'validate_file' };
|
|
592
658
|
} catch (err) {
|
|
593
659
|
return { success: true, valid: false, errors: err.stderr?.toString() || err.message, _tool: 'validate_file' };
|
|
@@ -599,12 +665,13 @@ print('OK: replaced')
|
|
|
599
665
|
try {
|
|
600
666
|
let cmd = args.command;
|
|
601
667
|
if (!cmd) {
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
else if (fs.existsSync(
|
|
668
|
+
const cwd = commandCwd(args);
|
|
669
|
+
if (fs.existsSync(path.join(cwd, 'package.json'))) cmd = 'npm run build';
|
|
670
|
+
else if (fs.existsSync(path.join(cwd, 'Makefile'))) cmd = 'make';
|
|
671
|
+
else if (fs.existsSync(path.join(cwd, 'Cargo.toml'))) cmd = 'cargo build';
|
|
605
672
|
else return { success: false, output: 'No build system detected', _tool: 'validate_build' };
|
|
606
673
|
}
|
|
607
|
-
const output = execSync(cmd, { stdio: 'pipe', timeout: 120_000 }).toString();
|
|
674
|
+
const output = execSync(cmd, { stdio: 'pipe', timeout: 120_000, cwd: commandCwd(args) }).toString();
|
|
608
675
|
return { success: true, output, _tool: 'validate_build' };
|
|
609
676
|
} catch (err) {
|
|
610
677
|
return { success: false, output: err.stderr?.toString() || err.message, _tool: 'validate_build' };
|
|
@@ -614,7 +681,9 @@ print('OK: replaced')
|
|
|
614
681
|
// 13. validate_structure
|
|
615
682
|
validate_structure: async (args) => {
|
|
616
683
|
const expected = args.expected || [];
|
|
617
|
-
const missing = expected.filter(f =>
|
|
684
|
+
const missing = expected.filter(f =>
|
|
685
|
+
!fs.existsSync(resolvePath(f, args, { allowMissing: true }))
|
|
686
|
+
);
|
|
618
687
|
return {
|
|
619
688
|
success: missing.length === 0,
|
|
620
689
|
missing,
|
|
@@ -626,14 +695,14 @@ print('OK: replaced')
|
|
|
626
695
|
// 14. lint_check
|
|
627
696
|
lint_check: async (args) => {
|
|
628
697
|
try {
|
|
629
|
-
const filePath = resolvePath(args.file_path || args.path);
|
|
698
|
+
const filePath = resolvePath(args.file_path || args.path, args);
|
|
630
699
|
const ext = path.extname(filePath);
|
|
631
700
|
let cmd;
|
|
632
701
|
if (ext === '.py') cmd = `python3 -m ruff check "${filePath}" 2>&1 || true`;
|
|
633
702
|
else if (['.js', '.mjs', '.ts', '.tsx'].includes(ext)) cmd = `npx eslint "${filePath}" 2>&1 || true`;
|
|
634
703
|
else return { success: true, issues: [], message: 'No linter for this file type', _tool: 'lint_check' };
|
|
635
704
|
|
|
636
|
-
const output = execSync(cmd, { stdio: 'pipe', timeout: 30_000 }).toString();
|
|
705
|
+
const output = execSync(cmd, { stdio: 'pipe', timeout: 30_000, cwd: projectRootFor(filePath) }).toString();
|
|
637
706
|
return { success: true, output, issues: output.split('\n').filter(Boolean), _tool: 'lint_check' };
|
|
638
707
|
} catch (err) {
|
|
639
708
|
return { success: false, output: err.message, _tool: 'lint_check' };
|
|
@@ -645,7 +714,7 @@ print('OK: replaced')
|
|
|
645
714
|
try {
|
|
646
715
|
const cmd = args.command || 'npm test';
|
|
647
716
|
const output = execSync(cmd, {
|
|
648
|
-
stdio: 'pipe', timeout: 120_000, cwd:
|
|
717
|
+
stdio: 'pipe', timeout: 120_000, cwd: commandCwd(args),
|
|
649
718
|
encoding: 'utf-8',
|
|
650
719
|
}).toString();
|
|
651
720
|
return { success: true, output: output.slice(-3000), _tool: 'run_tests' };
|
|
@@ -660,7 +729,7 @@ print('OK: replaced')
|
|
|
660
729
|
try {
|
|
661
730
|
const filePath = args.file_path ? `-- "${args.file_path}"` : '';
|
|
662
731
|
const output = execSync(`git diff ${filePath}`, {
|
|
663
|
-
stdio: 'pipe', timeout: 10_000, cwd:
|
|
732
|
+
stdio: 'pipe', timeout: 10_000, cwd: commandCwd(args), encoding: 'utf-8',
|
|
664
733
|
}).toString();
|
|
665
734
|
return { success: true, output: output.slice(-5000) || '(no changes)', _tool: 'git_diff' };
|
|
666
735
|
} catch (err) {
|
|
@@ -672,7 +741,7 @@ print('OK: replaced')
|
|
|
672
741
|
git_status: async (args) => {
|
|
673
742
|
try {
|
|
674
743
|
const output = execSync('git status --short', {
|
|
675
|
-
stdio: 'pipe', timeout: 10_000, cwd:
|
|
744
|
+
stdio: 'pipe', timeout: 10_000, cwd: commandCwd(args), encoding: 'utf-8',
|
|
676
745
|
}).toString();
|
|
677
746
|
return { success: true, output: output || '(clean)', _tool: 'git_status' };
|
|
678
747
|
} catch (err) {
|
|
@@ -684,7 +753,7 @@ print('OK: replaced')
|
|
|
684
753
|
// Returns function signatures, classes, imports instead of raw file contents
|
|
685
754
|
// 10x more token-efficient than read_file
|
|
686
755
|
analyze_code: async (args) => {
|
|
687
|
-
const filePath = resolvePath(args.file_path || args.path);
|
|
756
|
+
const filePath = resolvePath(args.file_path || args.path, args);
|
|
688
757
|
const result = analyzeCode(filePath, {
|
|
689
758
|
startLine: args.start_line,
|
|
690
759
|
endLine: args.end_line,
|
|
@@ -696,6 +765,49 @@ print('OK: replaced')
|
|
|
696
765
|
_tool: 'analyze_code',
|
|
697
766
|
};
|
|
698
767
|
},
|
|
768
|
+
|
|
769
|
+
// Project overview — on-demand index + skeleton
|
|
770
|
+
get_project_overview: async (args) => {
|
|
771
|
+
const projectPath = args.path || args.project_path;
|
|
772
|
+
const result = await projectRegistry.register(projectPath);
|
|
773
|
+
return {
|
|
774
|
+
success: true,
|
|
775
|
+
output: result.output,
|
|
776
|
+
project_resource: result.resource,
|
|
777
|
+
already_registered: result.already_registered,
|
|
778
|
+
_tool: 'get_project_overview',
|
|
779
|
+
};
|
|
780
|
+
},
|
|
781
|
+
|
|
782
|
+
// Portable skills — metadata first, full content only on demand.
|
|
783
|
+
skills_list: async (args) => ({
|
|
784
|
+
success: true,
|
|
785
|
+
output: JSON.stringify(skillsLoader.list({
|
|
786
|
+
query: args.query || '',
|
|
787
|
+
source: args.source || '',
|
|
788
|
+
scope: args.scope || '',
|
|
789
|
+
}), null, 2),
|
|
790
|
+
skills: skillsLoader.list({
|
|
791
|
+
query: args.query || '',
|
|
792
|
+
source: args.source || '',
|
|
793
|
+
scope: args.scope || '',
|
|
794
|
+
}),
|
|
795
|
+
_tool: 'skills_list',
|
|
796
|
+
}),
|
|
797
|
+
|
|
798
|
+
skill_view: async (args) => {
|
|
799
|
+
const skill = skillsLoader.view(
|
|
800
|
+
args.name,
|
|
801
|
+
args.path || null,
|
|
802
|
+
{ sourceId: args.source_id || null },
|
|
803
|
+
);
|
|
804
|
+
return {
|
|
805
|
+
success: true,
|
|
806
|
+
output: JSON.stringify(skill, null, 2),
|
|
807
|
+
skill,
|
|
808
|
+
_tool: 'skill_view',
|
|
809
|
+
};
|
|
810
|
+
},
|
|
699
811
|
};
|
|
700
812
|
|
|
701
813
|
return {
|
|
@@ -721,5 +833,28 @@ print('OK: replaced')
|
|
|
721
833
|
listTools() {
|
|
722
834
|
return Object.keys(toolMap);
|
|
723
835
|
},
|
|
836
|
+
|
|
837
|
+
getProjectResources() {
|
|
838
|
+
return projectRegistry.resources();
|
|
839
|
+
},
|
|
840
|
+
|
|
841
|
+
getAgentContext() {
|
|
842
|
+
const global = projectRegistry.getGlobalContext();
|
|
843
|
+
return {
|
|
844
|
+
identity: global.identity,
|
|
845
|
+
preferences: global.preferences,
|
|
846
|
+
global_skills: skillsLoader.list(),
|
|
847
|
+
source: 'cli',
|
|
848
|
+
};
|
|
849
|
+
},
|
|
850
|
+
|
|
851
|
+
reloadSkills(cwd = process.cwd()) {
|
|
852
|
+
skillsLoader.load(cwd);
|
|
853
|
+
return skillsLoader.list();
|
|
854
|
+
},
|
|
855
|
+
|
|
856
|
+
resetProjects() {
|
|
857
|
+
projectRegistry.reset();
|
|
858
|
+
},
|
|
724
859
|
};
|
|
725
860
|
}
|