skills-viewer 0.2.0

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,205 @@
1
+ "use strict";
2
+ /* コピー / ゴミ箱行き削除 / md 読み取り / エディタで開く の実体とパス検証 */
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.assertManagedPath = assertManagedPath;
38
+ exports.assertReadableMd = assertReadableMd;
39
+ exports.uniqueDest = uniqueDest;
40
+ exports.doCopy = doCopy;
41
+ exports.doDelete = doDelete;
42
+ exports.openInEditor = openInEditor;
43
+ const fs = __importStar(require("node:fs"));
44
+ const os = __importStar(require("node:os"));
45
+ const path = __importStar(require("node:path"));
46
+ const node_child_process_1 = require("node:child_process");
47
+ const scan_1 = require("./scan");
48
+ const errors_1 = require("./errors");
49
+ const HOME = os.homedir();
50
+ /* realpath 解決(存在しないパスは not-found に正規化) */
51
+ function realpathOrThrow(p) {
52
+ try {
53
+ return fs.realpathSync(p);
54
+ }
55
+ catch {
56
+ throw new errors_1.ApiError('not-found', p);
57
+ }
58
+ }
59
+ /* skills(ディレクトリ) / commands / agents(単一 .md) を管理対象とする */
60
+ function assertManagedPath(p) {
61
+ const real = realpathOrThrow(p);
62
+ const sep = path.sep;
63
+ // plugin 配下(.claude/plugins/<name>/skills/… 等)は kind 判定より先に弾く。
64
+ // within() は「.claude 直下の skills/」を要求するため、後段では到達しない
65
+ if (real.includes(sep + '.claude' + sep + 'plugins' + sep)) {
66
+ throw new errors_1.ApiError('plugin-managed');
67
+ }
68
+ const within = (sub) => real.includes(sep + '.claude' + sep + sub + sep);
69
+ const kind = within('skills')
70
+ ? 'skill'
71
+ : within('commands')
72
+ ? 'command'
73
+ : within('agents')
74
+ ? 'agent'
75
+ : null;
76
+ if (!kind)
77
+ throw new errors_1.ApiError('not-managed-path', real);
78
+ return { real, kind };
79
+ }
80
+ /* 読み取り専用は plugin 配下も許可(.claude 配下の .md のみ) */
81
+ function assertReadableMd(p) {
82
+ const real = realpathOrThrow(p);
83
+ if (!real.endsWith('.md'))
84
+ throw new errors_1.ApiError('not-md', real);
85
+ if (!real.includes(path.sep + '.claude' + path.sep))
86
+ throw new errors_1.ApiError('not-readable-path', real);
87
+ return real;
88
+ }
89
+ /* エディタで開くのは .claude 配下ならなんでも良い(settings.json 等も含む) */
90
+ function assertOpenablePath(p) {
91
+ const real = realpathOrThrow(p);
92
+ if (!real.includes(path.sep + '.claude' + path.sep))
93
+ throw new errors_1.ApiError('not-openable-path', real);
94
+ return real;
95
+ }
96
+ function assertKnownTarget(target, cwd) {
97
+ const resolved = path.resolve(target);
98
+ const known = new Set([HOME, ...(0, scan_1.listProjects)(cwd)]);
99
+ if (!known.has(resolved))
100
+ throw new errors_1.ApiError('unknown-copy-target', resolved);
101
+ return resolved;
102
+ }
103
+ /* 同名がある場合は -copy, -copy2, … サフィックス(design 仕様) */
104
+ function uniqueDest(to, isFile) {
105
+ if (!fs.existsSync(to))
106
+ return to;
107
+ const dir = path.dirname(to);
108
+ const base = isFile ? path.basename(to, '.md') : path.basename(to);
109
+ const ext = isFile ? '.md' : '';
110
+ for (let i = 1; i < 100; i++) {
111
+ const cand = path.join(dir, base + '-copy' + (i === 1 ? '' : i) + ext);
112
+ if (!fs.existsSync(cand))
113
+ return cand;
114
+ }
115
+ throw new errors_1.ApiError('no-free-name');
116
+ }
117
+ const KIND_SUBDIR = {
118
+ skill: 'skills',
119
+ command: 'commands',
120
+ agent: 'agents',
121
+ };
122
+ function doCopy({ src, target }, cwd) {
123
+ const { real, kind } = assertManagedPath(src);
124
+ const dstRoot = assertKnownTarget(target, cwd);
125
+ let from, to;
126
+ if (kind === 'skill') {
127
+ from = path.dirname(real); // skill ディレクトリ丸ごと(references 等を含む)
128
+ to = uniqueDest(path.join(dstRoot, '.claude', 'skills', path.basename(from)), false);
129
+ }
130
+ else {
131
+ from = real;
132
+ to = uniqueDest(path.join(dstRoot, '.claude', KIND_SUBDIR[kind], path.basename(real)), true);
133
+ }
134
+ fs.mkdirSync(path.dirname(to), { recursive: true });
135
+ fs.cpSync(from, to, { recursive: true });
136
+ const destMd = kind === 'skill' ? path.join(to, 'SKILL.md') : to;
137
+ const destName = kind === 'skill' ? path.basename(to) : path.basename(to, '.md');
138
+ return { ok: true, dest: to, destMd, destName };
139
+ }
140
+ /* ---- ゴミ箱行き削除(復元可能) ---- */
141
+ function trashRoot() {
142
+ if (process.platform === 'darwin')
143
+ return path.join(HOME, '.Trash');
144
+ const linuxTrash = path.join(HOME, '.local', 'share', 'Trash', 'files');
145
+ if (fs.existsSync(linuxTrash))
146
+ return linuxTrash;
147
+ return path.join(HOME, '.cache', 'skills-viewer', 'trash'); // 最終フォールバック
148
+ }
149
+ function moveToTrash(target) {
150
+ const root = trashRoot();
151
+ fs.mkdirSync(root, { recursive: true });
152
+ let dest = path.join(root, path.basename(target));
153
+ if (fs.existsSync(dest)) {
154
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
155
+ dest = path.join(root, path.basename(target) + ' ' + stamp);
156
+ }
157
+ try {
158
+ fs.renameSync(target, dest);
159
+ }
160
+ catch (e) {
161
+ if (e.code !== 'EXDEV')
162
+ throw e;
163
+ fs.cpSync(target, dest, { recursive: true }); // 別ボリューム(rename 不可)は copy + rm
164
+ fs.rmSync(target, { recursive: true });
165
+ }
166
+ return dest;
167
+ }
168
+ function doDelete({ src }) {
169
+ const { real, kind } = assertManagedPath(src);
170
+ const target = kind === 'skill' ? path.dirname(real) : real;
171
+ if (kind === 'skill' && path.basename(path.dirname(target)) !== 'skills') {
172
+ throw new errors_1.ApiError('unexpected-skill-dir', target);
173
+ }
174
+ const trashedTo = moveToTrash(target);
175
+ return { ok: true, deleted: target, trashedTo };
176
+ }
177
+ /* ---- エディタで開く(OS デフォルト設定時のフォールバック) ---- */
178
+ /* CSB_EDITOR → cursor → code → subl → zed の順で CLI を探し、無ければ OS 既定で開く */
179
+ let editorCache;
180
+ function detectEditor() {
181
+ if (editorCache)
182
+ return editorCache;
183
+ const candidates = [process.env.CSB_EDITOR, 'cursor', 'code', 'subl', 'zed'].filter((c) => Boolean(c));
184
+ for (const cmd of candidates) {
185
+ const r = (0, node_child_process_1.spawnSync)(process.platform === 'win32' ? 'where' : 'which', [cmd], {
186
+ stdio: 'ignore',
187
+ });
188
+ if (r.status === 0)
189
+ return (editorCache = { cmd });
190
+ }
191
+ return (editorCache = { cmd: null });
192
+ }
193
+ function openInEditor({ src }) {
194
+ const real = assertOpenablePath(src);
195
+ const { cmd } = detectEditor();
196
+ if (cmd) {
197
+ (0, node_child_process_1.spawn)(cmd, [real], { detached: true, stdio: 'ignore' }).unref();
198
+ return { ok: true, editor: cmd };
199
+ }
200
+ // Windows は cmd を経由しない(shell:true や cmd /c start はパス中の & 等が解釈され得る)。
201
+ // explorer.exe は引数をそのままファイルパスとして扱うためメタ文字が無害。
202
+ const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'explorer' : 'xdg-open';
203
+ (0, node_child_process_1.spawn)(opener, [real], { detached: true, stdio: 'ignore' }).unref();
204
+ return { ok: true, editor: opener };
205
+ }
@@ -0,0 +1,444 @@
1
+ "use strict";
2
+ /* skill / command / agent / hook / plugin / project のファイルシステムスキャン */
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.HOME = void 0;
38
+ exports.parseFrontmatter = parseFrontmatter;
39
+ exports.listProjects = listProjects;
40
+ exports.scanSections = scanSections;
41
+ const fs = __importStar(require("node:fs"));
42
+ const os = __importStar(require("node:os"));
43
+ const path = __importStar(require("node:path"));
44
+ exports.HOME = os.homedir();
45
+ /* ---------- frontmatter parsing (minimal YAML: scalars + block scalars) ---------- */
46
+ function parseFrontmatter(raw) {
47
+ const meta = {};
48
+ let body = raw;
49
+ const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
50
+ if (m) {
51
+ body = raw.slice(m[0].length);
52
+ const lines = m[1].split(/\r?\n/);
53
+ let i = 0;
54
+ while (i < lines.length) {
55
+ const kv = lines[i].match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
56
+ if (!kv) {
57
+ i++;
58
+ continue;
59
+ }
60
+ const key = kv[1];
61
+ let value = kv[2].trim();
62
+ if (value === '|' || value === '>' || value === '|-' || value === '>-') {
63
+ const block = [];
64
+ i++;
65
+ while (i < lines.length && (lines[i].startsWith(' ') || lines[i].trim() === '')) {
66
+ block.push(lines[i].replace(/^ {2}/, ''));
67
+ i++;
68
+ }
69
+ value = block.join('\n').trim();
70
+ }
71
+ else {
72
+ value = value.replace(/^["']|["']$/g, '');
73
+ i++;
74
+ }
75
+ meta[key] = value;
76
+ }
77
+ }
78
+ return { meta, body };
79
+ }
80
+ function firstBodyLine(body) {
81
+ for (const line of body.split(/\r?\n/)) {
82
+ const t = line.replace(/^#+\s*/, '').trim();
83
+ if (t)
84
+ return t;
85
+ }
86
+ return '';
87
+ }
88
+ function fileMtime(fp) {
89
+ try {
90
+ return fs.statSync(fp).mtimeMs;
91
+ }
92
+ catch {
93
+ return 0;
94
+ }
95
+ }
96
+ /* skill ディレクトリ内のファイル一覧(相対パス、深さ3・40件まで) */
97
+ function listFiles(dir, prefix = '', depth = 0, acc = []) {
98
+ if (depth > 3 || acc.length >= 40)
99
+ return acc;
100
+ let entries;
101
+ try {
102
+ entries = fs.readdirSync(dir, { withFileTypes: true });
103
+ }
104
+ catch {
105
+ return acc;
106
+ }
107
+ for (const e of entries) {
108
+ if (e.name === '.DS_Store')
109
+ continue;
110
+ if (acc.length >= 40)
111
+ break;
112
+ if (e.isDirectory())
113
+ listFiles(path.join(dir, e.name), prefix + e.name + '/', depth + 1, acc);
114
+ else
115
+ acc.push(prefix + e.name);
116
+ }
117
+ return acc;
118
+ }
119
+ /* ---------- scanners ---------- */
120
+ function readSkillDir(dir, nameHint) {
121
+ const skillMd = path.join(dir, 'SKILL.md');
122
+ if (!fs.existsSync(skillMd))
123
+ return null;
124
+ let raw;
125
+ try {
126
+ raw = fs.readFileSync(skillMd, 'utf8');
127
+ }
128
+ catch {
129
+ return null; // 権限エラー等で読めない skill はスキップ(一覧全体を落とさない)
130
+ }
131
+ const { meta, body } = parseFrontmatter(raw);
132
+ return {
133
+ name: meta.name || nameHint,
134
+ description: meta.description || firstBodyLine(body),
135
+ argumentHint: meta['argument-hint'] || '',
136
+ version: meta.version || '',
137
+ kind: 'skill',
138
+ path: skillMd,
139
+ updatedAt: fileMtime(skillMd),
140
+ files: listFiles(dir).sort(),
141
+ _body: body, // 参照抽出用(scanSections で refs 化して破棄)
142
+ };
143
+ }
144
+ function scanSkillsRoot(root) {
145
+ if (!fs.existsSync(root))
146
+ return [];
147
+ const items = [];
148
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
149
+ if (!entry.isDirectory())
150
+ continue;
151
+ const item = readSkillDir(path.join(root, entry.name), entry.name);
152
+ if (item)
153
+ items.push(item);
154
+ }
155
+ return items.sort((a, b) => a.name.localeCompare(b.name));
156
+ }
157
+ /* commands / agents は単一 .md 形式(kind だけ違う) */
158
+ function scanMdRoot(root, kind) {
159
+ if (!fs.existsSync(root))
160
+ return [];
161
+ const items = [];
162
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
163
+ if (!entry.isFile() || !entry.name.endsWith('.md'))
164
+ continue;
165
+ const fp = path.join(root, entry.name);
166
+ let raw;
167
+ try {
168
+ raw = fs.readFileSync(fp, 'utf8');
169
+ }
170
+ catch {
171
+ continue; // 読めないファイルはスキップ(一覧全体を落とさない)
172
+ }
173
+ const { meta, body } = parseFrontmatter(raw);
174
+ items.push({
175
+ name: meta.name || entry.name.replace(/\.md$/, ''),
176
+ description: meta.description || firstBodyLine(body),
177
+ argumentHint: kind === 'command' ? meta['argument-hint'] || '' : '',
178
+ version: meta.version || '',
179
+ kind,
180
+ path: fp,
181
+ updatedAt: fileMtime(fp),
182
+ files: [entry.name],
183
+ _body: body,
184
+ });
185
+ }
186
+ return items.sort((a, b) => a.name.localeCompare(b.name));
187
+ }
188
+ /* settings.json / settings.local.json の hooks 設定(1 hook = 1 item、読み取り専用) */
189
+ function scanHooks(claudeDir) {
190
+ const items = [];
191
+ for (const file of ['settings.json', 'settings.local.json']) {
192
+ const fp = path.join(claudeDir, file);
193
+ if (!fs.existsSync(fp))
194
+ continue;
195
+ let cfg;
196
+ try {
197
+ cfg = JSON.parse(fs.readFileSync(fp, 'utf8'));
198
+ }
199
+ catch {
200
+ continue;
201
+ }
202
+ for (const [event, matchers] of Object.entries(cfg.hooks || {})) {
203
+ for (const m of Array.isArray(matchers) ? matchers : []) {
204
+ for (const h of m.hooks || []) {
205
+ items.push({
206
+ name: event + (m.matcher ? ` (${m.matcher})` : ''),
207
+ description: h.command || JSON.stringify(h),
208
+ argumentHint: '',
209
+ version: '',
210
+ kind: 'hook',
211
+ path: fp,
212
+ updatedAt: fileMtime(fp),
213
+ files: [],
214
+ });
215
+ }
216
+ }
217
+ }
218
+ }
219
+ return items.sort((a, b) => a.name.localeCompare(b.name));
220
+ }
221
+ function scanClaudeDir(root) {
222
+ const claudeDir = path.join(root, '.claude');
223
+ return [
224
+ ...scanSkillsRoot(path.join(claudeDir, 'skills')),
225
+ ...scanMdRoot(path.join(claudeDir, 'commands'), 'command'),
226
+ ...scanMdRoot(path.join(claudeDir, 'agents'), 'agent'),
227
+ ...scanHooks(claudeDir),
228
+ ];
229
+ }
230
+ function scanPlugins() {
231
+ const manifest = path.join(exports.HOME, '.claude', 'plugins', 'installed_plugins.json');
232
+ if (!fs.existsSync(manifest))
233
+ return [];
234
+ let installed;
235
+ try {
236
+ installed = JSON.parse(fs.readFileSync(manifest, 'utf8'));
237
+ }
238
+ catch {
239
+ return [];
240
+ }
241
+ const items = [];
242
+ for (const [pluginKey, entries] of Object.entries(installed.plugins || {})) {
243
+ const pluginName = pluginKey.split('@')[0];
244
+ for (const entry of entries || []) {
245
+ const installPath = entry.installPath;
246
+ if (!installPath || !fs.existsSync(installPath))
247
+ continue;
248
+ for (const skill of scanSkillsRoot(path.join(installPath, 'skills'))) {
249
+ items.push({
250
+ ...skill,
251
+ name: `${pluginName}:${skill.name}`,
252
+ version: skill.version || entry.version || '',
253
+ });
254
+ }
255
+ for (const cmd of scanMdRoot(path.join(installPath, 'commands'), 'command')) {
256
+ items.push({
257
+ ...cmd,
258
+ name: `${pluginName}:${cmd.name}`,
259
+ version: cmd.version || entry.version || '',
260
+ });
261
+ }
262
+ for (const ag of scanMdRoot(path.join(installPath, 'agents'), 'agent')) {
263
+ items.push({
264
+ ...ag,
265
+ name: `${pluginName}:${ag.name}`,
266
+ version: ag.version || entry.version || '',
267
+ });
268
+ }
269
+ }
270
+ }
271
+ return items.sort((a, b) => a.name.localeCompare(b.name));
272
+ }
273
+ /* projects Claude Code has been used in (registry: ~/.claude.json) + cwd */
274
+ function listProjects(cwd) {
275
+ let registered = [];
276
+ try {
277
+ const cfg = JSON.parse(fs.readFileSync(path.join(exports.HOME, '.claude.json'), 'utf8'));
278
+ registered = Object.keys(cfg.projects || {});
279
+ }
280
+ catch {
281
+ /* no registry — fall back to cwd only */
282
+ }
283
+ const set = new Set(registered.map((p) => path.resolve(p)));
284
+ set.add(path.resolve(cwd));
285
+ set.delete(path.resolve(exports.HOME)); // user-level .claude is its own section
286
+ return [...set].filter((p) => {
287
+ try {
288
+ return fs.statSync(p).isDirectory();
289
+ }
290
+ catch {
291
+ return false;
292
+ }
293
+ });
294
+ }
295
+ /* built-in skills live inside the Claude Code binary — not scannable, so a static list */
296
+ const BUILTIN_DEFS = [
297
+ [
298
+ 'review',
299
+ 'GitHub PR のレビュー(作業中の差分は /code-review)',
300
+ 'Review a GitHub PR (use /code-review for your working diff)',
301
+ ],
302
+ [
303
+ 'security-review',
304
+ '現在ブランチの変更のセキュリティレビュー',
305
+ 'Security review of the changes on the current branch',
306
+ ],
307
+ [
308
+ 'code-review',
309
+ 'ローカル差分/ブランチのコードレビュー(ultra で multi-agent クラウドレビュー)',
310
+ 'Code review of a local diff / branch (ultra runs a multi-agent cloud review)',
311
+ ],
312
+ [
313
+ 'simplify',
314
+ '変更コードの再利用・簡素化・効率の観点でのクリーンアップ',
315
+ 'Clean up changed code for reuse, simplification and efficiency',
316
+ ],
317
+ [
318
+ 'verify',
319
+ '変更が実際に意図通り動くかをアプリを動かして検証',
320
+ 'Verify a change actually works by exercising the app',
321
+ ],
322
+ [
323
+ 'run',
324
+ 'プロジェクトのアプリを起動して変更を確認',
325
+ "Launch the project's app to see a change working",
326
+ ],
327
+ ['init', 'CLAUDE.md の新規作成', 'Initialize a new CLAUDE.md'],
328
+ [
329
+ 'loop',
330
+ 'プロンプト/コマンドの定期実行(常駐)',
331
+ 'Run a prompt or command on a recurring interval',
332
+ ],
333
+ [
334
+ 'schedule',
335
+ 'cron スケジュールのクラウドエージェント(routine)管理',
336
+ 'Manage scheduled cloud agents (routines) on a cron schedule',
337
+ ],
338
+ [
339
+ 'deep-research',
340
+ 'Web 多源リサーチ + 検証 + 引用付きレポート',
341
+ 'Multi-source web research with verification and a cited report',
342
+ ],
343
+ ['claude-api', 'Claude API / Anthropic SDK リファレンス', 'Claude API / Anthropic SDK reference'],
344
+ [
345
+ 'update-config',
346
+ 'settings.json / permissions / hooks の設定変更',
347
+ 'Configure settings.json / permissions / hooks',
348
+ ],
349
+ ['keybindings-help', 'キーボードショートカットのカスタマイズ', 'Customize keyboard shortcuts'],
350
+ [
351
+ 'fewer-permission-prompts',
352
+ '許可プロンプト削減のための allowlist 追加',
353
+ 'Add an allowlist to reduce permission prompts',
354
+ ],
355
+ ];
356
+ /* path='' は実ファイルなし(Claude Code 本体同梱)を表す。表示文言はクライアント側で解決 */
357
+ function builtinItems(lang) {
358
+ return BUILTIN_DEFS.map(([name, ja, en]) => ({
359
+ name,
360
+ description: lang === 'ja' ? ja : en,
361
+ argumentHint: '',
362
+ version: '',
363
+ kind: 'skill',
364
+ path: '',
365
+ files: [],
366
+ }));
367
+ }
368
+ /*
369
+ * SKILL.md 本文中の /skill名 を既知の skill 名と突き合わせて参照候補を抽出。
370
+ * - 照合スコープ: project の skill は「同一プロジェクト + user/plugin/built-in」のみ。
371
+ * 他プロジェクトの skill 名は候補にしない(別プロジェクトの同名語句への誤マッチ防止)。
372
+ * - スラッシュコマンドの形( `/x` が単語やパスの一部でない)だけをマッチ。
373
+ * 例: 「reuse/quality」「.claude/skills/foo」「/path/to/x」は対象外。
374
+ */
375
+ const SLASH_CMD_RE = /(?<![\w/.@-])\/([a-z0-9][a-z0-9:_-]*)(?![\w/-])/g;
376
+ function namesOf(sections) {
377
+ const set = new Set();
378
+ for (const s of sections) {
379
+ for (const it of s.items) {
380
+ set.add(it.name);
381
+ const short = it.name.split(':').pop();
382
+ if (short)
383
+ set.add(short);
384
+ }
385
+ }
386
+ return set;
387
+ }
388
+ function attachRefs(sections) {
389
+ const globalNames = namesOf(sections.filter((s) => s.source !== 'project'));
390
+ for (const s of sections) {
391
+ const known = s.source === 'project' ? new Set([...namesOf([s]), ...globalNames]) : globalNames;
392
+ for (const it of s.items) {
393
+ const refs = new Set();
394
+ for (const m of (it._body || '').matchAll(SLASH_CMD_RE)) {
395
+ const cand = m[1];
396
+ if (known.has(cand) && cand !== it.name && cand !== it.name.split(':').pop())
397
+ refs.add(cand);
398
+ }
399
+ it.refs = [...refs];
400
+ delete it._body;
401
+ }
402
+ }
403
+ }
404
+ /* 並び順: current プロジェクト → 他プロジェクト → user → plugin → built-in */
405
+ function scanSections(cwd, lang = 'en') {
406
+ const cwdResolved = path.resolve(cwd);
407
+ const projects = listProjects(cwd)
408
+ .map((p) => ({ path: p, items: scanClaudeDir(p), current: p === cwdResolved }))
409
+ .filter((p) => p.items.length > 0)
410
+ .sort((a, b) => Number(b.current) - Number(a.current) ||
411
+ path.basename(a.path).localeCompare(path.basename(b.path)));
412
+ const sections = [
413
+ ...projects.map((p, i) => ({
414
+ id: 'proj-' + i,
415
+ source: 'project',
416
+ projectName: path.basename(p.path),
417
+ isCurrent: p.current,
418
+ note: p.path,
419
+ manage: true,
420
+ items: p.items,
421
+ })),
422
+ {
423
+ id: 'user',
424
+ source: 'user',
425
+ manage: true,
426
+ note: path.join(exports.HOME, '.claude'),
427
+ items: scanClaudeDir(exports.HOME),
428
+ },
429
+ {
430
+ id: 'plugin',
431
+ source: 'plugin',
432
+ note: path.join(exports.HOME, '.claude', 'plugins'),
433
+ items: scanPlugins(),
434
+ },
435
+ {
436
+ id: 'builtin',
437
+ source: 'built-in',
438
+ note: '',
439
+ items: builtinItems(lang),
440
+ },
441
+ ];
442
+ attachRefs(sections);
443
+ return sections;
444
+ }