skills-viewer 0.4.0 → 0.6.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.
package/README.md CHANGED
@@ -17,9 +17,14 @@ npx skills-viewer
17
17
  ## Features
18
18
 
19
19
  - **All scopes in one view** — user (`~/.claude/skills`), every project's `.claude/skills` / `.claude/commands`, installed plugins, and Claude Code built-ins, grouped by source
20
+ - **Purpose grouping (AI)** — one haiku call classifies everything installed by _when you use it_ into 4–8 groups generated for your environment (planning / building / review / release / … as a role-agnostic guide — a designer's or PM's skills get their own groups). Switch the list between by-source, by-purpose and flat views; a frontmatter `category:` pins an item to a manual group that takes precedence
20
21
  - **Search / sort** — incremental search over name + description + usage; sort by name, usage count, last used, updated date, or token cost
21
22
  - **Diagnostics** — an _unused_ badge (no recorded use within the transcript retention window) with an all / used / unused filter, plus static description lint: missing / too-short / too-long descriptions, missing trigger conditions ("use when …") that make auto-invocation unlikely, and name-echo descriptions
22
23
  - **Token cost** — since every name + description is injected into each session, the estimated token overhead is shown per item, per scope, and as a per-session total for the current project
24
+ - **AI trigger diagnosis** — one click asks the model whether the description is likely to trigger auto-invocation, lists concrete issues, and proposes an improved description you can apply with one click (cached by content hash)
25
+ - **AI flow diagram** — extract the processing flow of orchestration-style skills (steps, branches, delegations, human gates) from the definition body and render it as a step diagram; delegated skills are clickable
26
+ - **AI model choice** — pick the model behind all AI features (haiku default / sonnet / opus) in settings; aliases are resolved by your claude CLI
27
+ - **Edit in the browser** — inline editor for SKILL.md / commands / agents (project & user scopes) with mtime conflict detection and a one-generation backup in `~/.cache/skills-viewer/backups/`
23
28
  - **What's changed** — a banner shows items added / updated / removed since your last launch (baseline advances only when you dismiss it); the CLI prints a one-line summary at startup too
24
29
  - **Usage sparkline** — the detail pane charts the last 30 days of per-day usage
25
30
  - **Usage stats** — invocation counts and last-used dates aggregated from Claude Code session transcripts (`~/.claude/projects/`), covering both user-typed `/skill` calls and model-invoked Skill tool calls
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ /*
3
+ * AI 発動診断: description が「モデルの自動発動判断」に足るかを haiku で診断し、
4
+ * 指摘と改善版 description を返す。summary.ts と同じく content hash + lang で
5
+ * ~/.cache/skills-viewer/diagnoses.json にキャッシュし、内容が変わらない限り再生成しない。
6
+ */
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.loadDiagnoses = loadDiagnoses;
42
+ exports.parseDiagnosis = parseDiagnosis;
43
+ exports.diagnoseOne = diagnoseOne;
44
+ exports.attachDiagnoses = attachDiagnoses;
45
+ const fs = __importStar(require("node:fs"));
46
+ const os = __importStar(require("node:os"));
47
+ const path = __importStar(require("node:path"));
48
+ const summary_1 = require("./summary");
49
+ const DIAG_FILE = path.join(os.homedir(), '.cache', 'skills-viewer', 'diagnoses.json');
50
+ function loadDiagnoses() {
51
+ try {
52
+ return JSON.parse(fs.readFileSync(DIAG_FILE, 'utf8'));
53
+ }
54
+ catch {
55
+ return {};
56
+ }
57
+ }
58
+ function saveDiagnoses(store) {
59
+ fs.mkdirSync(path.dirname(DIAG_FILE), { recursive: true });
60
+ fs.writeFileSync(DIAG_FILE, JSON.stringify(store, null, 1));
61
+ }
62
+ function buildPrompt(name, content, lang) {
63
+ if (lang === 'ja') {
64
+ return ('以下は Claude Code の skill 定義ファイルです。モデルは frontmatter の name と description **だけ**を見て、' +
65
+ 'このスキルを自動発動するか判断します(本文は発動後にしか読まれません)。\n' +
66
+ 'description の「発動性」を診断し、次の JSON だけを出力してください(前置き・コードフェンス不要):\n' +
67
+ '{"verdict": "good" | "weak",\n' +
68
+ ' "issues": ["問題点(日本語で各25字程度、最大4件。無ければ空配列)"],\n' +
69
+ ' "improved": "改善版 description(日本語)。本文から読み取れる発動条件(〜するとき、〜と頼まれたとき等)を必ず含め、200字以内"}\n\n' +
70
+ 'verdict は、description だけでモデルが適切な場面で発動を判断できるなら good、曖昧・条件不足なら weak。\n' +
71
+ 'improved は verdict が good でも、より良くできるなら改善版を出すこと(現状のままで良ければ元の文をそのまま)。\n\n' +
72
+ '# skill: ' +
73
+ name +
74
+ '\n\n' +
75
+ content);
76
+ }
77
+ return ('Below is a Claude Code skill definition file. The model decides whether to auto-invoke this skill ' +
78
+ 'based **only** on the frontmatter name and description (the body is read only after invocation).\n' +
79
+ 'Diagnose how well the description triggers invocation, and output ONLY this JSON (no preamble, no code fences):\n' +
80
+ '{"verdict": "good" | "weak",\n' +
81
+ ' "issues": ["problem (about 10 words each, max 4; empty array if none)"],\n' +
82
+ ' "improved": "improved description in English; MUST include trigger conditions readable from the body (\\"use when …\\"), max 400 chars"}\n\n' +
83
+ 'verdict: good if the description alone lets the model invoke at the right moments; weak if vague or missing conditions.\n' +
84
+ 'improved: even when verdict is good, propose a better version if possible (return the original text if it is already ideal).\n\n' +
85
+ '# skill: ' +
86
+ name +
87
+ '\n\n' +
88
+ content);
89
+ }
90
+ /* haiku の出力を検証つきでパース(壊れた出力は reject して UI にエラー表示) */
91
+ function parseDiagnosis(text) {
92
+ const stripped = text
93
+ .replace(/^```(?:json)?\s*/i, '')
94
+ .replace(/```\s*$/, '')
95
+ .trim();
96
+ const j = JSON.parse(stripped);
97
+ const verdict = j.verdict === 'good' ? 'good' : 'weak';
98
+ const issues = Array.isArray(j.issues)
99
+ ? j.issues.filter((x) => typeof x === 'string').map((s) => s.slice(0, 80))
100
+ : [];
101
+ const improved = String(j.improved || '').trim();
102
+ if (!improved)
103
+ throw new Error('empty improved description');
104
+ return { verdict, issues: issues.slice(0, 4), improved };
105
+ }
106
+ async function diagnoseOne(realPath, name, lang, model = 'haiku') {
107
+ const hash = (0, summary_1.contentHash)(realPath);
108
+ const store = loadDiagnoses();
109
+ const cached = store[realPath];
110
+ if (cached && cached.hash === hash && cached.lang === lang) {
111
+ return { verdict: cached.verdict, issues: cached.issues, improved: cached.improved };
112
+ }
113
+ const content = fs.readFileSync(realPath, 'utf8').slice(0, 12000);
114
+ const result = parseDiagnosis(await (0, summary_1.runClaude)(buildPrompt(name, content, lang), model));
115
+ store[realPath] = { ...result, hash, lang, model, generatedAt: new Date().toISOString() };
116
+ saveDiagnoses(store);
117
+ return result;
118
+ }
119
+ /* スキャン結果にキャッシュ済み診断を付与(内容が変わっていれば付けない) */
120
+ function attachDiagnoses(sections, lang) {
121
+ const store = loadDiagnoses();
122
+ for (const s of sections) {
123
+ for (const it of s.items) {
124
+ const cached = store[it.path];
125
+ if (cached &&
126
+ cached.lang === lang &&
127
+ it.path &&
128
+ fs.existsSync(it.path) &&
129
+ cached.hash === (0, summary_1.contentHash)(it.path)) {
130
+ it.aiDiagnosis = {
131
+ verdict: cached.verdict,
132
+ issues: cached.issues,
133
+ improved: cached.improved,
134
+ };
135
+ }
136
+ }
137
+ }
138
+ }
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ /*
3
+ * ブラウザ内編集の実体: SKILL.md / command / agent の保存と description 差し替え。
4
+ * 本ツール初の「ファイル内容の書き換え」なので、
5
+ * - パス検証は manage.ts の assertManagedPath(.claude/skills|commands|agents 限定、plugin 拒否)に乗せる
6
+ * - mtime による競合検出(エディタ等での外部変更を上書きしない)
7
+ * - 保存前に ~/.cache/skills-viewer/backups/ へ1世代バックアップ(trash と同じ思想)
8
+ * を必ず通す。
9
+ */
10
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ var desc = Object.getOwnPropertyDescriptor(m, k);
13
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
14
+ desc = { enumerable: true, get: function() { return m[k]; } };
15
+ }
16
+ Object.defineProperty(o, k2, desc);
17
+ }) : (function(o, m, k, k2) {
18
+ if (k2 === undefined) k2 = k;
19
+ o[k2] = m[k];
20
+ }));
21
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
22
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
23
+ }) : function(o, v) {
24
+ o["default"] = v;
25
+ });
26
+ var __importStar = (this && this.__importStar) || (function () {
27
+ var ownKeys = function(o) {
28
+ ownKeys = Object.getOwnPropertyNames || function (o) {
29
+ var ar = [];
30
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
31
+ return ar;
32
+ };
33
+ return ownKeys(o);
34
+ };
35
+ return function (mod) {
36
+ if (mod && mod.__esModule) return mod;
37
+ var result = {};
38
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
39
+ __setModuleDefault(result, mod);
40
+ return result;
41
+ };
42
+ })();
43
+ Object.defineProperty(exports, "__esModule", { value: true });
44
+ exports.assertEditableMd = assertEditableMd;
45
+ exports.doSave = doSave;
46
+ exports.replaceDescription = replaceDescription;
47
+ exports.doApplyDescription = doApplyDescription;
48
+ const fs = __importStar(require("node:fs"));
49
+ const os = __importStar(require("node:os"));
50
+ const path = __importStar(require("node:path"));
51
+ const manage_1 = require("./manage");
52
+ const errors_1 = require("./errors");
53
+ /* テストから隔離できるよう env で差し替え可能にする(通常運用では未設定)。
54
+ モジュール読み込み後に env を設定するテストのため、参照は呼び出し時に行う */
55
+ const backupDir = () => process.env.SKILLS_VIEWER_BACKUP_DIR ||
56
+ path.join(os.homedir(), '.cache', 'skills-viewer', 'backups');
57
+ const MAX_CONTENT = 512 * 1024; // request body 上限(1MB)より小さく、md として十分
58
+ function assertEditableMd(p) {
59
+ const { real } = (0, manage_1.assertManagedPath)(p);
60
+ if (!real.endsWith('.md'))
61
+ throw new errors_1.ApiError('not-md', real);
62
+ return real;
63
+ }
64
+ /* 上書き前の内容を1世代だけ退避(ファイルごとに1枠、保存のたびに更新) */
65
+ function backupOnce(real, content) {
66
+ try {
67
+ const dir = backupDir();
68
+ fs.mkdirSync(dir, { recursive: true });
69
+ fs.writeFileSync(path.join(dir, real.replace(/[^a-zA-Z0-9.]/g, '-')), content);
70
+ }
71
+ catch {
72
+ /* バックアップ失敗で保存自体は止めない */
73
+ }
74
+ }
75
+ /* mtime 競合チェック → バックアップ → 書き込み、の共通経路 */
76
+ function writeGuarded(real, next, baseMtime) {
77
+ const st = fs.statSync(real);
78
+ if (typeof baseMtime === 'number' && st.mtimeMs !== baseMtime) {
79
+ throw new errors_1.ApiError('edit-conflict', real);
80
+ }
81
+ backupOnce(real, fs.readFileSync(real, 'utf8'));
82
+ fs.writeFileSync(real, next);
83
+ return { mtime: fs.statSync(real).mtimeMs };
84
+ }
85
+ function doSave(data) {
86
+ const real = assertEditableMd(data.src);
87
+ const content = data.content;
88
+ if (typeof content !== 'string' || !content.trim())
89
+ throw new errors_1.ApiError('empty-content');
90
+ if (content.length > MAX_CONTENT)
91
+ throw new errors_1.ApiError('content-too-large');
92
+ const baseMtime = typeof data.baseMtime === 'number' ? data.baseMtime : undefined;
93
+ return { ok: true, ...writeGuarded(real, content, baseMtime) };
94
+ }
95
+ /*
96
+ * frontmatter の description だけを差し替える(AI 改善案のワンクリック適用用)。
97
+ * - 既存の description 行(block scalar 含む)を置き換え
98
+ * - description キーが無ければ frontmatter 先頭(name の次)に挿入
99
+ * - frontmatter 自体が無ければ新設
100
+ * 値は常に YAML double-quote(JSON.stringify)で書き、改行は空白に潰す。
101
+ */
102
+ function replaceDescription(raw, desc) {
103
+ const value = 'description: ' + JSON.stringify(desc.replace(/\s*\n\s*/g, ' ').trim());
104
+ const fm = raw.match(/^---\r?\n([\s\S]*?)\r?\n---(\r?\n?)/);
105
+ if (!fm)
106
+ return `---\n${value}\n---\n\n` + raw;
107
+ const lines = fm[1].split(/\r?\n/);
108
+ const out = [];
109
+ let replaced = false;
110
+ let i = 0;
111
+ while (i < lines.length) {
112
+ const m = lines[i].match(/^description:\s*(.*)$/);
113
+ if (!m || replaced) {
114
+ out.push(lines[i]);
115
+ i++;
116
+ continue;
117
+ }
118
+ out.push(value);
119
+ replaced = true;
120
+ i++;
121
+ if (['|', '>', '|-', '>-'].includes(m[1].trim())) {
122
+ // block scalar の継続行(インデント行・空行)を捨てる
123
+ while (i < lines.length && (lines[i].startsWith(' ') || lines[i].trim() === ''))
124
+ i++;
125
+ }
126
+ }
127
+ if (!replaced) {
128
+ // name の直後、無ければ先頭に挿入
129
+ const nameIdx = out.findIndex((l) => /^name:/.test(l));
130
+ out.splice(nameIdx >= 0 ? nameIdx + 1 : 0, 0, value);
131
+ }
132
+ return '---\n' + out.join('\n') + '\n---' + fm[2] + raw.slice(fm[0].length);
133
+ }
134
+ function doApplyDescription(data) {
135
+ const real = assertEditableMd(data.src);
136
+ if (typeof data.description !== 'string' || !data.description.trim()) {
137
+ throw new errors_1.ApiError('empty-content');
138
+ }
139
+ const prev = fs.readFileSync(real, 'utf8');
140
+ const baseMtime = typeof data.baseMtime === 'number' ? data.baseMtime : undefined;
141
+ return {
142
+ ok: true,
143
+ ...writeGuarded(real, replaceDescription(prev, data.description), baseMtime),
144
+ };
145
+ }
@@ -0,0 +1,157 @@
1
+ "use strict";
2
+ /*
3
+ * AI フロー図解: SKILL.md からオーケストレーションの処理フローを抽出する
4
+ * (docs/plans/09 参照)。diagnose.ts と同じオンデマンド + content hash + lang キャッシュ。
5
+ * スキーマは LLM が壊しにくい「直列 steps + 分岐注記」に制約し、任意の DAG は扱わない。
6
+ */
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
19
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
20
+ }) : function(o, v) {
21
+ o["default"] = v;
22
+ });
23
+ var __importStar = (this && this.__importStar) || (function () {
24
+ var ownKeys = function(o) {
25
+ ownKeys = Object.getOwnPropertyNames || function (o) {
26
+ var ar = [];
27
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
28
+ return ar;
29
+ };
30
+ return ownKeys(o);
31
+ };
32
+ return function (mod) {
33
+ if (mod && mod.__esModule) return mod;
34
+ var result = {};
35
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
36
+ __setModuleDefault(result, mod);
37
+ return result;
38
+ };
39
+ })();
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.loadFlows = loadFlows;
42
+ exports.parseFlow = parseFlow;
43
+ exports.flowOne = flowOne;
44
+ exports.attachFlows = attachFlows;
45
+ const fs = __importStar(require("node:fs"));
46
+ const os = __importStar(require("node:os"));
47
+ const path = __importStar(require("node:path"));
48
+ const summary_1 = require("./summary");
49
+ const FLOW_FILE = path.join(os.homedir(), '.cache', 'skills-viewer', 'flows.json');
50
+ function loadFlows() {
51
+ try {
52
+ return JSON.parse(fs.readFileSync(FLOW_FILE, 'utf8'));
53
+ }
54
+ catch {
55
+ return {};
56
+ }
57
+ }
58
+ function saveFlows(store) {
59
+ fs.mkdirSync(path.dirname(FLOW_FILE), { recursive: true });
60
+ fs.writeFileSync(FLOW_FILE, JSON.stringify(store, null, 1));
61
+ }
62
+ function buildPrompt(name, content, lang) {
63
+ if (lang === 'ja') {
64
+ return ('以下は Claude Code の skill 定義です。この skill が実行する処理フローを図解用に抽出し、次の JSON だけを出力してください(前置き・コードフェンス不要):\n' +
65
+ '{"steps": [{"title": "ステップ名(10字程度)", "detail": "何をするか(25字程度)",\n' +
66
+ ' "calls": ["このステップで起動/委譲する他の skill・コマンド名"],\n' +
67
+ ' "gate": "human" | "auto" | null,\n' +
68
+ ' "branches": [{"when": "分岐条件(15字程度)", "then": "その場合の挙動(20字程度)"}]}]}\n\n' +
69
+ '制約:\n' +
70
+ '- steps は実行順に 4〜8 個(単純な skill なら少なくてよい)\n' +
71
+ '- gate は人間の確認/承認を待つステップだけ "human"(自動で進むなら "auto"、該当なしは null)\n' +
72
+ '- calls は本文に実際に登場する名前のみ(幻覚禁止)\n' +
73
+ '- branches は中断・フォールバック等の分岐だけ(無ければ省略)\n\n' +
74
+ '# skill: ' +
75
+ name +
76
+ '\n\n' +
77
+ content);
78
+ }
79
+ return ('Below is a Claude Code skill definition. Extract the processing flow this skill executes, for a diagram, and output ONLY this JSON (no preamble, no code fences):\n' +
80
+ '{"steps": [{"title": "step name (2-4 words)", "detail": "what it does (about 10 words)",\n' +
81
+ ' "calls": ["other skill/command names this step invokes or delegates to"],\n' +
82
+ ' "gate": "human" | "auto" | null,\n' +
83
+ ' "branches": [{"when": "branch condition (about 5 words)", "then": "behavior in that case (about 7 words)"}]}]}\n\n' +
84
+ 'Constraints:\n' +
85
+ '- 4 to 8 steps in execution order (fewer is fine for simple skills)\n' +
86
+ '- gate is "human" ONLY for steps that wait for human confirmation/approval ("auto" if it proceeds automatically, null otherwise)\n' +
87
+ '- calls may contain only names that actually appear in the body (no hallucination)\n' +
88
+ '- branches only for aborts / fallbacks / real forks (omit when none)\n\n' +
89
+ '# skill: ' +
90
+ name +
91
+ '\n\n' +
92
+ content);
93
+ }
94
+ /* haiku/sonnet の出力を検証つきでパース(壊れた出力は throw して UI にエラー表示) */
95
+ function parseFlow(text) {
96
+ const stripped = text
97
+ .replace(/^```(?:json)?\s*/i, '')
98
+ .replace(/```\s*$/, '')
99
+ .trim();
100
+ const j = JSON.parse(stripped);
101
+ const steps = [];
102
+ for (const s of Array.isArray(j.steps) ? j.steps : []) {
103
+ const title = String(s?.title || '').trim();
104
+ if (!title)
105
+ continue;
106
+ steps.push({
107
+ title: title.slice(0, 60),
108
+ detail: String(s?.detail || '').slice(0, 120),
109
+ calls: (Array.isArray(s?.calls) ? s.calls : [])
110
+ .filter((c) => typeof c === 'string' && c)
111
+ .map((c) => c.slice(0, 60))
112
+ .slice(0, 6),
113
+ gate: s?.gate === 'human' || s?.gate === 'auto' ? s.gate : null,
114
+ branches: (Array.isArray(s?.branches) ? s.branches : [])
115
+ .filter((b) => b && typeof b.when === 'string')
116
+ .map((b) => ({
117
+ when: String(b.when).slice(0, 60),
118
+ then: String(b.then || '').slice(0, 80),
119
+ }))
120
+ .slice(0, 4),
121
+ });
122
+ if (steps.length >= 12)
123
+ break;
124
+ }
125
+ if (!steps.length)
126
+ throw new Error('no steps in output');
127
+ return { steps };
128
+ }
129
+ async function flowOne(realPath, name, lang, model = 'haiku') {
130
+ const hash = (0, summary_1.contentHash)(realPath);
131
+ const store = loadFlows();
132
+ const cached = store[realPath];
133
+ if (cached && cached.hash === hash && cached.lang === lang) {
134
+ return { steps: cached.steps };
135
+ }
136
+ const content = fs.readFileSync(realPath, 'utf8').slice(0, 12000);
137
+ const result = parseFlow(await (0, summary_1.runClaude)(buildPrompt(name, content, lang), model));
138
+ store[realPath] = { ...result, hash, lang, model, generatedAt: new Date().toISOString() };
139
+ saveFlows(store);
140
+ return result;
141
+ }
142
+ /* スキャン結果にキャッシュ済みフローを付与(内容が変わっていれば付けない) */
143
+ function attachFlows(sections, lang) {
144
+ const store = loadFlows();
145
+ for (const s of sections) {
146
+ for (const it of s.items) {
147
+ const cached = store[it.path];
148
+ if (cached &&
149
+ cached.lang === lang &&
150
+ it.path &&
151
+ fs.existsSync(it.path) &&
152
+ cached.hash === (0, summary_1.contentHash)(it.path)) {
153
+ it.aiFlow = { steps: cached.steps };
154
+ }
155
+ }
156
+ }
157
+ }
@@ -0,0 +1,202 @@
1
+ "use strict";
2
+ /*
3
+ * AI グルーピング: 環境内の全アイテム(name + description)を 1 回の haiku 呼び出しに渡し、
4
+ * 「用途グループの集合 + 各アイテムの割当」をまとめて生成する(docs/plans/08 参照)。
5
+ * グループ名はプロダクトに焼き込まず環境ごとに生成し、職種非依存の工程軸は
6
+ * 粒度ガイドとしてだけプロンプトに渡す。frontmatter に category を持つアイテムは
7
+ * 手動指定として AI 分類の対象外(クライアント側で category がそのままグループになる)。
8
+ * キャッシュは summary.ts と同思想だが、環境単位で言語ごとに 1 エントリ
9
+ * (全対象アイテムの name + description の hash)。アイテムの増減・description 変更で
10
+ * stale になるが、自動再生成はせず UI から手動で再分類する。
11
+ */
12
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ var desc = Object.getOwnPropertyDescriptor(m, k);
15
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
16
+ desc = { enumerable: true, get: function() { return m[k]; } };
17
+ }
18
+ Object.defineProperty(o, k2, desc);
19
+ }) : (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ o[k2] = m[k];
22
+ }));
23
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
24
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
25
+ }) : function(o, v) {
26
+ o["default"] = v;
27
+ });
28
+ var __importStar = (this && this.__importStar) || (function () {
29
+ var ownKeys = function(o) {
30
+ ownKeys = Object.getOwnPropertyNames || function (o) {
31
+ var ar = [];
32
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
33
+ return ar;
34
+ };
35
+ return ownKeys(o);
36
+ };
37
+ return function (mod) {
38
+ if (mod && mod.__esModule) return mod;
39
+ var result = {};
40
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
41
+ __setModuleDefault(result, mod);
42
+ return result;
43
+ };
44
+ })();
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.groupTargets = groupTargets;
47
+ exports.groupsHash = groupsHash;
48
+ exports.parseGroups = parseGroups;
49
+ exports.generateGroups = generateGroups;
50
+ exports.attachGroups = attachGroups;
51
+ const fs = __importStar(require("node:fs"));
52
+ const os = __importStar(require("node:os"));
53
+ const path = __importStar(require("node:path"));
54
+ const crypto = __importStar(require("node:crypto"));
55
+ const summary_1 = require("./summary");
56
+ const GROUPS_FILE = path.join(os.homedir(), '.cache', 'skills-viewer', 'groups.json');
57
+ function loadStore() {
58
+ try {
59
+ return JSON.parse(fs.readFileSync(GROUPS_FILE, 'utf8'));
60
+ }
61
+ catch {
62
+ return {};
63
+ }
64
+ }
65
+ function saveStore(store) {
66
+ fs.mkdirSync(path.dirname(GROUPS_FILE), { recursive: true });
67
+ fs.writeFileSync(GROUPS_FILE, JSON.stringify(store, null, 1));
68
+ }
69
+ /*
70
+ * 分類対象 = hook 以外の全アイテム(built-in 含む)を name で重複排除したもの。
71
+ * 同名アイテム(user と project の code-review 等)は同じグループに落とす。
72
+ * category 持ちは手動指定なので対象外。description は 200 字で切る(分類には十分)。
73
+ */
74
+ function groupTargets(sections) {
75
+ const seen = new Map();
76
+ for (const s of sections) {
77
+ for (const it of s.items) {
78
+ if (it.kind === 'hook' || it.category || seen.has(it.name))
79
+ continue;
80
+ seen.set(it.name, { name: it.name, description: it.description.slice(0, 200) });
81
+ }
82
+ }
83
+ return [...seen.values()].sort((a, b) => a.name.localeCompare(b.name));
84
+ }
85
+ function groupsHash(targets) {
86
+ const src = targets.map((t) => t.name + '\t' + t.description).join('\n');
87
+ return crypto.createHash('sha256').update(src).digest('hex').slice(0, 16);
88
+ }
89
+ /* 職種非依存の工程軸(粒度ガイド)。グループ名の直接指定ではない */
90
+ const AXIS_JA = '企画・要件 / 制作・実装 / レビュー・検証 / リリース・共有 / 調査・分析 / 記録・運用';
91
+ const AXIS_EN = 'planning & requirements / building & creating / review & verification / release & sharing / research & analysis / records & operations';
92
+ function buildPrompt(targets, lang) {
93
+ const list = targets.map((t) => t.name + ': ' + t.description).join('\n');
94
+ if (lang === 'ja') {
95
+ return ('以下は Claude Code にインストールされた skill / command / agent の一覧です(1行 = 「name: description」)。\n' +
96
+ 'これらを「いつ・何をするときに使うか」の観点でグループ分けし、次の JSON だけを出力してください(前置き・コードフェンス不要):\n' +
97
+ '{"groups": [{"id": "英小文字とハイフンのスラッグ(言語非依存)", "label": "グループ名(日本語で10字程度)", "emoji": "グループを表す絵文字1つ"}],\n' +
98
+ ' "assign": {"<name>": "<groupId>"}}\n\n' +
99
+ '制約:\n' +
100
+ '- グループ数は 4〜8。粒度の目安は職種を問わない工程軸「' +
101
+ AXIS_JA +
102
+ '」。ただしグループ名はこの一覧の実態に合わせること(例に無い分野があればそのグループを作ってよい)\n' +
103
+ '- assign には一覧の全 name を必ず 1 回ずつ含める。迷う場合も最も近いグループに割り当てる\n' +
104
+ '- assign の値は groups で定義した id のみ使用する\n\n' +
105
+ '# 一覧\n' +
106
+ list);
107
+ }
108
+ return ('Below is a list of skills / commands / agents installed for Claude Code (one per line, "name: description").\n' +
109
+ 'Group them by WHEN and FOR WHAT they are used, and output ONLY this JSON (no preamble, no code fences):\n' +
110
+ '{"groups": [{"id": "lowercase-hyphen slug (language-neutral)", "label": "group name in English (2-4 words)", "emoji": "one emoji for the group"}],\n' +
111
+ ' "assign": {"<name>": "<groupId>"}}\n\n' +
112
+ 'Constraints:\n' +
113
+ '- 4 to 8 groups. Use this role-agnostic workflow axis as a granularity guide: ' +
114
+ AXIS_EN +
115
+ '. Name the groups after what is actually in the list (create different groups if the list calls for them).\n' +
116
+ '- assign MUST contain every name from the list exactly once; when unsure, pick the closest group.\n' +
117
+ '- assign values must be ids defined in groups.\n\n' +
118
+ '# List\n' +
119
+ list);
120
+ }
121
+ /* id を言語非依存スラッグに正規化(空になったら null) */
122
+ function slugify(v) {
123
+ const s = String(v ?? '')
124
+ .toLowerCase()
125
+ .replace(/[^a-z0-9]+/g, '-')
126
+ .replace(/^-+|-+$/g, '');
127
+ return s || null;
128
+ }
129
+ /*
130
+ * haiku の出力を検証つきでパース。
131
+ * - groups: id をスラッグ正規化・重複排除し、最大 12 件
132
+ * - assign: 一覧に無い name(幻覚)と未定義グループへの割当は捨てる(→「その他」扱い)
133
+ * groups が 1 件も取れない出力はエラー(UI にエラー表示)
134
+ */
135
+ function parseGroups(text, names) {
136
+ const stripped = text
137
+ .replace(/^```(?:json)?\s*/i, '')
138
+ .replace(/```\s*$/, '')
139
+ .trim();
140
+ const j = JSON.parse(stripped);
141
+ const groups = [];
142
+ const ids = new Set();
143
+ for (const g of Array.isArray(j.groups) ? j.groups : []) {
144
+ const id = slugify(g?.id);
145
+ const label = String(g?.label || '').trim();
146
+ if (!id || !label || ids.has(id))
147
+ continue;
148
+ ids.add(id);
149
+ const emoji = String(g?.emoji || '').trim();
150
+ groups.push({ id, label: label.slice(0, 40), ...(emoji ? { emoji: emoji.slice(0, 8) } : {}) });
151
+ if (groups.length >= 12)
152
+ break;
153
+ }
154
+ if (!groups.length)
155
+ throw new Error('no groups in output');
156
+ const nameSet = new Set(names);
157
+ const assign = {};
158
+ for (const [name, gid] of Object.entries(j.assign || {})) {
159
+ const id = slugify(gid);
160
+ if (nameSet.has(name) && id && ids.has(id))
161
+ assign[name] = id;
162
+ }
163
+ return { groups, assign };
164
+ }
165
+ /*
166
+ * 環境全体を 1 回の claude 呼び出しで分類してキャッシュに保存する。
167
+ * 入力が大きく(全アイテム一覧)haiku でも 2 分近くかかるため、タイムアウトは
168
+ * 単体要約(120s)より長い 10 分にする(sonnet / opus はさらに遅い)
169
+ */
170
+ async function generateGroups(sections, lang, model = 'haiku') {
171
+ const targets = groupTargets(sections);
172
+ const result = parseGroups(await (0, summary_1.runClaude)(buildPrompt(targets, lang), model, 600000), targets.map((t) => t.name));
173
+ const store = loadStore();
174
+ store[lang] = {
175
+ ...result,
176
+ hash: groupsHash(targets),
177
+ model,
178
+ generatedAt: new Date().toISOString(),
179
+ };
180
+ saveStore(store);
181
+ return result;
182
+ }
183
+ /*
184
+ * スキャン結果にキャッシュ済みの割当を付与する。
185
+ * stale(生成後に構成が変わった)でも古い割当は表示価値があるので付与し、
186
+ * stale フラグで UI に再分類を促す。新規アイテムは割当なし(「その他」に落ちる)。
187
+ */
188
+ function attachGroups(sections, lang) {
189
+ const entry = loadStore()[lang];
190
+ if (!entry)
191
+ return { stale: false };
192
+ for (const s of sections) {
193
+ for (const it of s.items) {
194
+ if (it.kind === 'hook' || it.category)
195
+ continue;
196
+ const gid = entry.assign[it.name];
197
+ if (gid)
198
+ it.aiGroup = gid;
199
+ }
200
+ }
201
+ return { groups: entry.groups, stale: entry.hash !== groupsHash(groupTargets(sections)) };
202
+ }