skills-viewer 0.5.0 → 0.7.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,10 +17,13 @@ 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
23
- - **AI trigger diagnosis** — one click asks haiku 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)
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
24
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/`
25
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
26
29
  - **Usage sparkline** — the detail pane charts the last 30 days of per-day usage
@@ -103,7 +103,7 @@ function parseDiagnosis(text) {
103
103
  throw new Error('empty improved description');
104
104
  return { verdict, issues: issues.slice(0, 4), improved };
105
105
  }
106
- async function diagnoseOne(realPath, name, lang) {
106
+ async function diagnoseOne(realPath, name, lang, model = 'haiku') {
107
107
  const hash = (0, summary_1.contentHash)(realPath);
108
108
  const store = loadDiagnoses();
109
109
  const cached = store[realPath];
@@ -111,8 +111,8 @@ async function diagnoseOne(realPath, name, lang) {
111
111
  return { verdict: cached.verdict, issues: cached.issues, improved: cached.improved };
112
112
  }
113
113
  const content = fs.readFileSync(realPath, 'utf8').slice(0, 12000);
114
- const result = parseDiagnosis(await (0, summary_1.runHaiku)(buildPrompt(name, content, lang)));
115
- store[realPath] = { ...result, hash, lang, generatedAt: new Date().toISOString() };
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
116
  saveDiagnoses(store);
117
117
  return result;
118
118
  }
@@ -0,0 +1,178 @@
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
+ /*
51
+ * 抽出スキーマの世代。branches.to(ループ/スキップ)追加で 2。
52
+ * 旧世代キャッシュも表示には使い続ける(フローチャート描画は to 無しでも成立し、
53
+ * ループ矢印だけ出ない)が、再抽出時は stale 扱いして新スキーマで作り直す。
54
+ */
55
+ const FLOW_SCHEMA_V = 2;
56
+ function loadFlows() {
57
+ try {
58
+ return JSON.parse(fs.readFileSync(FLOW_FILE, 'utf8'));
59
+ }
60
+ catch {
61
+ return {};
62
+ }
63
+ }
64
+ function saveFlows(store) {
65
+ fs.mkdirSync(path.dirname(FLOW_FILE), { recursive: true });
66
+ fs.writeFileSync(FLOW_FILE, JSON.stringify(store, null, 1));
67
+ }
68
+ function buildPrompt(name, content, lang) {
69
+ if (lang === 'ja') {
70
+ return ('以下は Claude Code の skill 定義です。この skill が実行する処理フローを図解用に抽出し、次の JSON だけを出力してください(前置き・コードフェンス不要):\n' +
71
+ '{"steps": [{"title": "ステップ名(10字程度)", "detail": "何をするか(25字程度)",\n' +
72
+ ' "calls": ["このステップで起動/委譲する他の skill・コマンド名"],\n' +
73
+ ' "gate": "human" | "auto" | null,\n' +
74
+ ' "branches": [{"when": "分岐条件(15字程度)", "then": "その場合の挙動(20字程度)", "to": 行き先ステップ番号}]}]}\n\n' +
75
+ '制約:\n' +
76
+ '- steps は実行順に 4〜8 個(単純な skill なら少なくてよい)\n' +
77
+ '- gate は人間の確認/承認を待つステップだけ "human"(自動で進むなら "auto"、該当なしは null)\n' +
78
+ '- calls は本文に実際に登場する名前のみ(幻覚禁止)\n' +
79
+ '- branches は中断・フォールバック等の分岐だけ(無ければ省略)。when は「テスト失敗」のような判定できる条件文にする\n' +
80
+ '- to は分岐が別ステップへ移るときだけ 1 始まりの番号で(リトライ/ループで前へ戻る場合が典型)。単なる中断・終了なら省略\n\n' +
81
+ '# skill: ' +
82
+ name +
83
+ '\n\n' +
84
+ content);
85
+ }
86
+ 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' +
87
+ '{"steps": [{"title": "step name (2-4 words)", "detail": "what it does (about 10 words)",\n' +
88
+ ' "calls": ["other skill/command names this step invokes or delegates to"],\n' +
89
+ ' "gate": "human" | "auto" | null,\n' +
90
+ ' "branches": [{"when": "branch condition (about 5 words)", "then": "behavior in that case (about 7 words)", "to": target step number}]}]}\n\n' +
91
+ 'Constraints:\n' +
92
+ '- 4 to 8 steps in execution order (fewer is fine for simple skills)\n' +
93
+ '- gate is "human" ONLY for steps that wait for human confirmation/approval ("auto" if it proceeds automatically, null otherwise)\n' +
94
+ '- calls may contain only names that actually appear in the body (no hallucination)\n' +
95
+ '- branches only for aborts / fallbacks / real forks (omit when none); "when" must be a checkable condition like "tests fail"\n' +
96
+ '- to is the 1-based step number ONLY when the branch jumps to another step (typically looping back for a retry); omit for plain aborts/exits\n\n' +
97
+ '# skill: ' +
98
+ name +
99
+ '\n\n' +
100
+ content);
101
+ }
102
+ /* haiku/sonnet の出力を検証つきでパース(壊れた出力は throw して UI にエラー表示) */
103
+ function parseFlow(text) {
104
+ const stripped = text
105
+ .replace(/^```(?:json)?\s*/i, '')
106
+ .replace(/```\s*$/, '')
107
+ .trim();
108
+ const j = JSON.parse(stripped);
109
+ const steps = [];
110
+ for (const s of Array.isArray(j.steps) ? j.steps : []) {
111
+ const title = String(s?.title || '').trim();
112
+ if (!title)
113
+ continue;
114
+ steps.push({
115
+ title: title.slice(0, 60),
116
+ detail: String(s?.detail || '').slice(0, 120),
117
+ calls: (Array.isArray(s?.calls) ? s.calls : [])
118
+ .filter((c) => typeof c === 'string' && c)
119
+ .map((c) => c.slice(0, 60))
120
+ .slice(0, 6),
121
+ gate: s?.gate === 'human' || s?.gate === 'auto' ? s.gate : null,
122
+ branches: (Array.isArray(s?.branches) ? s.branches : [])
123
+ .filter((b) => b && typeof b.when === 'string')
124
+ .map((b) => ({
125
+ when: String(b.when).slice(0, 60),
126
+ then: String(b.then || '').slice(0, 80),
127
+ ...(Number.isInteger(b.to) && b.to >= 1 ? { to: b.to } : {}),
128
+ }))
129
+ .slice(0, 4),
130
+ });
131
+ if (steps.length >= 12)
132
+ break;
133
+ }
134
+ if (!steps.length)
135
+ throw new Error('no steps in output');
136
+ // to の上限検証は全 step が出揃ってから(範囲外は to だけ捨てて分岐テキストは残す)
137
+ for (const st of steps)
138
+ for (const b of st.branches)
139
+ if (b.to !== undefined && b.to > steps.length)
140
+ delete b.to;
141
+ return { steps };
142
+ }
143
+ async function flowOne(realPath, name, lang, model = 'haiku') {
144
+ const hash = (0, summary_1.contentHash)(realPath);
145
+ const store = loadFlows();
146
+ const cached = store[realPath];
147
+ if (cached && cached.hash === hash && cached.lang === lang && cached.v === FLOW_SCHEMA_V) {
148
+ return { steps: cached.steps };
149
+ }
150
+ const content = fs.readFileSync(realPath, 'utf8').slice(0, 12000);
151
+ const result = parseFlow(await (0, summary_1.runClaude)(buildPrompt(name, content, lang), model));
152
+ store[realPath] = {
153
+ ...result,
154
+ hash,
155
+ lang,
156
+ model,
157
+ v: FLOW_SCHEMA_V,
158
+ generatedAt: new Date().toISOString(),
159
+ };
160
+ saveFlows(store);
161
+ return result;
162
+ }
163
+ /* スキャン結果にキャッシュ済みフローを付与(内容が変わっていれば付けない) */
164
+ function attachFlows(sections, lang) {
165
+ const store = loadFlows();
166
+ for (const s of sections) {
167
+ for (const it of s.items) {
168
+ const cached = store[it.path];
169
+ if (cached &&
170
+ cached.lang === lang &&
171
+ it.path &&
172
+ fs.existsSync(it.path) &&
173
+ cached.hash === (0, summary_1.contentHash)(it.path)) {
174
+ it.aiFlow = { steps: cached.steps };
175
+ }
176
+ }
177
+ }
178
+ }
@@ -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
+ }
@@ -50,6 +50,8 @@ const summary_1 = require("./summary");
50
50
  const manage_1 = require("./manage");
51
51
  const edit_1 = require("./edit");
52
52
  const diagnose_1 = require("./diagnose");
53
+ const flow_1 = require("./flow");
54
+ const groups_1 = require("./groups");
53
55
  const snapshot_1 = require("./snapshot");
54
56
  const errors_1 = require("./errors");
55
57
  const locale_1 = require("./locale");
@@ -157,6 +159,8 @@ function collect(cwd, lang) {
157
159
  }
158
160
  }
159
161
  (0, diagnose_1.attachDiagnoses)(sections, lang);
162
+ (0, flow_1.attachFlows)(sections, lang);
163
+ const grp = (0, groups_1.attachGroups)(sections, lang);
160
164
  const aiStale = (0, summary_1.staleItems)(sections, lang).length;
161
165
  const targets = [
162
166
  { label: 'user skills', sub: '~/.claude/skills/', path: scan_1.HOME },
@@ -172,6 +176,8 @@ function collect(cwd, lang) {
172
176
  aiStale,
173
177
  usageAvailable,
174
178
  changes: (0, snapshot_1.computeChanges)(sections),
179
+ ...(grp.groups ? { groups: grp.groups } : {}),
180
+ ...(grp.stale ? { groupsStale: true } : {}),
175
181
  };
176
182
  }
177
183
  /* DNS rebinding 対策: same-origin GET には Origin が付かないため Host 側も検証する */
@@ -244,6 +250,7 @@ function handleApi(req, res, cwd) {
244
250
  return send(400, { error: 'bad-json', detail: '' });
245
251
  }
246
252
  const lang = langOf(data.lang);
253
+ const model = (0, summary_1.modelOf)(data.model);
247
254
  try {
248
255
  if (url.pathname === '/api/save')
249
256
  return send(200, (0, edit_1.doSave)(data));
@@ -252,11 +259,19 @@ function handleApi(req, res, cwd) {
252
259
  if (url.pathname === '/api/diagnose') {
253
260
  const real = (0, manage_1.assertReadableMd)(data.src);
254
261
  const name = data.name || path.basename(path.dirname(real));
255
- (0, diagnose_1.diagnoseOne)(real, name, lang)
262
+ (0, diagnose_1.diagnoseOne)(real, name, lang, model)
256
263
  .then((d) => send(200, { ok: true, ...d }))
257
264
  .catch((e) => send(400, (0, errors_1.toErrorBody)(e)));
258
265
  return;
259
266
  }
267
+ if (url.pathname === '/api/flow') {
268
+ const real = (0, manage_1.assertReadableMd)(data.src);
269
+ const name = data.name || path.basename(path.dirname(real));
270
+ (0, flow_1.flowOne)(real, name, lang, model)
271
+ .then((f) => send(200, { ok: true, ...f }))
272
+ .catch((e) => send(400, (0, errors_1.toErrorBody)(e)));
273
+ return;
274
+ }
260
275
  if (url.pathname === '/api/changes-ack') {
261
276
  (0, snapshot_1.ackChanges)((0, scan_1.scanSections)(cwd, lang));
262
277
  return send(200, { ok: true });
@@ -268,16 +283,23 @@ function handleApi(req, res, cwd) {
268
283
  if (url.pathname === '/api/open')
269
284
  return send(200, (0, manage_1.openInEditor)(data));
270
285
  if (url.pathname === '/api/summarize-all')
271
- return send(200, (0, summary_1.startSummarizeAll)((0, scan_1.scanSections)(cwd, lang), !!data.force, lang));
286
+ return send(200, (0, summary_1.startSummarizeAll)((0, scan_1.scanSections)(cwd, lang), !!data.force, lang, model));
287
+ if (url.pathname === '/api/group-generate') {
288
+ // 環境全体で 1 回の claude 呼び出し。完了時にグループ集合を返す(割当は再取得で反映)
289
+ (0, groups_1.generateGroups)((0, scan_1.scanSections)(cwd, lang), lang, model)
290
+ .then((r) => send(200, { ok: true, groups: r.groups }))
291
+ .catch((e) => send(400, (0, errors_1.toErrorBody)(e)));
292
+ return;
293
+ }
272
294
  if (url.pathname === '/api/summarize') {
273
295
  const real = (0, manage_1.assertReadableMd)(data.src);
274
296
  // refs(関係候補)はスキャン結果から復元する
275
297
  const sections = (0, scan_1.scanSections)(cwd, lang);
276
298
  const item = sections.flatMap((s) => s.items).find((x) => x.path === real);
277
299
  const name = data.name || item?.name || path.basename(path.dirname(real));
278
- (0, summary_1.summarizeOne)({ path: real, name, refs: item?.refs || [] }, lang)
300
+ (0, summary_1.summarizeOne)({ path: real, name, refs: item?.refs || [] }, lang, model)
279
301
  .then((analysis) => {
280
- (0, summary_1.saveSummary)(real, name, analysis, lang);
302
+ (0, summary_1.saveSummary)(real, name, analysis, lang, model);
281
303
  send(200, { ok: true, ...analysis });
282
304
  })
283
305
  .catch((e) => send(400, (0, errors_1.toErrorBody)(e)));
@@ -152,6 +152,7 @@ function readSkillDir(dir, nameHint) {
152
152
  path: skillMd,
153
153
  updatedAt: fileMtime(skillMd),
154
154
  files: listFiles(dir).sort(),
155
+ ...(meta.category ? { category: meta.category } : {}),
155
156
  ...(lint.length ? { lint } : {}),
156
157
  _body: body, // 参照抽出用(scanSections で refs 化して破棄)
157
158
  };
@@ -197,6 +198,7 @@ function scanMdRoot(root, kind) {
197
198
  path: fp,
198
199
  updatedAt: fileMtime(fp),
199
200
  files: [entry.name],
201
+ ...(meta.category ? { category: meta.category } : {}),
200
202
  ...(lint.length ? { lint } : {}),
201
203
  _body: body,
202
204
  });
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  /*
3
- * AI 要約 + 起動分類: claude CLI headless (haiku) で SKILL.md を分析し、
4
- * 内容ハッシュをキーに ~/.cache/skills-viewer/summaries.json へキャッシュ。
3
+ * AI 要約 + 起動分類: claude CLI headless(既定 haiku。設定でモデル変更可)で SKILL.md
4
+ * 分析し、内容ハッシュをキーに ~/.cache/skills-viewer/summaries.json へキャッシュ。
5
5
  * ハッシュが一致する限り再生成しない(mtime でなくハッシュなので同期や clone に強い)。
6
+ * モデルはキャッシュキーに含めない: 切替だけで全件 stale になるのを避け、
7
+ * 置き換えたい場合は強制再生成を使う(生成時のモデルは記録する)。
6
8
  */
7
9
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
10
  if (k2 === undefined) k2 = k;
@@ -40,7 +42,8 @@ var __importStar = (this && this.__importStar) || (function () {
40
42
  Object.defineProperty(exports, "__esModule", { value: true });
41
43
  exports.loadSummaries = loadSummaries;
42
44
  exports.contentHash = contentHash;
43
- exports.runHaiku = runHaiku;
45
+ exports.modelOf = modelOf;
46
+ exports.runClaude = runClaude;
44
47
  exports.summarizeOne = summarizeOne;
45
48
  exports.parseAnalysis = parseAnalysis;
46
49
  exports.saveSummary = saveSummary;
@@ -145,21 +148,26 @@ function buildPrompt(it, refs, content, lang) {
145
148
  '\n\n' +
146
149
  content);
147
150
  }
151
+ /* クライアント指定のモデルを許可リストで検証(不明値は既定の haiku に落とす) */
152
+ function modelOf(v) {
153
+ return v === 'sonnet' || v === 'opus' ? v : 'haiku';
154
+ }
148
155
  /*
149
- * claude CLI headless (haiku) にプロンプトを渡して生テキストを得る共通実行部。
156
+ * claude CLI headless にプロンプトを渡して生テキストを得る共通実行部。
157
+ * モデルはエイリアス指定(haiku / sonnet / opus)で、実体は CLI 側の解決に従う。
150
158
  * --tools '' で全ツールを無効化: SKILL.md は clone したリポジトリ由来もあり得るため、
151
159
  * 本文に指示が仕込まれていても純粋なテキスト生成の外に出られないようにする
152
160
  */
153
- function runHaiku(prompt) {
161
+ function runClaude(prompt, model = 'haiku', timeoutMs = 120000) {
154
162
  return new Promise((resolve, reject) => {
155
- const child = (0, node_child_process_1.spawn)('claude', ['-p', '--model', 'haiku', '--tools', ''], {
163
+ const child = (0, node_child_process_1.spawn)('claude', ['-p', '--model', model, '--tools', ''], {
156
164
  stdio: ['pipe', 'pipe', 'pipe'],
157
165
  });
158
166
  let out = '', errOut = '';
159
167
  const timer = setTimeout(() => {
160
168
  child.kill('SIGKILL');
161
- reject(new Error('timeout (120s)'));
162
- }, 120000);
169
+ reject(new Error(`timeout (${Math.round(timeoutMs / 1000)}s)`));
170
+ }, timeoutMs);
163
171
  child.stdout.on('data', (d) => {
164
172
  out += d;
165
173
  });
@@ -181,13 +189,13 @@ function runHaiku(prompt) {
181
189
  });
182
190
  }
183
191
  /*
184
- * 1 skill haiku で分析し {summary, invocation, invocationReason, relations} を返す。
192
+ * 1 skill を分析し {summary, invocation, invocationReason, relations} を返す。
185
193
  * relations の候補(refs)は静的解析で抽出済みの既知 skill 名のみに制限し、幻覚を防ぐ。
186
194
  */
187
- async function summarizeOne(it, lang) {
195
+ async function summarizeOne(it, lang, model = 'haiku') {
188
196
  const content = fs.readFileSync(it.path, 'utf8').slice(0, 12000);
189
197
  const refs = it.refs || [];
190
- const text = await runHaiku(buildPrompt(it, refs, content, lang));
198
+ const text = await runClaude(buildPrompt(it, refs, content, lang), model);
191
199
  return parseAnalysis(text, refs);
192
200
  }
193
201
  /* haiku の出力を検証つきでパース。壊れていたら全文を summary として扱う */
@@ -222,7 +230,7 @@ function parseAnalysis(text, refs) {
222
230
  return { summary: stripped, invocation: null, invocationReason: '', relations: [] };
223
231
  }
224
232
  }
225
- function saveSummary(realPath, name, analysis, lang) {
233
+ function saveSummary(realPath, name, analysis, lang, model = 'haiku') {
226
234
  const summaries = loadSummaries();
227
235
  summaries[realPath] = {
228
236
  hash: contentHash(realPath),
@@ -233,6 +241,7 @@ function saveSummary(realPath, name, analysis, lang) {
233
241
  relations: analysis.relations,
234
242
  generatedAt: new Date().toISOString(),
235
243
  lang,
244
+ model,
236
245
  };
237
246
  saveSummaries(summaries);
238
247
  }
@@ -262,7 +271,7 @@ function staleItems(sections, lang) {
262
271
  });
263
272
  }
264
273
  let summaryJob = null;
265
- function startSummarizeAll(sections, force, lang) {
274
+ function startSummarizeAll(sections, force, lang, model = 'haiku') {
266
275
  if (summaryJob && !summaryJob.finished)
267
276
  return summaryJob;
268
277
  const items = force ? summarizableItems(sections) : staleItems(sections, lang);
@@ -283,8 +292,8 @@ function startSummarizeAll(sections, force, lang) {
283
292
  const it = items[idx++];
284
293
  job.current = it.name;
285
294
  try {
286
- const analysis = await summarizeOne(it, lang);
287
- saveSummary(it.path, it.name, analysis, lang);
295
+ const analysis = await summarizeOne(it, lang, model);
296
+ saveSummary(it.path, it.name, analysis, lang, model);
288
297
  }
289
298
  catch (e) {
290
299
  job.errors.push(it.name + ': ' + (e instanceof Error ? e.message : String(e)));