codemini-cli 0.2.7 → 0.2.9

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
@@ -119,6 +119,111 @@ function splitLines(text) {
119
119
  return String(text || '').split('\n');
120
120
  }
121
121
 
122
+ function parseInlineReadRange(value) {
123
+ const text = String(value || '').trim();
124
+ if (!text) return null;
125
+ const match = text.match(/^(.*?):(\d+)(?:-(\d+))?$/);
126
+ if (!match) return null;
127
+ const [, maybePath, startRaw, endRaw] = match;
128
+ if (!maybePath || /^(?:[A-Za-z])$/.test(maybePath)) return null;
129
+ const startLine = Number(startRaw);
130
+ const endLine = Number(endRaw || startRaw);
131
+ if (!Number.isFinite(startLine) || startLine <= 0) return null;
132
+ if (!Number.isFinite(endLine) || endLine < startLine) return null;
133
+ return {
134
+ path: maybePath,
135
+ start_line: startLine,
136
+ end_line: endLine
137
+ };
138
+ }
139
+
140
+ function normalizeReadArgs(rawArgs) {
141
+ const source =
142
+ rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
143
+ ? { ...rawArgs }
144
+ : { path: typeof rawArgs === 'string' ? rawArgs : '' };
145
+
146
+ const normalized = { ...source };
147
+ const aliasPath = String(source.path || source.file_path || source.file || source.target || '').trim();
148
+ if (aliasPath) normalized.path = aliasPath;
149
+
150
+ if (!Number.isFinite(Number(normalized.start_line)) && Number.isFinite(Number(source.offset))) {
151
+ normalized.start_line = Number(source.offset);
152
+ }
153
+
154
+ if (!Number.isFinite(Number(normalized.end_line)) && Number.isFinite(Number(source.limit))) {
155
+ const startLine = Number(normalized.start_line);
156
+ const limit = Number(source.limit);
157
+ if (startLine > 0 && limit > 0) {
158
+ normalized.end_line = startLine + limit - 1;
159
+ }
160
+ }
161
+
162
+ const inlineRange = parseInlineReadRange(normalized.path);
163
+ if (inlineRange) {
164
+ normalized.path = inlineRange.path;
165
+ if (!Number.isFinite(Number(normalized.start_line))) normalized.start_line = inlineRange.start_line;
166
+ if (!Number.isFinite(Number(normalized.end_line))) normalized.end_line = inlineRange.end_line;
167
+ }
168
+
169
+ return normalized;
170
+ }
171
+
172
+ function normalizePathArgs(rawArgs, aliases = []) {
173
+ const source =
174
+ rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
175
+ ? { ...rawArgs }
176
+ : { path: typeof rawArgs === 'string' ? rawArgs : '' };
177
+ const normalized = { ...source };
178
+ const keys = ['path', ...aliases];
179
+ for (const key of keys) {
180
+ const value = String(source?.[key] || '').trim();
181
+ if (value) {
182
+ normalized.path = value;
183
+ break;
184
+ }
185
+ }
186
+ return normalized;
187
+ }
188
+
189
+ function normalizePatternArgs(rawArgs, aliases = [], defaultPathAliases = []) {
190
+ const source =
191
+ rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
192
+ ? { ...rawArgs }
193
+ : { pattern: typeof rawArgs === 'string' ? rawArgs : '' };
194
+ const normalized = { ...source };
195
+ for (const key of ['pattern', ...aliases]) {
196
+ const value = String(source?.[key] || '').trim();
197
+ if (value) {
198
+ normalized.pattern = value;
199
+ break;
200
+ }
201
+ }
202
+ for (const key of ['path', ...defaultPathAliases]) {
203
+ const value = String(source?.[key] || '').trim();
204
+ if (value) {
205
+ normalized.path = value;
206
+ break;
207
+ }
208
+ }
209
+ return normalized;
210
+ }
211
+
212
+ function normalizeWriteArgs(rawArgs) {
213
+ const source =
214
+ rawArgs && typeof rawArgs === 'object' && !Array.isArray(rawArgs)
215
+ ? { ...rawArgs }
216
+ : { path: typeof rawArgs === 'string' ? rawArgs : '' };
217
+ const normalized = { ...source };
218
+ const filePath = String(source.path || source.file_path || source.file || '').trim();
219
+ if (filePath) normalized.path = filePath;
220
+ if (normalized.content == null) {
221
+ if (source.text != null) normalized.content = source.text;
222
+ if (source.new_content != null) normalized.content = source.new_content;
223
+ }
224
+ return normalized;
225
+ }
226
+
122
227
  function findUniqueLineBlock(lines, blockContent) {
123
228
  const probeLines = splitLines(blockContent);
124
229
  if (probeLines.length === 0 || (probeLines.length === 1 && probeLines[0] === '')) return null;
@@ -665,16 +770,17 @@ async function getFileState(root, relativePath) {
665
770
  }
666
771
 
667
772
  async function readFile(root, args) {
668
- const target = resolveInWorkspace(root, args?.path);
773
+ const normalizedArgs = normalizeReadArgs(args);
774
+ const target = resolveInWorkspace(root, normalizedArgs?.path);
669
775
  const stat = await fs.stat(target);
670
776
  const text = await fs.readFile(target, 'utf8');
671
777
  const lines = splitLines(text);
672
778
  const totalLines = lines.length;
673
- const startLineRaw = Number(args?.start_line);
674
- const endLineRaw = Number(args?.end_line);
675
- const defaultLines = Number(args?.default_lines || 220);
676
- const maxChars = Number(args?.max_chars || 24000);
677
- const includeContent = Boolean(args?.include_content);
779
+ const startLineRaw = Number(normalizedArgs?.start_line);
780
+ const endLineRaw = Number(normalizedArgs?.end_line);
781
+ const defaultLines = Number(normalizedArgs?.default_lines || 220);
782
+ const maxChars = Number(normalizedArgs?.max_chars || 24000);
783
+ const wantsMetadataOnly = normalizedArgs?.metadata_only === true || normalizedArgs?.include_content === false;
678
784
 
679
785
  let startLine = Number.isFinite(startLineRaw) && startLineRaw > 0 ? startLineRaw : 1;
680
786
  let endLine =
@@ -684,12 +790,12 @@ async function readFile(root, args) {
684
790
  startLine = Math.max(1, Math.min(startLine, totalLines));
685
791
  endLine = Math.max(startLine, Math.min(endLine, totalLines));
686
792
 
687
- const tokenSeed = `${args?.path}|${stat.size}|${stat.mtimeMs}|${startLine}|${endLine}`;
793
+ const tokenSeed = `${normalizedArgs?.path}|${stat.size}|${stat.mtimeMs}|${startLine}|${endLine}`;
688
794
  const readToken = sha1(tokenSeed).slice(0, 16);
689
795
 
690
- if (!includeContent) {
796
+ if (wantsMetadataOnly) {
691
797
  return {
692
- path: args?.path,
798
+ path: normalizedArgs?.path,
693
799
  phase: 'metadata',
694
800
  size_bytes: stat.size,
695
801
  modified_at: new Date(stat.mtimeMs).toISOString(),
@@ -701,21 +807,6 @@ async function readFile(root, args) {
701
807
  };
702
808
  }
703
809
 
704
- if (String(args?.read_token || '') !== readToken) {
705
- return {
706
- path: args?.path,
707
- phase: 'metadata',
708
- error: 'read_token mismatch or missing',
709
- size_bytes: stat.size,
710
- modified_at: new Date(stat.mtimeMs).toISOString(),
711
- total_lines: totalLines,
712
- suggested_start_line: startLine,
713
- suggested_end_line: endLine,
714
- read_token: readToken,
715
- next: 'Retry with include_content=true and read_token from latest metadata'
716
- };
717
- }
718
-
719
810
  let content = lines.slice(startLine - 1, endLine).join('\n');
720
811
  let truncated = false;
721
812
  if (maxChars > 0 && content.length > maxChars) {
@@ -725,14 +816,14 @@ async function readFile(root, args) {
725
816
 
726
817
  // Read deduplication: if same path+range+mtime was read before, return a short stub
727
818
  const isDuplicate = checkReadDedup(
728
- args?.path,
819
+ normalizedArgs?.path,
729
820
  startLine,
730
821
  endLine,
731
822
  stat.mtimeMs
732
823
  );
733
824
  if (isDuplicate) {
734
825
  return {
735
- path: args?.path,
826
+ path: normalizedArgs?.path,
736
827
  phase: 'content',
737
828
  start_line: startLine,
738
829
  end_line: endLine,
@@ -744,7 +835,7 @@ async function readFile(root, args) {
744
835
  }
745
836
 
746
837
  return {
747
- path: args?.path,
838
+ path: normalizedArgs?.path,
748
839
  phase: 'content',
749
840
  start_line: startLine,
750
841
  end_line: endLine,
@@ -755,7 +846,8 @@ async function readFile(root, args) {
755
846
  }
756
847
 
757
848
  async function writeFile(root, args) {
758
- const rawPath = String(args?.path || args?.file_path || '').trim();
849
+ const normalizedArgs = normalizeWriteArgs(args);
850
+ const rawPath = String(normalizedArgs?.path || '').trim();
759
851
  if (!rawPath) {
760
852
  throw new Error('write requires a file path like weather/WeatherForecast.js');
761
853
  }
@@ -778,18 +870,18 @@ async function writeFile(root, args) {
778
870
  } catch {
779
871
  existed = false;
780
872
  }
781
- if (existed && !args?.append && !args?.full_file_rewrite && isCodeLikePath(rawPath)) {
873
+ if (existed && !normalizedArgs?.append && !normalizedArgs?.full_file_rewrite && isCodeLikePath(rawPath)) {
782
874
  throw new Error(
783
875
  'write blocks full overwrite for existing code files by default. Use grep/read -> edit for minimal edits, or pass full_file_rewrite=true when a whole-file rewrite is truly intended.'
784
876
  );
785
877
  }
786
878
  await fs.mkdir(path.dirname(target), { recursive: true });
787
- if (args?.append) {
788
- await fs.appendFile(target, args?.content || '', 'utf8');
879
+ if (normalizedArgs?.append) {
880
+ await fs.appendFile(target, normalizedArgs?.content || '', 'utf8');
789
881
  } else {
790
- await fs.writeFile(target, args?.content || '', 'utf8');
882
+ await fs.writeFile(target, normalizedArgs?.content || '', 'utf8');
791
883
  }
792
- const after = args?.append ? `${before}${args?.content || ''}` : args?.content || '';
884
+ const after = normalizedArgs?.append ? `${before}${normalizedArgs?.content || ''}` : normalizedArgs?.content || '';
793
885
  const beforeLines = splitLines(before);
794
886
  const afterLines = splitLines(after);
795
887
  let changeLine = 0;
@@ -805,7 +897,7 @@ async function writeFile(root, args) {
805
897
  return {
806
898
  ok: true,
807
899
  path: rawPath,
808
- action: args?.append ? 'append' : existed ? 'overwrite' : 'create',
900
+ action: normalizedArgs?.append ? 'append' : existed ? 'overwrite' : 'create',
809
901
  changed_line: changeLine || Math.max(1, afterLines.length),
810
902
  diff_preview: previewLines.map((line, idx) => `${previewStart + idx + 1}| ${line}`).join('\n')
811
903
  };
@@ -1208,12 +1300,13 @@ async function searchCode(root, args) {
1208
1300
  }
1209
1301
 
1210
1302
  async function grep(root, args) {
1211
- const pattern = String(args?.pattern || args?.query || '').trim();
1303
+ const normalizedArgs = normalizePatternArgs(args, ['query', 'symbol', 'q'], ['directory', 'dir', 'cwd']);
1304
+ const pattern = String(normalizedArgs?.pattern || '').trim();
1212
1305
  if (!pattern) throw new Error('grep requires pattern');
1213
- const maxResults = Math.max(1, Math.min(200, Number(args?.max_results || 50)));
1214
- const caseSensitive = Boolean(args?.case_sensitive);
1215
- const files = await walkTextFiles(root, args?.path || '.', normalizeFileTypes(args));
1216
- const regex = args?.regex
1306
+ const maxResults = Math.max(1, Math.min(200, Number(normalizedArgs?.max_results || 50)));
1307
+ const caseSensitive = Boolean(normalizedArgs?.case_sensitive);
1308
+ const files = await walkTextFiles(root, normalizedArgs?.path || '.', normalizeFileTypes(normalizedArgs));
1309
+ const regex = normalizedArgs?.regex
1217
1310
  ? new RegExp(pattern, caseSensitive ? 'g' : 'gi')
1218
1311
  : new RegExp(escapeRegex(pattern), caseSensitive ? 'g' : 'gi');
1219
1312
  const matches = [];
@@ -1242,12 +1335,13 @@ async function grep(root, args) {
1242
1335
  }
1243
1336
 
1244
1337
  async function glob(root, args) {
1245
- const pattern = String(args?.pattern || '').trim();
1338
+ const normalizedArgs = normalizePatternArgs(args, ['glob', 'query'], ['directory', 'dir', 'cwd']);
1339
+ const pattern = String(normalizedArgs?.pattern || '').trim();
1246
1340
  if (!pattern) throw new Error('glob requires pattern');
1247
- const maxResults = Math.max(1, Math.min(500, Number(args?.max_results || 200)));
1341
+ const maxResults = Math.max(1, Math.min(500, Number(normalizedArgs?.max_results || 200)));
1248
1342
  const regex = globToRegex(pattern);
1249
- const entries = await walkWorkspaceEntries(root, args?.path || '.', {
1250
- includeHidden: Boolean(args?.include_hidden)
1343
+ const entries = await walkWorkspaceEntries(root, normalizedArgs?.path || '.', {
1344
+ includeHidden: Boolean(normalizedArgs?.include_hidden)
1251
1345
  });
1252
1346
  const matches = entries
1253
1347
  .filter((entry) => entry.type === 'file' && regex.test(entry.path))
@@ -1261,10 +1355,11 @@ async function glob(root, args) {
1261
1355
  }
1262
1356
 
1263
1357
  async function list(root, args) {
1264
- const relativePath = String(args?.path || '.').trim() || '.';
1358
+ const normalizedArgs = normalizePathArgs(args, ['dir', 'directory', 'target']);
1359
+ const relativePath = String(normalizedArgs?.path || '.').trim() || '.';
1265
1360
  const target = resolveInWorkspace(root, relativePath);
1266
1361
  const entries = await fs.readdir(target, { withFileTypes: true });
1267
- const includeHidden = Boolean(args?.include_hidden);
1362
+ const includeHidden = Boolean(normalizedArgs?.include_hidden);
1268
1363
  const items = entries
1269
1364
  .filter((entry) => includeHidden || !entry.name.startsWith('.'))
1270
1365
  .map((entry) => ({
@@ -1552,6 +1647,8 @@ function normalizeEditTargetArgs(args = {}) {
1552
1647
  edit: normalizedEdit
1553
1648
  };
1554
1649
  }
1650
+ const topLevelOldText = args?.old_text ?? args?.old_string;
1651
+ const topLevelContent = args?.content;
1555
1652
  return {
1556
1653
  file,
1557
1654
  ast_target: args?.ast_target,
@@ -1560,7 +1657,7 @@ function normalizeEditTargetArgs(args = {}) {
1560
1657
  target: args?.target,
1561
1658
  new_content: args?.new_content ?? args?.content,
1562
1659
  old_text: args?.old_text,
1563
- new_text: args?.new_text,
1660
+ new_text: args?.new_text ?? (topLevelOldText != null && topLevelContent != null ? topLevelContent : undefined),
1564
1661
  old_string: args?.old_string,
1565
1662
  new_string: args?.new_string,
1566
1663
  anchor_text: args?.anchor_text,
@@ -1754,18 +1851,22 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1754
1851
  function: {
1755
1852
  name: 'read',
1756
1853
  description:
1757
- '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.',
1854
+ 'Inspect a file and return content directly by default. Demo-style aliases like file_path, offset, and limit are accepted. Use metadata_only=true only when you want file metadata without content. Do not use run with cat, head, or tail for file reads.',
1758
1855
  parameters: {
1759
1856
  type: 'object',
1760
1857
  properties: {
1761
- path: { type: 'string', description: 'File path to read' },
1858
+ path: { type: 'string', description: 'File path to read. You can also include an inline range like src/app.ts:10-40.' },
1859
+ file_path: { type: 'string', description: 'Alias for path' },
1762
1860
  start_line: { type: 'number', description: '1-based start line' },
1763
1861
  end_line: { type: 'number', description: 'Inclusive end line' },
1862
+ offset: { type: 'number', description: 'Alias for start_line' },
1863
+ limit: { type: 'number', description: 'Number of lines to read starting from offset/start_line' },
1764
1864
  max_chars: { type: 'number', description: 'Max chars to return' },
1765
- include_content: { type: 'boolean', description: 'Set true on the second call' },
1766
- read_token: { type: 'string', description: 'Token from the first call' }
1865
+ include_content: { type: 'boolean', description: 'Legacy compatibility flag. Content is returned by default.' },
1866
+ read_token: { type: 'string', description: 'Legacy compatibility token. No longer required for content reads.' },
1867
+ metadata_only: { type: 'boolean', description: 'Set true to return metadata without content.' }
1767
1868
  },
1768
- required: ['path']
1869
+ required: []
1769
1870
  }
1770
1871
  }
1771
1872
  },
@@ -1774,13 +1875,14 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1774
1875
  function: {
1775
1876
  name: 'grep',
1776
1877
  description:
1777
- 'Search file contents. Use this for code search before read or edit. Do not use run with grep or rg for normal code search.',
1878
+ 'Search file contents. Use this for code search before read or edit. Aliases like query and directory are accepted. Do not use run with grep or rg for normal code search.',
1778
1879
  parameters: {
1779
1880
  type: 'object',
1780
1881
  properties: {
1781
1882
  pattern: { type: 'string', description: 'Search pattern' },
1782
1883
  query: { type: 'string', description: 'Alias for pattern' },
1783
1884
  path: { type: 'string', description: 'Directory or file to search' },
1885
+ directory: { type: 'string', description: 'Alias for path' },
1784
1886
  regex: { type: 'boolean', description: 'Treat pattern as regex' },
1785
1887
  case_sensitive: { type: 'boolean', description: 'Case-sensitive matching' },
1786
1888
  max_results: { type: 'number', description: 'Max matches to return' },
@@ -1796,12 +1898,14 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1796
1898
  function: {
1797
1899
  name: 'glob',
1798
1900
  description:
1799
- 'Find files by glob pattern. Use this for file discovery before read. Do not use run with find for normal file lookup.',
1901
+ 'Find files by glob pattern. Use this for file discovery before read. Aliases like query and directory are accepted. Do not use run with find for normal file lookup.',
1800
1902
  parameters: {
1801
1903
  type: 'object',
1802
1904
  properties: {
1803
1905
  pattern: { type: 'string', description: 'Glob pattern' },
1804
1906
  path: { type: 'string', description: 'Directory to search' },
1907
+ query: { type: 'string', description: 'Alias for pattern' },
1908
+ directory: { type: 'string', description: 'Alias for path' },
1805
1909
  include_hidden: { type: 'boolean', description: 'Include dotfiles' },
1806
1910
  max_results: { type: 'number', description: 'Max results' }
1807
1911
  },
@@ -1813,11 +1917,12 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1813
1917
  type: 'function',
1814
1918
  function: {
1815
1919
  name: 'list',
1816
- description: 'List files and directories in a workspace path. Use this for quick directory discovery before deeper reads.',
1920
+ description: 'List files and directories in a workspace path. Use this for quick directory discovery before deeper reads. Aliases like directory are accepted, and plain string paths are tolerated by the runtime.',
1817
1921
  parameters: {
1818
1922
  type: 'object',
1819
1923
  properties: {
1820
1924
  path: { type: 'string', description: 'Directory path to list' },
1925
+ directory: { type: 'string', description: 'Alias for path' },
1821
1926
  include_hidden: { type: 'boolean', description: 'Include dotfiles' }
1822
1927
  }
1823
1928
  }
@@ -1859,13 +1964,16 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
1859
1964
  function: {
1860
1965
  name: 'write',
1861
1966
  description:
1862
- 'Create a new file or overwrite a file. Always include path (or file_path) and content. Use this for new files or explicit full rewrites only. Example: {path:"src/page.html", content:"..."} . If the file path is not decided yet, do not call write yet. Prefer edit for existing code changes.',
1967
+ 'Create a new file or overwrite a file. Always include path and content. Aliases like file, file_path, text, and new_content are accepted. Use this for new files or explicit full rewrites only. Example: {path:"src/page.html", content:"..."} . If the file path is not decided yet, do not call write yet. Prefer edit for existing code changes.',
1863
1968
  parameters: {
1864
1969
  type: 'object',
1865
1970
  properties: {
1866
1971
  path: { type: 'string', description: 'Required file path like src/app.js or pages/index.html. Never omit this.' },
1867
1972
  file_path: { type: 'string', description: 'Alias for path, compatible with simpler demo-style tool calls' },
1973
+ file: { type: 'string', description: 'Alias for path' },
1868
1974
  content: { type: 'string', description: 'Content to write' },
1975
+ text: { type: 'string', description: 'Alias for content' },
1976
+ new_content: { type: 'string', description: 'Alias for content' },
1869
1977
  append: { type: 'boolean', description: 'Append instead of overwrite' },
1870
1978
  full_file_rewrite: { type: 'boolean', description: 'Set true for whole-file rewrites' }
1871
1979
  },