skills-viewer 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +82 -0
- package/build/cli.js +35 -0
- package/build/server/errors.js +24 -0
- package/build/server/index.js +298 -0
- package/build/server/locale.js +14 -0
- package/build/server/manage.js +205 -0
- package/build/server/scan.js +444 -0
- package/build/server/summary.js +302 -0
- package/build/server/usage.js +165 -0
- package/build/shared/types.js +3 -0
- package/dist/assets/index-BF9RnD9N.js +67 -0
- package/dist/assets/index-D2xAPQHX.css +1 -0
- package/dist/index.html +13 -0
- package/package.json +63 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* AI 要約 + 起動分類: claude CLI headless (haiku) で SKILL.md を分析し、
|
|
4
|
+
* 内容ハッシュをキーに ~/.cache/skills-viewer/summaries.json へキャッシュ。
|
|
5
|
+
* ハッシュが一致する限り再生成しない(mtime でなくハッシュなので同期や clone に強い)。
|
|
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.loadSummaries = loadSummaries;
|
|
42
|
+
exports.contentHash = contentHash;
|
|
43
|
+
exports.summarizeOne = summarizeOne;
|
|
44
|
+
exports.parseAnalysis = parseAnalysis;
|
|
45
|
+
exports.saveSummary = saveSummary;
|
|
46
|
+
exports.staleItems = staleItems;
|
|
47
|
+
exports.startSummarizeAll = startSummarizeAll;
|
|
48
|
+
exports.summaryStatus = summaryStatus;
|
|
49
|
+
const fs = __importStar(require("node:fs"));
|
|
50
|
+
const os = __importStar(require("node:os"));
|
|
51
|
+
const path = __importStar(require("node:path"));
|
|
52
|
+
const crypto = __importStar(require("node:crypto"));
|
|
53
|
+
const node_child_process_1 = require("node:child_process");
|
|
54
|
+
const locale_1 = require("./locale");
|
|
55
|
+
const CACHE_DIR = path.join(os.homedir(), '.cache', 'skills-viewer');
|
|
56
|
+
const SUMMARY_FILE = path.join(CACHE_DIR, 'summaries.json');
|
|
57
|
+
// 旧パッケージ名時代のキャッシュがあれば一度だけ引き継ぐ
|
|
58
|
+
for (const legacy of ['claude-code-my-skills', 'claude-skills-browser']) {
|
|
59
|
+
const legacyDir = path.join(os.homedir(), '.cache', legacy);
|
|
60
|
+
if (!fs.existsSync(CACHE_DIR) && fs.existsSync(legacyDir)) {
|
|
61
|
+
try {
|
|
62
|
+
fs.renameSync(legacyDir, CACHE_DIR);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
/* 失敗しても新規生成される */
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/* relation type の正規化: 新キー以外(多言語対応前の日本語値・幻覚)は references に落とす */
|
|
70
|
+
const REL_TYPES = ['invokes', 'delegates', 'called-by', 'references'];
|
|
71
|
+
const LEGACY_REL = {
|
|
72
|
+
起動: 'invokes',
|
|
73
|
+
委譲: 'delegates',
|
|
74
|
+
呼ばれる側: 'called-by',
|
|
75
|
+
参照: 'references',
|
|
76
|
+
};
|
|
77
|
+
function normalizeRelType(t) {
|
|
78
|
+
if (REL_TYPES.includes(t))
|
|
79
|
+
return t;
|
|
80
|
+
return LEGACY_REL[String(t)] || 'references';
|
|
81
|
+
}
|
|
82
|
+
function loadSummaries() {
|
|
83
|
+
try {
|
|
84
|
+
const store = JSON.parse(fs.readFileSync(SUMMARY_FILE, 'utf8'));
|
|
85
|
+
// 多言語対応前のキャッシュは日本語生成・日本語 relation type なので読み込み時に正規化する
|
|
86
|
+
for (const e of Object.values(store)) {
|
|
87
|
+
if (!e.lang)
|
|
88
|
+
e.lang = 'ja';
|
|
89
|
+
for (const r of e.relations || [])
|
|
90
|
+
r.type = normalizeRelType(r.type);
|
|
91
|
+
}
|
|
92
|
+
return store;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return {};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function saveSummaries(s) {
|
|
99
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
100
|
+
fs.writeFileSync(SUMMARY_FILE, JSON.stringify(s, null, 1));
|
|
101
|
+
}
|
|
102
|
+
function contentHash(fp) {
|
|
103
|
+
try {
|
|
104
|
+
return crypto.createHash('sha256').update(fs.readFileSync(fp)).digest('hex').slice(0, 16);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/* 分析プロンプト。relation type は言語非依存のキーで出力させ、表示はクライアント側で解決 */
|
|
111
|
+
function buildPrompt(it, refs, content, lang) {
|
|
112
|
+
if (lang === 'ja') {
|
|
113
|
+
return ('以下は Claude Code の skill 定義ファイルです。読んで、次の JSON だけを出力してください(前置き・コードフェンス不要):\n' +
|
|
114
|
+
'{"summary": "何をする skill か・いつ使うかが伝わる日本語の要約(最大2文。手順の羅列でなく用途と価値)",\n' +
|
|
115
|
+
' "invocation": "human" | "agent" | "both",\n' +
|
|
116
|
+
' "invocationReason": "判定理由(日本語で15字程度)",\n' +
|
|
117
|
+
' "relations": [{"name": "skill名", "type": "invokes" | "delegates" | "called-by" | "references", "note": "関係の説明(日本語で15字程度)"}]}\n\n' +
|
|
118
|
+
'invocation は「人間が意図してコマンドとして打つ起点(human)」「他の skill やエージェントから部品・後続として呼ばれる想定(agent)」「両方(both)」のどれか。\n' +
|
|
119
|
+
'relations の type は「相手を起動する(invokes)」「相手に処理を委ねる(delegates)」「相手から呼ばれる(called-by)」「言及・参照のみ(references)」のどれか。\n' +
|
|
120
|
+
(refs.length
|
|
121
|
+
? 'relations の name は次の候補のみ使用し、実質的な関係が読み取れるものだけ含める: ' +
|
|
122
|
+
refs.join(', ') +
|
|
123
|
+
'\n'
|
|
124
|
+
: 'この skill に他 skill への言及は見つかっていないため relations は [] とすること。\n') +
|
|
125
|
+
'\n# skill: ' +
|
|
126
|
+
it.name +
|
|
127
|
+
'\n\n' +
|
|
128
|
+
content);
|
|
129
|
+
}
|
|
130
|
+
return ('Below is a Claude Code skill definition file. Read it and output ONLY the following JSON (no preamble, no code fences):\n' +
|
|
131
|
+
'{"summary": "English summary conveying what the skill does and when to use it (max 2 sentences; purpose and value, not a list of steps)",\n' +
|
|
132
|
+
' "invocation": "human" | "agent" | "both",\n' +
|
|
133
|
+
' "invocationReason": "reason for the verdict (about 8 words)",\n' +
|
|
134
|
+
' "relations": [{"name": "skill name", "type": "invokes" | "delegates" | "called-by" | "references", "note": "short note on the relation (about 8 words)"}]}\n\n' +
|
|
135
|
+
'invocation: "human" = a human deliberately types it as a command, "agent" = meant to be called by other skills/agents as a building block, "both" = both.\n' +
|
|
136
|
+
'relation type: "invokes" = this skill launches the other, "delegates" = hands work off to it, "called-by" = is called by it, "references" = merely mentions it.\n' +
|
|
137
|
+
(refs.length
|
|
138
|
+
? 'Use ONLY these candidates as relation names, and include only relations that are substantively present: ' +
|
|
139
|
+
refs.join(', ') +
|
|
140
|
+
'\n'
|
|
141
|
+
: 'No mentions of other skills were found, so relations MUST be [].\n') +
|
|
142
|
+
'\n# skill: ' +
|
|
143
|
+
it.name +
|
|
144
|
+
'\n\n' +
|
|
145
|
+
content);
|
|
146
|
+
}
|
|
147
|
+
/*
|
|
148
|
+
* 1 skill を haiku で分析し {summary, invocation, invocationReason, relations} を返す。
|
|
149
|
+
* relations の候補(refs)は静的解析で抽出済みの既知 skill 名のみに制限し、幻覚を防ぐ。
|
|
150
|
+
*/
|
|
151
|
+
function summarizeOne(it, lang) {
|
|
152
|
+
return new Promise((resolve, reject) => {
|
|
153
|
+
let content;
|
|
154
|
+
try {
|
|
155
|
+
content = fs.readFileSync(it.path, 'utf8').slice(0, 12000);
|
|
156
|
+
}
|
|
157
|
+
catch (e) {
|
|
158
|
+
return reject(e);
|
|
159
|
+
}
|
|
160
|
+
const refs = it.refs || [];
|
|
161
|
+
const prompt = buildPrompt(it, refs, content, lang);
|
|
162
|
+
// --tools '' で全ツールを無効化: SKILL.md は clone したリポジトリ由来もあり得るため、
|
|
163
|
+
// 本文に指示が仕込まれていても純粋なテキスト生成の外に出られないようにする
|
|
164
|
+
const child = (0, node_child_process_1.spawn)('claude', ['-p', '--model', 'haiku', '--tools', ''], {
|
|
165
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
166
|
+
});
|
|
167
|
+
let out = '', errOut = '';
|
|
168
|
+
const timer = setTimeout(() => {
|
|
169
|
+
child.kill('SIGKILL');
|
|
170
|
+
reject(new Error('timeout (120s)'));
|
|
171
|
+
}, 120000);
|
|
172
|
+
child.stdout.on('data', (d) => {
|
|
173
|
+
out += d;
|
|
174
|
+
});
|
|
175
|
+
child.stderr.on('data', (d) => {
|
|
176
|
+
errOut += d;
|
|
177
|
+
});
|
|
178
|
+
child.on('error', (e) => {
|
|
179
|
+
clearTimeout(timer);
|
|
180
|
+
reject(e);
|
|
181
|
+
});
|
|
182
|
+
child.on('close', (code) => {
|
|
183
|
+
clearTimeout(timer);
|
|
184
|
+
const text = out.trim();
|
|
185
|
+
if (code !== 0 || !text)
|
|
186
|
+
return reject(new Error(errOut.trim().slice(0, 200) || 'claude exited ' + code));
|
|
187
|
+
resolve(parseAnalysis(text, refs));
|
|
188
|
+
});
|
|
189
|
+
child.stdin.end(prompt);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
/* haiku の出力を検証つきでパース。壊れていたら全文を summary として扱う */
|
|
193
|
+
function parseAnalysis(text, refs) {
|
|
194
|
+
const stripped = text
|
|
195
|
+
.replace(/^```(?:json)?\s*/i, '')
|
|
196
|
+
.replace(/```\s*$/, '')
|
|
197
|
+
.trim();
|
|
198
|
+
try {
|
|
199
|
+
const j = JSON.parse(stripped);
|
|
200
|
+
const invocation = ['human', 'agent', 'both'].includes(j.invocation)
|
|
201
|
+
? j.invocation
|
|
202
|
+
: null;
|
|
203
|
+
const refSet = new Set(refs);
|
|
204
|
+
const relations = Array.isArray(j.relations)
|
|
205
|
+
? j.relations
|
|
206
|
+
.filter((r) => r && typeof r.name === 'string' && refSet.has(r.name))
|
|
207
|
+
.map((r) => ({
|
|
208
|
+
name: r.name,
|
|
209
|
+
type: normalizeRelType(r.type),
|
|
210
|
+
note: String(r.note || '').slice(0, 40),
|
|
211
|
+
}))
|
|
212
|
+
: [];
|
|
213
|
+
return {
|
|
214
|
+
summary: String(j.summary || '').trim() || stripped,
|
|
215
|
+
invocation,
|
|
216
|
+
invocationReason: String(j.invocationReason || '').slice(0, 40),
|
|
217
|
+
relations,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
return { summary: stripped, invocation: null, invocationReason: '', relations: [] };
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function saveSummary(realPath, name, analysis, lang) {
|
|
225
|
+
const summaries = loadSummaries();
|
|
226
|
+
summaries[realPath] = {
|
|
227
|
+
hash: contentHash(realPath),
|
|
228
|
+
name,
|
|
229
|
+
summary: analysis.summary,
|
|
230
|
+
invocation: analysis.invocation ?? undefined,
|
|
231
|
+
invocationReason: analysis.invocationReason,
|
|
232
|
+
relations: analysis.relations,
|
|
233
|
+
generatedAt: new Date().toISOString(),
|
|
234
|
+
lang,
|
|
235
|
+
};
|
|
236
|
+
saveSummaries(summaries);
|
|
237
|
+
}
|
|
238
|
+
/* 要約対象 = 実ファイルを持つ skill/command/agent(hook は設定エントリなので除外) */
|
|
239
|
+
function summarizableItems(sections) {
|
|
240
|
+
const out = [];
|
|
241
|
+
for (const sec of sections) {
|
|
242
|
+
for (const it of sec.items) {
|
|
243
|
+
if (it.kind === 'hook')
|
|
244
|
+
continue;
|
|
245
|
+
if (!fs.existsSync(it.path))
|
|
246
|
+
continue; // built-in
|
|
247
|
+
out.push({ path: it.path, name: it.name, refs: it.refs || [] });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
/* 未生成・内容変更に加え、要約の生成言語が表示言語と違うものも「未生成」扱いにする */
|
|
253
|
+
function staleItems(sections, lang) {
|
|
254
|
+
const summaries = loadSummaries();
|
|
255
|
+
return summarizableItems(sections).filter((it) => {
|
|
256
|
+
const cached = summaries[it.path];
|
|
257
|
+
return (!cached ||
|
|
258
|
+
cached.hash !== contentHash(it.path) ||
|
|
259
|
+
cached.invocation === undefined ||
|
|
260
|
+
cached.lang !== lang);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
let summaryJob = null;
|
|
264
|
+
function startSummarizeAll(sections, force, lang) {
|
|
265
|
+
if (summaryJob && !summaryJob.finished)
|
|
266
|
+
return summaryJob;
|
|
267
|
+
const items = force ? summarizableItems(sections) : staleItems(sections, lang);
|
|
268
|
+
summaryJob = {
|
|
269
|
+
total: items.length,
|
|
270
|
+
done: 0,
|
|
271
|
+
current: '',
|
|
272
|
+
errors: [],
|
|
273
|
+
finished: items.length === 0,
|
|
274
|
+
};
|
|
275
|
+
if (summaryJob.finished)
|
|
276
|
+
return summaryJob;
|
|
277
|
+
const job = summaryJob;
|
|
278
|
+
(async () => {
|
|
279
|
+
let idx = 0;
|
|
280
|
+
const worker = async () => {
|
|
281
|
+
while (idx < items.length) {
|
|
282
|
+
const it = items[idx++];
|
|
283
|
+
job.current = it.name;
|
|
284
|
+
try {
|
|
285
|
+
const analysis = await summarizeOne(it, lang);
|
|
286
|
+
saveSummary(it.path, it.name, analysis, lang);
|
|
287
|
+
}
|
|
288
|
+
catch (e) {
|
|
289
|
+
job.errors.push(it.name + ': ' + (e instanceof Error ? e.message : String(e)));
|
|
290
|
+
}
|
|
291
|
+
job.done++;
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
await Promise.all(Array.from({ length: Math.min(3, items.length) }, worker));
|
|
295
|
+
job.finished = true;
|
|
296
|
+
console.log((0, locale_1.srvMsg)(`summarize: ${job.done}件完了, エラー ${job.errors.length}件`, `summarize: ${job.done} done, ${job.errors.length} error(s)`));
|
|
297
|
+
})();
|
|
298
|
+
return summaryJob;
|
|
299
|
+
}
|
|
300
|
+
function summaryStatus() {
|
|
301
|
+
return summaryJob || { finished: true, total: 0, done: 0, errors: [] };
|
|
302
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* skill / agent 起動履歴の集計。~/.claude/projects/<proj>/<session>.jsonl に残る3形式を拾う:
|
|
4
|
+
* - user-typed slash: <command-name>/weall-ship</command-name>
|
|
5
|
+
* - model via tool: "name":"Skill","input":{"skill":"weall-ship"
|
|
6
|
+
* - subagent 起動: "subagent_type":"code-reviewer"
|
|
7
|
+
* ファイルごとに mtime でキャッシュ。トランスクリプトは Claude Code の保持期間で
|
|
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.extractHits = extractHits;
|
|
45
|
+
exports.scanUsageByDir = scanUsageByDir;
|
|
46
|
+
exports.encodeProjectPath = encodeProjectPath;
|
|
47
|
+
const fs = __importStar(require("node:fs"));
|
|
48
|
+
const os = __importStar(require("node:os"));
|
|
49
|
+
const path = __importStar(require("node:path"));
|
|
50
|
+
const node_string_decoder_1 = require("node:string_decoder");
|
|
51
|
+
const usageCache = new Map();
|
|
52
|
+
function scanLine(line, hits) {
|
|
53
|
+
const isCmd = line.includes('<command-name>');
|
|
54
|
+
const isSkill = line.includes('"name":"Skill"');
|
|
55
|
+
const isAgent = line.includes('"subagent_type"');
|
|
56
|
+
if (!isCmd && !isSkill && !isAgent)
|
|
57
|
+
return;
|
|
58
|
+
const tm = line.match(/"timestamp":"([^"]+)"/);
|
|
59
|
+
const ts = tm ? Date.parse(tm[1]) || 0 : 0;
|
|
60
|
+
if (isCmd) {
|
|
61
|
+
for (const m of line.matchAll(/<command-name>\/?([^<]+)<\/command-name>/g)) {
|
|
62
|
+
hits.push({ name: m[1].trim(), ts, via: 'typed' });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (isSkill) {
|
|
66
|
+
for (const m of line.matchAll(/"name":"Skill","input":\{"skill":"([^"]+)"/g)) {
|
|
67
|
+
hits.push({ name: m[1], ts, via: 'auto' });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (isAgent) {
|
|
71
|
+
// Agent/Task ツールによるサブエージェント起動(agent 定義の使用実績として扱う)
|
|
72
|
+
for (const m of line.matchAll(/"subagent_type":"([^"]+)"/g)) {
|
|
73
|
+
hits.push({ name: m[1], ts, via: 'auto' });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/*
|
|
78
|
+
* トランスクリプトは合計 GB 級になり得るため全文を一括読みせず、チャンク単位で読んで
|
|
79
|
+
* 行ごとに処理する(メモリ使用はチャンク + 改行待ちの1行分に収まる)。
|
|
80
|
+
* chunkSize はテスト用に指定可能。StringDecoder が境界で割れたマルチバイト文字を繋ぐ。
|
|
81
|
+
*/
|
|
82
|
+
function extractHits(fp, chunkSize = 1 << 20) {
|
|
83
|
+
const hits = [];
|
|
84
|
+
let fd;
|
|
85
|
+
try {
|
|
86
|
+
fd = fs.openSync(fp, 'r');
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return hits;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const buf = Buffer.alloc(chunkSize);
|
|
93
|
+
const decoder = new node_string_decoder_1.StringDecoder('utf8');
|
|
94
|
+
let rest = '';
|
|
95
|
+
let bytes;
|
|
96
|
+
while ((bytes = fs.readSync(fd, buf, 0, buf.length, null)) > 0) {
|
|
97
|
+
const lines = (rest + decoder.write(buf.subarray(0, bytes))).split('\n');
|
|
98
|
+
rest = lines.pop() || '';
|
|
99
|
+
for (const line of lines)
|
|
100
|
+
scanLine(line, hits);
|
|
101
|
+
}
|
|
102
|
+
rest += decoder.end();
|
|
103
|
+
if (rest)
|
|
104
|
+
scanLine(rest, hits);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
/* 途中で読めなくなったら部分結果を返す */
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
fs.closeSync(fd);
|
|
111
|
+
}
|
|
112
|
+
return hits;
|
|
113
|
+
}
|
|
114
|
+
/*
|
|
115
|
+
* トランスクリプトの親ディレクトリ = 呼び出し元プロジェクト(パスを [^a-zA-Z0-9]→'-' で
|
|
116
|
+
* エンコードした名前)。同名 skill を正しい定義に帰属させるため、ディレクトリ別に集計する。
|
|
117
|
+
*/
|
|
118
|
+
function scanUsageByDir() {
|
|
119
|
+
const byDir = {};
|
|
120
|
+
const root = path.join(os.homedir(), '.claude', 'projects');
|
|
121
|
+
let dirs;
|
|
122
|
+
try {
|
|
123
|
+
dirs = fs.readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return byDir;
|
|
127
|
+
}
|
|
128
|
+
for (const d of dirs) {
|
|
129
|
+
const dir = path.join(root, d.name);
|
|
130
|
+
let files;
|
|
131
|
+
try {
|
|
132
|
+
files = fs.readdirSync(dir).filter((f) => f.endsWith('.jsonl'));
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const agg = byDir[d.name] || (byDir[d.name] = {});
|
|
138
|
+
for (const f of files) {
|
|
139
|
+
const fp = path.join(dir, f);
|
|
140
|
+
let st;
|
|
141
|
+
try {
|
|
142
|
+
st = fs.statSync(fp);
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
let entry = usageCache.get(fp);
|
|
148
|
+
if (!entry || entry.mtimeMs !== st.mtimeMs) {
|
|
149
|
+
entry = { mtimeMs: st.mtimeMs, hits: extractHits(fp) };
|
|
150
|
+
usageCache.set(fp, entry);
|
|
151
|
+
}
|
|
152
|
+
for (const h of entry.hits) {
|
|
153
|
+
const a = agg[h.name] || (agg[h.name] = { typed: 0, auto: 0, last: 0 });
|
|
154
|
+
a[h.via === 'typed' ? 'typed' : 'auto']++;
|
|
155
|
+
if (h.ts > a.last)
|
|
156
|
+
a.last = h.ts;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return byDir;
|
|
161
|
+
}
|
|
162
|
+
/* Claude Code のトランスクリプトディレクトリ名と同じ規則でプロジェクトパスをエンコード */
|
|
163
|
+
function encodeProjectPath(p) {
|
|
164
|
+
return p.replace(/[^a-zA-Z0-9]/g, '-');
|
|
165
|
+
}
|