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.
@@ -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';