codemini-cli 0.5.8 → 0.5.10
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 +354 -225
- package/codemini-web/dist/assets/{highlighted-body-OFNGDK62-CCcxtQK_.js → highlighted-body-OFNGDK62-7HL7yft8.js} +1 -1
- package/codemini-web/dist/assets/{index-Cy4HN-FS.js → index-BK75hMb2.js} +95 -93
- package/codemini-web/dist/assets/index-BSdIdn3L.css +2 -0
- package/codemini-web/dist/assets/mermaid-GHXKKRXX-Dg9qh8mg.js +1 -0
- package/codemini-web/dist/index.html +2 -2
- package/codemini-web/lib/runtime-bridge.js +547 -546
- package/codemini-web/server.js +318 -233
- package/package.json +67 -67
- package/skills/brainstorm/SKILL.md +8 -3
- package/skills/codemini.skills.json +40 -0
- package/src/commands/skill.js +16 -5
- package/src/core/ast.js +30 -15
- package/src/core/chat-runtime.js +88 -16
- package/src/core/command-loader.js +120 -25
- package/src/core/command-policy.js +34 -10
- package/src/core/config-store.js +14 -1
- package/src/core/project-index.js +21 -2
- package/src/core/project-instructions.js +98 -0
- package/src/core/shell.js +79 -73
- package/src/core/system-prompt-composer.js +10 -0
- package/src/core/tools.js +114 -65
- package/codemini-web/dist/assets/index-CMISAOFr.css +0 -2
- package/codemini-web/dist/assets/mermaid-GHXKKRXX-BWFzYc7A.js +0 -1
|
@@ -11,6 +11,8 @@ import { readSkillRegistry } from './skill-registry.js';
|
|
|
11
11
|
|
|
12
12
|
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
13
13
|
const BUNDLED_SKILLS_DIR = path.resolve(MODULE_DIR, '..', '..', 'skills');
|
|
14
|
+
const SKILL_CATALOG_FILE = 'codemini.skills.json';
|
|
15
|
+
const FRONTMATTER_READ_BYTES = 16 * 1024;
|
|
14
16
|
|
|
15
17
|
function parseArrayText(value) {
|
|
16
18
|
const inner = value.slice(1, -1).trim();
|
|
@@ -46,6 +48,79 @@ function parseFrontmatter(raw) {
|
|
|
46
48
|
return { metadata, content };
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
function readFrontmatterMetadata(filePath) {
|
|
52
|
+
let fd;
|
|
53
|
+
try {
|
|
54
|
+
fd = fs.openSync(filePath, 'r');
|
|
55
|
+
const buffer = Buffer.alloc(FRONTMATTER_READ_BYTES);
|
|
56
|
+
const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, 0);
|
|
57
|
+
const raw = buffer.subarray(0, bytesRead).toString('utf8');
|
|
58
|
+
if (!raw.startsWith('---\n')) return {};
|
|
59
|
+
const end = raw.indexOf('\n---\n', 4);
|
|
60
|
+
if (end === -1) return {};
|
|
61
|
+
return parseFrontmatter(raw.slice(0, end + 5)).metadata;
|
|
62
|
+
} catch {
|
|
63
|
+
return {};
|
|
64
|
+
} finally {
|
|
65
|
+
if (fd !== undefined) {
|
|
66
|
+
try { fs.closeSync(fd); } catch {}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function readSkillCatalog(baseDir) {
|
|
72
|
+
const catalogPath = path.join(baseDir, SKILL_CATALOG_FILE);
|
|
73
|
+
try {
|
|
74
|
+
const parsed = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));
|
|
75
|
+
return parsed && typeof parsed === 'object' && parsed.skills && typeof parsed.skills === 'object'
|
|
76
|
+
? parsed.skills
|
|
77
|
+
: {};
|
|
78
|
+
} catch {
|
|
79
|
+
return {};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function normalizeStringArray(value) {
|
|
84
|
+
if (Array.isArray(value)) {
|
|
85
|
+
return value.map((item) => String(item || '').trim()).filter(Boolean);
|
|
86
|
+
}
|
|
87
|
+
const single = String(value || '').trim();
|
|
88
|
+
return single ? [single] : [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function catalogMetadata(catalog, name) {
|
|
92
|
+
const entry = catalog?.[name];
|
|
93
|
+
if (!entry || typeof entry !== 'object') return {};
|
|
94
|
+
return {
|
|
95
|
+
...(entry.description ? { description: String(entry.description) } : {}),
|
|
96
|
+
...(entry.mode ? { mode: String(entry.mode) } : {}),
|
|
97
|
+
...(entry.enabled !== undefined ? { enabled: entry.enabled !== false } : {}),
|
|
98
|
+
...(entry.priority !== undefined ? { priority: Number(entry.priority) } : {}),
|
|
99
|
+
triggers: normalizeStringArray(entry.triggers)
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function commandWithContent(command, parsedContent) {
|
|
104
|
+
if (parsedContent !== undefined) {
|
|
105
|
+
return { ...command, content: parsedContent };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let cached;
|
|
109
|
+
let loaded = false;
|
|
110
|
+
return Object.defineProperty({ ...command }, 'content', {
|
|
111
|
+
enumerable: true,
|
|
112
|
+
configurable: true,
|
|
113
|
+
get() {
|
|
114
|
+
if (!loaded) {
|
|
115
|
+
const raw = fs.readFileSync(command.path, 'utf8');
|
|
116
|
+
cached = parseFrontmatter(raw).content;
|
|
117
|
+
loaded = true;
|
|
118
|
+
}
|
|
119
|
+
return cached;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
49
124
|
function safeEntries(dir) {
|
|
50
125
|
try {
|
|
51
126
|
return fs.readdirSync(dir);
|
|
@@ -104,77 +179,95 @@ function loadMarkdownCommandsFromDir(baseDir, source, out) {
|
|
|
104
179
|
|
|
105
180
|
function loadLegacySkillsFromDir(baseDir, source, out) {
|
|
106
181
|
if (!fs.existsSync(baseDir)) return;
|
|
182
|
+
const catalog = readSkillCatalog(baseDir);
|
|
107
183
|
for (const entry of safeEntries(baseDir)) {
|
|
108
184
|
if (!isSafeEntry(entry)) continue;
|
|
109
185
|
const full = path.join(baseDir, entry);
|
|
110
186
|
const stat = fs.statSync(full);
|
|
111
187
|
if (!stat.isDirectory()) continue;
|
|
188
|
+
const catalogMeta = catalogMetadata(catalog, entry);
|
|
112
189
|
const skillFile = path.join(full, 'SKILL.md');
|
|
113
190
|
if (!fs.existsSync(skillFile)) continue;
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
setCommand(out, entry, {
|
|
191
|
+
const frontmatter = readFrontmatterMetadata(skillFile);
|
|
192
|
+
setCommand(out, entry, commandWithContent({
|
|
117
193
|
name: entry,
|
|
118
194
|
source: `${source}-skill`,
|
|
119
195
|
path: skillFile,
|
|
120
196
|
metadata: {
|
|
121
|
-
|
|
197
|
+
...frontmatter,
|
|
198
|
+
...catalogMeta,
|
|
199
|
+
description: catalogMeta.description || frontmatter.description || 'Legacy skill',
|
|
122
200
|
type: 'skill'
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
});
|
|
201
|
+
}
|
|
202
|
+
}));
|
|
126
203
|
}
|
|
127
204
|
}
|
|
128
205
|
|
|
129
206
|
function loadBundledSkillsFromDir(baseDir, out) {
|
|
130
207
|
if (!fs.existsSync(baseDir)) return;
|
|
208
|
+
const catalog = readSkillCatalog(baseDir);
|
|
131
209
|
for (const entry of safeEntries(baseDir)) {
|
|
132
210
|
if (!isSafeEntry(entry)) continue;
|
|
133
211
|
const full = path.join(baseDir, entry);
|
|
134
212
|
const stat = fs.statSync(full);
|
|
135
213
|
if (!stat.isDirectory()) continue;
|
|
214
|
+
const catalogMeta = catalogMetadata(catalog, entry);
|
|
136
215
|
const skillFile = path.join(full, 'SKILL.md');
|
|
137
216
|
if (!fs.existsSync(skillFile)) continue;
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
setCommand(out, entry, {
|
|
217
|
+
const frontmatter = readFrontmatterMetadata(skillFile);
|
|
218
|
+
setCommand(out, entry, commandWithContent({
|
|
141
219
|
name: entry,
|
|
142
220
|
source: 'bundled-skill',
|
|
143
221
|
path: skillFile,
|
|
144
222
|
metadata: {
|
|
145
|
-
...
|
|
223
|
+
...frontmatter,
|
|
224
|
+
...catalogMeta,
|
|
146
225
|
type: 'skill',
|
|
147
|
-
version:
|
|
148
|
-
description:
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
|
|
226
|
+
version: frontmatter.version || '0.1.0',
|
|
227
|
+
description: catalogMeta.description || frontmatter.description || 'Bundled skill'
|
|
228
|
+
}
|
|
229
|
+
}));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function applySkillCatalogPatches(baseDir, out) {
|
|
234
|
+
const catalog = readSkillCatalog(baseDir);
|
|
235
|
+
for (const name of Object.keys(catalog)) {
|
|
236
|
+
const existing = out.get(name);
|
|
237
|
+
if (!existing || existing.metadata?.type !== 'skill') continue;
|
|
238
|
+
const meta = catalogMetadata(catalog, name);
|
|
239
|
+
existing.metadata = {
|
|
240
|
+
...existing.metadata,
|
|
241
|
+
...meta,
|
|
242
|
+
description: meta.description || existing.metadata.description || ''
|
|
243
|
+
};
|
|
152
244
|
}
|
|
153
245
|
}
|
|
154
246
|
|
|
155
247
|
function loadInstalledSkillsFromRegistry(baseDir, registry, out) {
|
|
156
248
|
if (!registry || !Array.isArray(registry.skills)) return;
|
|
249
|
+
const catalog = readSkillCatalog(baseDir);
|
|
157
250
|
for (const skill of registry.skills) {
|
|
158
251
|
if (skill.enabled === false) continue;
|
|
159
252
|
const name = skill.name;
|
|
160
253
|
if (out.has(name)) continue;
|
|
254
|
+
const catalogMeta = catalogMetadata(catalog, name);
|
|
161
255
|
const entry = skill.entryFile || 'SKILL.md';
|
|
162
256
|
const full = path.join(baseDir, name, entry);
|
|
163
257
|
if (!fs.existsSync(full)) continue;
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
setCommand(out, name, {
|
|
258
|
+
const frontmatter = readFrontmatterMetadata(full);
|
|
259
|
+
setCommand(out, name, commandWithContent({
|
|
167
260
|
name,
|
|
168
261
|
source: 'registry-skill',
|
|
169
262
|
path: full,
|
|
170
263
|
metadata: {
|
|
171
|
-
...
|
|
264
|
+
...frontmatter,
|
|
265
|
+
...catalogMeta,
|
|
172
266
|
type: 'skill',
|
|
173
|
-
version: skill.version ||
|
|
174
|
-
description: skill.description ||
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
});
|
|
267
|
+
version: skill.version || frontmatter.version || '0.0.0',
|
|
268
|
+
description: catalogMeta.description || skill.description || frontmatter.description || 'Installed skill'
|
|
269
|
+
}
|
|
270
|
+
}));
|
|
178
271
|
}
|
|
179
272
|
}
|
|
180
273
|
|
|
@@ -201,10 +294,12 @@ export async function loadCommandsAndSkills(cwd = process.cwd()) {
|
|
|
201
294
|
const commands = new Map();
|
|
202
295
|
|
|
203
296
|
loadBundledSkillsFromDir(BUNDLED_SKILLS_DIR, commands);
|
|
297
|
+
applySkillCatalogPatches(getProjectSkillsDir(cwd), commands);
|
|
204
298
|
loadMarkdownCommandsFromDir(getCommandsDir(), 'global', commands);
|
|
205
299
|
loadMarkdownCommandsFromDir(getProjectCommandsDir(cwd), 'project', commands);
|
|
206
300
|
loadLegacySkillsFromDir(getSkillsDir(), 'global', commands);
|
|
207
301
|
loadLegacySkillsFromDir(getProjectSkillsDir(cwd), 'project', commands);
|
|
302
|
+
applySkillCatalogPatches(getProjectSkillsDir(cwd), commands);
|
|
208
303
|
const registry = await readSkillRegistry();
|
|
209
304
|
loadInstalledSkillsFromRegistry(getSkillsDir(), registry, commands);
|
|
210
305
|
|
|
@@ -196,7 +196,25 @@ function suggestionForToken(token, config) {
|
|
|
196
196
|
return 'Prefer structured tools like read, edit, write, grep, glob, and list first. If you need shell fallback, use allowed shell commands for search and local context such as rg, find, grep, sed, cat, or ls.';
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
-
function
|
|
199
|
+
function allowedPathRoots(workspaceRoot, config = {}) {
|
|
200
|
+
return [
|
|
201
|
+
workspaceRoot,
|
|
202
|
+
...(Array.isArray(config?.policy?.allowed_paths) ? config.policy.allowed_paths : [])
|
|
203
|
+
]
|
|
204
|
+
.map((item) => String(item || '').trim())
|
|
205
|
+
.filter(Boolean)
|
|
206
|
+
.map((item) => path.resolve(item));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function isWithinAnyRoot(candidatePath, roots = []) {
|
|
210
|
+
const resolvedCandidate = path.resolve(candidatePath);
|
|
211
|
+
return roots.some((root) => {
|
|
212
|
+
const relative = path.relative(root, resolvedCandidate);
|
|
213
|
+
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function validateCdSegment(command, workspaceRoot, config = {}) {
|
|
200
218
|
const tokens = tokenizeTopLevel(command);
|
|
201
219
|
if (tokens.length === 1) {
|
|
202
220
|
return { allowed: false, reason: 'cd requires a target path in safe mode' };
|
|
@@ -210,11 +228,9 @@ function validateCdSegment(command, workspaceRoot) {
|
|
|
210
228
|
return { allowed: false, reason: 'cd target is not allowed in safe mode' };
|
|
211
229
|
}
|
|
212
230
|
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
217
|
-
return { allowed: false, reason: `cd escapes workspace: ${rawTarget}` };
|
|
231
|
+
const resolvedTarget = path.resolve(path.resolve(workspaceRoot), rawTarget);
|
|
232
|
+
if (!isWithinAnyRoot(resolvedTarget, allowedPathRoots(workspaceRoot, config))) {
|
|
233
|
+
return { allowed: false, reason: `cd escapes workspace or allowed paths: ${rawTarget}` };
|
|
218
234
|
}
|
|
219
235
|
|
|
220
236
|
return { allowed: true };
|
|
@@ -246,7 +262,7 @@ export function evaluateCommandPolicy(command, config, workspaceRoot = process.c
|
|
|
246
262
|
for (const item of inspectedTokens) {
|
|
247
263
|
if (SHELL_KEYWORDS.has(item.token)) continue;
|
|
248
264
|
if (item.token === 'cd') {
|
|
249
|
-
const cdCheck = validateCdSegment(item.raw, workspaceRoot);
|
|
265
|
+
const cdCheck = validateCdSegment(item.raw, workspaceRoot, config);
|
|
250
266
|
if (!cdCheck.allowed) {
|
|
251
267
|
return { allowed: false, reason: cdCheck.reason, suggestion: suggestionForToken(item.token, config) };
|
|
252
268
|
}
|
|
@@ -263,11 +279,19 @@ export function evaluateCommandPolicy(command, config, workspaceRoot = process.c
|
|
|
263
279
|
}
|
|
264
280
|
}
|
|
265
281
|
|
|
266
|
-
const
|
|
282
|
+
const allowedLower = allowedPathRoots(workspaceRoot, config).map((item) => item.toLowerCase().replace(/\//g, '\\'));
|
|
267
283
|
const windowsAbsPath = lower.match(/[a-z]:\\[^\s'"]+/g) || [];
|
|
268
284
|
for (const p of windowsAbsPath) {
|
|
269
|
-
if (!p.startsWith(
|
|
270
|
-
return { allowed: false, reason: `absolute path outside workspace: ${p}`, suggestion: suggestionForToken(token, config) };
|
|
285
|
+
if (!allowedLower.some((root) => p === root || p.startsWith(`${root}\\`))) {
|
|
286
|
+
return { allowed: false, reason: `absolute path outside workspace or allowed paths: ${p}`, suggestion: suggestionForToken(token, config) };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const posixAbsPath = cmd.match(/(?<![:/\w])\/(?!\/)[^\s'"]+/g) || [];
|
|
291
|
+
const allowedResolved = allowedPathRoots(workspaceRoot, config);
|
|
292
|
+
for (const p of posixAbsPath) {
|
|
293
|
+
if (!isWithinAnyRoot(p, allowedResolved)) {
|
|
294
|
+
return { allowed: false, reason: `absolute path outside workspace or allowed paths: ${p}`, suggestion: suggestionForToken(token, config) };
|
|
271
295
|
}
|
|
272
296
|
}
|
|
273
297
|
|
package/src/core/config-store.js
CHANGED
|
@@ -36,7 +36,9 @@ const DEFAULT_CONFIG = {
|
|
|
36
36
|
read_file_max_chars: 24000,
|
|
37
37
|
prompt_budget_audit: false,
|
|
38
38
|
microcompact_enabled: true,
|
|
39
|
-
microcompact_keep_recent: 5
|
|
39
|
+
microcompact_keep_recent: 5,
|
|
40
|
+
project_instructions_enabled: true,
|
|
41
|
+
project_instructions_max_chars: 12000
|
|
40
42
|
},
|
|
41
43
|
execution: {
|
|
42
44
|
mode: 'normal',
|
|
@@ -89,6 +91,7 @@ const DEFAULT_CONFIG = {
|
|
|
89
91
|
policy: {
|
|
90
92
|
safe_mode: true,
|
|
91
93
|
allow_dangerous_commands: false,
|
|
94
|
+
allowed_paths: [],
|
|
92
95
|
command_allowlist: [],
|
|
93
96
|
blocked_commands: [],
|
|
94
97
|
blocked_path_patterns: [],
|
|
@@ -195,6 +198,9 @@ function normalizePolicyLists(config) {
|
|
|
195
198
|
next.policy.command_allowlist = uniqueStrings(
|
|
196
199
|
Array.isArray(next.policy.command_allowlist) ? next.policy.command_allowlist : []
|
|
197
200
|
);
|
|
201
|
+
next.policy.allowed_paths = uniqueStrings(
|
|
202
|
+
Array.isArray(next.policy.allowed_paths) ? next.policy.allowed_paths : []
|
|
203
|
+
);
|
|
198
204
|
next.policy.blocked_commands = uniqueStrings(
|
|
199
205
|
Array.isArray(next.policy.blocked_commands) ? next.policy.blocked_commands : []
|
|
200
206
|
);
|
|
@@ -212,6 +218,13 @@ function parseValue(input) {
|
|
|
212
218
|
if (input === 'true') return true;
|
|
213
219
|
if (input === 'false') return false;
|
|
214
220
|
if (input === 'null') return null;
|
|
221
|
+
if ((input.startsWith('[') && input.endsWith(']')) || (input.startsWith('{') && input.endsWith('}'))) {
|
|
222
|
+
try {
|
|
223
|
+
return JSON.parse(input);
|
|
224
|
+
} catch {
|
|
225
|
+
return input;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
215
228
|
if (!Number.isNaN(Number(input)) && input.trim() !== '') return Number(input);
|
|
216
229
|
return input;
|
|
217
230
|
}
|
|
@@ -25,6 +25,7 @@ const PROJECT_MARKER_FILES = new Set([
|
|
|
25
25
|
const LANGUAGE_BY_EXT = EXTENSION_LANGUAGE_MAP;
|
|
26
26
|
|
|
27
27
|
const initCache = new BoundedCache({ maxSize: 32, ttlMs: 10 * 60 * 1000 });
|
|
28
|
+
const ignoreRulesCache = new BoundedCache({ maxSize: 128, ttlMs: 60 * 1000 });
|
|
28
29
|
const PROJECT_CONTEXT_MAX_FILES = 6;
|
|
29
30
|
|
|
30
31
|
function clipList(values, max = 32) {
|
|
@@ -92,9 +93,24 @@ function gitignorePatternToRegex(pattern) {
|
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
async function readIgnoreFileRules(cwd, fileName) {
|
|
96
|
+
const filePath = path.join(cwd, fileName);
|
|
97
|
+
const stat = await safeStat(filePath);
|
|
98
|
+
const cacheKey = `${filePath}:${Number(stat?.mtimeMs || 0)}:${Number(stat?.size || 0)}`;
|
|
99
|
+
if (ignoreRulesCache.has(cacheKey)) return ignoreRulesCache.get(cacheKey);
|
|
100
|
+
|
|
101
|
+
for (const key of ignoreRulesCache.keys()) {
|
|
102
|
+
if (String(key).startsWith(`${filePath}:`) && key !== cacheKey) {
|
|
103
|
+
ignoreRulesCache.delete(key);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
95
107
|
try {
|
|
96
|
-
|
|
97
|
-
|
|
108
|
+
if (!stat?.isFile()) {
|
|
109
|
+
ignoreRulesCache.set(cacheKey, []);
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
113
|
+
const rules = raw
|
|
98
114
|
.split(/\r?\n/)
|
|
99
115
|
.map((line) => line.trim())
|
|
100
116
|
.filter((line) => line && !line.startsWith('#'))
|
|
@@ -114,7 +130,10 @@ async function readIgnoreFileRules(cwd, fileName) {
|
|
|
114
130
|
};
|
|
115
131
|
})
|
|
116
132
|
.filter((rule) => rule.normalized);
|
|
133
|
+
ignoreRulesCache.set(cacheKey, rules);
|
|
134
|
+
return rules;
|
|
117
135
|
} catch {
|
|
136
|
+
ignoreRulesCache.set(cacheKey, []);
|
|
118
137
|
return [];
|
|
119
138
|
}
|
|
120
139
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_MAX_CHARS = 12000;
|
|
5
|
+
const CANDIDATE_FILES = [
|
|
6
|
+
'AGENTS.md',
|
|
7
|
+
path.join('.agents', 'AGENTS.md'),
|
|
8
|
+
path.join('.agents', 'agents.md'),
|
|
9
|
+
path.join('.codemini', 'AGENTS.md'),
|
|
10
|
+
'CLAUDE.md'
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
function trimProjectInstructions(value, maxChars = DEFAULT_MAX_CHARS) {
|
|
14
|
+
const text = String(value || '').trim();
|
|
15
|
+
if (!text) return '';
|
|
16
|
+
const limit = Math.max(1000, Number(maxChars) || DEFAULT_MAX_CHARS);
|
|
17
|
+
if (text.length <= limit) return text;
|
|
18
|
+
return `${text.slice(0, limit - 120).trimEnd()}\n\n[Project instructions truncated: keep AGENTS.md concise or move details into linked docs.]`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function readFirstExistingFile(cwd, candidates = CANDIDATE_FILES) {
|
|
22
|
+
let current = path.resolve(cwd);
|
|
23
|
+
while (true) {
|
|
24
|
+
for (const candidate of candidates) {
|
|
25
|
+
const absolutePath = path.resolve(current, candidate);
|
|
26
|
+
let stat;
|
|
27
|
+
try {
|
|
28
|
+
stat = await fs.stat(absolutePath);
|
|
29
|
+
} catch {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (!stat?.isFile()) continue;
|
|
33
|
+
const content = await fs.readFile(absolutePath, 'utf8');
|
|
34
|
+
const relativePath = path.relative(cwd, absolutePath) || candidate;
|
|
35
|
+
return {
|
|
36
|
+
path: absolutePath,
|
|
37
|
+
relativePath: relativePath.split(path.sep).join('/'),
|
|
38
|
+
content
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const parent = path.dirname(current);
|
|
42
|
+
if (parent === current) break;
|
|
43
|
+
current = parent;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function loadProjectInstructions({
|
|
49
|
+
cwd = process.cwd(),
|
|
50
|
+
config = {},
|
|
51
|
+
maxChars = config?.context?.project_instructions_max_chars
|
|
52
|
+
} = {}) {
|
|
53
|
+
const enabled = config?.context?.project_instructions_enabled !== false;
|
|
54
|
+
if (!enabled) return '';
|
|
55
|
+
|
|
56
|
+
const found = await readFirstExistingFile(cwd);
|
|
57
|
+
if (!found) return '';
|
|
58
|
+
|
|
59
|
+
const body = trimProjectInstructions(found.content, maxChars);
|
|
60
|
+
if (!body) return '';
|
|
61
|
+
|
|
62
|
+
return [
|
|
63
|
+
'Project Instructions:',
|
|
64
|
+
`Source: ${found.relativePath}`,
|
|
65
|
+
body
|
|
66
|
+
].join('\n');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function buildDefaultAgentsMd() {
|
|
70
|
+
return `# AGENTS.md
|
|
71
|
+
|
|
72
|
+
This file gives coding agents stable project instructions. Keep it short and use it as a map, not as full documentation.
|
|
73
|
+
|
|
74
|
+
## Project
|
|
75
|
+
|
|
76
|
+
- Describe what this repository is and the main runtime or product surface.
|
|
77
|
+
- Note required runtime versions and package managers.
|
|
78
|
+
|
|
79
|
+
## Commands
|
|
80
|
+
|
|
81
|
+
- Install: \`npm install\`
|
|
82
|
+
- Test: \`npm test\`
|
|
83
|
+
- Build: add the project build command here.
|
|
84
|
+
|
|
85
|
+
## Task Routing
|
|
86
|
+
|
|
87
|
+
- CLI or command behavior: list the entry files here.
|
|
88
|
+
- Runtime behavior: list the core runtime files here.
|
|
89
|
+
- Web UI behavior: list the server, state, and component roots here.
|
|
90
|
+
- Tests: list focused test files for common changes.
|
|
91
|
+
|
|
92
|
+
## Rules
|
|
93
|
+
|
|
94
|
+
- Use project/file indexes for orientation, then inspect real source files before editing.
|
|
95
|
+
- Keep generated output and build artifacts out of manual edits.
|
|
96
|
+
- Put reusable workflows in skills; put always-needed project facts and routing rules here.
|
|
97
|
+
`;
|
|
98
|
+
}
|
package/src/core/shell.js
CHANGED
|
@@ -23,6 +23,74 @@ const READY_OUTPUT_PATTERNS = [
|
|
|
23
23
|
/\bhttp:\/\/127\.0\.0\.1\b/i,
|
|
24
24
|
/\bhttp:\/\/localhost\b/i
|
|
25
25
|
];
|
|
26
|
+
const INSTALL_COMMAND_PATTERNS = [
|
|
27
|
+
/\b(?:npm|pnpm|yarn|bun)\s+install\b/i,
|
|
28
|
+
/\b(?:npm|pnpm|yarn|bun)\s+(?:ci|i|add)\b/i,
|
|
29
|
+
/\buv\s+pip\s+install\b/i,
|
|
30
|
+
/\bpip\s+install\b/i,
|
|
31
|
+
/\bcargo\s+install\b/i,
|
|
32
|
+
/\bbundle\s+install\b/i,
|
|
33
|
+
/\bcomposer\s+install\b/i
|
|
34
|
+
];
|
|
35
|
+
const BUILD_COMMAND_RE = /\b(?:build|compile|bundle|pack|transpile)\b/i;
|
|
36
|
+
const TEST_COMMAND_PATTERNS = [
|
|
37
|
+
/\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:test|lint|check|typecheck)\b/i,
|
|
38
|
+
/\b(?:jest|vitest|mocha|ava|pytest|go\s+test|cargo\s+test|dotnet\s+test)\b/i
|
|
39
|
+
];
|
|
40
|
+
const FRONTEND_SERVICE_PATTERNS = [
|
|
41
|
+
/\bvite\b/i,
|
|
42
|
+
/\bnext\s+dev\b/i,
|
|
43
|
+
/\bnuxt\s+dev\b/i,
|
|
44
|
+
/\bastro\s+dev\b/i,
|
|
45
|
+
/\bremix\s+dev\b/i,
|
|
46
|
+
/\bsvelte-kit\s+dev\b/i,
|
|
47
|
+
/\bwebpack\s+serve\b/i,
|
|
48
|
+
/\bvue-cli-service\s+serve\b/i,
|
|
49
|
+
/\breact-scripts\s+start\b/i,
|
|
50
|
+
/\bstorybook\b/i,
|
|
51
|
+
/\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:dev|start|serve|preview)\b.*\b(?:client|frontend|front-end|web|ui)\b/i,
|
|
52
|
+
/\b(?:client|frontend|front-end|web|ui)\b.*\b(?:dev|start|serve|preview)\b/i
|
|
53
|
+
];
|
|
54
|
+
const BACKEND_SERVICE_PATTERNS = [
|
|
55
|
+
/\bpython\s+-m\s+http\.server\b/i,
|
|
56
|
+
/\buvicorn\b/i,
|
|
57
|
+
/\bgunicorn\b/i,
|
|
58
|
+
/\bflask\s+run\b/i,
|
|
59
|
+
/\bdjango\s+runserver\b/i,
|
|
60
|
+
/\brails\s+(?:s|server)\b/i,
|
|
61
|
+
/\bmvn(?:w)?\s+spring-boot:run\b/i,
|
|
62
|
+
/\bgradle(?:w)?\s+bootRun\b/i,
|
|
63
|
+
/\bgradle(?:w)?\s+run\b/i,
|
|
64
|
+
/\bjava\b.*\bserver\b/i,
|
|
65
|
+
/\bdotnet\s+run\b/i,
|
|
66
|
+
/\bgo\s+run\b.*\b(server|cmd\/server|main\.go)\b/i,
|
|
67
|
+
/\bnest\s+start\b/i,
|
|
68
|
+
/\bnodemon\b/i,
|
|
69
|
+
/\bts-node-dev\b/i,
|
|
70
|
+
/\bair\b/i,
|
|
71
|
+
/\bphp\s+artisan\s+serve\b/i,
|
|
72
|
+
/\bsymfony\s+server:start\b/i,
|
|
73
|
+
/\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:dev|start|serve|preview)\b.*\b(?:server|api|backend)\b/i,
|
|
74
|
+
/\b(?:server|api|backend)\b.*\b(?:dev|start|serve|preview)\b/i
|
|
75
|
+
];
|
|
76
|
+
const DATABASE_SERVICE_PATTERNS = [
|
|
77
|
+
/\bpostgres(?:ql)?\b/i,
|
|
78
|
+
/\bmysql\b/i,
|
|
79
|
+
/\bmariadb\b/i,
|
|
80
|
+
/\bmongod\b/i,
|
|
81
|
+
/\bredis-server\b/i,
|
|
82
|
+
/\b(?:docker|docker-compose|docker compose)\s+.*\b(?:db|database|postgres|mysql|mongo|redis)\b/i,
|
|
83
|
+
/\b(?:db|database|postgres|mysql|mongo|redis)\b.*\b(?:start|up|serve|run)\b/i
|
|
84
|
+
];
|
|
85
|
+
const DOCKER_SERVICE_PATTERNS = [
|
|
86
|
+
/\bdocker\s+compose\s+up\b/i,
|
|
87
|
+
/\bdocker-compose\s+up\b/i,
|
|
88
|
+
/\bdocker\s+run\b/i,
|
|
89
|
+
/\bdocker\s+start\b/i
|
|
90
|
+
];
|
|
91
|
+
const PACKAGE_SERVICE_RE = /\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:dev|start|serve|preview|watch)\b/i;
|
|
92
|
+
const VITE_OR_SERVE_RE = /\b(?:vite|serve)\b/i;
|
|
93
|
+
const SERVICE_HINT_RE = /\b(?:watch|serve|server|dev|preview)\b/i;
|
|
26
94
|
const AUTO_STOP_GRACE_MS = 150;
|
|
27
95
|
const LONG_RUNNING_STARTUP_WINDOW_MS = 1500;
|
|
28
96
|
|
|
@@ -41,105 +109,43 @@ export function classifyCommandIntent(command) {
|
|
|
41
109
|
return { kind: 'generic', longRunning: false };
|
|
42
110
|
}
|
|
43
111
|
|
|
44
|
-
if (
|
|
45
|
-
/\b(?:npm|pnpm|yarn|bun)\s+install\b/i.test(value) ||
|
|
46
|
-
/\b(?:npm|pnpm|yarn|bun)\s+(?:ci|i|add)\b/i.test(value) ||
|
|
47
|
-
/\buv\s+pip\s+install\b/i.test(value) ||
|
|
48
|
-
/\bpip\s+install\b/i.test(value) ||
|
|
49
|
-
/\bcargo\s+install\b/i.test(value) ||
|
|
50
|
-
/\bbundle\s+install\b/i.test(value) ||
|
|
51
|
-
/\bcomposer\s+install\b/i.test(value)
|
|
52
|
-
) {
|
|
112
|
+
if (matchesAny(value, INSTALL_COMMAND_PATTERNS)) {
|
|
53
113
|
return { kind: 'install', longRunning: false };
|
|
54
114
|
}
|
|
55
115
|
|
|
56
|
-
if (
|
|
116
|
+
if (BUILD_COMMAND_RE.test(value)) {
|
|
57
117
|
return { kind: 'build', longRunning: false };
|
|
58
118
|
}
|
|
59
119
|
|
|
60
|
-
if (
|
|
61
|
-
/\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:test|lint|check|typecheck)\b/i.test(value) ||
|
|
62
|
-
/\b(?:jest|vitest|mocha|ava|pytest|go\s+test|cargo\s+test|dotnet\s+test)\b/i.test(value)
|
|
63
|
-
) {
|
|
120
|
+
if (matchesAny(value, TEST_COMMAND_PATTERNS)) {
|
|
64
121
|
return { kind: 'test', longRunning: false };
|
|
65
122
|
}
|
|
66
123
|
|
|
67
|
-
|
|
68
|
-
/\bvite\b/i,
|
|
69
|
-
/\bnext\s+dev\b/i,
|
|
70
|
-
/\bnuxt\s+dev\b/i,
|
|
71
|
-
/\bastro\s+dev\b/i,
|
|
72
|
-
/\bremix\s+dev\b/i,
|
|
73
|
-
/\bsvelte-kit\s+dev\b/i,
|
|
74
|
-
/\bwebpack\s+serve\b/i,
|
|
75
|
-
/\bvue-cli-service\s+serve\b/i,
|
|
76
|
-
/\breact-scripts\s+start\b/i,
|
|
77
|
-
/\bstorybook\b/i,
|
|
78
|
-
/\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:dev|start|serve|preview)\b.*\b(?:client|frontend|front-end|web|ui)\b/i,
|
|
79
|
-
/\b(?:client|frontend|front-end|web|ui)\b.*\b(?:dev|start|serve|preview)\b/i
|
|
80
|
-
];
|
|
81
|
-
if (matchesAny(value, frontendServicePatterns)) {
|
|
124
|
+
if (matchesAny(value, FRONTEND_SERVICE_PATTERNS)) {
|
|
82
125
|
return { kind: 'frontend-service', longRunning: true };
|
|
83
126
|
}
|
|
84
127
|
|
|
85
|
-
|
|
86
|
-
/\bpython\s+-m\s+http\.server\b/i,
|
|
87
|
-
/\buvicorn\b/i,
|
|
88
|
-
/\bgunicorn\b/i,
|
|
89
|
-
/\bflask\s+run\b/i,
|
|
90
|
-
/\bdjango\s+runserver\b/i,
|
|
91
|
-
/\brails\s+(?:s|server)\b/i,
|
|
92
|
-
/\bmvn(?:w)?\s+spring-boot:run\b/i,
|
|
93
|
-
/\bgradle(?:w)?\s+bootRun\b/i,
|
|
94
|
-
/\bgradle(?:w)?\s+run\b/i,
|
|
95
|
-
/\bjava\b.*\bserver\b/i,
|
|
96
|
-
/\bdotnet\s+run\b/i,
|
|
97
|
-
/\bgo\s+run\b.*\b(server|cmd\/server|main\.go)\b/i,
|
|
98
|
-
/\bnest\s+start\b/i,
|
|
99
|
-
/\bnodemon\b/i,
|
|
100
|
-
/\bts-node-dev\b/i,
|
|
101
|
-
/\bair\b/i,
|
|
102
|
-
/\bphp\s+artisan\s+serve\b/i,
|
|
103
|
-
/\bsymfony\s+server:start\b/i,
|
|
104
|
-
/\b(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:dev|start|serve|preview)\b.*\b(?:server|api|backend)\b/i,
|
|
105
|
-
/\b(?:server|api|backend)\b.*\b(?:dev|start|serve|preview)\b/i
|
|
106
|
-
];
|
|
107
|
-
if (matchesAny(value, backendServicePatterns)) {
|
|
128
|
+
if (matchesAny(value, BACKEND_SERVICE_PATTERNS)) {
|
|
108
129
|
return { kind: 'backend-service', longRunning: true };
|
|
109
130
|
}
|
|
110
131
|
|
|
111
|
-
|
|
112
|
-
/\bpostgres(?:ql)?\b/i,
|
|
113
|
-
/\bmysql\b/i,
|
|
114
|
-
/\bmariadb\b/i,
|
|
115
|
-
/\bmongod\b/i,
|
|
116
|
-
/\bredis-server\b/i,
|
|
117
|
-
/\b(?:docker|docker-compose|docker compose)\s+.*\b(?:db|database|postgres|mysql|mongo|redis)\b/i,
|
|
118
|
-
/\b(?:db|database|postgres|mysql|mongo|redis)\b.*\b(?:start|up|serve|run)\b/i
|
|
119
|
-
];
|
|
120
|
-
if (matchesAny(value, databaseServicePatterns)) {
|
|
132
|
+
if (matchesAny(value, DATABASE_SERVICE_PATTERNS)) {
|
|
121
133
|
return { kind: 'database-service', longRunning: true };
|
|
122
134
|
}
|
|
123
135
|
|
|
124
|
-
|
|
125
|
-
/\bdocker\s+compose\s+up\b/i,
|
|
126
|
-
/\bdocker-compose\s+up\b/i,
|
|
127
|
-
/\bdocker\s+run\b/i,
|
|
128
|
-
/\bdocker\s+start\b/i
|
|
129
|
-
];
|
|
130
|
-
if (matchesAny(value, dockerServicePatterns)) {
|
|
136
|
+
if (matchesAny(value, DOCKER_SERVICE_PATTERNS)) {
|
|
131
137
|
return { kind: 'docker-service', longRunning: true };
|
|
132
138
|
}
|
|
133
139
|
|
|
134
140
|
if (
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
141
|
+
PACKAGE_SERVICE_RE.test(value) ||
|
|
142
|
+
VITE_OR_SERVE_RE.test(value) ||
|
|
143
|
+
SERVICE_HINT_RE.test(value)
|
|
138
144
|
) {
|
|
139
145
|
return { kind: 'service', longRunning: true };
|
|
140
146
|
}
|
|
141
147
|
|
|
142
|
-
if (
|
|
148
|
+
if (SERVICE_HINT_RE.test(value)) {
|
|
143
149
|
return { kind: 'service', longRunning: true };
|
|
144
150
|
}
|
|
145
151
|
|