codemini-cli 0.5.8 → 0.5.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.
@@ -1,4 +1,5 @@
1
1
  import { buildMemorySnapshot } from './memory-prompt.js';
2
+ import { loadProjectInstructions } from './project-instructions.js';
2
3
  import { buildSystemPromptWithReplyLanguage, stripReplyLanguageDirective } from './reply-language.js';
3
4
  import { buildSystemPromptWithSoul } from './soul.js';
4
5
 
@@ -17,6 +18,8 @@ export async function composeSystemPrompt({
17
18
  skillsPrompt = '',
18
19
  memorySnapshot,
19
20
  includeMemory = true,
21
+ projectInstructionsSnippet,
22
+ includeProjectInstructions = true,
20
23
  projectContextSnippet = '',
21
24
  projectContextGuidance = '',
22
25
  extraPrompts = [],
@@ -30,8 +33,15 @@ export async function composeSystemPrompt({
30
33
  : includeMemory
31
34
  ? await buildMemorySnapshot({ config, workspaceRoot }).catch(() => '')
32
35
  : '';
36
+ const projectInstructionsPrompt = projectInstructionsSnippet !== undefined
37
+ ? projectInstructionsSnippet
38
+ : includeProjectInstructions
39
+ ? await loadProjectInstructions({ cwd: workspaceRoot, config }).catch(() => '')
40
+ : '';
41
+ const hasProjectInstructions = /\bProject Instructions:\s*\n/i.test(shellAndSoul);
33
42
  const body = joinPromptParts([
34
43
  shellAndSoul,
44
+ hasProjectInstructions ? '' : projectInstructionsPrompt,
35
45
  skillsPrompt,
36
46
  memoryPrompt,
37
47
  projectContextSnippet,
package/src/core/tools.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs/promises';
2
+ import { realpathSync } from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import { spawn } from 'node:child_process';
4
5
  import net from 'node:net';
@@ -59,16 +60,48 @@ function isWithinResolvedRoot(resolvedRoot, candidatePath) {
59
60
  return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
60
61
  }
61
62
 
62
- async function resolveInWorkspace(root, targetPath = '.') {
63
+ async function getAllowedRealRoots(root, config = {}) {
64
+ const roots = [
65
+ root,
66
+ ...(Array.isArray(config?.policy?.allowed_paths) ? config.policy.allowed_paths : [])
67
+ ]
68
+ .map((item) => String(item || '').trim())
69
+ .filter(Boolean);
70
+ const out = [];
71
+ for (const item of roots) {
72
+ try {
73
+ out.push(await fs.realpath(path.resolve(item)));
74
+ } catch {
75
+ continue;
76
+ }
77
+ }
78
+ return out;
79
+ }
80
+
81
+ function isWithinAnyResolvedRoot(roots, candidatePath) {
82
+ return roots.some((resolvedRoot) => isWithinResolvedRoot(resolvedRoot, candidatePath));
83
+ }
84
+
85
+ function resolvesOutsideRoot(root, targetPath = '.') {
86
+ const text = String(targetPath || '').trim();
87
+ if (!text || text === '.') return false;
88
+ return !isWithinResolvedRoot(path.resolve(root), path.resolve(root, text));
89
+ }
90
+
91
+ async function resolveInWorkspace(root, targetPath = '.', config = {}) {
63
92
  const absRoot = path.resolve(root);
64
- const realRoot = await fs.realpath(absRoot);
93
+ const realRoots = await getAllowedRealRoots(absRoot, config);
94
+ if (realRoots.length === 0) {
95
+ throw new Error(`Path escapes workspace: ${targetPath}`);
96
+ }
65
97
  const absTarget = path.resolve(absRoot, targetPath);
66
98
  const realTarget = await realpathIfExists(absTarget);
67
99
  if (realTarget) {
68
- if (!isWithinResolvedRoot(realRoot, realTarget)) {
100
+ if (!isWithinAnyResolvedRoot(realRoots, realTarget)) {
69
101
  throw new Error(`Path escapes workspace: ${targetPath}`);
70
102
  }
71
- return realTarget;
103
+ const linkStat = await fs.lstat(absTarget);
104
+ return linkStat.isSymbolicLink() ? realTarget : absTarget;
72
105
  }
73
106
 
74
107
  let probe = path.dirname(absTarget);
@@ -84,10 +117,10 @@ async function resolveInWorkspace(root, targetPath = '.') {
84
117
  }
85
118
 
86
119
  const resolvedTarget = path.join(resolvedProbe, path.relative(probe, absTarget));
87
- if (!isWithinResolvedRoot(realRoot, resolvedTarget)) {
120
+ if (!isWithinAnyResolvedRoot(realRoots, resolvedTarget)) {
88
121
  throw new Error(`Path escapes workspace: ${targetPath}`);
89
122
  }
90
- return resolvedTarget;
123
+ return absTarget;
91
124
  }
92
125
 
93
126
  async function getBackgroundTasksDir(root) {
@@ -95,6 +128,17 @@ async function getBackgroundTasksDir(root) {
95
128
  }
96
129
 
97
130
  function toWorkspaceRelative(root, absPath) {
131
+ const roots = [path.resolve(root)];
132
+ try {
133
+ const realRoot = realpathSync(root);
134
+ if (realRoot) roots.push(realRoot);
135
+ } catch {}
136
+ for (const candidateRoot of roots) {
137
+ const relative = path.relative(candidateRoot, absPath);
138
+ if (relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))) {
139
+ return normalizePath(relative);
140
+ }
141
+ }
98
142
  return normalizePath(path.relative(path.resolve(root), absPath));
99
143
  }
100
144
 
@@ -544,8 +588,8 @@ async function mapLimit(items, limit, worker) {
544
588
 
545
589
  const WALKER_CONCURRENCY = 8;
546
590
 
547
- async function walkTextFiles(root, startPath = '.', fileTypes = []) {
548
- const abs = await resolveInWorkspace(root, startPath);
591
+ async function walkTextFiles(root, startPath = '.', fileTypes = [], config = {}) {
592
+ const abs = await resolveInWorkspace(root, startPath, config);
549
593
  const allowedExts = new Set((Array.isArray(fileTypes) ? fileTypes : []).map((item) => `.${String(item || '').replace(/^\./, '')}`));
550
594
 
551
595
  async function visit(current) {
@@ -565,8 +609,8 @@ async function walkTextFiles(root, startPath = '.', fileTypes = []) {
565
609
  return visit(abs);
566
610
  }
567
611
 
568
- async function walkWorkspaceEntries(root, startPath = '.', { includeHidden = false } = {}) {
569
- const abs = await resolveInWorkspace(root, startPath);
612
+ async function walkWorkspaceEntries(root, startPath = '.', { includeHidden = false, config = {} } = {}) {
613
+ const abs = await resolveInWorkspace(root, startPath, config);
570
614
 
571
615
  async function visit(current) {
572
616
  const stat = await fs.stat(current);
@@ -799,8 +843,8 @@ function findEnclosingSymbolLine(lines, anchorLine) {
799
843
  return 0;
800
844
  }
801
845
 
802
- async function getFileState(root, relativePath) {
803
- const target = await resolveInWorkspace(root, relativePath);
846
+ async function getFileState(root, relativePath, config = {}) {
847
+ const target = await resolveInWorkspace(root, relativePath, config);
804
848
  const stat = await fs.stat(target);
805
849
  const content = await fs.readFile(target, 'utf8');
806
850
  return {
@@ -811,9 +855,9 @@ async function getFileState(root, relativePath) {
811
855
  };
812
856
  }
813
857
 
814
- async function readFile(root, args) {
858
+ async function readFile(root, args, config = {}) {
815
859
  const normalizedArgs = normalizeReadArgs(args);
816
- const target = await resolveInWorkspace(root, normalizedArgs?.path);
860
+ const target = await resolveInWorkspace(root, normalizedArgs?.path, config);
817
861
  const stat = await fs.stat(target);
818
862
  const text = await fs.readFile(target, 'utf8');
819
863
  const lines = splitLines(text);
@@ -893,7 +937,7 @@ async function readFile(root, args) {
893
937
  };
894
938
  }
895
939
 
896
- async function writeFile(root, args) {
940
+ async function writeFile(root, args, config = {}) {
897
941
  const normalizedArgs = normalizeWriteArgs(args);
898
942
  const rawPath = String(normalizedArgs?.path || '').trim();
899
943
  if (!rawPath) {
@@ -902,7 +946,7 @@ async function writeFile(root, args) {
902
946
  if (rawPath === '.' || rawPath === './') {
903
947
  throw new Error('write requires a file path, not the workspace root');
904
948
  }
905
- const target = await resolveInWorkspace(root, rawPath);
949
+ const target = await resolveInWorkspace(root, rawPath, config);
906
950
  try {
907
951
  const stat = await fs.stat(target);
908
952
  if (stat.isDirectory()) {
@@ -954,21 +998,21 @@ async function writeFile(root, args) {
954
998
  };
955
999
  }
956
1000
 
957
- async function prepareDeleteTarget(root, args) {
1001
+ async function prepareDeleteTarget(root, args, config = {}) {
958
1002
  const normalizedArgs = normalizePathArgs(args, ['file', 'file_path', 'target', 'directory', 'dir']);
959
1003
  const rawPath = String(normalizedArgs?.path || '').trim();
960
1004
  if (!rawPath) {
961
1005
  throw new Error('delete requires a file or directory path');
962
1006
  }
963
1007
  const absRoot = path.resolve(root);
964
- const realRoot = await fs.realpath(absRoot);
1008
+ const realRoots = await getAllowedRealRoots(absRoot, config);
965
1009
  const originalTarget = path.resolve(absRoot, rawPath);
966
1010
  if (originalTarget === absRoot) {
967
1011
  throw new Error('delete requires a path inside the workspace, not the workspace root');
968
1012
  }
969
- const resolvedTarget = await resolveInWorkspace(root, rawPath);
970
- if (resolvedTarget === realRoot) {
971
- throw new Error('delete requires a path inside the workspace, not the workspace root');
1013
+ const resolvedTarget = await resolveInWorkspace(root, rawPath, config);
1014
+ if (realRoots.some((realRoot) => resolvedTarget === realRoot)) {
1015
+ throw new Error('delete requires a path inside the workspace or allowed paths, not an allowed root');
972
1016
  }
973
1017
 
974
1018
  let rawStat;
@@ -998,8 +1042,8 @@ async function prepareDeleteTarget(root, args) {
998
1042
  };
999
1043
  }
1000
1044
 
1001
- async function deletePath(root, args) {
1002
- const target = await prepareDeleteTarget(root, args);
1045
+ async function deletePath(root, args, config = {}) {
1046
+ const target = await prepareDeleteTarget(root, args, config);
1003
1047
  await fs.rm(target.originalTarget, { recursive: true, force: false });
1004
1048
 
1005
1049
  return {
@@ -1350,13 +1394,13 @@ async function stopBackgroundTask(_root, args) {
1350
1394
  return { ...snapshotBackgroundTask(task), stopped: true };
1351
1395
  }
1352
1396
 
1353
- async function builtinGrep(root, args) {
1397
+ async function builtinGrep(root, args, config = {}) {
1354
1398
  const normalizedArgs = normalizePatternArgs(args, ['query', 'symbol', 'q'], ['directory', 'dir', 'cwd']);
1355
1399
  const pattern = String(normalizedArgs?.pattern || '').trim();
1356
1400
  if (!pattern) throw new Error('grep requires pattern');
1357
1401
  const maxResults = Math.max(1, Math.min(200, Number(normalizedArgs?.max_results || 50)));
1358
1402
  const caseSensitive = Boolean(normalizedArgs?.case_sensitive);
1359
- const files = await walkTextFiles(root, normalizedArgs?.path || '.', normalizeFileTypes(normalizedArgs));
1403
+ const files = await walkTextFiles(root, normalizedArgs?.path || '.', normalizeFileTypes(normalizedArgs), config);
1360
1404
  const regex = normalizedArgs?.regex
1361
1405
  ? new RegExp(pattern, caseSensitive ? 'g' : 'gi')
1362
1406
  : new RegExp(escapeRegex(pattern), caseSensitive ? 'g' : 'gi');
@@ -1385,14 +1429,15 @@ async function builtinGrep(root, args) {
1385
1429
  return { pattern, matches, truncated: false };
1386
1430
  }
1387
1431
 
1388
- async function builtinGlob(root, args) {
1432
+ async function builtinGlob(root, args, config = {}) {
1389
1433
  const normalizedArgs = normalizePatternArgs(args, ['glob', 'query'], ['directory', 'dir', 'cwd']);
1390
1434
  const pattern = String(normalizedArgs?.pattern || '').trim();
1391
1435
  if (!pattern) throw new Error('glob requires pattern');
1392
1436
  const maxResults = Math.max(1, Math.min(500, Number(normalizedArgs?.max_results || 200)));
1393
1437
  const regex = globToRegex(pattern);
1394
1438
  const entries = await walkWorkspaceEntries(root, normalizedArgs?.path || '.', {
1395
- includeHidden: Boolean(normalizedArgs?.include_hidden)
1439
+ includeHidden: Boolean(normalizedArgs?.include_hidden),
1440
+ config
1396
1441
  });
1397
1442
  const matches = entries
1398
1443
  .filter((entry) => entry.type === 'file' && regex.test(entry.path))
@@ -1405,10 +1450,10 @@ async function builtinGlob(root, args) {
1405
1450
  };
1406
1451
  }
1407
1452
 
1408
- async function builtinList(root, args) {
1453
+ async function builtinList(root, args, config = {}) {
1409
1454
  const normalizedArgs = normalizePathArgs(args, ['dir', 'directory', 'target']);
1410
1455
  const relativePath = String(normalizedArgs?.path || '.').trim() || '.';
1411
- const target = await resolveInWorkspace(root, relativePath);
1456
+ const target = await resolveInWorkspace(root, relativePath, config);
1412
1457
  const entries = await fs.readdir(target, { withFileTypes: true });
1413
1458
  const includeHidden = Boolean(normalizedArgs?.include_hidden);
1414
1459
  const items = entries
@@ -1428,10 +1473,10 @@ async function builtinList(root, args) {
1428
1473
  };
1429
1474
  }
1430
1475
 
1431
- async function readBlock(root, args) {
1476
+ async function readBlock(root, args, config = {}) {
1432
1477
  const relativePath = String(args?.path || '').trim();
1433
1478
  if (!relativePath) throw new Error('read_block requires path');
1434
- const { lines } = await getFileState(root, relativePath);
1479
+ const { lines } = await getFileState(root, relativePath, config);
1435
1480
  const symbol = String(args?.symbol || '').trim();
1436
1481
  const anchorLine = symbol ? findSymbolDefinition(lines, symbol) : Number(args?.line || args?.anchor_line || 1);
1437
1482
  const range = findBlockRange(lines, anchorLine);
@@ -1445,12 +1490,12 @@ async function readBlock(root, args) {
1445
1490
  };
1446
1491
  }
1447
1492
 
1448
- async function readSymbolContext(root, args) {
1493
+ async function readSymbolContext(root, args, config = {}) {
1449
1494
  const relativePath = String(args?.path || '').trim();
1450
1495
  const symbol = String(args?.symbol || '').trim();
1451
1496
  if (!relativePath || !symbol) throw new Error('read_symbol_context requires path and symbol');
1452
- const { lines } = await getFileState(root, relativePath);
1453
- const mainBlock = await readBlock(root, { path: relativePath, symbol });
1497
+ const { lines } = await getFileState(root, relativePath, config);
1498
+ const mainBlock = await readBlock(root, { path: relativePath, symbol }, config);
1454
1499
  return {
1455
1500
  file: relativePath,
1456
1501
  symbol,
@@ -1468,11 +1513,11 @@ async function readSymbolContext(root, args) {
1468
1513
  };
1469
1514
  }
1470
1515
 
1471
- async function validateEdit(root, args) {
1516
+ async function validateEdit(root, args, config = {}) {
1472
1517
  const relativePath = String(args?.path || '').trim();
1473
1518
  const kind = String(args?.kind || '').trim();
1474
1519
  if (!relativePath || !kind) throw new Error('validate_edit requires path and kind');
1475
- const { content, lines } = await getFileState(root, relativePath);
1520
+ const { content, lines } = await getFileState(root, relativePath, config);
1476
1521
 
1477
1522
  if (kind === 'replace_block') {
1478
1523
  const startLine = Number(args?.target?.start_line || args?.start_line);
@@ -1563,11 +1608,11 @@ function editResult(pathText, action, beforeContent, afterContent, changedLine =
1563
1608
  };
1564
1609
  }
1565
1610
 
1566
- async function replaceBlock(root, args) {
1611
+ async function replaceBlock(root, args, config = {}) {
1567
1612
  const relativePath = String(args?.path || '').trim();
1568
1613
  const newContent = String(args?.new_content || args?.content || '');
1569
1614
  const target = args?.target || {};
1570
- const state = await getFileState(root, relativePath);
1615
+ const state = await getFileState(root, relativePath, config);
1571
1616
  const resolved = resolveReplaceBlockTarget(state, target);
1572
1617
  if (!resolved) {
1573
1618
  throw new Error('replace_block old_hash mismatch; retry through edit with a symbol or line hint');
@@ -1582,11 +1627,11 @@ async function replaceBlock(root, args) {
1582
1627
  return editResult(relativePath, 'replace_block', state.content, afterContent, resolved.start_line);
1583
1628
  }
1584
1629
 
1585
- async function replaceText(root, args) {
1630
+ async function replaceText(root, args, config = {}) {
1586
1631
  const relativePath = String(args?.path || '').trim();
1587
1632
  const oldText = String(args?.old_text || '');
1588
1633
  const newText = String(args?.new_text || '');
1589
- const state = await getFileState(root, relativePath);
1634
+ const state = await getFileState(root, relativePath, config);
1590
1635
  const occurrences = state.content.split(oldText).length - 1;
1591
1636
  if (occurrences !== 1) {
1592
1637
  throw new Error(
@@ -1601,11 +1646,11 @@ async function replaceText(root, args) {
1601
1646
  return editResult(relativePath, 'replace_text', state.content, afterContent, changedLine);
1602
1647
  }
1603
1648
 
1604
- async function insertRelative(root, args, mode) {
1649
+ async function insertRelative(root, args, mode, config = {}) {
1605
1650
  const relativePath = String(args?.path || '').trim();
1606
1651
  const anchorText = String(args?.anchor_text || '');
1607
1652
  const content = String(args?.content || '');
1608
- const state = await getFileState(root, relativePath);
1653
+ const state = await getFileState(root, relativePath, config);
1609
1654
  const occurrences = state.content.split(anchorText).length - 1;
1610
1655
  if (occurrences !== 1) {
1611
1656
  throw new Error(occurrences === 0 ? `${mode} anchor not found` : `${mode} anchor not unique`);
@@ -1617,7 +1662,7 @@ async function insertRelative(root, args, mode) {
1617
1662
  return editResult(relativePath, mode, state.content, afterContent, changedLine);
1618
1663
  }
1619
1664
 
1620
- async function openTarget(root, args) {
1665
+ async function openTarget(root, args, config = {}) {
1621
1666
  const file = String(args?.file || args?.path || '').trim();
1622
1667
  if (!file) throw new Error('open_target requires file');
1623
1668
  const symbol = String(args?.symbol || '').trim();
@@ -1629,8 +1674,8 @@ async function openTarget(root, args) {
1629
1674
  max_related_calls: args?.max_related_calls,
1630
1675
  max_related_imports: args?.max_related_imports,
1631
1676
  max_related_types: args?.max_related_types
1632
- })
1633
- : { file, symbol: '', main_block: await readBlock(root, { path: file, line }), related: { imports: [], local_symbols: [] } };
1677
+ }, config)
1678
+ : { file, symbol: '', main_block: await readBlock(root, { path: file, line }, config), related: { imports: [], local_symbols: [] } };
1634
1679
  const block = mainBlock.main_block || mainBlock;
1635
1680
  return {
1636
1681
  file,
@@ -1682,7 +1727,7 @@ function normalizeEditTargetArgs(args = {}) {
1682
1727
  };
1683
1728
  }
1684
1729
 
1685
- async function editTarget(root, args) {
1730
+ async function editTarget(root, args, config = {}) {
1686
1731
  const normalized = normalizeEditTargetArgs(args);
1687
1732
  const file = normalized.file;
1688
1733
  const astTarget = normalized.ast_target;
@@ -1736,26 +1781,26 @@ async function editTarget(root, args) {
1736
1781
  file,
1737
1782
  symbol: edit.symbol || args?.symbol,
1738
1783
  line: edit.line || args?.line
1739
- })
1784
+ }, config)
1740
1785
  ).edit;
1741
1786
  try {
1742
1787
  return await replaceBlock(root, {
1743
1788
  path: file,
1744
1789
  target: resolvedTarget,
1745
1790
  new_content: edit.new_content
1746
- });
1791
+ }, config);
1747
1792
  } catch (error) {
1748
1793
  if (!/old_hash mismatch/i.test(String(error?.message || ''))) throw error;
1749
1794
  const validation = await validateEdit(root, {
1750
1795
  path: file,
1751
1796
  kind: 'replace_block',
1752
1797
  target: resolvedTarget
1753
- });
1798
+ }, config);
1754
1799
  return replaceBlock(root, {
1755
1800
  path: file,
1756
1801
  target: validation.target,
1757
1802
  new_content: edit.new_content
1758
- });
1803
+ }, config);
1759
1804
  }
1760
1805
  }
1761
1806
  if (kind === 'replace_text') {
@@ -1763,20 +1808,20 @@ async function editTarget(root, args) {
1763
1808
  path: file,
1764
1809
  old_text: edit.old_text,
1765
1810
  new_text: edit.new_text
1766
- });
1811
+ }, config);
1767
1812
  }
1768
1813
  if (kind === 'insert_before') {
1769
- return insertRelative(root, { path: file, anchor_text: edit.anchor_text, content: edit.content }, 'insert_before');
1814
+ return insertRelative(root, { path: file, anchor_text: edit.anchor_text, content: edit.content }, 'insert_before', config);
1770
1815
  }
1771
1816
  if (kind === 'insert_after') {
1772
- return insertRelative(root, { path: file, anchor_text: edit.anchor_text, content: edit.content }, 'insert_after');
1817
+ return insertRelative(root, { path: file, anchor_text: edit.anchor_text, content: edit.content }, 'insert_after', config);
1773
1818
  }
1774
1819
  if (kind === 'rewrite_file') {
1775
1820
  return writeFile(root, {
1776
1821
  path: file,
1777
1822
  content: edit.new_content ?? edit.content ?? '',
1778
1823
  full_file_rewrite: true
1779
- });
1824
+ }, config);
1780
1825
  }
1781
1826
  throw new Error(`edit does not support kind: ${kind}`);
1782
1827
  }
@@ -2370,36 +2415,39 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2370
2415
  }
2371
2416
 
2372
2417
  async function grep(args) {
2373
- if (activeFffAdapter?.grep) {
2418
+ const normalizedArgs = normalizePatternArgs(args, ['query', 'symbol', 'q'], ['directory', 'dir', 'cwd']);
2419
+ if (!resolvesOutsideRoot(workspaceRoot, normalizedArgs?.path || '.') && activeFffAdapter?.grep) {
2374
2420
  try {
2375
2421
  await ensureFffConnected();
2376
2422
  const result = await activeFffAdapter.grep(args);
2377
2423
  if (result && Array.isArray(result.matches)) return result;
2378
2424
  } catch {}
2379
2425
  }
2380
- return builtinGrep(workspaceRoot, args);
2426
+ return builtinGrep(workspaceRoot, args, config);
2381
2427
  }
2382
2428
 
2383
2429
  async function glob(args) {
2384
- if (activeFffAdapter?.glob) {
2430
+ const normalizedArgs = normalizePatternArgs(args, ['glob', 'query'], ['directory', 'dir', 'cwd']);
2431
+ if (!resolvesOutsideRoot(workspaceRoot, normalizedArgs?.path || '.') && activeFffAdapter?.glob) {
2385
2432
  try {
2386
2433
  await ensureFffConnected();
2387
2434
  const result = await activeFffAdapter.glob(args);
2388
2435
  if (result && Array.isArray(result.matches)) return result;
2389
2436
  } catch {}
2390
2437
  }
2391
- return builtinGlob(workspaceRoot, args);
2438
+ return builtinGlob(workspaceRoot, args, config);
2392
2439
  }
2393
2440
 
2394
2441
  async function list(args) {
2395
- if (activeFffAdapter?.list) {
2442
+ const normalizedArgs = normalizePathArgs(args, ['dir', 'directory', 'target']);
2443
+ if (!resolvesOutsideRoot(workspaceRoot, normalizedArgs?.path || '.') && activeFffAdapter?.list) {
2396
2444
  try {
2397
2445
  await ensureFffConnected();
2398
2446
  const result = await activeFffAdapter.list(args);
2399
2447
  if (result && Array.isArray(result.items)) return result;
2400
2448
  } catch {}
2401
2449
  }
2402
- return builtinList(workspaceRoot, args);
2450
+ return builtinList(workspaceRoot, args, config);
2403
2451
  }
2404
2452
 
2405
2453
  const handlers = {
@@ -2455,7 +2503,7 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2455
2503
  typeof args?.max_chars === 'number'
2456
2504
  ? args.max_chars
2457
2505
  : config.context?.read_file_max_chars ?? 24000
2458
- });
2506
+ }, config);
2459
2507
  const readPath = String(result?.path || args?.path || '').trim();
2460
2508
  if (readPath) lastReadPath = readPath;
2461
2509
  return result;
@@ -2487,25 +2535,26 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2487
2535
  const astTarget = resolveCachedAstTarget(args, { requireAstScope: normalizedKind === 'replace_block' });
2488
2536
  const result = await editTarget(
2489
2537
  workspaceRoot,
2490
- astTarget ? { ...args, ast_target: astTarget, recent_file: lastReadPath } : { ...args, recent_file: lastReadPath }
2538
+ astTarget ? { ...args, ast_target: astTarget, recent_file: lastReadPath } : { ...args, recent_file: lastReadPath },
2539
+ config
2491
2540
  );
2492
2541
  if (result?.path) await refreshProjectFile(result.path);
2493
2542
  return result;
2494
2543
  },
2495
2544
  write: async (args) => {
2496
2545
  await ensureProjectIndex();
2497
- const result = await writeFile(workspaceRoot, args);
2546
+ const result = await writeFile(workspaceRoot, args, config);
2498
2547
  if (result?.path) await refreshProjectFile(result.path);
2499
2548
  return result;
2500
2549
  },
2501
2550
  delete: Object.assign(async (args) => {
2502
2551
  await ensureProjectIndex();
2503
- const result = await deletePath(workspaceRoot, args);
2552
+ const result = await deletePath(workspaceRoot, args, config);
2504
2553
  if (result?.path) await refreshProjectFile(result.path);
2505
2554
  return result;
2506
2555
  }, {
2507
2556
  prepareApproval: async (args) => {
2508
- const target = await prepareDeleteTarget(workspaceRoot, args);
2557
+ const target = await prepareDeleteTarget(workspaceRoot, args, config);
2509
2558
  return {
2510
2559
  path: target.path,
2511
2560
  name: target.name,