codemini-cli 0.2.2 → 0.2.4

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
@@ -15,6 +15,7 @@ import {
15
15
  import { evaluateCommandPolicy } from './command-policy.js';
16
16
  import { queryAst, readAstNode, resolveAstTarget } from './ast.js';
17
17
  import { initializeProjectIndex, refreshIndexedFile } from './project-index.js';
18
+ import { checkReadDedup } from './agent-loop.js';
18
19
 
19
20
  const SKIP_DIRS = new Set(['.git', 'node_modules', '.codemini', '.codemini-global', 'dist', 'coverage']);
20
21
  const TEXT_EXTENSIONS = new Set([
@@ -722,6 +723,26 @@ async function readFile(root, args) {
722
723
  truncated = true;
723
724
  }
724
725
 
726
+ // Read deduplication: if same path+range+mtime was read before, return a short stub
727
+ const isDuplicate = checkReadDedup(
728
+ args?.path,
729
+ startLine,
730
+ endLine,
731
+ stat.mtimeMs
732
+ );
733
+ if (isDuplicate) {
734
+ return {
735
+ path: args?.path,
736
+ phase: 'content',
737
+ start_line: startLine,
738
+ end_line: endLine,
739
+ total_lines: totalLines,
740
+ truncated: false,
741
+ unchanged: true,
742
+ content: `File unchanged since last read. The content from the earlier read tool_result in this conversation is still current -- refer to that instead of re-reading.`
743
+ };
744
+ }
745
+
725
746
  return {
726
747
  path: args?.path,
727
748
  phase: 'content',
@@ -1711,22 +1732,22 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1711
1732
  return null;
1712
1733
  }
1713
1734
  };
1714
- const definitions = [
1735
+ const primaryDefinitions = [
1715
1736
  {
1716
1737
  type: 'function',
1717
1738
  function: {
1718
1739
  name: 'read',
1719
1740
  description:
1720
- 'Primary read tool. First call returns metadata+read_token, second call with include_content=true and matching read_token returns content',
1741
+ 'Inspect a file. Call once for metadata and a read_token, then again with include_content=true and the same token to get content. Use this before editing. Do not use run with cat, head, or tail for file reads.',
1721
1742
  parameters: {
1722
1743
  type: 'object',
1723
1744
  properties: {
1724
- path: { type: 'string' },
1725
- start_line: { type: 'number' },
1726
- end_line: { type: 'number' },
1727
- max_chars: { type: 'number' },
1728
- include_content: { type: 'boolean' },
1729
- read_token: { type: 'string' }
1745
+ path: { type: 'string', description: 'File path to read' },
1746
+ start_line: { type: 'number', description: '1-based start line' },
1747
+ end_line: { type: 'number', description: 'Inclusive end line' },
1748
+ max_chars: { type: 'number', description: 'Max chars to return' },
1749
+ include_content: { type: 'boolean', description: 'Set true on the second call' },
1750
+ read_token: { type: 'string', description: 'Token from the first call' }
1730
1751
  },
1731
1752
  required: ['path']
1732
1753
  }
@@ -1736,18 +1757,19 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1736
1757
  type: 'function',
1737
1758
  function: {
1738
1759
  name: 'grep',
1739
- description: 'Search file contents using a plain string or regex pattern and return compact matches',
1760
+ description:
1761
+ 'Search file contents. Use this for code search before read or edit. Do not use run with grep or rg for normal code search.',
1740
1762
  parameters: {
1741
1763
  type: 'object',
1742
1764
  properties: {
1743
- pattern: { type: 'string' },
1744
- query: { type: 'string' },
1745
- path: { type: 'string' },
1746
- regex: { type: 'boolean' },
1747
- case_sensitive: { type: 'boolean' },
1748
- max_results: { type: 'number' },
1749
- language: { type: 'string' },
1750
- file_types: { type: 'array', items: { type: 'string' } }
1765
+ pattern: { type: 'string', description: 'Search pattern' },
1766
+ query: { type: 'string', description: 'Alias for pattern' },
1767
+ path: { type: 'string', description: 'Directory or file to search' },
1768
+ regex: { type: 'boolean', description: 'Treat pattern as regex' },
1769
+ case_sensitive: { type: 'boolean', description: 'Case-sensitive matching' },
1770
+ max_results: { type: 'number', description: 'Max matches to return' },
1771
+ language: { type: 'string', description: 'Filter by language' },
1772
+ file_types: { type: 'array', items: { type: 'string' }, description: 'Filter by file glob' }
1751
1773
  },
1752
1774
  required: ['pattern']
1753
1775
  }
@@ -1757,14 +1779,15 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1757
1779
  type: 'function',
1758
1780
  function: {
1759
1781
  name: 'glob',
1760
- description: 'Find files by glob pattern such as **/*.ts or src/**/*.tsx',
1782
+ description:
1783
+ 'Find files by glob pattern. Use this for file discovery before read. Do not use run with find for normal file lookup.',
1761
1784
  parameters: {
1762
1785
  type: 'object',
1763
1786
  properties: {
1764
- pattern: { type: 'string' },
1765
- path: { type: 'string' },
1766
- include_hidden: { type: 'boolean' },
1767
- max_results: { type: 'number' }
1787
+ pattern: { type: 'string', description: 'Glob pattern' },
1788
+ path: { type: 'string', description: 'Directory to search' },
1789
+ include_hidden: { type: 'boolean', description: 'Include dotfiles' },
1790
+ max_results: { type: 'number', description: 'Max results' }
1768
1791
  },
1769
1792
  required: ['pattern']
1770
1793
  }
@@ -1774,12 +1797,12 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1774
1797
  type: 'function',
1775
1798
  function: {
1776
1799
  name: 'list',
1777
- description: 'List files and directories in a workspace path',
1800
+ description: 'List files and directories in a workspace path. Use this for quick directory discovery before deeper reads.',
1778
1801
  parameters: {
1779
1802
  type: 'object',
1780
1803
  properties: {
1781
- path: { type: 'string' },
1782
- include_hidden: { type: 'boolean' }
1804
+ path: { type: 'string', description: 'Directory path to list' },
1805
+ include_hidden: { type: 'boolean', description: 'Include dotfiles' }
1783
1806
  }
1784
1807
  }
1785
1808
  }
@@ -1789,24 +1812,24 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1789
1812
  function: {
1790
1813
  name: 'edit',
1791
1814
  description:
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.',
1815
+ 'Edit existing files. Use block edits, exact replacements, or anchored inserts. When ast_target is provided, keep the edit constrained to that node. Read first unless the exact target is already known. Prefer this over write for code changes.',
1793
1816
  parameters: {
1794
1817
  type: 'object',
1795
1818
  properties: {
1796
- file: { type: 'string' },
1797
- path: { type: 'string' },
1798
- new_content: { type: 'string' },
1799
- old_text: { type: 'string' },
1800
- new_text: { type: 'string' },
1801
- anchor_text: { type: 'string' },
1802
- content: { type: 'string' },
1803
- position: { type: 'string' },
1804
- kind: { type: 'string' },
1805
- target: { type: 'object' },
1806
- ast_target: { type: 'object' },
1807
- symbol: { type: 'string' },
1808
- line: { type: 'number' },
1809
- edit: { type: 'object' },
1819
+ file: { type: 'string', description: 'File path to edit' },
1820
+ path: { type: 'string', description: 'Alias for file' },
1821
+ new_content: { type: 'string', description: 'Replacement content' },
1822
+ old_text: { type: 'string', description: 'Exact text to replace' },
1823
+ new_text: { type: 'string', description: 'Replacement text' },
1824
+ anchor_text: { type: 'string', description: 'Anchor text for inserts' },
1825
+ content: { type: 'string', description: 'Content to insert or append' },
1826
+ position: { type: 'string', description: 'before or after' },
1827
+ kind: { type: 'string', description: 'replace_block, replace_text, insert_before, insert_after, or rewrite_file' },
1828
+ target: { type: 'object', description: 'Location object with symbol or line info' },
1829
+ ast_target: { type: 'object', description: 'AST target from ast_query' },
1830
+ symbol: { type: 'string', description: 'Symbol to target' },
1831
+ line: { type: 'number', description: 'Line to target' },
1832
+ edit: { type: 'object', description: 'Structured edit input' }
1810
1833
  },
1811
1834
  required: ['file']
1812
1835
  }
@@ -1815,77 +1838,96 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1815
1838
  {
1816
1839
  type: 'function',
1817
1840
  function: {
1818
- name: 'ast_query',
1841
+ name: 'write',
1819
1842
  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.',
1843
+ 'Create a new file or overwrite a file. Use this for new files or explicit full rewrites. Prefer edit for existing code changes.',
1821
1844
  parameters: {
1822
1845
  type: 'object',
1823
1846
  properties: {
1824
- path: { type: 'string' },
1825
- language: { type: 'string' },
1826
- query: { type: 'string' },
1827
- capture_name: { type: 'string' },
1828
- max_results: { type: 'number' }
1847
+ path: { type: 'string', description: 'File path to create or overwrite' },
1848
+ content: { type: 'string', description: 'Content to write' },
1849
+ append: { type: 'boolean', description: 'Append instead of overwrite' },
1850
+ full_file_rewrite: { type: 'boolean', description: 'Set true for whole-file rewrites' }
1829
1851
  },
1830
- required: ['path', 'query']
1852
+ required: ['path', 'content']
1831
1853
  }
1832
1854
  }
1833
1855
  },
1834
1856
  {
1835
1857
  type: 'function',
1836
1858
  function: {
1837
- name: 'read_ast_node',
1859
+ name: 'run',
1838
1860
  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.',
1861
+ 'Run a one-shot shell command such as install, build, or test. Do not use for long-running services or file search.',
1840
1862
  parameters: {
1841
1863
  type: 'object',
1842
1864
  properties: {
1843
- path: { type: 'string' },
1844
- language: { type: 'string' },
1845
- ast_target: { type: 'object' }
1865
+ command: { type: 'string', description: 'Shell command to execute' },
1866
+ timeout: { type: 'number', description: 'Timeout in milliseconds' }
1846
1867
  },
1847
- required: ['path', 'ast_target']
1868
+ required: ['command']
1848
1869
  }
1849
1870
  }
1850
1871
  },
1851
1872
  {
1852
1873
  type: 'function',
1853
1874
  function: {
1854
- name: 'write',
1875
+ name: 'tool_search',
1876
+ description:
1877
+ 'Load one deferred tool schema by name. Use this when a needed tool is not in the current tool list.',
1878
+ parameters: {
1879
+ type: 'object',
1880
+ properties: {
1881
+ query: { type: 'string', description: 'Tool name to load, or "all"' }
1882
+ },
1883
+ required: ['query']
1884
+ }
1885
+ }
1886
+ }
1887
+ ];
1888
+
1889
+ const deferredDefinitions = {
1890
+ ast_query: {
1891
+ type: 'function',
1892
+ function: {
1893
+ name: 'ast_query',
1855
1894
  description:
1856
- 'Primary write tool. Create a UTF-8 text file or overwrite an existing file. Existing code files require full_file_rewrite=true for whole-file overwrites.',
1895
+ 'Run a Tree-sitter query on a code file and return ast_target objects. Use this when you need node-scoped reads or edits for functions, classes, or methods.',
1857
1896
  parameters: {
1858
1897
  type: 'object',
1859
1898
  properties: {
1860
1899
  path: { type: 'string' },
1861
- content: { type: 'string' },
1862
- append: { type: 'boolean' },
1863
- full_file_rewrite: { type: 'boolean' }
1900
+ language: { type: 'string' },
1901
+ query: { type: 'string' },
1902
+ capture_name: { type: 'string' },
1903
+ max_results: { type: 'number' }
1864
1904
  },
1865
- required: ['path', 'content']
1905
+ required: ['path', 'query']
1866
1906
  }
1867
1907
  }
1868
1908
  },
1869
- {
1909
+ read_ast_node: {
1870
1910
  type: 'function',
1871
1911
  function: {
1872
- name: 'run',
1912
+ name: 'read_ast_node',
1873
1913
  description:
1874
- 'Primary run tool. Execute a one-shot shell command in workspace such as install, build, test, or other finite tasks. Do not use for long-running services or watchers.',
1914
+ 'Read a previously selected AST node with compact structural context. Use this after ast_query before a scoped structural edit.',
1875
1915
  parameters: {
1876
1916
  type: 'object',
1877
1917
  properties: {
1878
- command: { type: 'string' }
1918
+ path: { type: 'string' },
1919
+ language: { type: 'string' },
1920
+ ast_target: { type: 'object' }
1879
1921
  },
1880
- required: ['command']
1922
+ required: ['path', 'ast_target']
1881
1923
  }
1882
1924
  }
1883
1925
  },
1884
- {
1926
+ generate_diff: {
1885
1927
  type: 'function',
1886
1928
  function: {
1887
1929
  name: 'generate_diff',
1888
- description: 'Generate a unified diff between the current file and proposed content',
1930
+ description: 'Generate a unified diff for proposed content. Use this when you want to preview or prepare a patch before applying it.',
1889
1931
  parameters: {
1890
1932
  type: 'object',
1891
1933
  properties: {
@@ -1896,11 +1938,11 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1896
1938
  }
1897
1939
  }
1898
1940
  },
1899
- {
1941
+ patch: {
1900
1942
  type: 'function',
1901
1943
  function: {
1902
1944
  name: 'patch',
1903
- description: 'Apply one or more unified diff hunks to files in the workspace',
1945
+ description: 'Apply one or more unified diff hunks to workspace files. Use this for prepared unified diffs instead of ad-hoc shell patching.',
1904
1946
  parameters: {
1905
1947
  type: 'object',
1906
1948
  properties: {
@@ -1911,12 +1953,12 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1911
1953
  }
1912
1954
  }
1913
1955
  },
1914
- {
1956
+ start_service: {
1915
1957
  type: 'function',
1916
1958
  function: {
1917
1959
  name: 'start_service',
1918
1960
  description:
1919
- 'Start a long-running local service, such as a frontend, backend, database, or dev watcher, and return a compact service handle instead of blocking on process exit.',
1961
+ 'Start a long-running local service and return a compact handle. Do not use run for watchers, dev servers, or other persistent processes.',
1920
1962
  parameters: {
1921
1963
  type: 'object',
1922
1964
  properties: {
@@ -1939,22 +1981,22 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1939
1981
  }
1940
1982
  }
1941
1983
  },
1942
- {
1984
+ list_services: {
1943
1985
  type: 'function',
1944
1986
  function: {
1945
1987
  name: 'list_services',
1946
- description: 'List all tracked local services and their compact current status.',
1988
+ description: 'List tracked local services and their current status. Use this to find existing service handles before starting another one.',
1947
1989
  parameters: {
1948
1990
  type: 'object',
1949
1991
  properties: {}
1950
1992
  }
1951
1993
  }
1952
1994
  },
1953
- {
1995
+ get_service_status: {
1954
1996
  type: 'function',
1955
1997
  function: {
1956
1998
  name: 'get_service_status',
1957
- description: 'Get the current status of a previously started service.',
1999
+ description: 'Get the status of a started service. Use this to confirm startup or diagnose a stalled service.',
1958
2000
  parameters: {
1959
2001
  type: 'object',
1960
2002
  properties: {
@@ -1964,11 +2006,11 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1964
2006
  }
1965
2007
  }
1966
2008
  },
1967
- {
2009
+ get_service_logs: {
1968
2010
  type: 'function',
1969
2011
  function: {
1970
2012
  name: 'get_service_logs',
1971
- description: 'Read recent logs from a previously started service.',
2013
+ description: 'Read recent logs from a started service. Use this for targeted diagnosis instead of restarting blindly.',
1972
2014
  parameters: {
1973
2015
  type: 'object',
1974
2016
  properties: {
@@ -1980,11 +2022,11 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1980
2022
  }
1981
2023
  }
1982
2024
  },
1983
- {
2025
+ stop_service: {
1984
2026
  type: 'function',
1985
2027
  function: {
1986
2028
  name: 'stop_service',
1987
- description: 'Stop a previously started service.',
2029
+ description: 'Stop a started service when it is no longer needed or when you need a clean restart.',
1988
2030
  parameters: {
1989
2031
  type: 'object',
1990
2032
  properties: {
@@ -1994,7 +2036,9 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1994
2036
  }
1995
2037
  }
1996
2038
  }
1997
- ].filter(Boolean);
2039
+ };
2040
+
2041
+ const definitions = [...primaryDefinitions];
1998
2042
 
1999
2043
  const handlers = {
2000
2044
  read: (args) =>
@@ -2052,8 +2096,205 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
2052
2096
  list_services: () => listServices(workspaceRoot),
2053
2097
  get_service_status: (args) => getServiceStatus(workspaceRoot, args),
2054
2098
  get_service_logs: (args) => getServiceLogs(workspaceRoot, args),
2055
- stop_service: (args) => stopService(workspaceRoot, args)
2099
+ stop_service: (args) => stopService(workspaceRoot, args),
2100
+ tool_search: (args) => {
2101
+ const query = String(args?.query || '').trim().toLowerCase();
2102
+ if (query === 'all') {
2103
+ const all = Object.values(deferredDefinitions);
2104
+ return {
2105
+ loaded: Object.keys(deferredDefinitions),
2106
+ schemas: all,
2107
+ message: `Loaded all ${all.length} deferred tools. You can now call them directly.`
2108
+ };
2109
+ }
2110
+ const match = Object.entries(deferredDefinitions).find(([name]) => name === query);
2111
+ if (!match) {
2112
+ const available = Object.keys(deferredDefinitions).join(', ');
2113
+ return { error: `Unknown tool: "${query}". Available deferred tools: ${available}` };
2114
+ }
2115
+ return {
2116
+ loaded: [match[0]],
2117
+ schemas: [match[1]],
2118
+ message: `Loaded tool "${match[0]}". You can now call it in your next response.`
2119
+ };
2120
+ }
2121
+ };
2122
+
2123
+ const formatters = {
2124
+ read(result) {
2125
+ if (typeof result === 'string') return result;
2126
+ if (!result || typeof result !== 'object') return String(result);
2127
+ // Phase 1 metadata: small, return as-is
2128
+ if (result.phase === 'metadata') {
2129
+ return JSON.stringify(result);
2130
+ }
2131
+ // Phase 2 content: structured header + head/tail content
2132
+ if (result.phase === 'content') {
2133
+ const header = `[File: ${result.path}, lines ${result.start_line || 1}-${result.end_line || '?'}${result.total_lines ? ` of ${result.total_lines}` : ''}${result.truncated ? ', truncated' : ''}]`;
2134
+ const content = result.content || '';
2135
+ if (typeof content !== 'string' || content.length <= 3000) {
2136
+ return `${header}\n${content}`;
2137
+ }
2138
+ const headLen = 1800;
2139
+ const tailLen = 800;
2140
+ return `${header}\n${content.slice(0, headLen)}\n... [omitted ${content.length - headLen - tailLen} chars] ...\n${content.slice(-tailLen)}`;
2141
+ }
2142
+ return JSON.stringify(result);
2143
+ },
2144
+
2145
+ grep(result) {
2146
+ if (!result || typeof result !== 'object') return String(result);
2147
+ const { pattern, matches, truncated } = result;
2148
+ const header = pattern ? `[grep: "${pattern}"]` : '';
2149
+ if (!Array.isArray(matches) || matches.length === 0) return `${header}\nNo matches found.`;
2150
+ if (matches.length <= 30) {
2151
+ const lines = matches.map((m) => `${m.path}:${m.line}: ${String(m.preview || '').slice(0, 120)}`);
2152
+ return `${header}\n${lines.join('\n')}`;
2153
+ }
2154
+ const shown = matches.slice(0, 30).map((m) => `${m.path}:${m.line}: ${String(m.preview || '').slice(0, 120)}`);
2155
+ return `${header}\n${shown.join('\n')}\n... and ${matches.length - 30} more matches [total: ${matches.length}${truncated ? ', results were truncated' : ''}]`;
2156
+ },
2157
+
2158
+ glob(result) {
2159
+ if (!result || typeof result !== 'object') return String(result);
2160
+ const { pattern, matches, truncated } = result;
2161
+ const header = pattern ? `[glob: "${pattern}"]` : '';
2162
+ if (!Array.isArray(matches) || matches.length === 0) return `${header}\nNo files found.`;
2163
+ if (matches.length <= 50) {
2164
+ return `${header}\n${matches.join('\n')}`;
2165
+ }
2166
+ const shown = matches.slice(0, 50);
2167
+ return `${header}\n${shown.join('\n')}\n... and ${matches.length - 50} more files [total: ${matches.length}${truncated ? ', results were truncated' : ''}]`;
2168
+ },
2169
+
2170
+ list(result) {
2171
+ if (!result || typeof result !== 'object') return String(result);
2172
+ if (!Array.isArray(result.items)) return JSON.stringify(result);
2173
+ const header = result.path ? `[${result.path}]` : '';
2174
+ const dirs = result.items.filter((i) => i.type === 'dir').map((i) => `${i.name}/`);
2175
+ const files = result.items.filter((i) => i.type === 'file').map((i) => i.name);
2176
+ return `${header}\n${dirs.join('\n')}${dirs.length && files.length ? '\n' : ''}${files.join('\n')}`;
2177
+ },
2178
+
2179
+ edit(result) {
2180
+ if (!result || typeof result !== 'object') return String(result);
2181
+ const p = result.path || '';
2182
+ const action = result.action || '';
2183
+ const line = result.changed_line || 0;
2184
+ const summary = `${action} ${p}${line > 0 ? ` @L${line}` : ''}`;
2185
+ const diffPreview = result.diff_preview || '';
2186
+ if (diffPreview) {
2187
+ const trimmed = diffPreview.length > 600 ? `${diffPreview.slice(0, 597)}...` : diffPreview;
2188
+ return `${summary}\n${trimmed}`;
2189
+ }
2190
+ return summary + (result.ok !== false ? '' : ` [FAILED: ${result.error || 'unknown'}]`);
2191
+ },
2192
+
2193
+ write(result) {
2194
+ if (!result || typeof result !== 'object') return String(result);
2195
+ const p = result.path || '';
2196
+ const action = result.action || 'write';
2197
+ const line = result.changed_line || 0;
2198
+ const summary = `${action} ${p}${line > 0 ? ` @L${line}` : ''}`;
2199
+ const diffPreview = result.diff_preview || '';
2200
+ if (diffPreview) {
2201
+ const trimmed = diffPreview.length > 600 ? `${diffPreview.slice(0, 597)}...` : diffPreview;
2202
+ return `${summary}\n${trimmed}`;
2203
+ }
2204
+ return summary;
2205
+ },
2206
+
2207
+ run(result) {
2208
+ if (!result || typeof result !== 'object') return String(result);
2209
+ const command = String(result.command || '').slice(0, 200);
2210
+ const stdout = String(result.stdout || '').slice(0, 500);
2211
+ const stderr = String(result.stderr || '').slice(0, 500);
2212
+ const code = result.code ?? 0;
2213
+ const parts = [`[exit: ${code}]`];
2214
+ if (command) parts.push(`command: ${command}`);
2215
+ if (stdout) parts.push(`stdout:\n${stdout}`);
2216
+ if (stderr) parts.push(`stderr:\n${stderr}`);
2217
+ return parts.join('\n');
2218
+ },
2219
+
2220
+ generate_diff(result) {
2221
+ if (!result || typeof result !== 'object') return String(result);
2222
+ const p = result.path || '';
2223
+ const diff = result.diff || '';
2224
+ if (diff.length <= 2000) return `${p ? `[${p}]\n` : ''}${diff}`;
2225
+ return `${p ? `[${p}]\n` : ''}${diff.slice(0, 1997)}...\n[diff truncated: ${diff.length} chars total]`;
2226
+ },
2227
+
2228
+ patch(result) {
2229
+ if (!result || typeof result !== 'object') return String(result);
2230
+ if (Array.isArray(result.files)) {
2231
+ const names = result.files.slice(0, 10).map((f) => typeof f === 'string' ? f : f.path || '?');
2232
+ return `patched ${result.files.length} file(s): ${names.join(', ')}${result.files.length > 10 ? ` ... +${result.files.length - 10} more` : ''}`;
2233
+ }
2234
+ const p = result.path || '';
2235
+ const line = result.changed_line || 0;
2236
+ return `patched ${p}${line > 0 ? ` @L${line}` : ''}${result.ok === false ? ` [FAILED: ${result.error || ''}]` : ''}`;
2237
+ },
2238
+
2239
+ ast_query(result) {
2240
+ if (!result || typeof result !== 'object') return String(result);
2241
+ if (!Array.isArray(result.matches)) return JSON.stringify(result);
2242
+ const header = `[ast_query: ${result.matches.length} match(es)]`;
2243
+ const lines = result.matches.slice(0, 20).map((m) => {
2244
+ const name = m.name || m.ast_target?.name || '?';
2245
+ const kind = m.kind || m.ast_target?.kind || '?';
2246
+ return ` ${kind} ${name}`;
2247
+ });
2248
+ return `${header}\n${lines.join('\n')}${result.matches.length > 20 ? `\n... +${result.matches.length - 20} more` : ''}`;
2249
+ },
2250
+
2251
+ read_ast_node(result) {
2252
+ if (typeof result === 'string') return result;
2253
+ if (!result || typeof result !== 'object') return String(result);
2254
+ const name = result.name || '';
2255
+ const kind = result.kind || '';
2256
+ const content = result.content || result.source || '';
2257
+ const header = `${kind} ${name}`;
2258
+ if (typeof content !== 'string' || content.length <= 2000) return `${header}\n${content}`;
2259
+ return `${header}\n${content.slice(0, 1200)}\n... [omitted ${content.length - 1600} chars] ...\n${content.slice(-400)}`;
2260
+ },
2261
+
2262
+ start_service(result) {
2263
+ if (!result || typeof result !== 'object') return String(result);
2264
+ const tid = result.task_id || '';
2265
+ const status = result.status || 'unknown';
2266
+ const confirmed = result.startup_confirmed ? 'ready' : 'starting';
2267
+ const url = result.url || '';
2268
+ return `${tid} ${status} (${confirmed})${url ? ` -> ${url}` : ''}`;
2269
+ },
2270
+
2271
+ list_services(result) {
2272
+ if (!result || typeof result !== 'object') return String(result);
2273
+ if (!Array.isArray(result.services)) return JSON.stringify(result);
2274
+ if (result.services.length === 0) return 'No services running.';
2275
+ return result.services.map((s) => `${s.task_id || '?'} ${s.status || 'unknown'}${s.command ? ` (${s.command.slice(0, 60)})` : ''}`).join('\n');
2276
+ },
2277
+
2278
+ get_service_status(result) {
2279
+ if (!result || typeof result !== 'object') return String(result);
2280
+ const tid = result.task_id || '';
2281
+ const status = result.status || 'unknown';
2282
+ const url = result.url || '';
2283
+ const logs = Array.isArray(result.recent_logs) ? result.recent_logs.slice(-3).join('\n') : '';
2284
+ return `${tid} ${status}${url ? ` -> ${url}` : ''}${logs ? `\n${logs}` : ''}`;
2285
+ },
2286
+
2287
+ get_service_logs(result) {
2288
+ if (!result || typeof result !== 'object') return String(result);
2289
+ const logs = Array.isArray(result.recent_logs) ? result.recent_logs.join('\n') : '';
2290
+ return logs || 'No recent logs.';
2291
+ },
2292
+
2293
+ stop_service(result) {
2294
+ if (!result || typeof result !== 'object') return String(result);
2295
+ return `${result.task_id || '?'} stopped${result.exit_code != null ? ` (exit ${result.exit_code})` : ''}`;
2296
+ }
2056
2297
  };
2057
2298
 
2058
- return { definitions, handlers };
2299
+ return { definitions, handlers, formatters, deferredDefinitions };
2059
2300
  }