codemini-cli 0.2.0 → 0.2.2

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/src/core/tools.js CHANGED
@@ -13,8 +13,10 @@ import {
13
13
  terminateChild
14
14
  } from './shell.js';
15
15
  import { evaluateCommandPolicy } from './command-policy.js';
16
+ import { queryAst, readAstNode, resolveAstTarget } from './ast.js';
17
+ import { initializeProjectIndex, refreshIndexedFile } from './project-index.js';
16
18
 
17
- const SKIP_DIRS = new Set(['.git', 'node_modules', '.coder', '.codemini-cli', 'dist', 'coverage']);
19
+ const SKIP_DIRS = new Set(['.git', 'node_modules', '.codemini', '.codemini-global', 'dist', 'coverage']);
18
20
  const TEXT_EXTENSIONS = new Set([
19
21
  '.js',
20
22
  '.jsx',
@@ -1525,11 +1527,13 @@ function normalizeEditTargetArgs(args = {}) {
1525
1527
  }
1526
1528
  return {
1527
1529
  file,
1530
+ ast_target: normalizedEdit.ast_target ?? args?.ast_target,
1528
1531
  edit: normalizedEdit
1529
1532
  };
1530
1533
  }
1531
1534
  return {
1532
1535
  file,
1536
+ ast_target: args?.ast_target,
1533
1537
  edit: {
1534
1538
  kind: args?.kind,
1535
1539
  target: args?.target,
@@ -1545,6 +1549,7 @@ function normalizeEditTargetArgs(args = {}) {
1545
1549
  async function editTarget(root, args) {
1546
1550
  const normalized = normalizeEditTargetArgs(args);
1547
1551
  const file = normalized.file;
1552
+ const astTarget = normalized.ast_target;
1548
1553
  const edit = normalized.edit || {};
1549
1554
  let kind = String(edit.kind || '').trim();
1550
1555
  const hasContent = edit.new_content != null || edit.content != null;
@@ -1561,6 +1566,19 @@ async function editTarget(root, args) {
1561
1566
  }
1562
1567
  }
1563
1568
  if (!file || !kind) throw new Error('edit requires file and edit.kind');
1569
+ if (astTarget) {
1570
+ if (kind !== 'replace_block') {
1571
+ throw new Error('AST-scoped edit only supports replace_block');
1572
+ }
1573
+ const resolved = await resolveAstTarget(root, file, astTarget);
1574
+ const beforeContent = resolved.content;
1575
+ const node = resolved.node;
1576
+ const afterContent = `${beforeContent.slice(0, node.startIndex)}${edit.new_content || ''}${beforeContent.slice(node.endIndex)}`;
1577
+ await fs.writeFile(resolved.absolutePath, afterContent, 'utf8');
1578
+ resolved.tree.delete();
1579
+ resolved.parser.delete();
1580
+ return editResult(file, 'replace_block', beforeContent, afterContent, node.startPosition.row + 1);
1581
+ }
1564
1582
  if (kind === 'replace_block') {
1565
1583
  const resolvedTarget =
1566
1584
  edit.target ||
@@ -1614,7 +1632,85 @@ async function editTarget(root, args) {
1614
1632
  throw new Error(`edit does not support kind: ${kind}`);
1615
1633
  }
1616
1634
 
1617
- export function getBuiltinTools({ workspaceRoot = process.cwd(), config }) {
1635
+ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSystemEvent }) {
1636
+ const emitSystemTool = (event) => {
1637
+ if (typeof onSystemEvent === 'function' && event) onSystemEvent(event);
1638
+ };
1639
+ const astSelectionCache = new Map();
1640
+ let lastAstTarget = null;
1641
+ const rememberAstSelection = (filePath, astTarget) => {
1642
+ const key = String(filePath || '').trim();
1643
+ if (!key || !astTarget) return;
1644
+ lastAstTarget = astTarget;
1645
+ astSelectionCache.set(key, astTarget);
1646
+ };
1647
+ const hasExplicitBlockHints = (args = {}) =>
1648
+ Boolean(
1649
+ args?.ast_target ||
1650
+ args?.symbol ||
1651
+ args?.line ||
1652
+ args?.target ||
1653
+ args?.edit?.ast_target ||
1654
+ args?.edit?.symbol ||
1655
+ args?.edit?.line ||
1656
+ args?.edit?.target
1657
+ );
1658
+ const resolveCachedAstTarget = (args = {}, { requireAstScope = false } = {}) => {
1659
+ const file = String(args?.path || args?.file || args?.ast_target?.path || '').trim();
1660
+ if (args?.ast_target) return args.ast_target;
1661
+ if (file) {
1662
+ if (requireAstScope && hasExplicitBlockHints(args)) return null;
1663
+ return astSelectionCache.get(file) || lastAstTarget || null;
1664
+ }
1665
+ return lastAstTarget || null;
1666
+ };
1667
+ const ensureProjectIndex = async () => {
1668
+ const eventId = `project-index:${Date.now()}`;
1669
+ const name = 'project_index(.codemini-project/project-map.json,.codemini-project/file-index.json)';
1670
+ try {
1671
+ const result = await initializeProjectIndex(workspaceRoot);
1672
+ if (result?.skipped || !result?.summary) {
1673
+ return result;
1674
+ }
1675
+ emitSystemTool({ type: 'system_tool:end', id: eventId, name, summary: result?.summary });
1676
+ return result;
1677
+ } catch (error) {
1678
+ emitSystemTool({
1679
+ type: 'system_tool:error',
1680
+ id: eventId,
1681
+ name,
1682
+ summary: error instanceof Error ? error.message : String(error)
1683
+ });
1684
+ return null;
1685
+ }
1686
+ };
1687
+ const refreshProjectFile = async (filePath) => {
1688
+ const relativePath = String(filePath || '').trim();
1689
+ if (!relativePath) return null;
1690
+ const eventId = `file-index:${relativePath}:${Date.now()}`;
1691
+ const name = `file_index(${relativePath})`;
1692
+ try {
1693
+ const result = await refreshIndexedFile(workspaceRoot, relativePath);
1694
+ if (!result?.summary) {
1695
+ return result;
1696
+ }
1697
+ emitSystemTool({
1698
+ type: 'system_tool:end',
1699
+ id: eventId,
1700
+ name,
1701
+ summary: result?.summary || `updated .codemini-project for ${relativePath}`
1702
+ });
1703
+ return result;
1704
+ } catch (error) {
1705
+ emitSystemTool({
1706
+ type: 'system_tool:error',
1707
+ id: eventId,
1708
+ name,
1709
+ summary: error instanceof Error ? error.message : String(error)
1710
+ });
1711
+ return null;
1712
+ }
1713
+ };
1618
1714
  const definitions = [
1619
1715
  {
1620
1716
  type: 'function',
@@ -1693,7 +1789,7 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config }) {
1693
1789
  function: {
1694
1790
  name: 'edit',
1695
1791
  description:
1696
- 'Preferred edit tool for existing files. Accepts natural forms such as file + new_content for whole-file rewrites, file + symbol/line + new_content for block edits, file + old_text + new_text for exact replacements, and file + anchor_text + content for anchored inserts. A nested edit object is also supported.',
1792
+ 'Preferred edit tool for existing files. Accepts natural forms such as file + new_content for whole-file rewrites, file + symbol/line + new_content for block edits, file + old_text + new_text for exact replacements, and file + anchor_text + content for anchored inserts. When ast_target is provided, only replace_block is allowed and the write is constrained to that exact syntax node. If a file has just been selected via ast_query, the cached ast_target may be reused when omitted.',
1697
1793
  parameters: {
1698
1794
  type: 'object',
1699
1795
  properties: {
@@ -1707,6 +1803,7 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config }) {
1707
1803
  position: { type: 'string' },
1708
1804
  kind: { type: 'string' },
1709
1805
  target: { type: 'object' },
1806
+ ast_target: { type: 'object' },
1710
1807
  symbol: { type: 'string' },
1711
1808
  line: { type: 'number' },
1712
1809
  edit: { type: 'object' },
@@ -1715,6 +1812,42 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config }) {
1715
1812
  }
1716
1813
  }
1717
1814
  },
1815
+ {
1816
+ type: 'function',
1817
+ function: {
1818
+ name: 'ast_query',
1819
+ description:
1820
+ 'Run a Tree-sitter query against a code file and return explicit ast_target objects that can be passed into read_ast_node or edit for node-scoped changes. Prefer the returned ast_target verbatim in the next read_ast_node or edit call.',
1821
+ parameters: {
1822
+ type: 'object',
1823
+ properties: {
1824
+ path: { type: 'string' },
1825
+ language: { type: 'string' },
1826
+ query: { type: 'string' },
1827
+ capture_name: { type: 'string' },
1828
+ max_results: { type: 'number' }
1829
+ },
1830
+ required: ['path', 'query']
1831
+ }
1832
+ }
1833
+ },
1834
+ {
1835
+ type: 'function',
1836
+ function: {
1837
+ name: 'read_ast_node',
1838
+ description:
1839
+ 'Read the current source and compact structural context for a previously selected AST node using ast_target. If omitted, the most recent ast_query selection for the same file may be reused.',
1840
+ parameters: {
1841
+ type: 'object',
1842
+ properties: {
1843
+ path: { type: 'string' },
1844
+ language: { type: 'string' },
1845
+ ast_target: { type: 'object' }
1846
+ },
1847
+ required: ['path', 'ast_target']
1848
+ }
1849
+ }
1850
+ },
1718
1851
  {
1719
1852
  type: 'function',
1720
1853
  function: {
@@ -1876,10 +2009,44 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config }) {
1876
2009
  grep: (args) => grep(workspaceRoot, args),
1877
2010
  glob: (args) => glob(workspaceRoot, args),
1878
2011
  list: (args) => list(workspaceRoot, args),
1879
- edit: (args) => editTarget(workspaceRoot, args),
2012
+ ast_query: async (args) => {
2013
+ const result = await queryAst(workspaceRoot, args);
2014
+ const firstTarget = result?.matches?.[0]?.ast_target;
2015
+ if (firstTarget?.path) rememberAstSelection(firstTarget.path, firstTarget);
2016
+ return result;
2017
+ },
2018
+ read_ast_node: (args) => {
2019
+ const astTarget = resolveCachedAstTarget(args);
2020
+ if (!astTarget) throw new Error('read_ast_node requires ast_target or a prior ast_query on the same file');
2021
+ if (astTarget.path) rememberAstSelection(astTarget.path, astTarget);
2022
+ return readAstNode(workspaceRoot, { ...args, ast_target: astTarget });
2023
+ },
2024
+ edit: async (args) => {
2025
+ await ensureProjectIndex();
2026
+ const normalizedKind = String(args?.edit?.kind || args?.kind || '').trim();
2027
+ const astTarget = resolveCachedAstTarget(args, { requireAstScope: normalizedKind === 'replace_block' });
2028
+ const result = await editTarget(workspaceRoot, astTarget ? { ...args, ast_target: astTarget } : args);
2029
+ if (result?.path) await refreshProjectFile(result.path);
2030
+ return result;
2031
+ },
1880
2032
  generate_diff: (args) => generateDiff(workspaceRoot, args),
1881
- patch: (args) => applyPatch(workspaceRoot, args),
1882
- write: (args) => writeFile(workspaceRoot, args),
2033
+ patch: async (args) => {
2034
+ await ensureProjectIndex();
2035
+ const result = await applyPatch(workspaceRoot, args);
2036
+ if (result?.path) await refreshProjectFile(result.path);
2037
+ if (Array.isArray(result?.files)) {
2038
+ for (const item of result.files) {
2039
+ if (item?.path) await refreshProjectFile(item.path);
2040
+ }
2041
+ }
2042
+ return result;
2043
+ },
2044
+ write: async (args) => {
2045
+ await ensureProjectIndex();
2046
+ const result = await writeFile(workspaceRoot, args);
2047
+ if (result?.path) await refreshProjectFile(result.path);
2048
+ return result;
2049
+ },
1883
2050
  run: (args) => runCommand(workspaceRoot, config, args),
1884
2051
  start_service: (args) => startService(workspaceRoot, config, args),
1885
2052
  list_services: () => listServices(workspaceRoot),