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/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/ast.js +310 -0
- package/src/core/chat-runtime.js +37 -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/shell-profile.js +1 -1
- package/src/core/task-store.js +3 -2
- package/src/core/tools.js +173 -6
- package/src/tui/chat-app.js +221 -47
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getShellSystemPrompt } from './shell-profile.js';
|
|
2
2
|
|
|
3
3
|
export function buildDefaultSystemPrompt(config = {}) {
|
|
4
|
-
return `${getShellSystemPrompt(config?.shell?.default)} If a command or tool is blocked or fails, inspect the error and retry with allowed commands or tools. Do not claim filesystem access is impossible unless the allowed search/read tools also fail.`;
|
|
4
|
+
return `${getShellSystemPrompt(config?.shell?.default)} If a command or tool is blocked or fails, inspect the error and retry with allowed commands or tools. For AST-scoped edits, if edit rejects a call because kind=replace_block or ast_target is missing or stale, fix the tool arguments and retry instead of switching to a broader text edit. Do not claim filesystem access is impossible unless the allowed search/read tools also fail.`;
|
|
5
5
|
}
|
package/src/core/paths.js
CHANGED
|
@@ -1,75 +1,33 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
1
|
import os from 'node:os';
|
|
3
2
|
import path from 'node:path';
|
|
4
3
|
|
|
5
|
-
const
|
|
6
|
-
const
|
|
4
|
+
const GLOBAL_APP_DIR = 'codemini-global';
|
|
5
|
+
const PROJECT_APP_DIR = '.codemini';
|
|
6
|
+
const PROJECT_INDEX_DIR = '.codemini-project';
|
|
7
7
|
|
|
8
|
-
function
|
|
9
|
-
if (process.env.
|
|
10
|
-
return process.env.
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
if (process.env.COMPANY_CODER_CONFIG_DIR) {
|
|
14
|
-
return process.env.COMPANY_CODER_CONFIG_DIR;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
if (process.platform === 'win32') {
|
|
18
|
-
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
|
|
19
|
-
return path.join(appData, APP_DIR);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (process.platform === 'darwin') {
|
|
23
|
-
return path.join(os.homedir(), 'Library', 'Preferences', APP_DIR);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (process.env.XDG_CONFIG_HOME) {
|
|
27
|
-
return path.join(process.env.XDG_CONFIG_HOME, APP_DIR);
|
|
8
|
+
export function getBaseConfigDir() {
|
|
9
|
+
if (process.env.CODEMINI_GLOBAL_DIR) {
|
|
10
|
+
return process.env.CODEMINI_GLOBAL_DIR;
|
|
28
11
|
}
|
|
29
12
|
|
|
30
|
-
// Fallback for restricted/sandboxed non-Windows environments.
|
|
31
|
-
return path.join(process.cwd(), '.codemini-cli');
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function getLegacyBaseConfigDir() {
|
|
35
13
|
if (process.platform === 'win32') {
|
|
36
14
|
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
|
|
37
|
-
return path.join(appData,
|
|
15
|
+
return path.join(appData, GLOBAL_APP_DIR);
|
|
38
16
|
}
|
|
39
17
|
|
|
40
18
|
if (process.platform === 'darwin') {
|
|
41
|
-
return path.join(os.homedir(), 'Library', 'Preferences',
|
|
19
|
+
return path.join(os.homedir(), 'Library', 'Preferences', GLOBAL_APP_DIR);
|
|
42
20
|
}
|
|
43
21
|
|
|
44
22
|
if (process.env.XDG_CONFIG_HOME) {
|
|
45
|
-
return path.join(process.env.XDG_CONFIG_HOME,
|
|
23
|
+
return path.join(process.env.XDG_CONFIG_HOME, GLOBAL_APP_DIR);
|
|
46
24
|
}
|
|
47
25
|
|
|
48
|
-
return path.join(process.cwd(), '.
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function tryMigrateLegacyDir(preferred, legacy) {
|
|
52
|
-
if (!preferred || !legacy || preferred === legacy) return preferred;
|
|
53
|
-
if (fs.existsSync(preferred) || !fs.existsSync(legacy)) return preferred;
|
|
54
|
-
try {
|
|
55
|
-
fs.renameSync(legacy, preferred);
|
|
56
|
-
return preferred;
|
|
57
|
-
} catch {
|
|
58
|
-
return preferred;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function getBaseConfigDir() {
|
|
63
|
-
const preferred = getPreferredBaseConfigDir();
|
|
64
|
-
if (process.env.CODEMINI_CONFIG_DIR || process.env.COMPANY_CODER_CONFIG_DIR) {
|
|
65
|
-
return preferred;
|
|
66
|
-
}
|
|
67
|
-
const legacy = getLegacyBaseConfigDir();
|
|
68
|
-
return tryMigrateLegacyDir(preferred, legacy);
|
|
26
|
+
return path.join(process.cwd(), '.codemini-global');
|
|
69
27
|
}
|
|
70
28
|
|
|
71
29
|
export function getLegacyConfigDir() {
|
|
72
|
-
return
|
|
30
|
+
return getBaseConfigDir();
|
|
73
31
|
}
|
|
74
32
|
|
|
75
33
|
export function getConfigFilePath() {
|
|
@@ -96,14 +54,50 @@ export function getInputHistoryFilePath() {
|
|
|
96
54
|
return path.join(getBaseConfigDir(), 'input-history.json');
|
|
97
55
|
}
|
|
98
56
|
|
|
57
|
+
export function getProjectWorkspaceDir(cwd = process.cwd()) {
|
|
58
|
+
return path.join(cwd, PROJECT_APP_DIR);
|
|
59
|
+
}
|
|
60
|
+
|
|
99
61
|
export function getProjectCommandsDir(cwd = process.cwd()) {
|
|
100
|
-
return path.join(cwd, '
|
|
62
|
+
return path.join(getProjectWorkspaceDir(cwd), 'commands');
|
|
101
63
|
}
|
|
102
64
|
|
|
103
|
-
export function
|
|
104
|
-
return path.join(cwd, '
|
|
65
|
+
export function getProjectSkillsDir(cwd = process.cwd()) {
|
|
66
|
+
return path.join(getProjectWorkspaceDir(cwd), 'skills');
|
|
105
67
|
}
|
|
106
68
|
|
|
107
|
-
export function
|
|
108
|
-
return
|
|
69
|
+
export function getProjectSpecsDir(cwd = process.cwd(), sessionId = '') {
|
|
70
|
+
return sessionId
|
|
71
|
+
? path.join(getProjectWorkspaceDir(cwd), 'specs', String(sessionId))
|
|
72
|
+
: path.join(getProjectWorkspaceDir(cwd), 'specs');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function getProjectPlansDir(cwd = process.cwd(), sessionId = '') {
|
|
76
|
+
return sessionId
|
|
77
|
+
? path.join(getProjectWorkspaceDir(cwd), 'plans', String(sessionId))
|
|
78
|
+
: path.join(getProjectWorkspaceDir(cwd), 'plans');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function getProjectCheckpointsDir(cwd = process.cwd()) {
|
|
82
|
+
return path.join(getProjectWorkspaceDir(cwd), 'checkpoints');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function getProjectTasksDir(cwd = process.cwd()) {
|
|
86
|
+
return path.join(getProjectWorkspaceDir(cwd), 'tasks');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function getProjectLegacyTasksFilePath(cwd = process.cwd()) {
|
|
90
|
+
return path.join(getProjectWorkspaceDir(cwd), 'tasks.json');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function getProjectMapPath(cwd = process.cwd()) {
|
|
94
|
+
return path.join(cwd, PROJECT_INDEX_DIR, 'project-map.json');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function getFileIndexPath(cwd = process.cwd()) {
|
|
98
|
+
return path.join(cwd, PROJECT_INDEX_DIR, 'file-index.json');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function getProjectIndexDir(cwd = process.cwd()) {
|
|
102
|
+
return path.join(cwd, PROJECT_INDEX_DIR);
|
|
109
103
|
}
|
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { getFileIndexPath, getProjectIndexDir, getProjectMapPath, getProjectWorkspaceDir } from './paths.js';
|
|
5
|
+
|
|
6
|
+
const SKIP_DIRS = new Set(['.git', 'node_modules', '.codemini', '.codemini-project', '.codemini-global', 'dist', 'coverage']);
|
|
7
|
+
const PROJECT_MARKER_FILES = new Set([
|
|
8
|
+
'package.json',
|
|
9
|
+
'tsconfig.json',
|
|
10
|
+
'pyproject.toml',
|
|
11
|
+
'requirements.txt',
|
|
12
|
+
'go.mod',
|
|
13
|
+
'Cargo.toml',
|
|
14
|
+
'composer.json',
|
|
15
|
+
'Gemfile',
|
|
16
|
+
'pom.xml',
|
|
17
|
+
'build.gradle',
|
|
18
|
+
'build.gradle.kts',
|
|
19
|
+
'Makefile',
|
|
20
|
+
'.gitignore'
|
|
21
|
+
]);
|
|
22
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
23
|
+
'.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx', '.py', '.go', '.c', '.h', '.cpp', '.cc', '.cxx', '.hpp', '.hh',
|
|
24
|
+
'.sh', '.bash', '.java', '.rs', '.cs', '.php', '.rb'
|
|
25
|
+
]);
|
|
26
|
+
const LANGUAGE_BY_EXT = {
|
|
27
|
+
'.js': 'js', '.jsx': 'jsx', '.mjs': 'js', '.cjs': 'js', '.ts': 'ts', '.tsx': 'tsx', '.py': 'python',
|
|
28
|
+
'.go': 'go', '.c': 'c', '.h': 'c', '.cpp': 'cpp', '.cc': 'cpp', '.cxx': 'cpp', '.hpp': 'cpp', '.hh': 'cpp',
|
|
29
|
+
'.sh': 'bash', '.bash': 'bash', '.java': 'java', '.rs': 'rust', '.cs': 'csharp', '.php': 'php', '.rb': 'ruby'
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const initCache = new Map();
|
|
33
|
+
const PROJECT_CONTEXT_MAX_FILES = 6;
|
|
34
|
+
|
|
35
|
+
function sha1(input) {
|
|
36
|
+
return crypto.createHash('sha1').update(String(input || '')).digest('hex');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function clipList(values, max = 32) {
|
|
40
|
+
return [...new Set((Array.isArray(values) ? values : []).filter(Boolean))].slice(0, max);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function rel(cwd, filePath) {
|
|
44
|
+
return path.relative(cwd, filePath).replace(/\\/g, '/');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function normalizeRelativePath(value) {
|
|
48
|
+
return String(value || '').replace(/\\/g, '/').replace(/^\.\/+/, '').replace(/^\/+/, '');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function safeStat(filePath) {
|
|
52
|
+
try {
|
|
53
|
+
return await fs.stat(filePath);
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function safeReadJson(filePath, fallback) {
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(await fs.readFile(filePath, 'utf8'));
|
|
62
|
+
} catch {
|
|
63
|
+
return fallback;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function tokenizeQuery(text) {
|
|
68
|
+
return [...new Set(String(text || '').toLowerCase().match(/[a-z0-9_./-]+/g) || [])].filter(Boolean);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function trimInline(value, max = 240) {
|
|
72
|
+
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
73
|
+
if (!text) return '';
|
|
74
|
+
if (text.length <= max) return text;
|
|
75
|
+
return `${text.slice(0, max - 3)}...`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function trimMultiline(value, max = 1800) {
|
|
79
|
+
const text = String(value || '').trim();
|
|
80
|
+
if (!text) return '';
|
|
81
|
+
if (text.length <= max) return text;
|
|
82
|
+
return `${text.slice(0, max - 3).trimEnd()}...`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function writeJson(filePath, value) {
|
|
86
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
87
|
+
await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function escapeRegex(value) {
|
|
91
|
+
return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function gitignorePatternToRegex(pattern) {
|
|
95
|
+
const normalized = normalizeRelativePath(pattern);
|
|
96
|
+
let regexBody = '';
|
|
97
|
+
for (let index = 0; index < normalized.length; index += 1) {
|
|
98
|
+
const ch = normalized[index];
|
|
99
|
+
const next = normalized[index + 1];
|
|
100
|
+
if (ch === '*') {
|
|
101
|
+
if (next === '*') {
|
|
102
|
+
regexBody += '.*';
|
|
103
|
+
index += 1;
|
|
104
|
+
} else {
|
|
105
|
+
regexBody += '[^/]*';
|
|
106
|
+
}
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
if (ch === '?') {
|
|
110
|
+
regexBody += '[^/]';
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
regexBody += escapeRegex(ch);
|
|
114
|
+
}
|
|
115
|
+
return new RegExp(`^${regexBody}$`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function readGitignoreRules(cwd) {
|
|
119
|
+
try {
|
|
120
|
+
const raw = await fs.readFile(path.join(cwd, '.gitignore'), 'utf8');
|
|
121
|
+
return raw
|
|
122
|
+
.split(/\r?\n/)
|
|
123
|
+
.map((line) => line.trim())
|
|
124
|
+
.filter((line) => line && !line.startsWith('#'))
|
|
125
|
+
.map((line) => {
|
|
126
|
+
const negated = line.startsWith('!');
|
|
127
|
+
const source = negated ? line.slice(1) : line;
|
|
128
|
+
const dirOnly = source.endsWith('/');
|
|
129
|
+
const anchored = source.startsWith('/');
|
|
130
|
+
const normalized = normalizeRelativePath(dirOnly ? source.slice(0, -1) : source);
|
|
131
|
+
return {
|
|
132
|
+
negated,
|
|
133
|
+
dirOnly,
|
|
134
|
+
anchored,
|
|
135
|
+
normalized,
|
|
136
|
+
hasSlash: normalized.includes('/'),
|
|
137
|
+
regex: gitignorePatternToRegex(normalized)
|
|
138
|
+
};
|
|
139
|
+
})
|
|
140
|
+
.filter((rule) => rule.normalized);
|
|
141
|
+
} catch {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function matchesGitignoreRule(rule, relativePath, isDirectory) {
|
|
147
|
+
if (!rule || !relativePath) return false;
|
|
148
|
+
if (rule.dirOnly && !isDirectory) return false;
|
|
149
|
+
const normalizedPath = normalizeRelativePath(relativePath);
|
|
150
|
+
if (!normalizedPath) return false;
|
|
151
|
+
if (rule.anchored || rule.hasSlash) {
|
|
152
|
+
return rule.regex.test(normalizedPath);
|
|
153
|
+
}
|
|
154
|
+
return normalizedPath.split('/').some((segment) => rule.regex.test(segment));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function shouldIgnorePath(relativePath, isDirectory, gitignoreRules = []) {
|
|
158
|
+
const normalizedPath = normalizeRelativePath(relativePath);
|
|
159
|
+
if (!normalizedPath) return false;
|
|
160
|
+
const topName = normalizedPath.split('/')[0];
|
|
161
|
+
if (topName && SKIP_DIRS.has(topName)) return true;
|
|
162
|
+
let ignored = false;
|
|
163
|
+
for (const rule of gitignoreRules) {
|
|
164
|
+
if (!matchesGitignoreRule(rule, normalizedPath, isDirectory)) continue;
|
|
165
|
+
ignored = !rule.negated;
|
|
166
|
+
}
|
|
167
|
+
return ignored;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function detectWorkspaceKind(cwd) {
|
|
171
|
+
const gitDir = await safeStat(path.join(cwd, '.git'));
|
|
172
|
+
if (gitDir?.isDirectory()) return 'project';
|
|
173
|
+
for (const marker of PROJECT_MARKER_FILES) {
|
|
174
|
+
const stat = await safeStat(path.join(cwd, marker));
|
|
175
|
+
if (stat?.isFile()) return 'project';
|
|
176
|
+
}
|
|
177
|
+
return 'directory';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function findNearestProjectRoot(startDir, workspaceRoot) {
|
|
181
|
+
let current = path.resolve(startDir);
|
|
182
|
+
const root = path.resolve(workspaceRoot);
|
|
183
|
+
while (current.startsWith(root)) {
|
|
184
|
+
if ((await detectWorkspaceKind(current)) === 'project') return current;
|
|
185
|
+
if (current === root) break;
|
|
186
|
+
const parent = path.dirname(current);
|
|
187
|
+
if (parent === current) break;
|
|
188
|
+
current = parent;
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function findProjectRootFromFile(workspaceRoot, relativePath = '') {
|
|
194
|
+
const absolutePath = path.resolve(workspaceRoot, String(relativePath || '.'));
|
|
195
|
+
const stat = await safeStat(absolutePath);
|
|
196
|
+
const probeStart = stat?.isDirectory() ? absolutePath : path.dirname(absolutePath);
|
|
197
|
+
return findNearestProjectRoot(probeStart, workspaceRoot);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function findNearestIndexedProjectRoot(startDir, workspaceRoot) {
|
|
201
|
+
let current = path.resolve(startDir);
|
|
202
|
+
const root = path.resolve(workspaceRoot);
|
|
203
|
+
while (current.startsWith(root)) {
|
|
204
|
+
const projectMapStat = await safeStat(getProjectMapPath(current));
|
|
205
|
+
const fileIndexStat = await safeStat(getFileIndexPath(current));
|
|
206
|
+
if (projectMapStat?.isFile() && fileIndexStat?.isFile()) return current;
|
|
207
|
+
if (current === root) break;
|
|
208
|
+
const parent = path.dirname(current);
|
|
209
|
+
if (parent === current) break;
|
|
210
|
+
current = parent;
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function walkFiles(cwd, start = cwd, out = [], gitignoreRules = []) {
|
|
216
|
+
const entries = await fs.readdir(start, { withFileTypes: true });
|
|
217
|
+
for (const entry of entries) {
|
|
218
|
+
const absolutePath = path.join(start, entry.name);
|
|
219
|
+
const relativePath = rel(cwd, absolutePath);
|
|
220
|
+
if (entry.isDirectory()) {
|
|
221
|
+
if (shouldIgnorePath(relativePath, true, gitignoreRules)) continue;
|
|
222
|
+
await walkFiles(cwd, absolutePath, out, gitignoreRules);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
if (shouldIgnorePath(relativePath, false, gitignoreRules)) continue;
|
|
226
|
+
out.push(absolutePath);
|
|
227
|
+
}
|
|
228
|
+
return out;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function categorizeDirectory(relativeDir) {
|
|
232
|
+
const text = String(relativeDir || '').toLowerCase();
|
|
233
|
+
if (!text || text === '.') return 'root';
|
|
234
|
+
if (/(^|\/)(src|app|apps)\b/.test(text)) return 'source';
|
|
235
|
+
if (/(^|\/)(test|tests|__tests__|spec)\b/.test(text)) return 'test';
|
|
236
|
+
if (/(^|\/)(scripts|bin)\b/.test(text)) return 'script';
|
|
237
|
+
if (/(^|\/)(config|configs)\b/.test(text)) return 'config';
|
|
238
|
+
return 'other';
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function extractMatches(regex, text, group = 1) {
|
|
242
|
+
const out = [];
|
|
243
|
+
for (const match of String(text || '').matchAll(regex)) {
|
|
244
|
+
const value = String(match[group] || '').trim();
|
|
245
|
+
if (value) out.push(value);
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function buildFileEntry(relativePath, content, stat) {
|
|
251
|
+
const ext = path.extname(relativePath).toLowerCase();
|
|
252
|
+
const imports = clipList([
|
|
253
|
+
...extractMatches(/import\s+(?:[^'"]*from\s+)?['"]([^'"]+)['"]/g, content),
|
|
254
|
+
...extractMatches(/require\(\s*['"]([^'"]+)['"]\s*\)/g, content),
|
|
255
|
+
...extractMatches(/\buse\s+([A-Za-z0-9_:\\]+)/g, ext === '.rs' ? content : '')
|
|
256
|
+
]);
|
|
257
|
+
const exports = clipList([
|
|
258
|
+
...extractMatches(/export\s+(?:async\s+)?function\s+([A-Za-z0-9_$]+)/g, content),
|
|
259
|
+
...extractMatches(/export\s+class\s+([A-Za-z0-9_$]+)/g, content),
|
|
260
|
+
...extractMatches(/export\s+const\s+([A-Za-z0-9_$]+)/g, content),
|
|
261
|
+
...extractMatches(/module\.exports\s*=\s*([A-Za-z0-9_$]+)/g, content),
|
|
262
|
+
...extractMatches(/exports\.([A-Za-z0-9_$]+)/g, content)
|
|
263
|
+
]);
|
|
264
|
+
const functions = clipList([
|
|
265
|
+
...extractMatches(/\bfunction\s+([A-Za-z0-9_$]+)/g, content),
|
|
266
|
+
...extractMatches(/\bdef\s+([A-Za-z0-9_]+)/g, content),
|
|
267
|
+
...extractMatches(/\bfunc\s+([A-Za-z0-9_]+)/g, content),
|
|
268
|
+
...extractMatches(/\bfn\s+([A-Za-z0-9_]+)/g, content),
|
|
269
|
+
...extractMatches(/^\s*(?:public|private|protected|internal)?\s*(?:static\s+)?[A-Za-z0-9_<>,[\]?]+\s+([A-Za-z0-9_]+)\s*\(/gm, content),
|
|
270
|
+
...extractMatches(/^\s*function\s+([A-Za-z0-9_]+)/gm, content),
|
|
271
|
+
...extractMatches(/^\s*def\s+([A-Za-z0-9_]+)/gm, content)
|
|
272
|
+
]);
|
|
273
|
+
const classes = clipList([
|
|
274
|
+
...extractMatches(/\bclass\s+([A-Za-z0-9_$]+)/g, content)
|
|
275
|
+
]);
|
|
276
|
+
const calls = clipList([
|
|
277
|
+
...extractMatches(/\b([A-Za-z0-9_$]+)\s*\(/g, content).filter((name) => !['if', 'for', 'while', 'switch', 'return', 'function', 'class', 'catch'].includes(name))
|
|
278
|
+
], 64);
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
file: relativePath,
|
|
282
|
+
language: LANGUAGE_BY_EXT[ext] || 'text',
|
|
283
|
+
hash: sha1(content),
|
|
284
|
+
size: Number(stat?.size || content.length || 0),
|
|
285
|
+
mtimeMs: Number(stat?.mtimeMs || 0),
|
|
286
|
+
imports,
|
|
287
|
+
exports,
|
|
288
|
+
functions,
|
|
289
|
+
classes,
|
|
290
|
+
calls
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function scanProject(cwd) {
|
|
295
|
+
const workspaceKind = await detectWorkspaceKind(cwd);
|
|
296
|
+
if (workspaceKind !== 'project') {
|
|
297
|
+
return {
|
|
298
|
+
workspaceKind,
|
|
299
|
+
projectMap: null,
|
|
300
|
+
fileIndex: null,
|
|
301
|
+
gitignoreRules: []
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const gitignoreRules = await readGitignoreRules(cwd);
|
|
306
|
+
const allFiles = await walkFiles(cwd, cwd, [], gitignoreRules);
|
|
307
|
+
const relativeFiles = allFiles.map((filePath) => rel(cwd, filePath));
|
|
308
|
+
const sourceFiles = allFiles.filter((filePath) => SOURCE_EXTENSIONS.has(path.extname(filePath).toLowerCase()));
|
|
309
|
+
|
|
310
|
+
const packageJson = await safeReadJson(path.join(cwd, 'package.json'), null);
|
|
311
|
+
const tsconfigExists = Boolean(await safeStat(path.join(cwd, 'tsconfig.json')));
|
|
312
|
+
const sourceRoots = clipList(relativeFiles.filter((value) => /^(src|app|apps)\b/.test(value)).map((value) => value.split('/')[0]), 12);
|
|
313
|
+
const testRoots = clipList(relativeFiles.filter((value) => /^(tests|test|__tests__)\b/.test(value)).map((value) => value.split('/')[0]), 12);
|
|
314
|
+
const entryCandidates = clipList(
|
|
315
|
+
relativeFiles.filter((value) => /(^|\/)(main|index|server|app)\.(js|jsx|mjs|cjs|ts|tsx|py|go|rs|java|cs|php|rb)$/.test(value)),
|
|
316
|
+
16
|
|
317
|
+
);
|
|
318
|
+
const languages = clipList(sourceFiles.map((filePath) => LANGUAGE_BY_EXT[path.extname(filePath).toLowerCase()] || '').filter(Boolean), 16);
|
|
319
|
+
const importantFiles = clipList(
|
|
320
|
+
relativeFiles.filter((value) => ['package.json', 'tsconfig.json', 'pyproject.toml', 'go.mod', 'Cargo.toml', 'composer.json', 'Gemfile'].includes(value)),
|
|
321
|
+
16
|
|
322
|
+
);
|
|
323
|
+
const packageManagers = clipList([
|
|
324
|
+
packageJson ? 'npm' : '',
|
|
325
|
+
relativeFiles.includes('bun.lockb') ? 'bun' : '',
|
|
326
|
+
relativeFiles.includes('pnpm-lock.yaml') ? 'pnpm' : '',
|
|
327
|
+
relativeFiles.includes('yarn.lock') ? 'yarn' : ''
|
|
328
|
+
].filter(Boolean));
|
|
329
|
+
const frameworkHints = clipList([
|
|
330
|
+
packageJson?.dependencies?.react || packageJson?.devDependencies?.react ? 'react' : '',
|
|
331
|
+
packageJson?.dependencies?.express ? 'express' : '',
|
|
332
|
+
packageJson?.dependencies?.vue ? 'vue' : '',
|
|
333
|
+
packageJson?.dependencies?.next ? 'next' : '',
|
|
334
|
+
tsconfigExists ? 'typescript' : ''
|
|
335
|
+
].filter(Boolean));
|
|
336
|
+
|
|
337
|
+
const directories = {};
|
|
338
|
+
for (const value of relativeFiles) {
|
|
339
|
+
const dir = path.posix.dirname(value);
|
|
340
|
+
if (!dir || dir === '.') continue;
|
|
341
|
+
if (!(dir in directories)) directories[dir] = categorizeDirectory(dir);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const files = [];
|
|
345
|
+
for (const filePath of sourceFiles) {
|
|
346
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
347
|
+
const stat = await fs.stat(filePath);
|
|
348
|
+
files.push(buildFileEntry(rel(cwd, filePath), content, stat));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
workspaceKind,
|
|
353
|
+
projectMap: {
|
|
354
|
+
projectRoot: cwd,
|
|
355
|
+
workspaceKind,
|
|
356
|
+
languages,
|
|
357
|
+
packageManagers,
|
|
358
|
+
importantFiles,
|
|
359
|
+
sourceRoots,
|
|
360
|
+
testRoots,
|
|
361
|
+
entryCandidates,
|
|
362
|
+
frameworkHints,
|
|
363
|
+
directories,
|
|
364
|
+
gitignoreEnabled: gitignoreRules.length > 0,
|
|
365
|
+
updatedAt: new Date().toISOString()
|
|
366
|
+
},
|
|
367
|
+
fileIndex: {
|
|
368
|
+
updatedAt: new Date().toISOString(),
|
|
369
|
+
files
|
|
370
|
+
},
|
|
371
|
+
gitignoreRules
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export async function initializeProjectIndex(cwd = process.cwd()) {
|
|
376
|
+
const targetRoot = (await findNearestProjectRoot(cwd, cwd)) || path.resolve(cwd);
|
|
377
|
+
const cacheKey = targetRoot;
|
|
378
|
+
if (initCache.has(cacheKey)) return initCache.get(cacheKey);
|
|
379
|
+
const promise = (async () => {
|
|
380
|
+
const workspaceDir = getProjectWorkspaceDir(cwd);
|
|
381
|
+
await fs.mkdir(workspaceDir, { recursive: true });
|
|
382
|
+
const { workspaceKind, projectMap, fileIndex } = await scanProject(targetRoot);
|
|
383
|
+
if (workspaceKind !== 'project' || !projectMap || !fileIndex) {
|
|
384
|
+
return {
|
|
385
|
+
workspaceKind,
|
|
386
|
+
projectRoot: null,
|
|
387
|
+
projectMap: null,
|
|
388
|
+
fileIndex: null,
|
|
389
|
+
summary: '',
|
|
390
|
+
skipped: true
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
await fs.mkdir(getProjectIndexDir(targetRoot), { recursive: true });
|
|
394
|
+
await writeJson(getProjectMapPath(targetRoot), projectMap);
|
|
395
|
+
await writeJson(getFileIndexPath(targetRoot), fileIndex);
|
|
396
|
+
return {
|
|
397
|
+
workspaceKind,
|
|
398
|
+
projectRoot: targetRoot,
|
|
399
|
+
projectMap,
|
|
400
|
+
fileIndex,
|
|
401
|
+
summary: `initialized ${path.basename(targetRoot) || '.'}/.codemini-project (${Array.isArray(fileIndex?.files) ? fileIndex.files.length : 0} files)`
|
|
402
|
+
};
|
|
403
|
+
})();
|
|
404
|
+
initCache.set(cacheKey, promise);
|
|
405
|
+
try {
|
|
406
|
+
return await promise;
|
|
407
|
+
} catch (error) {
|
|
408
|
+
initCache.delete(cacheKey);
|
|
409
|
+
throw error;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export async function refreshIndexedFile(cwd = process.cwd(), relativePath = '') {
|
|
414
|
+
if (!relativePath) return null;
|
|
415
|
+
const workspaceDir = getProjectWorkspaceDir(cwd);
|
|
416
|
+
await fs.mkdir(workspaceDir, { recursive: true });
|
|
417
|
+
const projectRoot = await findProjectRootFromFile(cwd, relativePath);
|
|
418
|
+
if (!projectRoot) return null;
|
|
419
|
+
const fileIndexPath = getFileIndexPath(projectRoot);
|
|
420
|
+
const gitignoreRules = await readGitignoreRules(projectRoot);
|
|
421
|
+
const absolutePath = path.join(cwd, relativePath);
|
|
422
|
+
const stat = await safeStat(absolutePath);
|
|
423
|
+
let action = 'updated';
|
|
424
|
+
const projectRelativePath = path.relative(projectRoot, absolutePath).replace(/\\/g, '/');
|
|
425
|
+
const current = await safeReadJson(fileIndexPath, { updatedAt: '', files: [] });
|
|
426
|
+
const files = Array.isArray(current.files) ? [...current.files] : [];
|
|
427
|
+
const index = files.findIndex((entry) => entry.file === projectRelativePath);
|
|
428
|
+
|
|
429
|
+
if (shouldIgnorePath(projectRelativePath, Boolean(stat?.isDirectory?.()), gitignoreRules)) {
|
|
430
|
+
if (index >= 0) files.splice(index, 1);
|
|
431
|
+
action = 'removed';
|
|
432
|
+
} else if (!stat || !stat.isFile()) {
|
|
433
|
+
if (index >= 0) files.splice(index, 1);
|
|
434
|
+
action = 'removed';
|
|
435
|
+
} else {
|
|
436
|
+
const ext = path.extname(relativePath).toLowerCase();
|
|
437
|
+
if (!SOURCE_EXTENSIONS.has(ext)) {
|
|
438
|
+
if (index >= 0) files.splice(index, 1);
|
|
439
|
+
action = 'removed';
|
|
440
|
+
} else {
|
|
441
|
+
const content = await fs.readFile(absolutePath, 'utf8');
|
|
442
|
+
const nextEntry = buildFileEntry(projectRelativePath, content, stat);
|
|
443
|
+
if (index >= 0) {
|
|
444
|
+
files[index] = nextEntry;
|
|
445
|
+
} else {
|
|
446
|
+
files.push(nextEntry);
|
|
447
|
+
action = 'added';
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
await writeJson(fileIndexPath, {
|
|
453
|
+
updatedAt: new Date().toISOString(),
|
|
454
|
+
files: files.sort((left, right) => left.file.localeCompare(right.file))
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
return {
|
|
458
|
+
path: projectRelativePath,
|
|
459
|
+
projectRoot,
|
|
460
|
+
action,
|
|
461
|
+
summary: `${action} ${path.basename(projectRoot) || '.'}/.codemini-project for ${projectRelativePath}`
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export async function buildProjectContextSnippet(cwd = process.cwd(), userText = '') {
|
|
466
|
+
const indexedRoot = await findNearestIndexedProjectRoot(cwd, cwd);
|
|
467
|
+
if (!indexedRoot) return '';
|
|
468
|
+
|
|
469
|
+
const projectMap = await safeReadJson(getProjectMapPath(indexedRoot), null);
|
|
470
|
+
const fileIndex = await safeReadJson(getFileIndexPath(indexedRoot), null);
|
|
471
|
+
if (!projectMap || !Array.isArray(fileIndex?.files)) return '';
|
|
472
|
+
|
|
473
|
+
const lines = [
|
|
474
|
+
'Project Context:',
|
|
475
|
+
`- project_root: ${indexedRoot}`,
|
|
476
|
+
`- languages: ${(projectMap.languages || []).slice(0, 6).join(', ') || 'unknown'}`,
|
|
477
|
+
`- source_roots: ${(projectMap.sourceRoots || []).slice(0, 6).join(', ') || 'none'}`,
|
|
478
|
+
`- test_roots: ${(projectMap.testRoots || []).slice(0, 6).join(', ') || 'none'}`,
|
|
479
|
+
`- entry_candidates: ${(projectMap.entryCandidates || []).slice(0, 6).join(', ') || 'none'}`,
|
|
480
|
+
`- framework_hints: ${(projectMap.frameworkHints || []).slice(0, 6).join(', ') || 'none'}`
|
|
481
|
+
];
|
|
482
|
+
|
|
483
|
+
const tokens = tokenizeQuery(userText);
|
|
484
|
+
const scored = [];
|
|
485
|
+
for (const entry of fileIndex.files) {
|
|
486
|
+
let score = 0;
|
|
487
|
+
const fileText = String(entry.file || '').toLowerCase();
|
|
488
|
+
for (const token of tokens) {
|
|
489
|
+
if (fileText.includes(token)) score += 5;
|
|
490
|
+
if ((entry.exports || []).some((value) => String(value).toLowerCase() === token)) score += 4;
|
|
491
|
+
if ((entry.functions || []).some((value) => String(value).toLowerCase() === token)) score += 4;
|
|
492
|
+
if ((entry.classes || []).some((value) => String(value).toLowerCase() === token)) score += 4;
|
|
493
|
+
if ((entry.imports || []).some((value) => String(value).toLowerCase().includes(token))) score += 1;
|
|
494
|
+
}
|
|
495
|
+
if (score > 0) scored.push({ entry, score });
|
|
496
|
+
}
|
|
497
|
+
scored.sort((left, right) => right.score - left.score || String(left.entry.file).localeCompare(String(right.entry.file)));
|
|
498
|
+
const selected = scored.slice(0, PROJECT_CONTEXT_MAX_FILES).map((item) => item.entry);
|
|
499
|
+
if (selected.length > 0) {
|
|
500
|
+
lines.push('- relevant_files:');
|
|
501
|
+
for (const entry of selected) {
|
|
502
|
+
lines.push(
|
|
503
|
+
` - ${entry.file} :: exports=[${(entry.exports || []).slice(0, 4).join(', ')}] functions=[${(entry.functions || []).slice(0, 4).join(', ')}] classes=[${(entry.classes || []).slice(0, 4).join(', ')}]`
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const snippet = trimMultiline(lines.join('\n'));
|
|
509
|
+
return snippet;
|
|
510
|
+
}
|
|
@@ -118,5 +118,5 @@ export function getEffectivePolicy(config) {
|
|
|
118
118
|
|
|
119
119
|
export function getShellSystemPrompt(value) {
|
|
120
120
|
const profile = getShellProfile(value);
|
|
121
|
-
return `You are CodeMini CLI working in a ${profile.label} shell environment. Prefer OpenCode-style primary tools first: use read to inspect files, grep to search file contents, glob to find files by pattern, list to inspect directories, edit to modify existing files, write to create or fully rewrite files when appropriate, patch to apply unified diffs, and run for one-shot shell commands like install, build, test, or other finite tasks. Classify frontend, backend, database, and Docker work carefully: use run for finite commands, and use start_service, list_services, get_service_status, get_service_logs, and stop_service for long-running servers, watchers, and dev processes. Treat edit as the default editing path for existing code. Internal low-level edit strategies such as target resolution, block replacement, exact text replacement, and anchored inserts are handled inside edit rather than exposed as separate tools. Use generate_diff when you need a structured preview of a proposed file change. For existing code files, prefer grep/read/edit and only use write with full_file_rewrite=true when a whole-file rewrite is truly intended. Avoid unnecessary tool calls.`;
|
|
121
|
+
return `You are CodeMini CLI working in a ${profile.label} shell environment. Prefer OpenCode-style primary tools first: use read to inspect files, grep to search file contents, glob to find files by pattern, list to inspect directories, edit to modify existing files, write to create or fully rewrite files when appropriate, patch to apply unified diffs, and run for one-shot shell commands like install, build, test, or other finite tasks. For structural code edits such as changing a function, method, or class, prefer the AST-first workflow: use ast_query to select the syntax node, use read_ast_node to inspect that node, then use edit with ast_target and kind=replace_block so the write stays constrained to the selected node. Fall back to plain grep/read/edit only when AST selection is not appropriate. Classify frontend, backend, database, and Docker work carefully: use run for finite commands, and use start_service, list_services, get_service_status, get_service_logs, and stop_service for long-running servers, watchers, and dev processes. Treat edit as the default editing path for existing code. Internal low-level edit strategies such as target resolution, block replacement, exact text replacement, and anchored inserts are handled inside edit rather than exposed as separate tools. Use generate_diff when you need a structured preview of a proposed file change. For existing code files, prefer grep/read/edit and only use write with full_file_rewrite=true when a whole-file rewrite is truly intended. Avoid unnecessary tool calls.`;
|
|
122
122
|
}
|
package/src/core/task-store.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { getProjectLegacyTasksFilePath, getProjectTasksDir } from './paths.js';
|
|
3
4
|
|
|
4
5
|
function legacyTasksFilePath(cwd = process.cwd()) {
|
|
5
|
-
return
|
|
6
|
+
return getProjectLegacyTasksFilePath(cwd);
|
|
6
7
|
}
|
|
7
8
|
|
|
8
9
|
function tasksFilePath(cwd = process.cwd(), sessionId = '') {
|
|
9
10
|
const sid = String(sessionId || '').trim();
|
|
10
11
|
if (!sid) return legacyTasksFilePath(cwd);
|
|
11
|
-
return path.join(cwd,
|
|
12
|
+
return path.join(getProjectTasksDir(cwd), `${sid}.json`);
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
async function ensureDir(filePath) {
|