claudecode-omc 5.6.3 → 5.6.5
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/.local/skills/prompt-optimizer/SKILL.md +793 -0
- package/bundled/manifest.json +1 -1
- package/bundled/upstream/ecc/skills/prompt-optimizer/SKILL.md +398 -21
- package/package.json +1 -1
- package/src/cli/index.js +1 -0
- package/src/cli/setup.js +9 -0
- package/src/cli/skill-index.js +209 -0
- package/src/cli/skill.js +4 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const fsp = require('fs/promises');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const { getProjectRoot } = require('../config/paths');
|
|
7
|
+
|
|
8
|
+
const INDEX_FILENAME = '_index.md';
|
|
9
|
+
const MAX_DESCRIPTION_CHARS = 280;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parse YAML frontmatter from a SKILL.md, returning { name, description }.
|
|
13
|
+
*
|
|
14
|
+
* Handles:
|
|
15
|
+
* - `key: value` single-line
|
|
16
|
+
* - `key: >-` or `key: |` folded/literal block scalars (collects indented
|
|
17
|
+
* continuation lines, joined with spaces)
|
|
18
|
+
* - quoted values: stripped of surrounding quotes
|
|
19
|
+
*
|
|
20
|
+
* Returns null if frontmatter is missing or name field is absent.
|
|
21
|
+
*/
|
|
22
|
+
function parseFrontmatter(skillFile) {
|
|
23
|
+
if (!fs.existsSync(skillFile)) return null;
|
|
24
|
+
|
|
25
|
+
const content = fs.readFileSync(skillFile, 'utf8');
|
|
26
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
27
|
+
if (!match) return null;
|
|
28
|
+
|
|
29
|
+
const lines = match[1].split('\n');
|
|
30
|
+
const meta = {};
|
|
31
|
+
let currentKey = null;
|
|
32
|
+
let blockMode = false; // when value is `>-` or `|`
|
|
33
|
+
|
|
34
|
+
for (const raw of lines) {
|
|
35
|
+
if (!raw.trim()) continue;
|
|
36
|
+
|
|
37
|
+
// top-level key: detect by leading non-whitespace + colon
|
|
38
|
+
const keyMatch = raw.match(/^([a-zA-Z_-][\w-]*)\s*:\s*(.*)$/);
|
|
39
|
+
const isIndented = /^\s/.test(raw);
|
|
40
|
+
|
|
41
|
+
if (keyMatch && !isIndented) {
|
|
42
|
+
currentKey = keyMatch[1];
|
|
43
|
+
const valueRaw = keyMatch[2].trim();
|
|
44
|
+
if (valueRaw === '>-' || valueRaw === '>' || valueRaw === '|' || valueRaw === '|-') {
|
|
45
|
+
meta[currentKey] = '';
|
|
46
|
+
blockMode = true;
|
|
47
|
+
} else {
|
|
48
|
+
meta[currentKey] = stripQuotes(valueRaw);
|
|
49
|
+
blockMode = false;
|
|
50
|
+
}
|
|
51
|
+
} else if (currentKey && (blockMode || isIndented)) {
|
|
52
|
+
// continuation line
|
|
53
|
+
const continued = raw.trim();
|
|
54
|
+
if (continued) {
|
|
55
|
+
meta[currentKey] = (meta[currentKey] ? meta[currentKey] + ' ' : '') + continued;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!meta.name) return null;
|
|
61
|
+
return meta;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function stripQuotes(s) {
|
|
65
|
+
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
|
66
|
+
return s.slice(1, -1);
|
|
67
|
+
}
|
|
68
|
+
return s;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function truncateDescription(desc) {
|
|
72
|
+
if (!desc) return '';
|
|
73
|
+
// Collapse whitespace
|
|
74
|
+
const flat = desc.replace(/\s+/g, ' ').trim();
|
|
75
|
+
if (flat.length <= MAX_DESCRIPTION_CHARS) return flat;
|
|
76
|
+
// Truncate at sentence boundary if possible
|
|
77
|
+
const truncated = flat.slice(0, MAX_DESCRIPTION_CHARS);
|
|
78
|
+
const lastPeriod = truncated.lastIndexOf('.');
|
|
79
|
+
if (lastPeriod > MAX_DESCRIPTION_CHARS * 0.6) {
|
|
80
|
+
return truncated.slice(0, lastPeriod + 1);
|
|
81
|
+
}
|
|
82
|
+
return truncated + '…';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function escapeTableCell(s) {
|
|
86
|
+
return (s || '').replace(/\|/g, '\\|').replace(/\n/g, ' ');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Scan a directory of skills (each subdir contains SKILL.md) and return
|
|
91
|
+
* sorted list of { name, description }.
|
|
92
|
+
*/
|
|
93
|
+
function buildEntries(skillsDir) {
|
|
94
|
+
if (!fs.existsSync(skillsDir)) return [];
|
|
95
|
+
|
|
96
|
+
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
97
|
+
const items = [];
|
|
98
|
+
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
if (!entry.isDirectory()) continue;
|
|
101
|
+
if (entry.name.startsWith('_') || entry.name.startsWith('.')) continue;
|
|
102
|
+
|
|
103
|
+
const skillFile = path.join(skillsDir, entry.name, 'SKILL.md');
|
|
104
|
+
const meta = parseFrontmatter(skillFile);
|
|
105
|
+
if (!meta) {
|
|
106
|
+
process.stderr.write(` skipped: ${entry.name} (no frontmatter)\n`);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
items.push({
|
|
110
|
+
name: meta.name,
|
|
111
|
+
description: truncateDescription(meta.description),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
items.sort((a, b) => a.name.localeCompare(b.name));
|
|
116
|
+
return items;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Render entries as a Markdown table with a header preamble.
|
|
121
|
+
*/
|
|
122
|
+
function renderIndex(skillsDir, entries) {
|
|
123
|
+
const lines = [];
|
|
124
|
+
lines.push('# Skill Index');
|
|
125
|
+
lines.push('');
|
|
126
|
+
lines.push(`Auto-generated by \`omc-manage skill index\`. Do not edit by hand —`);
|
|
127
|
+
lines.push(`rerun \`omc-manage skill index\` after adding or removing skills.`);
|
|
128
|
+
lines.push('');
|
|
129
|
+
lines.push(`Source directory: \`${skillsDir.replace(os.homedir(), '~')}\``);
|
|
130
|
+
lines.push(`Generated: ${new Date().toISOString()}`);
|
|
131
|
+
lines.push(`Entries: ${entries.length}`);
|
|
132
|
+
lines.push('');
|
|
133
|
+
lines.push('## Usage for LLMs');
|
|
134
|
+
lines.push('');
|
|
135
|
+
lines.push('`grep -i "<keyword>" _index.md` to find skills matching an intent.');
|
|
136
|
+
lines.push('Each row is `name | description` — both fields searchable.');
|
|
137
|
+
lines.push('');
|
|
138
|
+
lines.push('| Skill | Description |');
|
|
139
|
+
lines.push('|---|---|');
|
|
140
|
+
for (const e of entries) {
|
|
141
|
+
lines.push(`| \`${escapeTableCell(e.name)}\` | ${escapeTableCell(e.description)} |`);
|
|
142
|
+
}
|
|
143
|
+
lines.push('');
|
|
144
|
+
return lines.join('\n');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Build and write the index. Returns { path, count }.
|
|
149
|
+
*/
|
|
150
|
+
async function buildAndWriteIndex(skillsDir, { quiet = false } = {}) {
|
|
151
|
+
const entries = buildEntries(skillsDir);
|
|
152
|
+
const indexPath = path.join(skillsDir, INDEX_FILENAME);
|
|
153
|
+
await fsp.writeFile(indexPath, renderIndex(skillsDir, entries), 'utf8');
|
|
154
|
+
if (!quiet) {
|
|
155
|
+
console.log(` wrote ${indexPath.replace(os.homedir(), '~')} (${entries.length} entries)`);
|
|
156
|
+
}
|
|
157
|
+
return { path: indexPath, count: entries.length };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Resolve the skills install directory for the given scope.
|
|
162
|
+
*/
|
|
163
|
+
function resolveSkillsDir(scope) {
|
|
164
|
+
if (scope === 'project') {
|
|
165
|
+
const root = getProjectRoot();
|
|
166
|
+
return path.join(root, '.claude', 'skills');
|
|
167
|
+
}
|
|
168
|
+
// default: user scope
|
|
169
|
+
return path.join(os.homedir(), '.claude', 'skills');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* CLI entrypoint: `omc-manage skill index [--scope user|project] [--quiet]`
|
|
174
|
+
*/
|
|
175
|
+
async function indexCommand(args, flags = {}) {
|
|
176
|
+
const scope = flags.scope || 'user';
|
|
177
|
+
if (scope !== 'user' && scope !== 'project') {
|
|
178
|
+
console.error(`Error: invalid --scope "${scope}" (expected user or project)`);
|
|
179
|
+
process.exitCode = 1;
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const skillsDir = resolveSkillsDir(scope);
|
|
184
|
+
if (!fs.existsSync(skillsDir)) {
|
|
185
|
+
console.error(`Error: skills directory not found: ${skillsDir}`);
|
|
186
|
+
console.error('Run `omc-manage setup` first to install skills.');
|
|
187
|
+
process.exitCode = 1;
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
console.log(`omc-manage skill index`);
|
|
192
|
+
console.log(`======================`);
|
|
193
|
+
console.log(`Scope: ${scope}`);
|
|
194
|
+
console.log(`Target: ${skillsDir.replace(os.homedir(), '~')}`);
|
|
195
|
+
console.log('');
|
|
196
|
+
|
|
197
|
+
const result = await buildAndWriteIndex(skillsDir, { quiet: flags.quiet });
|
|
198
|
+
|
|
199
|
+
console.log('');
|
|
200
|
+
console.log(`Indexed ${result.count} skills.`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
module.exports = {
|
|
204
|
+
indexCommand,
|
|
205
|
+
buildAndWriteIndex,
|
|
206
|
+
resolveSkillsDir,
|
|
207
|
+
parseFrontmatter, // exported for tests
|
|
208
|
+
truncateDescription, // exported for tests
|
|
209
|
+
};
|
package/src/cli/skill.js
CHANGED
|
@@ -271,6 +271,10 @@ async function skill(args, flags = {}) {
|
|
|
271
271
|
if (cmd === 'recommend') {
|
|
272
272
|
return recommend(args, flags);
|
|
273
273
|
}
|
|
274
|
+
if (cmd === 'index') {
|
|
275
|
+
const { indexCommand } = require('./skill-index');
|
|
276
|
+
return indexCommand(args.slice(1), flags);
|
|
277
|
+
}
|
|
274
278
|
|
|
275
279
|
// Fall through to artifact subcommands (list, prefer, conflicts)
|
|
276
280
|
flags.type = 'skills';
|