codemini-cli 0.1.19 → 0.2.1
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/README.md +44 -20
- package/package.json +4 -2
- package/src/cli.js +1 -1
- package/src/commands/chat.js +1 -0
- package/src/core/agent-loop.js +7 -2
- package/src/core/ast.js +310 -0
- package/src/core/chat-runtime.js +47 -15
- package/src/core/checkpoint-store.js +2 -1
- package/src/core/command-loader.js +3 -4
- package/src/core/config-store.js +6 -3
- package/src/core/default-system-prompt.js +1 -1
- package/src/core/paths.js +52 -58
- package/src/core/project-index.js +510 -0
- package/src/core/provider/openai-compatible.js +9 -0
- package/src/core/shell-profile.js +1 -1
- package/src/core/shell.js +122 -2
- package/src/core/task-store.js +3 -2
- package/src/core/tools.js +188 -9
- package/src/tui/chat-app.js +694 -47
package/src/core/tools.js
CHANGED
|
@@ -4,6 +4,7 @@ import crypto from 'node:crypto';
|
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
5
|
import net from 'node:net';
|
|
6
6
|
import {
|
|
7
|
+
classifyCommandIntent,
|
|
7
8
|
hasReadyOutput,
|
|
8
9
|
isDangerousCommand,
|
|
9
10
|
isLikelyLongRunningCommand,
|
|
@@ -12,8 +13,10 @@ import {
|
|
|
12
13
|
terminateChild
|
|
13
14
|
} from './shell.js';
|
|
14
15
|
import { evaluateCommandPolicy } from './command-policy.js';
|
|
16
|
+
import { queryAst, readAstNode, resolveAstTarget } from './ast.js';
|
|
17
|
+
import { initializeProjectIndex, refreshIndexedFile } from './project-index.js';
|
|
15
18
|
|
|
16
|
-
const SKIP_DIRS = new Set(['.git', 'node_modules', '.
|
|
19
|
+
const SKIP_DIRS = new Set(['.git', 'node_modules', '.codemini', '.codemini-global', 'dist', 'coverage']);
|
|
17
20
|
const TEXT_EXTENSIONS = new Set([
|
|
18
21
|
'.js',
|
|
19
22
|
'.jsx',
|
|
@@ -793,7 +796,16 @@ async function runCommand(root, config, args) {
|
|
|
793
796
|
throw new Error('run requires command');
|
|
794
797
|
}
|
|
795
798
|
if (isLikelyLongRunningCommand(command)) {
|
|
796
|
-
|
|
799
|
+
const intent = classifyCommandIntent(command);
|
|
800
|
+
const labelMap = {
|
|
801
|
+
'frontend-service': 'frontend service',
|
|
802
|
+
'backend-service': 'backend service',
|
|
803
|
+
'database-service': 'database service',
|
|
804
|
+
'docker-service': 'Docker service',
|
|
805
|
+
service: 'long-running service'
|
|
806
|
+
};
|
|
807
|
+
const label = labelMap[intent.kind] || 'long-running service';
|
|
808
|
+
throw new Error(`Command looks like a ${label}. Use start_service instead of run.`);
|
|
797
809
|
}
|
|
798
810
|
if (
|
|
799
811
|
!config.policy.allow_dangerous_commands &&
|
|
@@ -1515,11 +1527,13 @@ function normalizeEditTargetArgs(args = {}) {
|
|
|
1515
1527
|
}
|
|
1516
1528
|
return {
|
|
1517
1529
|
file,
|
|
1530
|
+
ast_target: normalizedEdit.ast_target ?? args?.ast_target,
|
|
1518
1531
|
edit: normalizedEdit
|
|
1519
1532
|
};
|
|
1520
1533
|
}
|
|
1521
1534
|
return {
|
|
1522
1535
|
file,
|
|
1536
|
+
ast_target: args?.ast_target,
|
|
1523
1537
|
edit: {
|
|
1524
1538
|
kind: args?.kind,
|
|
1525
1539
|
target: args?.target,
|
|
@@ -1535,6 +1549,7 @@ function normalizeEditTargetArgs(args = {}) {
|
|
|
1535
1549
|
async function editTarget(root, args) {
|
|
1536
1550
|
const normalized = normalizeEditTargetArgs(args);
|
|
1537
1551
|
const file = normalized.file;
|
|
1552
|
+
const astTarget = normalized.ast_target;
|
|
1538
1553
|
const edit = normalized.edit || {};
|
|
1539
1554
|
let kind = String(edit.kind || '').trim();
|
|
1540
1555
|
const hasContent = edit.new_content != null || edit.content != null;
|
|
@@ -1551,6 +1566,19 @@ async function editTarget(root, args) {
|
|
|
1551
1566
|
}
|
|
1552
1567
|
}
|
|
1553
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
|
+
}
|
|
1554
1582
|
if (kind === 'replace_block') {
|
|
1555
1583
|
const resolvedTarget =
|
|
1556
1584
|
edit.target ||
|
|
@@ -1604,7 +1632,85 @@ async function editTarget(root, args) {
|
|
|
1604
1632
|
throw new Error(`edit does not support kind: ${kind}`);
|
|
1605
1633
|
}
|
|
1606
1634
|
|
|
1607
|
-
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
|
+
};
|
|
1608
1714
|
const definitions = [
|
|
1609
1715
|
{
|
|
1610
1716
|
type: 'function',
|
|
@@ -1683,7 +1789,7 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config }) {
|
|
|
1683
1789
|
function: {
|
|
1684
1790
|
name: 'edit',
|
|
1685
1791
|
description:
|
|
1686
|
-
'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.
|
|
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.',
|
|
1687
1793
|
parameters: {
|
|
1688
1794
|
type: 'object',
|
|
1689
1795
|
properties: {
|
|
@@ -1697,6 +1803,7 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config }) {
|
|
|
1697
1803
|
position: { type: 'string' },
|
|
1698
1804
|
kind: { type: 'string' },
|
|
1699
1805
|
target: { type: 'object' },
|
|
1806
|
+
ast_target: { type: 'object' },
|
|
1700
1807
|
symbol: { type: 'string' },
|
|
1701
1808
|
line: { type: 'number' },
|
|
1702
1809
|
edit: { type: 'object' },
|
|
@@ -1705,6 +1812,42 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config }) {
|
|
|
1705
1812
|
}
|
|
1706
1813
|
}
|
|
1707
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
|
+
},
|
|
1708
1851
|
{
|
|
1709
1852
|
type: 'function',
|
|
1710
1853
|
function: {
|
|
@@ -1727,7 +1870,8 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config }) {
|
|
|
1727
1870
|
type: 'function',
|
|
1728
1871
|
function: {
|
|
1729
1872
|
name: 'run',
|
|
1730
|
-
description:
|
|
1873
|
+
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.',
|
|
1731
1875
|
parameters: {
|
|
1732
1876
|
type: 'object',
|
|
1733
1877
|
properties: {
|
|
@@ -1771,7 +1915,8 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config }) {
|
|
|
1771
1915
|
type: 'function',
|
|
1772
1916
|
function: {
|
|
1773
1917
|
name: 'start_service',
|
|
1774
|
-
description:
|
|
1918
|
+
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.',
|
|
1775
1920
|
parameters: {
|
|
1776
1921
|
type: 'object',
|
|
1777
1922
|
properties: {
|
|
@@ -1864,10 +2009,44 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config }) {
|
|
|
1864
2009
|
grep: (args) => grep(workspaceRoot, args),
|
|
1865
2010
|
glob: (args) => glob(workspaceRoot, args),
|
|
1866
2011
|
list: (args) => list(workspaceRoot, args),
|
|
1867
|
-
|
|
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
|
+
},
|
|
1868
2032
|
generate_diff: (args) => generateDiff(workspaceRoot, args),
|
|
1869
|
-
patch: (args) =>
|
|
1870
|
-
|
|
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
|
+
},
|
|
1871
2050
|
run: (args) => runCommand(workspaceRoot, config, args),
|
|
1872
2051
|
start_service: (args) => startService(workspaceRoot, config, args),
|
|
1873
2052
|
list_services: () => listServices(workspaceRoot),
|