skills-viewer 0.3.0 → 0.5.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 +5 -1
- package/build/server/diagnose.js +138 -0
- package/build/server/edit.js +145 -0
- package/build/server/index.js +67 -2
- package/build/server/scan.js +12 -1
- package/build/server/snapshot.js +114 -0
- package/build/server/summary.js +16 -15
- package/build/server/usage.js +12 -1
- package/dist/assets/{index-CIxN1tbB.css → index-C3s9H-vL.css} +1 -1
- package/dist/assets/index-N1uk01dp.js +67 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/dist/assets/index-DoP2Wrhx.js +0 -67
package/README.md
CHANGED
|
@@ -18,8 +18,12 @@ npx skills-viewer
|
|
|
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
20
|
- **Search / sort** — incremental search over name + description + usage; sort by name, usage count, last used, updated date, or token cost
|
|
21
|
-
- **Diagnostics** — an _unused_ badge (no recorded use within the transcript retention window) with an unused
|
|
21
|
+
- **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
22
|
- **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
|
+
- **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
|
+
- **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
|
+
- **Usage sparkline** — the detail pane charts the last 30 days of per-day usage
|
|
23
27
|
- **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
|
|
24
28
|
- **AI summaries** — one-click summarization of each SKILL.md via `claude -p --model haiku`, cached by content hash in `~/.cache/skills-viewer/` so unchanged skills are never re-summarized
|
|
25
29
|
- **Detail pane** — master/detail layout with an Overview tab (description, usage, bundled files) and a rendered SKILL.md tab
|
|
@@ -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) {
|
|
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.runHaiku)(buildPrompt(name, content, lang)));
|
|
115
|
+
store[realPath] = { ...result, hash, lang, 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
|
+
}
|
package/build/server/index.js
CHANGED
|
@@ -48,6 +48,9 @@ const scan_1 = require("./scan");
|
|
|
48
48
|
const usage_1 = require("./usage");
|
|
49
49
|
const summary_1 = require("./summary");
|
|
50
50
|
const manage_1 = require("./manage");
|
|
51
|
+
const edit_1 = require("./edit");
|
|
52
|
+
const diagnose_1 = require("./diagnose");
|
|
53
|
+
const snapshot_1 = require("./snapshot");
|
|
51
54
|
const errors_1 = require("./errors");
|
|
52
55
|
const locale_1 = require("./locale");
|
|
53
56
|
const TOKEN = crypto.randomBytes(16).toString('hex');
|
|
@@ -120,6 +123,11 @@ function attributeUsage(sections) {
|
|
|
120
123
|
target.autoCount = (target.autoCount || 0) + u.auto;
|
|
121
124
|
target.useCount = (target.useCount || 0) + u.typed + u.auto;
|
|
122
125
|
target.lastUsed = Math.max(target.lastUsed || 0, u.last);
|
|
126
|
+
if (u.daily) {
|
|
127
|
+
const du = target.dailyUse || (target.dailyUse = {});
|
|
128
|
+
for (const [day, n] of Object.entries(u.daily))
|
|
129
|
+
du[day] = (du[day] || 0) + n;
|
|
130
|
+
}
|
|
123
131
|
}
|
|
124
132
|
}
|
|
125
133
|
return Object.keys(byDir).length > 0;
|
|
@@ -148,6 +156,7 @@ function collect(cwd, lang) {
|
|
|
148
156
|
}
|
|
149
157
|
}
|
|
150
158
|
}
|
|
159
|
+
(0, diagnose_1.attachDiagnoses)(sections, lang);
|
|
151
160
|
const aiStale = (0, summary_1.staleItems)(sections, lang).length;
|
|
152
161
|
const targets = [
|
|
153
162
|
{ label: 'user skills', sub: '~/.claude/skills/', path: scan_1.HOME },
|
|
@@ -155,7 +164,15 @@ function collect(cwd, lang) {
|
|
|
155
164
|
.sort((a, b) => path.basename(a).localeCompare(path.basename(b)))
|
|
156
165
|
.map((p) => ({ label: path.basename(p), sub: p, path: p })),
|
|
157
166
|
];
|
|
158
|
-
return {
|
|
167
|
+
return {
|
|
168
|
+
generatedAt: new Date().toISOString(),
|
|
169
|
+
cwd,
|
|
170
|
+
sections,
|
|
171
|
+
targets,
|
|
172
|
+
aiStale,
|
|
173
|
+
usageAvailable,
|
|
174
|
+
changes: (0, snapshot_1.computeChanges)(sections),
|
|
175
|
+
};
|
|
159
176
|
}
|
|
160
177
|
/* DNS rebinding 対策: same-origin GET には Origin が付かないため Host 側も検証する */
|
|
161
178
|
function hostOk(req) {
|
|
@@ -197,7 +214,11 @@ function handleApi(req, res, cwd) {
|
|
|
197
214
|
return send(200, (0, summary_1.summaryStatus)());
|
|
198
215
|
if (url.pathname === '/api/file') {
|
|
199
216
|
const real = (0, manage_1.assertReadableMd)(url.searchParams.get('src') || '');
|
|
200
|
-
|
|
217
|
+
// mtime は編集画面の競合検出(/api/save の baseMtime)に使う
|
|
218
|
+
return send(200, {
|
|
219
|
+
content: fs.readFileSync(real, 'utf8'),
|
|
220
|
+
mtime: fs.statSync(real).mtimeMs,
|
|
221
|
+
});
|
|
201
222
|
}
|
|
202
223
|
throw new errors_1.ApiError('unknown-endpoint', url.pathname);
|
|
203
224
|
}
|
|
@@ -224,6 +245,22 @@ function handleApi(req, res, cwd) {
|
|
|
224
245
|
}
|
|
225
246
|
const lang = langOf(data.lang);
|
|
226
247
|
try {
|
|
248
|
+
if (url.pathname === '/api/save')
|
|
249
|
+
return send(200, (0, edit_1.doSave)(data));
|
|
250
|
+
if (url.pathname === '/api/apply-description')
|
|
251
|
+
return send(200, (0, edit_1.doApplyDescription)(data));
|
|
252
|
+
if (url.pathname === '/api/diagnose') {
|
|
253
|
+
const real = (0, manage_1.assertReadableMd)(data.src);
|
|
254
|
+
const name = data.name || path.basename(path.dirname(real));
|
|
255
|
+
(0, diagnose_1.diagnoseOne)(real, name, lang)
|
|
256
|
+
.then((d) => send(200, { ok: true, ...d }))
|
|
257
|
+
.catch((e) => send(400, (0, errors_1.toErrorBody)(e)));
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (url.pathname === '/api/changes-ack') {
|
|
261
|
+
(0, snapshot_1.ackChanges)((0, scan_1.scanSections)(cwd, lang));
|
|
262
|
+
return send(200, { ok: true });
|
|
263
|
+
}
|
|
227
264
|
if (url.pathname === '/api/copy')
|
|
228
265
|
return send(200, (0, manage_1.doCopy)(data, cwd));
|
|
229
266
|
if (url.pathname === '/api/delete')
|
|
@@ -271,6 +308,33 @@ function serveStatic(req, res) {
|
|
|
271
308
|
res.writeHead(200, { 'Content-Type': MIME[path.extname(fp)] || 'application/octet-stream' });
|
|
272
309
|
res.end(fs.readFileSync(fp));
|
|
273
310
|
}
|
|
311
|
+
/*
|
|
312
|
+
* 起動時の1〜2行サマリー(--no-open 運用でも価値が出るように)。
|
|
313
|
+
* 前回からの差分 + セッション注入トークン概算 + 未使用件数。失敗しても起動は止めない。
|
|
314
|
+
*/
|
|
315
|
+
function printStartupSummary(cwd) {
|
|
316
|
+
try {
|
|
317
|
+
const data = collect(cwd, locale_1.serverLang);
|
|
318
|
+
const ch = data.changes;
|
|
319
|
+
if (ch) {
|
|
320
|
+
console.log((0, locale_1.srvMsg)(`前回から: 追加 ${ch.added.length} / 更新 ${ch.updated.length} / 削除 ${ch.removed.length}`, `Since last run: ${ch.added.length} added / ${ch.updated.length} updated / ${ch.removed.length} removed`));
|
|
321
|
+
}
|
|
322
|
+
const sessionTokens = data.sections
|
|
323
|
+
.filter((s) => s.source !== 'project' || s.isCurrent)
|
|
324
|
+
.flatMap((s) => s.items)
|
|
325
|
+
.reduce((sum, it) => sum + (it.tokens || 0), 0);
|
|
326
|
+
const unused = data.usageAvailable
|
|
327
|
+
? data.sections.flatMap((s) => s.items).filter((it) => it.kind !== 'hook' && !it.useCount)
|
|
328
|
+
.length
|
|
329
|
+
: null;
|
|
330
|
+
console.log((0, locale_1.srvMsg)(`スキル定義のセッション注入 ≈${sessionTokens.toLocaleString()}tok` +
|
|
331
|
+
(unused !== null ? ` / 未使用 ${unused} 件` : ''), `Skill definitions inject ≈${sessionTokens.toLocaleString()} tok/session` +
|
|
332
|
+
(unused !== null ? ` / ${unused} unused` : '')));
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
/* サマリーは補助情報。失敗しても起動を妨げない */
|
|
336
|
+
}
|
|
337
|
+
}
|
|
274
338
|
function start({ port = 4763, open = true, cwd = process.cwd() } = {}, attempt = 0) {
|
|
275
339
|
const server = http.createServer((req, res) => {
|
|
276
340
|
if ((req.url || '').startsWith('/api/'))
|
|
@@ -289,6 +353,7 @@ function start({ port = 4763, open = true, cwd = process.cwd() } = {}, attempt =
|
|
|
289
353
|
server.listen(port, '127.0.0.1', () => {
|
|
290
354
|
const url = `http://127.0.0.1:${port}`;
|
|
291
355
|
console.log(`skills-viewer: ${url} (cwd: ${cwd})`);
|
|
356
|
+
printStartupSummary(cwd);
|
|
292
357
|
console.log((0, locale_1.srvMsg)('Ctrl+C で終了。ページ再読み込みで再スキャンされます。', 'Press Ctrl+C to quit. Reloading the page rescans.'));
|
|
293
358
|
if (open && process.platform === 'darwin')
|
|
294
359
|
(0, node_child_process_1.execFile)('open', [url], () => { });
|
package/build/server/scan.js
CHANGED
|
@@ -70,7 +70,18 @@ function parseFrontmatter(raw) {
|
|
|
70
70
|
value = block.join('\n').trim();
|
|
71
71
|
}
|
|
72
72
|
else {
|
|
73
|
-
|
|
73
|
+
if (/^".*"$/.test(value)) {
|
|
74
|
+
// double-quote は JSON 互換エスケープ(\" 等)を解釈する(edit.ts の書き込みと対)
|
|
75
|
+
try {
|
|
76
|
+
value = JSON.parse(value);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
value = value.slice(1, -1);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
value = value.replace(/^'|'$/g, '');
|
|
84
|
+
}
|
|
74
85
|
i++;
|
|
75
86
|
}
|
|
76
87
|
meta[key] = value;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* 前回起動時のアイテム一覧スナップショット(~/.cache/skills-viewer/snapshot.json)と
|
|
4
|
+
* 現在のスキャン結果との diff(追加・更新・削除)。「前回から何が変わったか」の基盤。
|
|
5
|
+
* - hook は同一 path・同一 name の複数定義があり識別子が安定しないため対象外
|
|
6
|
+
* - built-in は実ファイルが無く description が表示言語依存のため対象外(path='' で除外される)
|
|
7
|
+
* スナップショットの更新はユーザーの「既読」操作時のみ(差分バナーはリロードしても消えない)。
|
|
8
|
+
*/
|
|
9
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
12
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
13
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
14
|
+
}
|
|
15
|
+
Object.defineProperty(o, k2, desc);
|
|
16
|
+
}) : (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
o[k2] = m[k];
|
|
19
|
+
}));
|
|
20
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
21
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
22
|
+
}) : function(o, v) {
|
|
23
|
+
o["default"] = v;
|
|
24
|
+
});
|
|
25
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
26
|
+
var ownKeys = function(o) {
|
|
27
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
28
|
+
var ar = [];
|
|
29
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
30
|
+
return ar;
|
|
31
|
+
};
|
|
32
|
+
return ownKeys(o);
|
|
33
|
+
};
|
|
34
|
+
return function (mod) {
|
|
35
|
+
if (mod && mod.__esModule) return mod;
|
|
36
|
+
var result = {};
|
|
37
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
38
|
+
__setModuleDefault(result, mod);
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
})();
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.buildSnapshot = buildSnapshot;
|
|
44
|
+
exports.diffSnapshot = diffSnapshot;
|
|
45
|
+
exports.computeChanges = computeChanges;
|
|
46
|
+
exports.ackChanges = ackChanges;
|
|
47
|
+
const fs = __importStar(require("node:fs"));
|
|
48
|
+
const os = __importStar(require("node:os"));
|
|
49
|
+
const path = __importStar(require("node:path"));
|
|
50
|
+
const summary_1 = require("./summary");
|
|
51
|
+
const SNAPSHOT_FILE = path.join(os.homedir(), '.cache', 'skills-viewer', 'snapshot.json');
|
|
52
|
+
function buildSnapshot(sections) {
|
|
53
|
+
const snap = {};
|
|
54
|
+
for (const s of sections) {
|
|
55
|
+
for (const it of s.items) {
|
|
56
|
+
if (it.kind === 'hook' || !it.path)
|
|
57
|
+
continue;
|
|
58
|
+
snap[it.path] = { name: it.name, kind: it.kind, hash: (0, summary_1.contentHash)(it.path) };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return snap;
|
|
62
|
+
}
|
|
63
|
+
function loadSnapshot() {
|
|
64
|
+
try {
|
|
65
|
+
return JSON.parse(fs.readFileSync(SNAPSHOT_FILE, 'utf8'));
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function saveSnapshot(snap) {
|
|
72
|
+
try {
|
|
73
|
+
fs.mkdirSync(path.dirname(SNAPSHOT_FILE), { recursive: true });
|
|
74
|
+
fs.writeFileSync(SNAPSHOT_FILE, JSON.stringify(snap, null, 1));
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
/* キャッシュが書けなくても本体機能には影響させない */
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function diffSnapshot(prev, cur) {
|
|
81
|
+
const added = [];
|
|
82
|
+
const updated = [];
|
|
83
|
+
const removed = [];
|
|
84
|
+
for (const [p, e] of Object.entries(cur)) {
|
|
85
|
+
const old = prev[p];
|
|
86
|
+
if (!old)
|
|
87
|
+
added.push({ name: e.name, kind: e.kind, path: p });
|
|
88
|
+
else if (old.hash !== e.hash)
|
|
89
|
+
updated.push({ name: e.name, kind: e.kind, path: p });
|
|
90
|
+
}
|
|
91
|
+
for (const [p, e] of Object.entries(prev)) {
|
|
92
|
+
if (!cur[p])
|
|
93
|
+
removed.push({ name: e.name, kind: e.kind, path: p });
|
|
94
|
+
}
|
|
95
|
+
return { added, updated, removed };
|
|
96
|
+
}
|
|
97
|
+
/*
|
|
98
|
+
* 現在のスキャン結果と前回スナップショットの差分。
|
|
99
|
+
* 初回(スナップショット無し)は全件が「追加」になってしまうため、基準だけ保存して null。
|
|
100
|
+
*/
|
|
101
|
+
function computeChanges(sections) {
|
|
102
|
+
const cur = buildSnapshot(sections);
|
|
103
|
+
const prev = loadSnapshot();
|
|
104
|
+
if (!prev) {
|
|
105
|
+
saveSnapshot(cur);
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const d = diffSnapshot(prev, cur);
|
|
109
|
+
return d.added.length || d.updated.length || d.removed.length ? d : null;
|
|
110
|
+
}
|
|
111
|
+
/* 「既読にする」: 現在の状態を新しい基準として保存する */
|
|
112
|
+
function ackChanges(sections) {
|
|
113
|
+
saveSnapshot(buildSnapshot(sections));
|
|
114
|
+
}
|
package/build/server/summary.js
CHANGED
|
@@ -40,6 +40,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
40
40
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
41
41
|
exports.loadSummaries = loadSummaries;
|
|
42
42
|
exports.contentHash = contentHash;
|
|
43
|
+
exports.runHaiku = runHaiku;
|
|
43
44
|
exports.summarizeOne = summarizeOne;
|
|
44
45
|
exports.parseAnalysis = parseAnalysis;
|
|
45
46
|
exports.saveSummary = saveSummary;
|
|
@@ -145,22 +146,12 @@ function buildPrompt(it, refs, content, lang) {
|
|
|
145
146
|
content);
|
|
146
147
|
}
|
|
147
148
|
/*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
149
|
+
* claude CLI headless (haiku) にプロンプトを渡して生テキストを得る共通実行部。
|
|
150
|
+
* --tools '' で全ツールを無効化: SKILL.md は clone したリポジトリ由来もあり得るため、
|
|
151
|
+
* 本文に指示が仕込まれていても純粋なテキスト生成の外に出られないようにする
|
|
150
152
|
*/
|
|
151
|
-
function
|
|
153
|
+
function runHaiku(prompt) {
|
|
152
154
|
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
155
|
const child = (0, node_child_process_1.spawn)('claude', ['-p', '--model', 'haiku', '--tools', ''], {
|
|
165
156
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
166
157
|
});
|
|
@@ -184,11 +175,21 @@ function summarizeOne(it, lang) {
|
|
|
184
175
|
const text = out.trim();
|
|
185
176
|
if (code !== 0 || !text)
|
|
186
177
|
return reject(new Error(errOut.trim().slice(0, 200) || 'claude exited ' + code));
|
|
187
|
-
resolve(
|
|
178
|
+
resolve(text);
|
|
188
179
|
});
|
|
189
180
|
child.stdin.end(prompt);
|
|
190
181
|
});
|
|
191
182
|
}
|
|
183
|
+
/*
|
|
184
|
+
* 1 skill を haiku で分析し {summary, invocation, invocationReason, relations} を返す。
|
|
185
|
+
* relations の候補(refs)は静的解析で抽出済みの既知 skill 名のみに制限し、幻覚を防ぐ。
|
|
186
|
+
*/
|
|
187
|
+
async function summarizeOne(it, lang) {
|
|
188
|
+
const content = fs.readFileSync(it.path, 'utf8').slice(0, 12000);
|
|
189
|
+
const refs = it.refs || [];
|
|
190
|
+
const text = await runHaiku(buildPrompt(it, refs, content, lang));
|
|
191
|
+
return parseAnalysis(text, refs);
|
|
192
|
+
}
|
|
192
193
|
/* haiku の出力を検証つきでパース。壊れていたら全文を summary として扱う */
|
|
193
194
|
function parseAnalysis(text, refs) {
|
|
194
195
|
const stripped = text
|
package/build/server/usage.js
CHANGED
|
@@ -41,6 +41,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
41
41
|
};
|
|
42
42
|
})();
|
|
43
43
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
exports.dayKey = dayKey;
|
|
44
45
|
exports.extractHits = extractHits;
|
|
45
46
|
exports.scanUsageByDir = scanUsageByDir;
|
|
46
47
|
exports.encodeProjectPath = encodeProjectPath;
|
|
@@ -48,6 +49,12 @@ const fs = __importStar(require("node:fs"));
|
|
|
48
49
|
const os = __importStar(require("node:os"));
|
|
49
50
|
const path = __importStar(require("node:path"));
|
|
50
51
|
const node_string_decoder_1 = require("node:string_decoder");
|
|
52
|
+
/* ローカルタイムゾーンの日付キー。web 側のスパークラインと同じ形式であること */
|
|
53
|
+
function dayKey(ts) {
|
|
54
|
+
const d = new Date(ts);
|
|
55
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
56
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
|
|
57
|
+
}
|
|
51
58
|
const usageCache = new Map();
|
|
52
59
|
function scanLine(line, hits) {
|
|
53
60
|
const isCmd = line.includes('<command-name>');
|
|
@@ -150,10 +157,14 @@ function scanUsageByDir() {
|
|
|
150
157
|
usageCache.set(fp, entry);
|
|
151
158
|
}
|
|
152
159
|
for (const h of entry.hits) {
|
|
153
|
-
const a = agg[h.name] || (agg[h.name] = { typed: 0, auto: 0, last: 0 });
|
|
160
|
+
const a = agg[h.name] || (agg[h.name] = { typed: 0, auto: 0, last: 0, daily: {} });
|
|
154
161
|
a[h.via === 'typed' ? 'typed' : 'auto']++;
|
|
155
162
|
if (h.ts > a.last)
|
|
156
163
|
a.last = h.ts;
|
|
164
|
+
if (h.ts > 0) {
|
|
165
|
+
const day = dayKey(h.ts);
|
|
166
|
+
a.daily[day] = (a.daily[day] || 0) + 1;
|
|
167
|
+
}
|
|
157
168
|
}
|
|
158
169
|
}
|
|
159
170
|
}
|