xibecode 0.6.2 → 0.7.3

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.
Files changed (47) hide show
  1. package/README.md +11 -2
  2. package/dist/commands/chat.d.ts +1 -0
  3. package/dist/commands/chat.d.ts.map +1 -1
  4. package/dist/commands/chat.js +2 -1
  5. package/dist/commands/chat.js.map +1 -1
  6. package/dist/commands/config.d.ts +2 -0
  7. package/dist/commands/config.d.ts.map +1 -1
  8. package/dist/commands/config.js +15 -0
  9. package/dist/commands/config.js.map +1 -1
  10. package/dist/commands/run-pr.d.ts +3 -0
  11. package/dist/commands/run-pr.d.ts.map +1 -1
  12. package/dist/commands/run-pr.js +470 -69
  13. package/dist/commands/run-pr.js.map +1 -1
  14. package/dist/commands/run.d.ts +3 -0
  15. package/dist/commands/run.d.ts.map +1 -1
  16. package/dist/commands/run.js +36 -5
  17. package/dist/commands/run.js.map +1 -1
  18. package/dist/core/agent.d.ts +27 -0
  19. package/dist/core/agent.d.ts.map +1 -1
  20. package/dist/core/agent.js +90 -3
  21. package/dist/core/agent.js.map +1 -1
  22. package/dist/core/context-pruner.d.ts +19 -0
  23. package/dist/core/context-pruner.d.ts.map +1 -0
  24. package/dist/core/context-pruner.js +103 -0
  25. package/dist/core/context-pruner.js.map +1 -0
  26. package/dist/core/modes.d.ts.map +1 -1
  27. package/dist/core/modes.js +1 -0
  28. package/dist/core/modes.js.map +1 -1
  29. package/dist/core/session-memory.d.ts +45 -0
  30. package/dist/core/session-memory.d.ts.map +1 -0
  31. package/dist/core/session-memory.js +103 -0
  32. package/dist/core/session-memory.js.map +1 -0
  33. package/dist/core/tools.d.ts +3 -0
  34. package/dist/core/tools.d.ts.map +1 -1
  35. package/dist/core/tools.js +415 -357
  36. package/dist/core/tools.js.map +1 -1
  37. package/dist/index.js +9 -0
  38. package/dist/index.js.map +1 -1
  39. package/dist/utils/config.d.ts +26 -2
  40. package/dist/utils/config.d.ts.map +1 -1
  41. package/dist/utils/config.js +43 -2
  42. package/dist/utils/config.js.map +1 -1
  43. package/dist/utils/safety.d.ts +21 -0
  44. package/dist/utils/safety.d.ts.map +1 -1
  45. package/dist/utils/safety.js +36 -0
  46. package/dist/utils/safety.js.map +1 -1
  47. package/package.json +2 -2
@@ -7,7 +7,7 @@ import { MODE_CONFIG, isToolAllowed, isValidMode } from './modes.js';
7
7
  import { FileEditor } from './editor.js';
8
8
  import { GitUtils } from '../utils/git.js';
9
9
  import { TestRunnerDetector } from '../utils/testRunner.js';
10
- import { SafetyChecker } from '../utils/safety.js';
10
+ import { SafetyChecker, sanitizePath, sanitizeUrl } from '../utils/safety.js';
11
11
  import { PluginManager } from './plugins.js';
12
12
  import { BrowserManager } from '../tools/browser.js';
13
13
  import * as os from 'os';
@@ -86,6 +86,8 @@ export class CodingToolExecutor {
86
86
  platform;
87
87
  dryRun;
88
88
  testCommandOverride;
89
+ /** Session-scoped tools synthesized by the agent (meta-agent). Execution is sandboxed via run_command. */
90
+ dynamicTools = new Map();
89
91
  /**
90
92
  * Creates a new CodingToolExecutor instance
91
93
  *
@@ -297,401 +299,426 @@ export class CodingToolExecutor {
297
299
  riskAssessment.warnings.push(`Suggestion: ${suggestion}`);
298
300
  }
299
301
  }
300
- switch (toolName) {
301
- case 'read_file': {
302
- if (!p.path || typeof p.path !== 'string') {
303
- return { error: true, success: false, message: 'Missing required parameter: path (string). Example: {"path": "src/index.ts"}' };
304
- }
305
- return this.readFile(p.path, p.start_line, p.end_line);
302
+ try {
303
+ if (this.dynamicTools.has(toolName)) {
304
+ return this.runDynamicTool(toolName, p);
306
305
  }
307
- case 'read_multiple_files': {
308
- if (!Array.isArray(p.paths) || p.paths.length === 0) {
309
- return { error: true, success: false, message: 'Missing required parameter: paths (non-empty array of strings). Example: {"paths": ["file1.ts", "file2.ts"]}' };
306
+ switch (toolName) {
307
+ case 'read_file': {
308
+ if (!p.path || typeof p.path !== 'string') {
309
+ return { error: true, success: false, message: 'Missing required parameter: path (string). Example: {"path": "src/index.ts"}' };
310
+ }
311
+ return this.readFile(p.path, p.start_line, p.end_line);
310
312
  }
311
- const validPaths = p.paths.filter((x) => typeof x === 'string');
312
- if (validPaths.length === 0) {
313
- return { error: true, success: false, message: 'paths array must contain strings. Example: {"paths": ["file1.ts", "file2.ts"]}' };
313
+ case 'read_multiple_files': {
314
+ if (!Array.isArray(p.paths) || p.paths.length === 0) {
315
+ return { error: true, success: false, message: 'Missing required parameter: paths (non-empty array of strings). Example: {"paths": ["file1.ts", "file2.ts"]}' };
316
+ }
317
+ const validPaths = p.paths.filter((x) => typeof x === 'string');
318
+ if (validPaths.length === 0) {
319
+ return { error: true, success: false, message: 'paths array must contain strings. Example: {"paths": ["file1.ts", "file2.ts"]}' };
320
+ }
321
+ return this.readMultipleFiles(validPaths);
314
322
  }
315
- return this.readMultipleFiles(validPaths);
316
- }
317
- case 'write_file': {
318
- if (!p.path || typeof p.path !== 'string') {
319
- return { error: true, success: false, message: 'Missing required parameter: path (string)' };
323
+ case 'write_file': {
324
+ if (!p.path || typeof p.path !== 'string') {
325
+ return { error: true, success: false, message: 'Missing required parameter: path (string)' };
326
+ }
327
+ if (typeof p.content !== 'string') {
328
+ return { error: true, success: false, message: 'Missing required parameter: content (string)' };
329
+ }
330
+ return this.writeFile(p.path, p.content);
320
331
  }
321
- if (typeof p.content !== 'string') {
322
- return { error: true, success: false, message: 'Missing required parameter: content (string)' };
332
+ case 'edit_file': {
333
+ if (!p.path || typeof p.path !== 'string') {
334
+ return { error: true, success: false, message: 'Missing required parameter: path (string)' };
335
+ }
336
+ if (typeof p.search !== 'string') {
337
+ return { error: true, success: false, message: 'Missing required parameter: search (string)' };
338
+ }
339
+ if (typeof p.replace !== 'string') {
340
+ return { error: true, success: false, message: 'Missing required parameter: replace (string)' };
341
+ }
342
+ return this.editFile(p.path, p.search, p.replace, p.all);
323
343
  }
324
- return this.writeFile(p.path, p.content);
325
- }
326
- case 'edit_file': {
327
- if (!p.path || typeof p.path !== 'string') {
328
- return { error: true, success: false, message: 'Missing required parameter: path (string)' };
344
+ case 'edit_lines': {
345
+ if (!p.path || typeof p.path !== 'string') {
346
+ return { error: true, success: false, message: 'Missing required parameter: path (string)' };
347
+ }
348
+ if (typeof p.start_line !== 'number' || typeof p.end_line !== 'number') {
349
+ return { error: true, success: false, message: 'Missing required parameters: start_line, end_line (numbers)' };
350
+ }
351
+ if (typeof p.new_content !== 'string') {
352
+ return { error: true, success: false, message: 'Missing required parameter: new_content (string)' };
353
+ }
354
+ return this.editLines(p.path, p.start_line, p.end_line, p.new_content);
329
355
  }
330
- if (typeof p.search !== 'string') {
331
- return { error: true, success: false, message: 'Missing required parameter: search (string)' };
356
+ case 'insert_at_line': {
357
+ if (!p.path || typeof p.path !== 'string') {
358
+ return { error: true, success: false, message: 'Missing required parameter: path (string)' };
359
+ }
360
+ if (typeof p.line !== 'number') {
361
+ return { error: true, success: false, message: 'Missing required parameter: line (number)' };
362
+ }
363
+ return this.insertAtLine(p.path, p.line, p.content ?? '');
332
364
  }
333
- if (typeof p.replace !== 'string') {
334
- return { error: true, success: false, message: 'Missing required parameter: replace (string)' };
365
+ case 'verified_edit': {
366
+ if (!p.path || typeof p.path !== 'string') {
367
+ return { error: true, success: false, message: 'Missing required parameter: path (string)' };
368
+ }
369
+ if (typeof p.start_line !== 'number' || typeof p.end_line !== 'number') {
370
+ return { error: true, success: false, message: 'Missing required parameters: start_line, end_line (numbers)' };
371
+ }
372
+ if (typeof p.old_content !== 'string') {
373
+ return { error: true, success: false, message: 'Missing required parameter: old_content (string) - the content currently at those lines' };
374
+ }
375
+ if (typeof p.new_content !== 'string') {
376
+ return { error: true, success: false, message: 'Missing required parameter: new_content (string)' };
377
+ }
378
+ return this.verifiedEditFile(p.path, p.start_line, p.end_line, p.old_content, p.new_content);
335
379
  }
336
- return this.editFile(p.path, p.search, p.replace, p.all);
337
- }
338
- case 'edit_lines': {
339
- if (!p.path || typeof p.path !== 'string') {
340
- return { error: true, success: false, message: 'Missing required parameter: path (string)' };
380
+ case 'list_directory':
381
+ return this.listDirectory(p.path || '.');
382
+ case 'search_files': {
383
+ if (!p.pattern || typeof p.pattern !== 'string') {
384
+ return { error: true, success: false, message: 'Missing required parameter: pattern (string). Example: {"pattern": "**/*.ts"}' };
385
+ }
386
+ return this.searchFiles(p.pattern, p.path);
341
387
  }
342
- if (typeof p.start_line !== 'number' || typeof p.end_line !== 'number') {
343
- return { error: true, success: false, message: 'Missing required parameters: start_line, end_line (numbers)' };
388
+ case 'run_command': {
389
+ if (!p.command || typeof p.command !== 'string') {
390
+ return { error: true, success: false, message: 'Missing required parameter: command (string)' };
391
+ }
392
+ return this.runCommand(p.command, p.cwd, p.input, p.timeout);
344
393
  }
345
- if (typeof p.new_content !== 'string') {
346
- return { error: true, success: false, message: 'Missing required parameter: new_content (string)' };
394
+ case 'create_directory': {
395
+ if (!p.path || typeof p.path !== 'string') {
396
+ return { error: true, success: false, message: 'Missing required parameter: path (string)' };
397
+ }
398
+ return this.createDirectory(p.path);
347
399
  }
348
- return this.editLines(p.path, p.start_line, p.end_line, p.new_content);
349
- }
350
- case 'insert_at_line': {
351
- if (!p.path || typeof p.path !== 'string') {
352
- return { error: true, success: false, message: 'Missing required parameter: path (string)' };
400
+ case 'delete_file': {
401
+ if (!p.path || typeof p.path !== 'string') {
402
+ return { error: true, success: false, message: 'Missing required parameter: path (string)' };
403
+ }
404
+ return this.deleteFile(p.path);
353
405
  }
354
- if (typeof p.line !== 'number') {
355
- return { error: true, success: false, message: 'Missing required parameter: line (number)' };
406
+ case 'move_file': {
407
+ if (!p.source || typeof p.source !== 'string') {
408
+ return { error: true, success: false, message: 'Missing required parameter: source (string)' };
409
+ }
410
+ if (!p.destination || typeof p.destination !== 'string') {
411
+ return { error: true, success: false, message: 'Missing required parameter: destination (string)' };
412
+ }
413
+ return this.moveFile(p.source, p.destination);
356
414
  }
357
- return this.insertAtLine(p.path, p.line, p.content ?? '');
358
- }
359
- case 'verified_edit': {
360
- if (!p.path || typeof p.path !== 'string') {
361
- return { error: true, success: false, message: 'Missing required parameter: path (string)' };
415
+ case 'get_context': {
416
+ if (!Array.isArray(p.files)) {
417
+ return { error: true, success: false, message: 'Missing required parameter: files (array of strings)' };
418
+ }
419
+ return this.getContext(p.files.filter((f) => typeof f === 'string'));
362
420
  }
363
- if (typeof p.start_line !== 'number' || typeof p.end_line !== 'number') {
364
- return { error: true, success: false, message: 'Missing required parameters: start_line, end_line (numbers)' };
421
+ case 'revert_file': {
422
+ if (!p.path || typeof p.path !== 'string') {
423
+ return { error: true, success: false, message: 'Missing required parameter: path (string)' };
424
+ }
425
+ return this.revertFile(p.path, p.backup_index);
365
426
  }
366
- if (typeof p.old_content !== 'string') {
367
- return { error: true, success: false, message: 'Missing required parameter: old_content (string) - the content currently at those lines' };
427
+ case 'run_tests': {
428
+ return this.runTests(p.command, p.cwd);
368
429
  }
369
- if (typeof p.new_content !== 'string') {
370
- return { error: true, success: false, message: 'Missing required parameter: new_content (string)' };
430
+ case 'get_test_status': {
431
+ return this.getTestStatus();
371
432
  }
372
- return this.verifiedEditFile(p.path, p.start_line, p.end_line, p.old_content, p.new_content);
373
- }
374
- case 'list_directory':
375
- return this.listDirectory(p.path || '.');
376
- case 'search_files': {
377
- if (!p.pattern || typeof p.pattern !== 'string') {
378
- return { error: true, success: false, message: 'Missing required parameter: pattern (string). Example: {"pattern": "**/*.ts"}' };
433
+ case 'get_git_status': {
434
+ return this.getGitStatus();
379
435
  }
380
- return this.searchFiles(p.pattern, p.path);
381
- }
382
- case 'run_command': {
383
- if (!p.command || typeof p.command !== 'string') {
384
- return { error: true, success: false, message: 'Missing required parameter: command (string)' };
436
+ case 'get_git_diff_summary': {
437
+ return this.getGitDiffSummary(p.target);
385
438
  }
386
- return this.runCommand(p.command, p.cwd, p.input, p.timeout);
387
- }
388
- case 'create_directory': {
389
- if (!p.path || typeof p.path !== 'string') {
390
- return { error: true, success: false, message: 'Missing required parameter: path (string)' };
439
+ case 'get_git_changed_files': {
440
+ return this.getGitChangedFiles(p.target);
391
441
  }
392
- return this.createDirectory(p.path);
393
- }
394
- case 'delete_file': {
395
- if (!p.path || typeof p.path !== 'string') {
396
- return { error: true, success: false, message: 'Missing required parameter: path (string)' };
442
+ case 'git_commit': {
443
+ if (!p.message || typeof p.message !== 'string')
444
+ return { error: true, success: false, message: 'Missing message' };
445
+ return this.gitCommit(p.message, p.agent_name);
397
446
  }
398
- return this.deleteFile(p.path);
399
- }
400
- case 'move_file': {
401
- if (!p.source || typeof p.source !== 'string') {
402
- return { error: true, success: false, message: 'Missing required parameter: source (string)' };
447
+ case 'git_blame_ai': {
448
+ if (!p.file_path || typeof p.file_path !== 'string')
449
+ return { error: true, success: false, message: 'Missing file_path' };
450
+ return this.gitBlameAi(p.file_path);
403
451
  }
404
- if (!p.destination || typeof p.destination !== 'string') {
405
- return { error: true, success: false, message: 'Missing required parameter: destination (string)' };
452
+ case 'create_git_checkpoint': {
453
+ if (!p.message || typeof p.message !== 'string') {
454
+ return { error: true, success: false, message: 'Missing required parameter: message (string)' };
455
+ }
456
+ return this.createGitCheckpoint(p.message, p.strategy);
406
457
  }
407
- return this.moveFile(p.source, p.destination);
408
- }
409
- case 'get_context': {
410
- if (!Array.isArray(p.files)) {
411
- return { error: true, success: false, message: 'Missing required parameter: files (array of strings)' };
458
+ case 'revert_to_git_checkpoint': {
459
+ if (!p.checkpoint_id || typeof p.checkpoint_id !== 'string') {
460
+ return { error: true, success: false, message: 'Missing required parameter: checkpoint_id (string)' };
461
+ }
462
+ if (!p.confirm) {
463
+ return { error: true, success: false, message: 'Revert requires explicit confirmation. Set confirm: true' };
464
+ }
465
+ return this.revertToGitCheckpoint(p.checkpoint_id, p.checkpoint_type, p.confirm);
412
466
  }
413
- return this.getContext(p.files.filter((f) => typeof f === 'string'));
414
- }
415
- case 'revert_file': {
416
- if (!p.path || typeof p.path !== 'string') {
417
- return { error: true, success: false, message: 'Missing required parameter: path (string)' };
467
+ case 'git_show_diff': {
468
+ return this.gitShowDiff(p.file_path, p.target);
418
469
  }
419
- return this.revertFile(p.path, p.backup_index);
420
- }
421
- case 'run_tests': {
422
- return this.runTests(p.command, p.cwd);
423
- }
424
- case 'get_test_status': {
425
- return this.getTestStatus();
426
- }
427
- case 'get_git_status': {
428
- return this.getGitStatus();
429
- }
430
- case 'get_git_diff_summary': {
431
- return this.getGitDiffSummary(p.target);
432
- }
433
- case 'get_git_changed_files': {
434
- return this.getGitChangedFiles(p.target);
435
- }
436
- case 'git_commit': {
437
- if (!p.message || typeof p.message !== 'string')
438
- return { error: true, success: false, message: 'Missing message' };
439
- return this.gitCommit(p.message, p.agent_name);
440
- }
441
- case 'git_blame_ai': {
442
- if (!p.file_path || typeof p.file_path !== 'string')
443
- return { error: true, success: false, message: 'Missing file_path' };
444
- return this.gitBlameAi(p.file_path);
445
- }
446
- case 'create_git_checkpoint': {
447
- if (!p.message || typeof p.message !== 'string') {
448
- return { error: true, success: false, message: 'Missing required parameter: message (string)' };
470
+ case 'get_mcp_status': {
471
+ return this.getMCPStatus();
449
472
  }
450
- return this.createGitCheckpoint(p.message, p.strategy);
451
- }
452
- case 'revert_to_git_checkpoint': {
453
- if (!p.checkpoint_id || typeof p.checkpoint_id !== 'string') {
454
- return { error: true, success: false, message: 'Missing required parameter: checkpoint_id (string)' };
473
+ case 'grep_code': {
474
+ if (!p.pattern || typeof p.pattern !== 'string') {
475
+ return { error: true, success: false, message: 'Missing required parameter: pattern (string)' };
476
+ }
477
+ return this.grepCode(p.pattern, p.path, p.ignore_case, p.file_pattern, p.max_results);
455
478
  }
456
- if (!p.confirm) {
457
- return { error: true, success: false, message: 'Revert requires explicit confirmation. Set confirm: true' };
479
+ case 'web_search': {
480
+ if (!p.query || typeof p.query !== 'string') {
481
+ return { error: true, success: false, message: 'Missing required parameter: query (string)' };
482
+ }
483
+ return this.webSearch(p.query, p.max_results);
458
484
  }
459
- return this.revertToGitCheckpoint(p.checkpoint_id, p.checkpoint_type, p.confirm);
460
- }
461
- case 'git_show_diff': {
462
- return this.gitShowDiff(p.file_path, p.target);
463
- }
464
- case 'get_mcp_status': {
465
- return this.getMCPStatus();
466
- }
467
- case 'grep_code': {
468
- if (!p.pattern || typeof p.pattern !== 'string') {
469
- return { error: true, success: false, message: 'Missing required parameter: pattern (string)' };
485
+ case 'fetch_url': {
486
+ if (!p.url || typeof p.url !== 'string') {
487
+ return { error: true, success: false, message: 'Missing required parameter: url (string)' };
488
+ }
489
+ return this.fetchUrl(p.url, p.max_length);
470
490
  }
471
- return this.grepCode(p.pattern, p.path, p.ignore_case, p.file_pattern, p.max_results);
472
- }
473
- case 'web_search': {
474
- if (!p.query || typeof p.query !== 'string') {
475
- return { error: true, success: false, message: 'Missing required parameter: query (string)' };
491
+ case 'update_memory': {
492
+ if (!p.content || typeof p.content !== 'string') {
493
+ return { error: true, success: false, message: 'Missing required parameter: content (string)' };
494
+ }
495
+ return this.updateMemory(p.content, p.append);
476
496
  }
477
- return this.webSearch(p.query, p.max_results);
478
- }
479
- case 'fetch_url': {
480
- if (!p.url || typeof p.url !== 'string') {
481
- return { error: true, success: false, message: 'Missing required parameter: url (string)' };
497
+ case 'remember_lesson': {
498
+ if (!this.memory) {
499
+ return { error: true, success: false, message: 'Memory system is not initialized.' };
500
+ }
501
+ if (!p.trigger || typeof p.trigger !== 'string')
502
+ return { error: true, success: false, message: 'Missing trigger' };
503
+ if (!p.action || typeof p.action !== 'string')
504
+ return { error: true, success: false, message: 'Missing action' };
505
+ if (!p.outcome || typeof p.outcome !== 'string')
506
+ return { error: true, success: false, message: 'Missing outcome' };
507
+ await this.memory.addMemory(p.trigger, p.action, p.outcome, p.tags || []);
508
+ return { success: true, message: 'Lesson learned and saved to neural memory.' };
509
+ }
510
+ case 'take_screenshot': {
511
+ if (!p.url || typeof p.url !== 'string')
512
+ return { error: true, success: false, message: 'Missing url' };
513
+ if (!p.path || typeof p.path !== 'string')
514
+ return { error: true, success: false, message: 'Missing path' };
515
+ return this.browserManager.takeScreenshot(p.url, p.path, p.fullPage !== false);
516
+ }
517
+ case 'get_console_logs': {
518
+ if (!p.url || typeof p.url !== 'string')
519
+ return { error: true, success: false, message: 'Missing url' };
520
+ return this.browserManager.getConsoleLogs(p.url);
521
+ }
522
+ case 'run_visual_test': {
523
+ if (!p.url || typeof p.url !== 'string')
524
+ return { error: true, success: false, message: 'Missing url' };
525
+ if (!p.baseline_path || typeof p.baseline_path !== 'string')
526
+ return { error: true, success: false, message: 'Missing baseline_path' };
527
+ const outputDir = p.output_dir || '.playwright-baselines';
528
+ return this.browserManager.runVisualTest(p.url, this.resolvePath(p.baseline_path), this.resolvePath(outputDir));
529
+ }
530
+ case 'check_accessibility': {
531
+ if (!p.url || typeof p.url !== 'string')
532
+ return { error: true, success: false, message: 'Missing url' };
533
+ return this.browserManager.checkAccessibility(p.url);
534
+ }
535
+ case 'measure_performance': {
536
+ if (!p.url || typeof p.url !== 'string')
537
+ return { error: true, success: false, message: 'Missing url' };
538
+ return this.browserManager.measurePerformance(p.url);
539
+ }
540
+ case 'test_responsive': {
541
+ if (!p.url || typeof p.url !== 'string')
542
+ return { error: true, success: false, message: 'Missing url' };
543
+ const outputDir = p.output_dir || '.responsive-screenshots';
544
+ return this.browserManager.testResponsive(p.url, this.resolvePath(outputDir), p.viewports);
545
+ }
546
+ case 'capture_network': {
547
+ if (!p.url || typeof p.url !== 'string')
548
+ return { error: true, success: false, message: 'Missing url' };
549
+ return this.browserManager.captureNetworkRequests(p.url);
550
+ }
551
+ case 'run_playwright_test': {
552
+ if (!p.test_path || typeof p.test_path !== 'string')
553
+ return { error: true, success: false, message: 'Missing test_path' };
554
+ return this.browserManager.runPlaywrightTest(this.resolvePath(p.test_path), {
555
+ headed: p.headed,
556
+ browser: p.browser,
557
+ timeout: p.timeout,
558
+ });
482
559
  }
483
- return this.fetchUrl(p.url, p.max_length);
484
- }
485
- case 'update_memory': {
486
- if (!p.content || typeof p.content !== 'string') {
487
- return { error: true, success: false, message: 'Missing required parameter: content (string)' };
560
+ case 'search_skills_sh': {
561
+ if (!p.query || typeof p.query !== 'string') {
562
+ return { error: true, success: false, message: 'Missing required parameter: query (string)' };
563
+ }
564
+ return this.searchSkillsSh(p.query);
488
565
  }
489
- return this.updateMemory(p.content, p.append);
490
- }
491
- case 'remember_lesson': {
492
- if (!this.memory) {
493
- return { error: true, success: false, message: 'Memory system is not initialized.' };
494
- }
495
- if (!p.trigger || typeof p.trigger !== 'string')
496
- return { error: true, success: false, message: 'Missing trigger' };
497
- if (!p.action || typeof p.action !== 'string')
498
- return { error: true, success: false, message: 'Missing action' };
499
- if (!p.outcome || typeof p.outcome !== 'string')
500
- return { error: true, success: false, message: 'Missing outcome' };
501
- await this.memory.addMemory(p.trigger, p.action, p.outcome, p.tags || []);
502
- return { success: true, message: 'Lesson learned and saved to neural memory.' };
503
- }
504
- case 'take_screenshot': {
505
- if (!p.url || typeof p.url !== 'string')
506
- return { error: true, success: false, message: 'Missing url' };
507
- if (!p.path || typeof p.path !== 'string')
508
- return { error: true, success: false, message: 'Missing path' };
509
- return this.browserManager.takeScreenshot(p.url, p.path, p.fullPage !== false);
510
- }
511
- case 'get_console_logs': {
512
- if (!p.url || typeof p.url !== 'string')
513
- return { error: true, success: false, message: 'Missing url' };
514
- return this.browserManager.getConsoleLogs(p.url);
515
- }
516
- case 'run_visual_test': {
517
- if (!p.url || typeof p.url !== 'string')
518
- return { error: true, success: false, message: 'Missing url' };
519
- if (!p.baseline_path || typeof p.baseline_path !== 'string')
520
- return { error: true, success: false, message: 'Missing baseline_path' };
521
- const outputDir = p.output_dir || '.playwright-baselines';
522
- return this.browserManager.runVisualTest(p.url, this.resolvePath(p.baseline_path), this.resolvePath(outputDir));
523
- }
524
- case 'check_accessibility': {
525
- if (!p.url || typeof p.url !== 'string')
526
- return { error: true, success: false, message: 'Missing url' };
527
- return this.browserManager.checkAccessibility(p.url);
528
- }
529
- case 'measure_performance': {
530
- if (!p.url || typeof p.url !== 'string')
531
- return { error: true, success: false, message: 'Missing url' };
532
- return this.browserManager.measurePerformance(p.url);
533
- }
534
- case 'test_responsive': {
535
- if (!p.url || typeof p.url !== 'string')
536
- return { error: true, success: false, message: 'Missing url' };
537
- const outputDir = p.output_dir || '.responsive-screenshots';
538
- return this.browserManager.testResponsive(p.url, this.resolvePath(outputDir), p.viewports);
539
- }
540
- case 'capture_network': {
541
- if (!p.url || typeof p.url !== 'string')
542
- return { error: true, success: false, message: 'Missing url' };
543
- return this.browserManager.captureNetworkRequests(p.url);
544
- }
545
- case 'run_playwright_test': {
546
- if (!p.test_path || typeof p.test_path !== 'string')
547
- return { error: true, success: false, message: 'Missing test_path' };
548
- return this.browserManager.runPlaywrightTest(this.resolvePath(p.test_path), {
549
- headed: p.headed,
550
- browser: p.browser,
551
- timeout: p.timeout,
552
- });
553
- }
554
- case 'search_skills_sh': {
555
- if (!p.query || typeof p.query !== 'string') {
556
- return { error: true, success: false, message: 'Missing required parameter: query (string)' };
566
+ case 'install_skill_from_skills_sh': {
567
+ if (!p.skill_id || typeof p.skill_id !== 'string') {
568
+ return { error: true, success: false, message: 'Missing required parameter: skill_id (string)' };
569
+ }
570
+ return this.installSkillFromSkillsSh(p.skill_id);
557
571
  }
558
- return this.searchSkillsSh(p.query);
559
- }
560
- case 'install_skill_from_skills_sh': {
561
- if (!p.skill_id || typeof p.skill_id !== 'string') {
562
- return { error: true, success: false, message: 'Missing required parameter: skill_id (string)' };
572
+ // AI Test Generation Tools
573
+ case 'generate_tests': {
574
+ if (!p.file_path || typeof p.file_path !== 'string') {
575
+ return { error: true, success: false, message: 'Missing required parameter: file_path (string)' };
576
+ }
577
+ return this.generateTests(p.file_path, {
578
+ framework: p.framework,
579
+ outputDir: p.output_dir,
580
+ includeEdgeCases: p.include_edge_cases,
581
+ includeMocks: p.include_mocks,
582
+ maxTestsPerFunction: p.max_tests_per_function,
583
+ }, p.write_file);
584
+ }
585
+ case 'preview_app': {
586
+ if (!p.url || typeof p.url !== 'string') {
587
+ return { error: true, success: false, message: 'Missing required parameter: url (string)' };
588
+ }
589
+ return this.visualFeedback.capture(p.url, { fullPage: p.full_page });
563
590
  }
564
- return this.installSkillFromSkillsSh(p.skill_id);
565
- }
566
- // AI Test Generation Tools
567
- case 'generate_tests': {
568
- if (!p.file_path || typeof p.file_path !== 'string') {
569
- return { error: true, success: false, message: 'Missing required parameter: file_path (string)' };
570
- }
571
- return this.generateTests(p.file_path, {
572
- framework: p.framework,
573
- outputDir: p.output_dir,
574
- includeEdgeCases: p.include_edge_cases,
575
- includeMocks: p.include_mocks,
576
- maxTestsPerFunction: p.max_tests_per_function,
577
- }, p.write_file);
578
- }
579
- case 'preview_app': {
580
- if (!p.url || typeof p.url !== 'string') {
581
- return { error: true, success: false, message: 'Missing required parameter: url (string)' };
591
+ case 'mine_project_patterns': {
592
+ const patterns = await this.patternMiner.mine();
593
+ if (patterns.length === 0) {
594
+ return { success: true, message: 'No significant repeated patterns found in the project.' };
595
+ }
596
+ // Format for AI consumption
597
+ const summary = patterns.map(p => `Pattern: ${p.description}\n` +
598
+ `- Occurrences: ${p.frequency}\n` +
599
+ `- Locations: ${p.chunks.map(c => `${path.relative(this.workingDir, c.filePath)}:${c.startLine}`).join(', ')}\n` +
600
+ `- Example Code:\n\`\`\`typescript\n${p.chunks[0].content}\n\`\`\`\n`).join('\n---\n\n');
601
+ return {
602
+ success: true,
603
+ message: `Found ${patterns.length} pattern clusters.`,
604
+ patterns: summary
605
+ };
582
606
  }
583
- return this.visualFeedback.capture(p.url, { fullPage: p.full_page });
584
- }
585
- case 'mine_project_patterns': {
586
- const patterns = await this.patternMiner.mine();
587
- if (patterns.length === 0) {
588
- return { success: true, message: 'No significant repeated patterns found in the project.' };
589
- }
590
- // Format for AI consumption
591
- const summary = patterns.map(p => `Pattern: ${p.description}\n` +
592
- `- Occurrences: ${p.frequency}\n` +
593
- `- Locations: ${p.chunks.map(c => `${path.relative(this.workingDir, c.filePath)}:${c.startLine}`).join(', ')}\n` +
594
- `- Example Code:\n\`\`\`typescript\n${p.chunks[0].content}\n\`\`\`\n`).join('\n---\n\n');
595
- return {
596
- success: true,
597
- message: `Found ${patterns.length} pattern clusters.`,
598
- patterns: summary
599
- };
600
- }
601
- case 'start_background_task': {
602
- if (!p.prompt || typeof p.prompt !== 'string') {
603
- return { error: true, success: false, message: 'Missing required parameter: prompt (string)' };
607
+ case 'start_background_task': {
608
+ if (!p.prompt || typeof p.prompt !== 'string') {
609
+ return { error: true, success: false, message: 'Missing required parameter: prompt (string)' };
610
+ }
611
+ const taskId = await this.backgroundAgent.startTask(p.prompt);
612
+ return { success: true, message: `Background task started with ID: ${taskId}`, task_id: taskId };
604
613
  }
605
- const taskId = await this.backgroundAgent.startTask(p.prompt);
606
- return { success: true, message: `Background task started with ID: ${taskId}`, task_id: taskId };
607
- }
608
- case 'list_background_tasks': {
609
- const tasks = await this.backgroundAgent.listTasks();
610
- const summary = tasks.map(t => `ID: ${t.id} | Status: ${t.status} | Started: ${new Date(t.startTime).toISOString()} | Prompt: "${t.prompt.substring(0, 50)}..."`).join('\n');
611
- return { success: true, message: `Active Tasks:\n${summary || 'No active tasks.'}` };
612
- }
613
- case 'check_background_task': {
614
- if (!p.task_id || typeof p.task_id !== 'string') {
615
- return { error: true, success: false, message: 'Missing required parameter: task_id (string)' };
614
+ case 'list_background_tasks': {
615
+ const tasks = await this.backgroundAgent.listTasks();
616
+ const summary = tasks.map(t => `ID: ${t.id} | Status: ${t.status} | Started: ${new Date(t.startTime).toISOString()} | Prompt: "${t.prompt.substring(0, 50)}..."`).join('\n');
617
+ return { success: true, message: `Active Tasks:\n${summary || 'No active tasks.'}` };
616
618
  }
617
- const logs = await this.backgroundAgent.getTaskLogs(p.task_id);
618
- return { success: true, logs };
619
- }
620
- case 'start_background_task': {
621
- if (!p.prompt || typeof p.prompt !== 'string') {
622
- return { error: true, success: false, message: 'Missing required parameter: prompt (string)' };
619
+ case 'check_background_task': {
620
+ if (!p.task_id || typeof p.task_id !== 'string') {
621
+ return { error: true, success: false, message: 'Missing required parameter: task_id (string)' };
622
+ }
623
+ const logs = await this.backgroundAgent.getTaskLogs(p.task_id);
624
+ return { success: true, logs };
623
625
  }
624
- const taskId = await this.backgroundAgent.startTask(p.prompt);
625
- return { success: true, message: `Background task started with ID: ${taskId}`, task_id: taskId };
626
- }
627
- case 'list_background_tasks': {
628
- const tasks = await this.backgroundAgent.listTasks();
629
- const summary = tasks.map(t => `ID: ${t.id} | Status: ${t.status} | Started: ${new Date(t.startTime).toISOString()} | Prompt: "${t.prompt.substring(0, 50)}..."`).join('\n');
630
- return { success: true, message: `Active Tasks:\n${summary || 'No active tasks.'}` };
631
- }
632
- case 'check_background_task': {
633
- if (!p.task_id || typeof p.task_id !== 'string') {
634
- return { error: true, success: false, message: 'Missing required parameter: task_id (string)' };
626
+ case 'start_background_task': {
627
+ if (!p.prompt || typeof p.prompt !== 'string') {
628
+ return { error: true, success: false, message: 'Missing required parameter: prompt (string)' };
629
+ }
630
+ const taskId = await this.backgroundAgent.startTask(p.prompt);
631
+ return { success: true, message: `Background task started with ID: ${taskId}`, task_id: taskId };
635
632
  }
636
- const logs = await this.backgroundAgent.getTaskLogs(p.task_id);
637
- return { success: true, logs };
638
- }
639
- case 'search_code_graph': {
640
- if (!p.query || typeof p.query !== 'string') {
641
- return { error: true, success: false, message: 'Missing required parameter: query (symbol name)' };
633
+ case 'list_background_tasks': {
634
+ const tasks = await this.backgroundAgent.listTasks();
635
+ const summary = tasks.map(t => `ID: ${t.id} | Status: ${t.status} | Started: ${new Date(t.startTime).toISOString()} | Prompt: "${t.prompt.substring(0, 50)}..."`).join('\n');
636
+ return { success: true, message: `Active Tasks:\n${summary || 'No active tasks.'}` };
642
637
  }
643
- const results = await this.codeGraph.findReferences(p.query);
644
- return { success: true, message: results };
645
- }
646
- case 'analyze_code_for_tests': {
647
- if (!p.file_path || typeof p.file_path !== 'string') {
648
- return { error: true, success: false, message: 'Missing required parameter: file_path (string)' };
638
+ case 'check_background_task': {
639
+ if (!p.task_id || typeof p.task_id !== 'string') {
640
+ return { error: true, success: false, message: 'Missing required parameter: task_id (string)' };
641
+ }
642
+ const logs = await this.backgroundAgent.getTaskLogs(p.task_id);
643
+ return { success: true, logs };
649
644
  }
650
- return this.analyzeCodeForTests(p.file_path);
651
- }
652
- case 'resolve_merge_conflicts': {
653
- const files = await this.conflictSolver.findConflictingFiles();
654
- if (files.length === 0) {
655
- return { success: true, message: 'No merge conflicts found in the project.' };
656
- }
657
- // If specific file requested, use it; otherwise default to first
658
- const targetFile = (p.file_path && typeof p.file_path === 'string') ? p.file_path : files[0];
659
- // Ensure target is in the list or we try to parse it anyway
660
- const conflictData = await this.conflictSolver.parseConflicts(targetFile);
661
- if (!conflictData) {
662
- return { success: false, message: `Could not parse conflicts in ${targetFile}. Markers might be missing or malformed.`, other_files: files };
645
+ case 'search_code_graph': {
646
+ if (!p.query || typeof p.query !== 'string') {
647
+ return { error: true, success: false, message: 'Missing required parameter: query (symbol name)' };
648
+ }
649
+ const results = await this.codeGraph.findReferences(p.query);
650
+ return { success: true, message: results };
663
651
  }
664
- return {
665
- success: true,
666
- message: `Found ${files.length} conflicting files. Showing conflicts for: ${targetFile}`,
667
- conflicts: conflictData.conflicts.map(c => ({
668
- id: c.index,
669
- lines: `${c.startLine}-${c.endLine}`,
670
- ours: c.ours,
671
- theirs: c.theirs,
672
- base: c.base
673
- })),
674
- other_conflicting_files: files.filter(f => f !== targetFile)
675
- };
676
- }
677
- case 'delegate_subtask': {
678
- if (!p.task || typeof p.task !== 'string')
679
- return { error: true, success: false, message: 'Missing task' };
680
- if (!p.worker_type || typeof p.worker_type !== 'string')
681
- return { error: true, success: false, message: 'Missing worker_type (agent mode)' };
682
- if (!isValidMode(p.worker_type)) {
683
- return { error: true, success: false, message: `Invalid worker_type: ${p.worker_type}. Must be a valid AgentMode.` };
684
- }
685
- const result = await this.swarmOrchestrator.delegateSubtask(p.worker_type, p.task);
686
- return {
687
- success: result.success,
688
- result: result.result,
689
- status: result.status,
690
- worker: p.worker_type
691
- };
652
+ case 'analyze_code_for_tests': {
653
+ if (!p.file_path || typeof p.file_path !== 'string') {
654
+ return { error: true, success: false, message: 'Missing required parameter: file_path (string)' };
655
+ }
656
+ return this.analyzeCodeForTests(p.file_path);
657
+ }
658
+ case 'resolve_merge_conflicts': {
659
+ const files = await this.conflictSolver.findConflictingFiles();
660
+ if (files.length === 0) {
661
+ return { success: true, message: 'No merge conflicts found in the project.' };
662
+ }
663
+ // If specific file requested, use it; otherwise default to first
664
+ const targetFile = (p.file_path && typeof p.file_path === 'string') ? p.file_path : files[0];
665
+ // Ensure target is in the list or we try to parse it anyway
666
+ const conflictData = await this.conflictSolver.parseConflicts(targetFile);
667
+ if (!conflictData) {
668
+ return { success: false, message: `Could not parse conflicts in ${targetFile}. Markers might be missing or malformed.`, other_files: files };
669
+ }
670
+ return {
671
+ success: true,
672
+ message: `Found ${files.length} conflicting files. Showing conflicts for: ${targetFile}`,
673
+ conflicts: conflictData.conflicts.map(c => ({
674
+ id: c.index,
675
+ lines: `${c.startLine}-${c.endLine}`,
676
+ ours: c.ours,
677
+ theirs: c.theirs,
678
+ base: c.base
679
+ })),
680
+ other_conflicting_files: files.filter(f => f !== targetFile)
681
+ };
682
+ }
683
+ case 'delegate_subtask': {
684
+ if (!p.task || typeof p.task !== 'string')
685
+ return { error: true, success: false, message: 'Missing task' };
686
+ if (!p.worker_type || typeof p.worker_type !== 'string')
687
+ return { error: true, success: false, message: 'Missing worker_type (agent mode)' };
688
+ if (!isValidMode(p.worker_type)) {
689
+ return { error: true, success: false, message: `Invalid worker_type: ${p.worker_type}. Must be a valid AgentMode.` };
690
+ }
691
+ const result = await this.swarmOrchestrator.delegateSubtask(p.worker_type, p.task);
692
+ return {
693
+ success: result.success,
694
+ result: result.result,
695
+ status: result.status,
696
+ worker: p.worker_type
697
+ };
698
+ }
699
+ case 'synthesize_tool': {
700
+ const name = typeof p.name === 'string' ? p.name.trim() : '';
701
+ const description = typeof p.description === 'string' ? p.description.trim() : '';
702
+ const script = typeof p.script === 'string' ? p.script.trim() : '';
703
+ if (!name || !script) {
704
+ return { error: true, success: false, message: 'Missing required parameters: name (string), script (string). description is optional.' };
705
+ }
706
+ if (!/^[a-z][a-z0-9_]*$/.test(name)) {
707
+ return { error: true, success: false, message: 'Tool name must be lowercase letters, numbers, underscores only (e.g. my_helper).' };
708
+ }
709
+ const reserved = new Set(['read_file', 'write_file', 'run_command', 'synthesize_tool', 'get_context']);
710
+ if (reserved.has(name)) {
711
+ return { error: true, success: false, message: `Cannot override built-in tool: ${name}` };
712
+ }
713
+ this.dynamicTools.set(name, { description: description || name, script });
714
+ return { success: true, message: `Tool "${name}" registered. You can call it with the same name. Execution is sandboxed.` };
715
+ }
716
+ default:
717
+ return { error: true, success: false, message: `Unknown tool: ${toolName}. Available tools: read_file, read_multiple_files, write_file, edit_file, edit_lines, insert_at_line, verified_edit, list_directory, search_files, run_command, create_directory, delete_file, move_file, get_context, revert_file, run_tests, get_test_status, get_git_status, get_git_diff_summary, get_git_changed_files, create_git_checkpoint, revert_to_git_checkpoint, git_show_diff, get_mcp_status, grep_code, web_search, fetch_url, remember_lesson, synthesize_tool, take_screenshot, get_console_logs, run_visual_test, check_accessibility, measure_performance, test_responsive, capture_network, run_playwright_test, search_skills_sh, install_skill_from_skills_sh, preview_app, delegate_subtask` };
692
718
  }
693
- default:
694
- return { error: true, success: false, message: `Unknown tool: ${toolName}. Available tools: read_file, read_multiple_files, write_file, edit_file, edit_lines, insert_at_line, verified_edit, list_directory, search_files, run_command, create_directory, delete_file, move_file, get_context, revert_file, run_tests, get_test_status, get_git_status, get_git_diff_summary, get_git_changed_files, create_git_checkpoint, revert_to_git_checkpoint, git_show_diff, get_mcp_status, grep_code, web_search, fetch_url, remember_lesson, take_screenshot, get_console_logs, run_visual_test, check_accessibility, measure_performance, test_responsive, capture_network, run_playwright_test, search_skills_sh, install_skill_from_skills_sh, preview_app, delegate_subtask` };
719
+ }
720
+ catch (err) {
721
+ return { error: true, success: false, message: err?.message ?? String(err) };
695
722
  }
696
723
  }
697
724
  /**
@@ -1335,6 +1362,19 @@ export class CodingToolExecutor {
1335
1362
  required: ['trigger', 'action', 'outcome']
1336
1363
  }
1337
1364
  },
1365
+ {
1366
+ name: 'synthesize_tool',
1367
+ description: 'Register a new session-scoped tool (meta-agent). Use when you need a reusable script for repeated operations or after repeated failures. The script runs in the same sandbox as run_command. Name must be lowercase with underscores (e.g. my_helper).',
1368
+ input_schema: {
1369
+ type: 'object',
1370
+ properties: {
1371
+ name: { type: 'string', description: 'Tool name (lowercase, letters/numbers/underscores only)' },
1372
+ description: { type: 'string', description: 'Short description of what the tool does' },
1373
+ script: { type: 'string', description: 'Shell command or script to run when the tool is invoked (e.g. "grep -r pattern src/")' }
1374
+ },
1375
+ required: ['name', 'script']
1376
+ }
1377
+ },
1338
1378
  {
1339
1379
  name: 'take_screenshot',
1340
1380
  description: 'Take a screenshot of a web page. Useful for verifying UI appearance or capturing the state of a web app.',
@@ -1576,7 +1616,12 @@ export class CodingToolExecutor {
1576
1616
  }
1577
1617
  // Merge plugin tools
1578
1618
  const pluginTools = this.pluginManager.getPluginTools();
1579
- return [...coreTools, ...mcpTools, ...pluginTools];
1619
+ const dynamicToolDefs = Array.from(this.dynamicTools.entries()).map(([name, def]) => ({
1620
+ name,
1621
+ description: `[Session tool] ${def.description}`,
1622
+ input_schema: { type: 'object', properties: {} },
1623
+ }));
1624
+ return [...coreTools, ...dynamicToolDefs, ...mcpTools, ...pluginTools];
1580
1625
  }
1581
1626
  /**
1582
1627
  * Resolve relative file path to absolute path
@@ -1587,7 +1632,10 @@ export class CodingToolExecutor {
1587
1632
  * @internal
1588
1633
  */
1589
1634
  resolvePath(filePath) {
1590
- return path.resolve(this.workingDir, filePath);
1635
+ const result = sanitizePath(this.workingDir, filePath);
1636
+ if (!result.ok)
1637
+ throw new Error(result.message);
1638
+ return result.path;
1591
1639
  }
1592
1640
  /**
1593
1641
  * Read file contents
@@ -1901,6 +1949,12 @@ export class CodingToolExecutor {
1901
1949
  * @risk-level High
1902
1950
  * @since 0.1.0
1903
1951
  */
1952
+ async runDynamicTool(toolName, _input) {
1953
+ const def = this.dynamicTools.get(toolName);
1954
+ if (!def)
1955
+ return { error: true, success: false, message: `Dynamic tool "${toolName}" not found` };
1956
+ return this.runCommand(def.script, this.workingDir, undefined, 60);
1957
+ }
1904
1958
  async runCommand(command, cwd, input, timeout) {
1905
1959
  const workDir = cwd ? this.resolvePath(cwd) : this.workingDir;
1906
1960
  const timeoutMs = (timeout || 120) * 1000;
@@ -2584,8 +2638,12 @@ export class CodingToolExecutor {
2584
2638
  }
2585
2639
  // ── fetch_url: read any URL as text ────────────────────────
2586
2640
  async fetchUrl(url, maxLength = 20000) {
2641
+ const urlResult = sanitizeUrl(url.trim());
2642
+ if (!urlResult.ok) {
2643
+ return { error: true, success: false, message: urlResult.message };
2644
+ }
2587
2645
  try {
2588
- const response = await fetch(url, {
2646
+ const response = await fetch(urlResult.url, {
2589
2647
  headers: {
2590
2648
  'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
2591
2649
  'Accept': 'text/html,application/xhtml+xml,text/plain,application/json',
@@ -2628,7 +2686,7 @@ export class CodingToolExecutor {
2628
2686
  const content = truncated ? text.slice(0, maxLength) + '\n... [truncated]' : text;
2629
2687
  return {
2630
2688
  success: true,
2631
- url,
2689
+ url: urlResult.url,
2632
2690
  contentType: contentType.split(';')[0],
2633
2691
  length: text.length,
2634
2692
  truncated,