codemini-cli 0.5.10 → 0.5.11
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/OPERATIONS.md +242 -242
- package/README.md +588 -588
- package/codemini-web/dist/assets/{highlighted-body-OFNGDK62-7HL7yft8.js → highlighted-body-OFNGDK62-CANOG7Xg.js} +1 -1
- package/codemini-web/dist/assets/{index-BK75hMb2.js → index-B71xykPM.js} +108 -108
- package/codemini-web/dist/assets/index-Dkq1DdDX.css +2 -0
- package/codemini-web/dist/assets/mermaid-GHXKKRXX-Z_w7M93P.js +1 -0
- package/codemini-web/dist/index.html +23 -23
- package/codemini-web/lib/approval-manager.js +32 -32
- package/codemini-web/lib/runtime-bridge.js +17 -11
- package/codemini-web/server.js +534 -205
- package/deployment.md +212 -212
- package/package.json +1 -1
- package/skills/brainstorm/SKILL.md +77 -77
- package/skills/codemini.skills.json +40 -40
- package/skills/grill-me/SKILL.md +30 -30
- package/skills/superpowers-lite/SKILL.md +82 -82
- package/src/cli.js +74 -74
- package/src/commands/chat.js +210 -210
- package/src/commands/run.js +313 -313
- package/src/commands/skill.js +438 -304
- package/src/commands/web.js +57 -57
- package/src/core/agent-loop.js +980 -980
- package/src/core/ast.js +309 -307
- package/src/core/chat-runtime.js +6261 -6253
- package/src/core/command-evaluator.js +72 -72
- package/src/core/command-loader.js +311 -311
- package/src/core/command-policy.js +301 -301
- package/src/core/command-risk.js +156 -156
- package/src/core/config-store.js +289 -289
- package/src/core/constants.js +18 -1
- package/src/core/context-compact.js +365 -365
- package/src/core/default-system-prompt.js +114 -107
- package/src/core/dream-audit.js +105 -105
- package/src/core/dream-consolidate.js +229 -229
- package/src/core/dream-evaluator.js +185 -185
- package/src/core/fff-adapter.js +383 -383
- package/src/core/memory-store.js +543 -543
- package/src/core/project-index.js +737 -548
- package/src/core/project-instructions.js +98 -98
- package/src/core/provider/anthropic.js +514 -514
- package/src/core/provider/openai-compatible.js +501 -501
- package/src/core/reflect-skill.js +178 -178
- package/src/core/reply-language.js +40 -40
- package/src/core/session-store.js +474 -474
- package/src/core/shell-profile.js +237 -237
- package/src/core/shell.js +323 -323
- package/src/core/soul.js +69 -69
- package/src/core/system-prompt-composer.js +52 -52
- package/src/core/tool-args.js +199 -154
- package/src/core/tool-output.js +184 -184
- package/src/core/tool-result-store.js +206 -206
- package/src/core/tools.js +3024 -2893
- package/src/core/version.js +11 -11
- package/src/tui/chat-app.js +5171 -5171
- package/src/tui/tool-activity/presenters/misc.js +30 -30
- package/src/tui/tool-activity/presenters/system.js +20 -20
- package/templates/project-requirements/report-shell.html +582 -582
- package/codemini-web/dist/assets/index-BSdIdn3L.css +0 -2
- package/codemini-web/dist/assets/mermaid-GHXKKRXX-Dg9qh8mg.js +0 -1
|
@@ -1,311 +1,311 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import {
|
|
5
|
-
getCommandsDir,
|
|
6
|
-
getProjectCommandsDir,
|
|
7
|
-
getProjectSkillsDir,
|
|
8
|
-
getSkillsDir
|
|
9
|
-
} from './paths.js';
|
|
10
|
-
import { readSkillRegistry } from './skill-registry.js';
|
|
11
|
-
|
|
12
|
-
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
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;
|
|
16
|
-
|
|
17
|
-
function parseArrayText(value) {
|
|
18
|
-
const inner = value.slice(1, -1).trim();
|
|
19
|
-
if (!inner) return [];
|
|
20
|
-
return inner.split(',').map((item) => item.trim().replace(/^["']|["']$/g, ''));
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function parseFrontmatter(raw) {
|
|
24
|
-
if (!raw.startsWith('---\n')) {
|
|
25
|
-
return { metadata: {}, content: raw };
|
|
26
|
-
}
|
|
27
|
-
const end = raw.indexOf('\n---\n', 4);
|
|
28
|
-
if (end === -1) {
|
|
29
|
-
return { metadata: {}, content: raw };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const metaRaw = raw.slice(4, end).trim();
|
|
33
|
-
const content = raw.slice(end + 5).trim();
|
|
34
|
-
const metadata = {};
|
|
35
|
-
|
|
36
|
-
for (const line of metaRaw.split('\n')) {
|
|
37
|
-
const idx = line.indexOf(':');
|
|
38
|
-
if (idx <= 0) continue;
|
|
39
|
-
const key = line.slice(0, idx).trim();
|
|
40
|
-
const value = line.slice(idx + 1).trim();
|
|
41
|
-
if (value.startsWith('[') && value.endsWith(']')) {
|
|
42
|
-
metadata[key] = parseArrayText(value);
|
|
43
|
-
} else {
|
|
44
|
-
metadata[key] = value.replace(/^["']|["']$/g, '');
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return { metadata, content };
|
|
49
|
-
}
|
|
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
|
-
|
|
124
|
-
function safeEntries(dir) {
|
|
125
|
-
try {
|
|
126
|
-
return fs.readdirSync(dir);
|
|
127
|
-
} catch {
|
|
128
|
-
return [];
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function isSafeEntry(entry) {
|
|
133
|
-
return entry !== '.' && entry !== '..' && !entry.includes('/') && !entry.includes('\\');
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function setCommand(out, name, command) {
|
|
137
|
-
const existing = out.get(name);
|
|
138
|
-
if (existing?.source === 'bundled-skill') return;
|
|
139
|
-
out.set(name, command);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function loadMarkdownCommandsFromDir(baseDir, source, out) {
|
|
143
|
-
if (!fs.existsSync(baseDir)) return;
|
|
144
|
-
for (const entry of safeEntries(baseDir)) {
|
|
145
|
-
if (!isSafeEntry(entry)) continue;
|
|
146
|
-
const full = path.join(baseDir, entry);
|
|
147
|
-
const stat = fs.statSync(full);
|
|
148
|
-
|
|
149
|
-
if (stat.isDirectory()) {
|
|
150
|
-
const commandFile = path.join(full, `${entry}.md`);
|
|
151
|
-
if (fs.existsSync(commandFile)) {
|
|
152
|
-
const raw = fs.readFileSync(commandFile, 'utf8');
|
|
153
|
-
const parsed = parseFrontmatter(raw);
|
|
154
|
-
setCommand(out, entry, {
|
|
155
|
-
name: entry,
|
|
156
|
-
source,
|
|
157
|
-
path: commandFile,
|
|
158
|
-
metadata: parsed.metadata,
|
|
159
|
-
content: parsed.content
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (entry.endsWith('.md')) {
|
|
166
|
-
const name = entry.replace(/\.md$/, '');
|
|
167
|
-
const raw = fs.readFileSync(full, 'utf8');
|
|
168
|
-
const parsed = parseFrontmatter(raw);
|
|
169
|
-
setCommand(out, name, {
|
|
170
|
-
name,
|
|
171
|
-
source,
|
|
172
|
-
path: full,
|
|
173
|
-
metadata: parsed.metadata,
|
|
174
|
-
content: parsed.content
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function loadLegacySkillsFromDir(baseDir, source, out) {
|
|
181
|
-
if (!fs.existsSync(baseDir)) return;
|
|
182
|
-
const catalog = readSkillCatalog(baseDir);
|
|
183
|
-
for (const entry of safeEntries(baseDir)) {
|
|
184
|
-
if (!isSafeEntry(entry)) continue;
|
|
185
|
-
const full = path.join(baseDir, entry);
|
|
186
|
-
const stat = fs.statSync(full);
|
|
187
|
-
if (!stat.isDirectory()) continue;
|
|
188
|
-
const catalogMeta = catalogMetadata(catalog, entry);
|
|
189
|
-
const skillFile = path.join(full, 'SKILL.md');
|
|
190
|
-
if (!fs.existsSync(skillFile)) continue;
|
|
191
|
-
const frontmatter = readFrontmatterMetadata(skillFile);
|
|
192
|
-
setCommand(out, entry, commandWithContent({
|
|
193
|
-
name: entry,
|
|
194
|
-
source: `${source}-skill`,
|
|
195
|
-
path: skillFile,
|
|
196
|
-
metadata: {
|
|
197
|
-
...frontmatter,
|
|
198
|
-
...catalogMeta,
|
|
199
|
-
description: catalogMeta.description || frontmatter.description || 'Legacy skill',
|
|
200
|
-
type: 'skill'
|
|
201
|
-
}
|
|
202
|
-
}));
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function loadBundledSkillsFromDir(baseDir, out) {
|
|
207
|
-
if (!fs.existsSync(baseDir)) return;
|
|
208
|
-
const catalog = readSkillCatalog(baseDir);
|
|
209
|
-
for (const entry of safeEntries(baseDir)) {
|
|
210
|
-
if (!isSafeEntry(entry)) continue;
|
|
211
|
-
const full = path.join(baseDir, entry);
|
|
212
|
-
const stat = fs.statSync(full);
|
|
213
|
-
if (!stat.isDirectory()) continue;
|
|
214
|
-
const catalogMeta = catalogMetadata(catalog, entry);
|
|
215
|
-
const skillFile = path.join(full, 'SKILL.md');
|
|
216
|
-
if (!fs.existsSync(skillFile)) continue;
|
|
217
|
-
const frontmatter = readFrontmatterMetadata(skillFile);
|
|
218
|
-
setCommand(out, entry, commandWithContent({
|
|
219
|
-
name: entry,
|
|
220
|
-
source: 'bundled-skill',
|
|
221
|
-
path: skillFile,
|
|
222
|
-
metadata: {
|
|
223
|
-
...frontmatter,
|
|
224
|
-
...catalogMeta,
|
|
225
|
-
type: 'skill',
|
|
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
|
-
};
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function loadInstalledSkillsFromRegistry(baseDir, registry, out) {
|
|
248
|
-
if (!registry || !Array.isArray(registry.skills)) return;
|
|
249
|
-
const catalog = readSkillCatalog(baseDir);
|
|
250
|
-
for (const skill of registry.skills) {
|
|
251
|
-
if (skill.enabled === false) continue;
|
|
252
|
-
const name = skill.name;
|
|
253
|
-
if (out.has(name)) continue;
|
|
254
|
-
const catalogMeta = catalogMetadata(catalog, name);
|
|
255
|
-
const entry = skill.entryFile || 'SKILL.md';
|
|
256
|
-
const full = path.join(baseDir, name, entry);
|
|
257
|
-
if (!fs.existsSync(full)) continue;
|
|
258
|
-
const frontmatter = readFrontmatterMetadata(full);
|
|
259
|
-
setCommand(out, name, commandWithContent({
|
|
260
|
-
name,
|
|
261
|
-
source: 'registry-skill',
|
|
262
|
-
path: full,
|
|
263
|
-
metadata: {
|
|
264
|
-
...frontmatter,
|
|
265
|
-
...catalogMeta,
|
|
266
|
-
type: 'skill',
|
|
267
|
-
version: skill.version || frontmatter.version || '0.0.0',
|
|
268
|
-
description: catalogMeta.description || skill.description || frontmatter.description || 'Installed skill'
|
|
269
|
-
}
|
|
270
|
-
}));
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
export function formatLocalDate(date = new Date()) {
|
|
275
|
-
const value = date instanceof Date ? date : new Date(date);
|
|
276
|
-
const year = value.getFullYear();
|
|
277
|
-
const month = String(value.getMonth() + 1).padStart(2, '0');
|
|
278
|
-
const day = String(value.getDate()).padStart(2, '0');
|
|
279
|
-
return `${year}-${month}-${day}`;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function substituteVariables(text, args = []) {
|
|
283
|
-
let out = text;
|
|
284
|
-
args.forEach((arg, index) => {
|
|
285
|
-
out = out.replaceAll(`{{${index + 1}}}`, arg);
|
|
286
|
-
});
|
|
287
|
-
out = out.replaceAll('{{args}}', args.join(' '));
|
|
288
|
-
out = out.replaceAll('{{cwd}}', process.cwd());
|
|
289
|
-
out = out.replaceAll('{{date}}', formatLocalDate());
|
|
290
|
-
return out;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
export async function loadCommandsAndSkills(cwd = process.cwd()) {
|
|
294
|
-
const commands = new Map();
|
|
295
|
-
|
|
296
|
-
loadBundledSkillsFromDir(BUNDLED_SKILLS_DIR, commands);
|
|
297
|
-
applySkillCatalogPatches(getProjectSkillsDir(cwd), commands);
|
|
298
|
-
loadMarkdownCommandsFromDir(getCommandsDir(), 'global', commands);
|
|
299
|
-
loadMarkdownCommandsFromDir(getProjectCommandsDir(cwd), 'project', commands);
|
|
300
|
-
loadLegacySkillsFromDir(getSkillsDir(), 'global', commands);
|
|
301
|
-
loadLegacySkillsFromDir(getProjectSkillsDir(cwd), 'project', commands);
|
|
302
|
-
applySkillCatalogPatches(getProjectSkillsDir(cwd), commands);
|
|
303
|
-
const registry = await readSkillRegistry();
|
|
304
|
-
loadInstalledSkillsFromRegistry(getSkillsDir(), registry, commands);
|
|
305
|
-
|
|
306
|
-
return commands;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
export function renderCommandPrompt(command, args) {
|
|
310
|
-
return `[Executing ${command.metadata.type === 'skill' ? 'skill' : 'command'}: /${command.name}]\n\n${substituteVariables(command.content, args)}`;
|
|
311
|
-
}
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import {
|
|
5
|
+
getCommandsDir,
|
|
6
|
+
getProjectCommandsDir,
|
|
7
|
+
getProjectSkillsDir,
|
|
8
|
+
getSkillsDir
|
|
9
|
+
} from './paths.js';
|
|
10
|
+
import { readSkillRegistry } from './skill-registry.js';
|
|
11
|
+
|
|
12
|
+
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
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;
|
|
16
|
+
|
|
17
|
+
function parseArrayText(value) {
|
|
18
|
+
const inner = value.slice(1, -1).trim();
|
|
19
|
+
if (!inner) return [];
|
|
20
|
+
return inner.split(',').map((item) => item.trim().replace(/^["']|["']$/g, ''));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function parseFrontmatter(raw) {
|
|
24
|
+
if (!raw.startsWith('---\n')) {
|
|
25
|
+
return { metadata: {}, content: raw };
|
|
26
|
+
}
|
|
27
|
+
const end = raw.indexOf('\n---\n', 4);
|
|
28
|
+
if (end === -1) {
|
|
29
|
+
return { metadata: {}, content: raw };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const metaRaw = raw.slice(4, end).trim();
|
|
33
|
+
const content = raw.slice(end + 5).trim();
|
|
34
|
+
const metadata = {};
|
|
35
|
+
|
|
36
|
+
for (const line of metaRaw.split('\n')) {
|
|
37
|
+
const idx = line.indexOf(':');
|
|
38
|
+
if (idx <= 0) continue;
|
|
39
|
+
const key = line.slice(0, idx).trim();
|
|
40
|
+
const value = line.slice(idx + 1).trim();
|
|
41
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
42
|
+
metadata[key] = parseArrayText(value);
|
|
43
|
+
} else {
|
|
44
|
+
metadata[key] = value.replace(/^["']|["']$/g, '');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return { metadata, content };
|
|
49
|
+
}
|
|
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
|
+
|
|
124
|
+
function safeEntries(dir) {
|
|
125
|
+
try {
|
|
126
|
+
return fs.readdirSync(dir);
|
|
127
|
+
} catch {
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isSafeEntry(entry) {
|
|
133
|
+
return entry !== '.' && entry !== '..' && !entry.includes('/') && !entry.includes('\\');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function setCommand(out, name, command) {
|
|
137
|
+
const existing = out.get(name);
|
|
138
|
+
if (existing?.source === 'bundled-skill') return;
|
|
139
|
+
out.set(name, command);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function loadMarkdownCommandsFromDir(baseDir, source, out) {
|
|
143
|
+
if (!fs.existsSync(baseDir)) return;
|
|
144
|
+
for (const entry of safeEntries(baseDir)) {
|
|
145
|
+
if (!isSafeEntry(entry)) continue;
|
|
146
|
+
const full = path.join(baseDir, entry);
|
|
147
|
+
const stat = fs.statSync(full);
|
|
148
|
+
|
|
149
|
+
if (stat.isDirectory()) {
|
|
150
|
+
const commandFile = path.join(full, `${entry}.md`);
|
|
151
|
+
if (fs.existsSync(commandFile)) {
|
|
152
|
+
const raw = fs.readFileSync(commandFile, 'utf8');
|
|
153
|
+
const parsed = parseFrontmatter(raw);
|
|
154
|
+
setCommand(out, entry, {
|
|
155
|
+
name: entry,
|
|
156
|
+
source,
|
|
157
|
+
path: commandFile,
|
|
158
|
+
metadata: parsed.metadata,
|
|
159
|
+
content: parsed.content
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (entry.endsWith('.md')) {
|
|
166
|
+
const name = entry.replace(/\.md$/, '');
|
|
167
|
+
const raw = fs.readFileSync(full, 'utf8');
|
|
168
|
+
const parsed = parseFrontmatter(raw);
|
|
169
|
+
setCommand(out, name, {
|
|
170
|
+
name,
|
|
171
|
+
source,
|
|
172
|
+
path: full,
|
|
173
|
+
metadata: parsed.metadata,
|
|
174
|
+
content: parsed.content
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function loadLegacySkillsFromDir(baseDir, source, out) {
|
|
181
|
+
if (!fs.existsSync(baseDir)) return;
|
|
182
|
+
const catalog = readSkillCatalog(baseDir);
|
|
183
|
+
for (const entry of safeEntries(baseDir)) {
|
|
184
|
+
if (!isSafeEntry(entry)) continue;
|
|
185
|
+
const full = path.join(baseDir, entry);
|
|
186
|
+
const stat = fs.statSync(full);
|
|
187
|
+
if (!stat.isDirectory()) continue;
|
|
188
|
+
const catalogMeta = catalogMetadata(catalog, entry);
|
|
189
|
+
const skillFile = path.join(full, 'SKILL.md');
|
|
190
|
+
if (!fs.existsSync(skillFile)) continue;
|
|
191
|
+
const frontmatter = readFrontmatterMetadata(skillFile);
|
|
192
|
+
setCommand(out, entry, commandWithContent({
|
|
193
|
+
name: entry,
|
|
194
|
+
source: `${source}-skill`,
|
|
195
|
+
path: skillFile,
|
|
196
|
+
metadata: {
|
|
197
|
+
...frontmatter,
|
|
198
|
+
...catalogMeta,
|
|
199
|
+
description: catalogMeta.description || frontmatter.description || 'Legacy skill',
|
|
200
|
+
type: 'skill'
|
|
201
|
+
}
|
|
202
|
+
}));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function loadBundledSkillsFromDir(baseDir, out) {
|
|
207
|
+
if (!fs.existsSync(baseDir)) return;
|
|
208
|
+
const catalog = readSkillCatalog(baseDir);
|
|
209
|
+
for (const entry of safeEntries(baseDir)) {
|
|
210
|
+
if (!isSafeEntry(entry)) continue;
|
|
211
|
+
const full = path.join(baseDir, entry);
|
|
212
|
+
const stat = fs.statSync(full);
|
|
213
|
+
if (!stat.isDirectory()) continue;
|
|
214
|
+
const catalogMeta = catalogMetadata(catalog, entry);
|
|
215
|
+
const skillFile = path.join(full, 'SKILL.md');
|
|
216
|
+
if (!fs.existsSync(skillFile)) continue;
|
|
217
|
+
const frontmatter = readFrontmatterMetadata(skillFile);
|
|
218
|
+
setCommand(out, entry, commandWithContent({
|
|
219
|
+
name: entry,
|
|
220
|
+
source: 'bundled-skill',
|
|
221
|
+
path: skillFile,
|
|
222
|
+
metadata: {
|
|
223
|
+
...frontmatter,
|
|
224
|
+
...catalogMeta,
|
|
225
|
+
type: 'skill',
|
|
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
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function loadInstalledSkillsFromRegistry(baseDir, registry, out) {
|
|
248
|
+
if (!registry || !Array.isArray(registry.skills)) return;
|
|
249
|
+
const catalog = readSkillCatalog(baseDir);
|
|
250
|
+
for (const skill of registry.skills) {
|
|
251
|
+
if (skill.enabled === false) continue;
|
|
252
|
+
const name = skill.name;
|
|
253
|
+
if (out.has(name)) continue;
|
|
254
|
+
const catalogMeta = catalogMetadata(catalog, name);
|
|
255
|
+
const entry = skill.entryFile || 'SKILL.md';
|
|
256
|
+
const full = path.join(baseDir, name, entry);
|
|
257
|
+
if (!fs.existsSync(full)) continue;
|
|
258
|
+
const frontmatter = readFrontmatterMetadata(full);
|
|
259
|
+
setCommand(out, name, commandWithContent({
|
|
260
|
+
name,
|
|
261
|
+
source: 'registry-skill',
|
|
262
|
+
path: full,
|
|
263
|
+
metadata: {
|
|
264
|
+
...frontmatter,
|
|
265
|
+
...catalogMeta,
|
|
266
|
+
type: 'skill',
|
|
267
|
+
version: skill.version || frontmatter.version || '0.0.0',
|
|
268
|
+
description: catalogMeta.description || skill.description || frontmatter.description || 'Installed skill'
|
|
269
|
+
}
|
|
270
|
+
}));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function formatLocalDate(date = new Date()) {
|
|
275
|
+
const value = date instanceof Date ? date : new Date(date);
|
|
276
|
+
const year = value.getFullYear();
|
|
277
|
+
const month = String(value.getMonth() + 1).padStart(2, '0');
|
|
278
|
+
const day = String(value.getDate()).padStart(2, '0');
|
|
279
|
+
return `${year}-${month}-${day}`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function substituteVariables(text, args = []) {
|
|
283
|
+
let out = text;
|
|
284
|
+
args.forEach((arg, index) => {
|
|
285
|
+
out = out.replaceAll(`{{${index + 1}}}`, arg);
|
|
286
|
+
});
|
|
287
|
+
out = out.replaceAll('{{args}}', args.join(' '));
|
|
288
|
+
out = out.replaceAll('{{cwd}}', process.cwd());
|
|
289
|
+
out = out.replaceAll('{{date}}', formatLocalDate());
|
|
290
|
+
return out;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export async function loadCommandsAndSkills(cwd = process.cwd()) {
|
|
294
|
+
const commands = new Map();
|
|
295
|
+
|
|
296
|
+
loadBundledSkillsFromDir(BUNDLED_SKILLS_DIR, commands);
|
|
297
|
+
applySkillCatalogPatches(getProjectSkillsDir(cwd), commands);
|
|
298
|
+
loadMarkdownCommandsFromDir(getCommandsDir(), 'global', commands);
|
|
299
|
+
loadMarkdownCommandsFromDir(getProjectCommandsDir(cwd), 'project', commands);
|
|
300
|
+
loadLegacySkillsFromDir(getSkillsDir(), 'global', commands);
|
|
301
|
+
loadLegacySkillsFromDir(getProjectSkillsDir(cwd), 'project', commands);
|
|
302
|
+
applySkillCatalogPatches(getProjectSkillsDir(cwd), commands);
|
|
303
|
+
const registry = await readSkillRegistry();
|
|
304
|
+
loadInstalledSkillsFromRegistry(getSkillsDir(), registry, commands);
|
|
305
|
+
|
|
306
|
+
return commands;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function renderCommandPrompt(command, args) {
|
|
310
|
+
return `[Executing ${command.metadata.type === 'skill' ? 'skill' : 'command'}: /${command.name}]\n\n${substituteVariables(command.content, args)}`;
|
|
311
|
+
}
|