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 +4 -1
- package/build/server/diagnose.js +3 -3
- package/build/server/flow.js +178 -0
- package/build/server/groups.js +202 -0
- package/build/server/index.js +26 -4
- package/build/server/scan.js +2 -0
- package/build/server/summary.js +24 -15
- package/dist/assets/index-C9NICndS.css +1 -0
- package/dist/assets/index-bdDgkHOX.js +67 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/dist/assets/index-C3s9H-vL.css +0 -1
- package/dist/assets/index-N1uk01dp.js +0 -67
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
|
|
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
|
package/build/server/diagnose.js
CHANGED
|
@@ -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.
|
|
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
|
+
}
|
package/build/server/index.js
CHANGED
|
@@ -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)));
|
package/build/server/scan.js
CHANGED
|
@@ -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
|
});
|
package/build/server/summary.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/*
|
|
3
|
-
* AI 要約 + 起動分類: claude CLI headless
|
|
4
|
-
*
|
|
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.
|
|
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
|
|
156
|
+
* claude CLI headless にプロンプトを渡して生テキストを得る共通実行部。
|
|
157
|
+
* モデルはエイリアス指定(haiku / sonnet / opus)で、実体は CLI 側の解決に従う。
|
|
150
158
|
* --tools '' で全ツールを無効化: SKILL.md は clone したリポジトリ由来もあり得るため、
|
|
151
159
|
* 本文に指示が仕込まれていても純粋なテキスト生成の外に出られないようにする
|
|
152
160
|
*/
|
|
153
|
-
function
|
|
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',
|
|
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(
|
|
162
|
-
},
|
|
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
|
|
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
|
|
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)));
|