codemini-cli 0.5.8 → 0.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,67 +1,67 @@
1
- {
2
- "name": "codemini-cli",
3
- "version": "0.5.8",
4
- "description": "Coding CLI optimized for small-model workflows and Windows PowerShell",
5
- "keywords": [
6
- "cli",
7
- "ai",
8
- "coding-assistant",
9
- "developer-tools",
10
- "terminal",
11
- "windows",
12
- "powershell"
13
- ],
14
- "type": "module",
15
- "homepage": "https://github.com/havingautism/Codemini-CLI#readme",
16
- "bugs": {
17
- "url": "https://github.com/havingautism/Codemini-CLI/issues"
18
- },
19
- "repository": {
20
- "type": "git",
21
- "url": "git+https://github.com/havingautism/Codemini-CLI.git"
22
- },
23
- "bin": {
24
- "codemini": "bin/coder.js",
25
- "coder": "bin/coder.js"
26
- },
27
- "scripts": {
28
- "start": "node bin/coder.js",
29
- "test": "node --test tests/*.test.js",
30
- "build:web": "npm install --prefix codemini-web && npm run build --prefix codemini-web",
31
- "prepack": "npm run build:web",
32
- "pack:offline": "npm pack",
33
- "bump:patch": "npm version patch --no-git-tag-version",
34
- "bump:minor": "npm version minor --no-git-tag-version",
35
- "bump:major": "npm version major --no-git-tag-version"
36
- },
37
- "files": [
38
- "bin",
39
- "src",
40
- "codemini-web/server.js",
41
- "codemini-web/lib",
42
- "codemini-web/dist",
43
- "codemini-web/codemini_logo.png",
44
- "souls",
45
- "templates",
46
- "skills",
47
- "README.md",
48
- "OPERATIONS.md",
49
- "deployment.md"
50
- ],
51
- "engines": {
52
- "node": ">=22"
53
- },
54
- "publishConfig": {
55
- "access": "public"
56
- },
57
- "dependencies": {
58
- "@cursorless/tree-sitter-wasms": "^0.8.1",
59
- "cheerio": "^1.1.2",
60
- "cli-truncate": "^6.0.0",
61
- "ink": "^7.0.0",
62
- "react": "^19.2.5",
63
- "strip-ansi": "^7.2.0",
64
- "web-tree-sitter": "^0.26.8"
65
- },
66
- "license": "MIT"
67
- }
1
+ {
2
+ "name": "codemini-cli",
3
+ "version": "0.5.9",
4
+ "description": "Coding CLI optimized for small-model workflows and Windows PowerShell",
5
+ "keywords": [
6
+ "cli",
7
+ "ai",
8
+ "coding-assistant",
9
+ "developer-tools",
10
+ "terminal",
11
+ "windows",
12
+ "powershell"
13
+ ],
14
+ "type": "module",
15
+ "homepage": "https://github.com/havingautism/Codemini-CLI#readme",
16
+ "bugs": {
17
+ "url": "https://github.com/havingautism/Codemini-CLI/issues"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/havingautism/Codemini-CLI.git"
22
+ },
23
+ "bin": {
24
+ "codemini": "bin/coder.js",
25
+ "coder": "bin/coder.js"
26
+ },
27
+ "scripts": {
28
+ "start": "node bin/coder.js",
29
+ "test": "node --test tests/*.test.js",
30
+ "build:web": "npm install --prefix codemini-web && npm run build --prefix codemini-web",
31
+ "prepack": "npm run build:web",
32
+ "pack:offline": "npm pack",
33
+ "bump:patch": "npm version patch --no-git-tag-version",
34
+ "bump:minor": "npm version minor --no-git-tag-version",
35
+ "bump:major": "npm version major --no-git-tag-version"
36
+ },
37
+ "files": [
38
+ "bin",
39
+ "src",
40
+ "codemini-web/server.js",
41
+ "codemini-web/lib",
42
+ "codemini-web/dist",
43
+ "codemini-web/codemini_logo.png",
44
+ "souls",
45
+ "templates",
46
+ "skills",
47
+ "README.md",
48
+ "OPERATIONS.md",
49
+ "deployment.md"
50
+ ],
51
+ "engines": {
52
+ "node": ">=22"
53
+ },
54
+ "publishConfig": {
55
+ "access": "public"
56
+ },
57
+ "dependencies": {
58
+ "@cursorless/tree-sitter-wasms": "^0.8.1",
59
+ "cheerio": "^1.1.2",
60
+ "cli-truncate": "^6.0.0",
61
+ "ink": "^7.0.0",
62
+ "react": "^19.2.5",
63
+ "strip-ansi": "^7.2.0",
64
+ "web-tree-sitter": "^0.26.8"
65
+ },
66
+ "license": "MIT"
67
+ }
@@ -0,0 +1,40 @@
1
+ {
2
+ "version": 1,
3
+ "skills": {
4
+ "superpowers-lite": {
5
+ "description": "Default concise workflow for coding tasks; keep context tight, choose the right route, and verify before claiming success.",
6
+ "mode": "always",
7
+ "triggers": [],
8
+ "enabled": true,
9
+ "priority": 100
10
+ },
11
+ "brainstorm": {
12
+ "description": "Use when a feature or behavior request has multiple reasonable approaches and the missing piece is user preference, tradeoff choice, or key constraint.",
13
+ "mode": "agent_requested",
14
+ "triggers": [],
15
+ "enabled": true,
16
+ "priority": 70
17
+ },
18
+ "writing-plans": {
19
+ "description": "Use when you have a clear goal or approved design for a non-trivial task, before touching code.",
20
+ "mode": "agent_requested",
21
+ "triggers": [],
22
+ "enabled": true,
23
+ "priority": 75
24
+ },
25
+ "grill-me": {
26
+ "description": "Use when the user explicitly asks to pressure-test a plan, architecture choice, PR, launch, or product idea.",
27
+ "mode": "agent_requested",
28
+ "triggers": [],
29
+ "enabled": true,
30
+ "priority": 65
31
+ },
32
+ "project-requirements": {
33
+ "description": "Create a project requirements report with repository inspection and structured output artifacts.",
34
+ "mode": "manual",
35
+ "triggers": [],
36
+ "enabled": true,
37
+ "priority": 50
38
+ }
39
+ }
40
+ }
@@ -77,9 +77,15 @@ export async function listSkillEntries({ scope = 'all', cwd = process.cwd() } =
77
77
  name: command.name,
78
78
  version: command.metadata?.version || '0.0.0',
79
79
  description: command.metadata?.description || '',
80
+ mode: command.metadata?.mode || '',
81
+ triggers: Array.isArray(command.metadata?.triggers) ? command.metadata.triggers : [],
80
82
  scope: itemScope,
81
83
  path: command.path,
82
- enabled: itemScope === 'builtin' ? true : config.skills?.enabled?.[command.name] !== false
84
+ enabled: command.metadata?.enabled === false
85
+ ? false
86
+ : itemScope === 'builtin'
87
+ ? true
88
+ : config.skills?.enabled?.[command.name] !== false
83
89
  });
84
90
  }
85
91
  return entries.sort((a, b) => `${a.scope}:${a.name}`.localeCompare(`${b.scope}:${b.name}`));
@@ -93,14 +99,19 @@ async function readSkillMeta(name, { scope = 'all', cwd = process.cwd() } = {})
93
99
  }
94
100
  const dir = path.dirname(found.path);
95
101
  const manifestPath = path.join(dir, 'manifest.json');
102
+ const catalogPath = path.join(path.dirname(dir), 'codemini.skills.json');
96
103
  let manifest = null;
97
104
  try {
98
- manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
105
+ const catalog = JSON.parse(await fs.readFile(catalogPath, 'utf8'));
106
+ manifest = catalog?.skills?.[found.name] || null;
99
107
  } catch {
100
- manifest = null;
108
+ try {
109
+ manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
110
+ } catch {
111
+ manifest = null;
112
+ }
101
113
  }
102
- const entryFile = manifest?.entry || 'SKILL.md';
103
- const skillPath = found.path || path.join(dir, entryFile);
114
+ const skillPath = found.path || path.join(dir, 'SKILL.md');
104
115
  try {
105
116
  const content = await fs.readFile(skillPath, 'utf8');
106
117
  const firstLines = content.split('\n').slice(0, 20).join('\n');
@@ -136,6 +136,7 @@ function getCompletionCopy(language = 'zh') {
136
136
  'soul.preset': 'soul 预设',
137
137
  'soul.custom_path': '自定义 soul 路径',
138
138
  'policy.safe_mode': '安全模式开关',
139
+ 'policy.allowed_paths': '安全模式目录白名单',
139
140
  'policy.allow_dangerous_commands': '危险命令开关'
140
141
  },
141
142
  optionHints: {
@@ -145,6 +146,7 @@ function getCompletionCopy(language = 'zh') {
145
146
  'execution.mode': '可选:auto | normal | plan',
146
147
  'shell.default': '常用:bash | powershell',
147
148
  'policy.safe_mode': '可选:true | false',
149
+ 'policy.allowed_paths': 'JSON 数组,例如 ["D:\\\\shared"]',
148
150
  'policy.allow_dangerous_commands': '可选:true | false',
149
151
  'context.prompt_budget_audit': '可选:true | false'
150
152
  },
@@ -243,6 +245,7 @@ function getCompletionCopy(language = 'zh') {
243
245
  'soul.preset': 'soul preset',
244
246
  'soul.custom_path': 'custom soul prompt path',
245
247
  'policy.safe_mode': 'safe mode switch',
248
+ 'policy.allowed_paths': 'safe-mode allowed path roots',
246
249
  'policy.allow_dangerous_commands': 'dangerous command allowance'
247
250
  },
248
251
  optionHints: {
@@ -252,6 +255,7 @@ function getCompletionCopy(language = 'zh') {
252
255
  'execution.mode': 'options: auto | normal | plan',
253
256
  'shell.default': 'common: bash | powershell',
254
257
  'policy.safe_mode': 'options: true | false',
258
+ 'policy.allowed_paths': 'JSON array, for example ["D:\\\\shared"]',
255
259
  'policy.allow_dangerous_commands': 'options: true | false',
256
260
  'context.prompt_budget_audit': 'options: true | false'
257
261
  },
@@ -1265,6 +1269,7 @@ function isBundledSkillCommand(command) {
1265
1269
  }
1266
1270
 
1267
1271
  function isSkillEnabled(config, name, command = null) {
1272
+ if (command?.metadata?.enabled === false) return false;
1268
1273
  if (isBundledSkillCommand(command)) return true;
1269
1274
  return config.skills?.enabled?.[name] !== false;
1270
1275
  }
@@ -2301,6 +2306,9 @@ function buildRuntimeStateSnapshot({ currentSession, config, model, executionMod
2301
2306
  maxContextTokens,
2302
2307
  pendingPlanApproval: planState?.status === 'pending_approval'
2303
2308
  ? { goal: planState.goal, summary: planState.finalSummary || planState.summary, filePath: planState.filePath, steps: planState.steps || [] }
2309
+ : null,
2310
+ pendingReflectSkill: planState?.status === 'pending_reflect_skill'
2311
+ ? buildPendingReflectSkillSnapshot(planState)
2304
2312
  : null
2305
2313
  };
2306
2314
  Object.defineProperties(snapshot, {
@@ -2314,11 +2322,6 @@ function buildRuntimeStateSnapshot({ currentSession, config, model, executionMod
2314
2322
  enumerable: false,
2315
2323
  writable: false
2316
2324
  },
2317
- pendingReflectSkill: {
2318
- value: currentSession?.planState?.status === 'pending_reflect_skill',
2319
- enumerable: false,
2320
- writable: false
2321
- },
2322
2325
  replyLanguage: {
2323
2326
  value: getReplyLanguage(config),
2324
2327
  enumerable: false,
@@ -2329,7 +2332,6 @@ function buildRuntimeStateSnapshot({ currentSession, config, model, executionMod
2329
2332
  ...snapshot,
2330
2333
  currentContextTokens,
2331
2334
  contextUsagePct,
2332
- pendingReflectSkill: currentSession?.planState?.status === 'pending_reflect_skill',
2333
2335
  replyLanguage: getReplyLanguage(config)
2334
2336
  }),
2335
2337
  enumerable: false,
@@ -2513,6 +2515,21 @@ function buildPendingReflectSkillMessage(reflectState) {
2513
2515
  return lines.join('\n');
2514
2516
  }
2515
2517
 
2518
+ function buildPendingReflectSkillSnapshot(reflectState) {
2519
+ const candidates = Array.isArray(reflectState?.candidates) ? reflectState.candidates : [];
2520
+ const candidate = candidates[0] || null;
2521
+ if (!candidate) return null;
2522
+ return {
2523
+ scope: reflectState?.targetScope || 'project',
2524
+ request: reflectState?.request || '',
2525
+ name: candidate.name || '',
2526
+ description: candidate.description || '',
2527
+ confidence: Number(candidate.confidence ?? 0.75),
2528
+ targetPath: candidate.targetPath || '',
2529
+ content: candidate.content || ''
2530
+ };
2531
+ }
2532
+
2516
2533
  function buildApprovedPlanExecutionPrompt(planState, approvalText = '') {
2517
2534
  const requirementPacket = buildGoalRequirementPacket(planState?.goal || '', 'coder');
2518
2535
  const lines = [
@@ -2740,8 +2757,10 @@ async function askModel({
2740
2757
  }
2741
2758
  }
2742
2759
 
2760
+ const shouldGenerateTitle = text
2761
+ ? !session.messages.some((msg) => msg?.role === 'user')
2762
+ : false;
2743
2763
  if (text) {
2744
- const shouldGenerateTitle = !session.messages.some((msg) => msg?.role === 'user');
2745
2764
  const modelExtra =
2746
2765
  typeof modelText === 'string' && modelText && modelText !== text ? { model_content: modelText } : {};
2747
2766
  const userMessage = stampedMessage('user', text, modelExtra);
@@ -2773,7 +2792,16 @@ async function askModel({
2773
2792
 
2774
2793
  const { definitions, handlers, formatters, deferredDefinitions, dispose: disposeTools } = getBuiltinTools({
2775
2794
  workspaceRoot: process.cwd(),
2776
- config,
2795
+ config: {
2796
+ ...config,
2797
+ policy: {
2798
+ ...(config.policy || {}),
2799
+ allowed_paths: [
2800
+ ...(Array.isArray(config.policy?.allowed_paths) ? config.policy.allowed_paths : []),
2801
+ path.join(getSessionsDir(), String(session.id))
2802
+ ]
2803
+ }
2804
+ },
2777
2805
  sessionId: session.id,
2778
2806
  onSystemEvent: onAgentEvent,
2779
2807
  getTodos: () => normalizeTodos(session.todos),
@@ -3004,7 +3032,7 @@ async function askModel({
3004
3032
  await flushScheduledSave();
3005
3033
  await saveSession(session);
3006
3034
  // Generate a better title asynchronously after saving
3007
- if (shouldReplaceSessionTitle(session.title)) {
3035
+ if (shouldGenerateTitle) {
3008
3036
  const titleSessionId = session.id;
3009
3037
  generateSessionTitle({
3010
3038
  userText: text,
@@ -4242,11 +4270,22 @@ export async function createChatRuntime({
4242
4270
  if (hasPendingPlanApproval(currentSession)) {
4243
4271
  executionMode = 'plan';
4244
4272
  }
4273
+ let compactState = null;
4274
+ const normalizeCompactThreshold = (value, fallback = 60) => {
4275
+ const num = Number(value);
4276
+ if (!Number.isFinite(num)) return fallback;
4277
+ return Math.min(95, Math.max(50, num));
4278
+ };
4279
+ const syncCompactStateFromConfig = () => {
4280
+ if (!compactState) return;
4281
+ compactState.threshold = normalizeCompactThreshold(config.context?.preflight_trigger_pct, 60);
4282
+ };
4245
4283
  const syncRuntimeFromConfig = async ({ model: nextModel } = {}) => {
4246
4284
  const configuredMode = String(config.execution?.mode || 'normal');
4247
4285
  executionMode = hasPendingPlanApproval(currentSession)
4248
4286
  ? 'plan'
4249
4287
  : (['normal', 'auto', 'plan'].includes(configuredMode) ? configuredMode : 'normal');
4288
+ syncCompactStateFromConfig();
4250
4289
 
4251
4290
  const resolvedModel = String(nextModel || '').trim();
4252
4291
  if (resolvedModel) {
@@ -4269,10 +4308,10 @@ export async function createChatRuntime({
4269
4308
  // Set up tool result store under session directory
4270
4309
  const sessionResultsDir = path.join(getSessionsDir(), String(currentSession.id));
4271
4310
  setResultDir(sessionResultsDir);
4272
- const compactState = {
4311
+ compactState = {
4273
4312
  backupMessages: null,
4274
4313
  autoEnabled: true,
4275
- threshold: 60,
4314
+ threshold: normalizeCompactThreshold(config.context?.preflight_trigger_pct, 60),
4276
4315
  mode: 'conservative'
4277
4316
  };
4278
4317
  let compactedForModel = currentSession.compact?.view || null;
@@ -4356,6 +4395,7 @@ export async function createChatRuntime({
4356
4395
  'soul.preset',
4357
4396
  'soul.custom_path',
4358
4397
  'policy.safe_mode',
4398
+ 'policy.allowed_paths',
4359
4399
  'policy.allow_dangerous_commands'
4360
4400
  ];
4361
4401
 
@@ -4777,6 +4817,11 @@ export async function createChatRuntime({
4777
4817
  };
4778
4818
 
4779
4819
  const persistAssistantExchange = async (userText, assistantText, { includeUser = true, extra = {} } = {}) => {
4820
+ const priorUserCount = currentSession.messages.filter((msg) => msg?.role === 'user').length;
4821
+ const priorAssistantCount = currentSession.messages.filter((msg) => msg?.role === 'assistant').length;
4822
+ const shouldGenerateTitle =
4823
+ (includeUser && userText && priorUserCount === 0) ||
4824
+ (!includeUser && userText && priorUserCount === 1 && priorAssistantCount === 0);
4780
4825
  if (includeUser && userText) {
4781
4826
  appendSessionMessage(stampedMessage('user', userText));
4782
4827
  }
@@ -4787,7 +4832,7 @@ export async function createChatRuntime({
4787
4832
  currentSession.mode = executionMode || config.execution?.mode || 'normal';
4788
4833
  await saveSession(currentSession);
4789
4834
  // Generate a better title asynchronously after saving
4790
- if (shouldReplaceSessionTitle(currentSession.title)) {
4835
+ if (shouldGenerateTitle || shouldReplaceSessionTitle(currentSession.title)) {
4791
4836
  const titleSessionId = currentSession.id;
4792
4837
  generateSessionTitle({
4793
4838
  userText,
@@ -5115,6 +5160,7 @@ export async function createChatRuntime({
5115
5160
  });
5116
5161
  currentSession.planState = null;
5117
5162
  executionMode = 'auto';
5163
+ if (onAgentEvent) onAgentEvent({ type: 'reflect:approval_cleared' });
5118
5164
  await reloadCommandsAndSkills();
5119
5165
  const text = `Reflect skill written and loaded: /${written.draft.name}\nPath: ${written.filePath}`;
5120
5166
  await persistLocalExchange(line, text, { includeUser: false });
@@ -5172,6 +5218,12 @@ export async function createChatRuntime({
5172
5218
  workspaceRoot: process.cwd()
5173
5219
  })
5174
5220
  };
5221
+ if (onAgentEvent) {
5222
+ onAgentEvent({
5223
+ type: 'reflect:pending_approval',
5224
+ draft: buildPendingReflectSkillSnapshot(currentSession.planState)
5225
+ });
5226
+ }
5175
5227
  const text = `Reflect skill draft revised.\n${buildPendingReflectSkillMessage(currentSession.planState)}`;
5176
5228
  await persistLocalExchange(line, text);
5177
5229
  return { type: 'system', text };
@@ -5209,6 +5261,7 @@ export async function createChatRuntime({
5209
5261
  if (hasPendingReflectSkill(currentSession)) {
5210
5262
  currentSession.planState = null;
5211
5263
  executionMode = 'auto';
5264
+ if (onAgentEvent) onAgentEvent({ type: 'reflect:approval_cleared' });
5212
5265
  const text = 'Reflect skill draft discarded.';
5213
5266
  await persistLocalExchange(line, text, { includeUser: false });
5214
5267
  return { type: 'system', text };
@@ -5668,6 +5721,12 @@ export async function createChatRuntime({
5668
5721
  request: parsedReflect.request,
5669
5722
  candidates
5670
5723
  };
5724
+ if (onAgentEvent) {
5725
+ onAgentEvent({
5726
+ type: 'reflect:pending_approval',
5727
+ draft: buildPendingReflectSkillSnapshot(currentSession.planState)
5728
+ });
5729
+ }
5671
5730
  const text = buildPendingReflectSkillMessage(currentSession.planState);
5672
5731
  await persistLocalExchange(line, text);
5673
5732
  return { type: 'system', text };
@@ -5735,7 +5794,7 @@ export async function createChatRuntime({
5735
5794
  await resetConfig();
5736
5795
  config = await loadConfig();
5737
5796
  await syncRuntimeFromConfig({ model: resolveDefaultModel(config) });
5738
- compactState.threshold = 60;
5797
+ syncCompactStateFromConfig();
5739
5798
  compactState.mode = 'conservative';
5740
5799
  compactState.autoEnabled = true;
5741
5800
  const text = 'Config reset complete';
@@ -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 raw = fs.readFileSync(skillFile, 'utf8');
115
- const parsed = parseFrontmatter(raw);
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
- description: parsed.metadata.description || 'Legacy skill',
197
+ ...frontmatter,
198
+ ...catalogMeta,
199
+ description: catalogMeta.description || frontmatter.description || 'Legacy skill',
122
200
  type: 'skill'
123
- },
124
- content: parsed.content
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 raw = fs.readFileSync(skillFile, 'utf8');
139
- const parsed = parseFrontmatter(raw);
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
- ...parsed.metadata,
223
+ ...frontmatter,
224
+ ...catalogMeta,
146
225
  type: 'skill',
147
- version: parsed.metadata.version || '0.1.0',
148
- description: parsed.metadata.description || 'Bundled skill'
149
- },
150
- content: parsed.content
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 raw = fs.readFileSync(full, 'utf8');
165
- const parsed = parseFrontmatter(raw);
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
- ...parsed.metadata,
264
+ ...frontmatter,
265
+ ...catalogMeta,
172
266
  type: 'skill',
173
- version: skill.version || parsed.metadata.version || '0.0.0',
174
- description: skill.description || parsed.metadata.description || 'Installed skill'
175
- },
176
- content: parsed.content
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