skills-viewer 0.6.0 → 0.8.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
@@ -1,6 +1,6 @@
1
1
  # skills-viewer
2
2
 
3
- Browse, search and manage your [Claude Code](https://code.claude.com) skills, commands, agents and hooks in the browser.
3
+ Browse, search and manage your [Claude Code](https://code.claude.com) skills, commands, agents, hooks and auto memory in the browser.
4
4
 
5
5
  Scans every project registered in `~/.claude.json` (plus user scope, plugins and built-ins), and serves a local web UI to explore them — with usage stats, AI-generated summaries, full SKILL.md rendering, same-name diffs, and copy/delete management.
6
6
 
@@ -19,11 +19,12 @@ npx skills-viewer
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
  - **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
21
21
  - **Search / sort** — incremental search over name + description + usage; sort by name, usage count, last used, updated date, or token cost
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
+ - **Diagnostics** — a _no recent use_ badge (no recorded use within the transcript retention window) with an all / recent use / no recent use 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
23
23
  - **Token cost** — since every name + description is injected into each session, the estimated token overhead is shown per item, per scope, and as a per-session total for the current project
24
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
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
26
  - **AI model choice** — pick the model behind all AI features (haiku default / sonnet / opus) in settings; aliases are resolved by your claude CLI
27
+ - **Memory triage** — a _Memory_ view lists Claude Code's auto memory (`~/.claude/projects/<project>/memory/`) per project with its context cost split into the always-on part (the `MEMORY.md` index line injected into every session) and the pay-per-use part (the body, read on demand), plus Read / Write counts from transcripts, `[[link]]` resolution and backlinks. **AI triage** reads every memory of a project with the claude CLI (one call, split into a few for very large projects) and proposes a destination per memory — keep / shrink / move to CLAUDE.md / move to docs / move to a skill / delete / wrong project — with the reasoning and a paste-ready instruction for Claude Code (it is asked to always cover removing the `MEMORY.md` index line and re-pointing `[[link]]`s). Each memory is first judged for freshness (current / outdated / historical / obsolete) from mechanical signals — dates in the body, missing paths, merged or deleted branches, references to another project — and a _rewrite the body_ verdict covers memories whose gist still holds; the triage also flags when the `MEMORY.md` index line disagrees with the body. For feedback memories the instruction is built from a fixed template, so it reads the same on every model. The viewer never writes to memory: you paste the instruction into Claude Code, which inspects, asks when unsure, and executes after your approval
27
28
  - **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/`
28
29
  - **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
29
30
  - **Usage sparkline** — the detail pane charts the last 30 days of per-day usage
@@ -35,7 +36,7 @@ npx skills-viewer
35
36
  - **Same-name diff** — when the same skill name exists in multiple scopes, the detail page lists the other definitions and shows a line diff between them
36
37
  - **Open in editor** — via URL scheme (VS Code / Cursor / Zed / Windsurf / custom, configurable in the ⚙ settings modal), or the OS default opener
37
38
  - **English / 日本語** — UI language auto-detected from the browser and switchable in settings; AI summaries are generated in the selected language (CLI messages follow `LANG`)
38
- - **URL routing** — `/skills/:id?tab=md`, search/sort/grouping in query params; links are shareable across reloads
39
+ - **URL routing** — `/skills/:id?tab=md`, `/memory/:id`, `/memory/triage/:project`, search/sort/grouping in query params; links are shareable across reloads
39
40
 
40
41
  ## Usage
41
42
 
@@ -77,11 +78,12 @@ dist/ # prebuilt UI shipped in the npm package (generated by prepack
77
78
 
78
79
  ## Notes
79
80
 
80
- - Usage stats only cover the transcript retention window of Claude Code (`cleanupPeriodDays`, default 30 days) — the _unused_ badge has the same limitation
81
+ - Usage stats only cover the transcript retention window of Claude Code (`cleanupPeriodDays`, default 30 days) — the _no recent use_ badge and memory Read counts have the same limitation (projects without transcripts show no usage columns at all)
81
82
  - Token costs are heuristic estimates (≈4 chars/token for ASCII, ≈1.5 for CJK), not exact tokenizer counts
82
83
  - Usage is attributed per calling project (worktrees roll up to their parent project by encoded-path prefix). When scopes share a name, the resolution order project > user > plugin > built-in is assumed
83
84
  - The built-in skill list is hardcoded in `server/scan.js` (they live inside the Claude Code binary); check `/skills` inside Claude Code for the authoritative list
84
85
  - AI summaries require a logged-in `claude` CLI
86
+ - Memory triage sends each memory's body to the claude CLI, together with the project's `MEMORY.md` index, the **headings** of `CLAUDE.md` / `.claude/CLAUDE.md` / `~/.claude/CLAUDE.md` and the names + descriptions of the skills available to that project (never their bodies) so it can spot duplicates and promotion targets. Branch status for the freshness signals comes from local `git` (no network). Results are cached per memory by content hash (plus the index line); memory files themselves are never modified
85
87
 
86
88
  ## License
87
89
 
package/build/cli.js CHANGED
File without changes
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ /*
3
+ * AI 機能のキャッシュ(~/.cache/skills-viewer/*.json)の共通処理。
4
+ * どのキャッシュも「実パス → エントリ」の形なので、掃除の規則を 1 箇所に置く。
5
+ */
6
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
+ if (k2 === undefined) k2 = k;
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
13
+ }) : (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ o[k2] = m[k];
16
+ }));
17
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
18
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
19
+ }) : function(o, v) {
20
+ o["default"] = v;
21
+ });
22
+ var __importStar = (this && this.__importStar) || (function () {
23
+ var ownKeys = function(o) {
24
+ ownKeys = Object.getOwnPropertyNames || function (o) {
25
+ var ar = [];
26
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
27
+ return ar;
28
+ };
29
+ return ownKeys(o);
30
+ };
31
+ return function (mod) {
32
+ if (mod && mod.__esModule) return mod;
33
+ var result = {};
34
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
35
+ __setModuleDefault(result, mod);
36
+ return result;
37
+ };
38
+ })();
39
+ Object.defineProperty(exports, "__esModule", { value: true });
40
+ exports.pruneMissing = pruneMissing;
41
+ const fs = __importStar(require("node:fs"));
42
+ /*
43
+ * 消えたファイル(削除・リネーム・別プロジェクトの撤去)のエントリを落とす。
44
+ * 呼ぶのは保存時だけにする — GET(読み取り API)で書き込みを発生させないため。
45
+ */
46
+ function pruneMissing(store) {
47
+ const out = {};
48
+ for (const [key, value] of Object.entries(store)) {
49
+ if (fs.existsSync(key))
50
+ out[key] = value;
51
+ }
52
+ return out;
53
+ }
@@ -45,6 +45,7 @@ exports.attachDiagnoses = attachDiagnoses;
45
45
  const fs = __importStar(require("node:fs"));
46
46
  const os = __importStar(require("node:os"));
47
47
  const path = __importStar(require("node:path"));
48
+ const cache_1 = require("./cache");
48
49
  const summary_1 = require("./summary");
49
50
  const DIAG_FILE = path.join(os.homedir(), '.cache', 'skills-viewer', 'diagnoses.json');
50
51
  function loadDiagnoses() {
@@ -57,7 +58,8 @@ function loadDiagnoses() {
57
58
  }
58
59
  function saveDiagnoses(store) {
59
60
  fs.mkdirSync(path.dirname(DIAG_FILE), { recursive: true });
60
- fs.writeFileSync(DIAG_FILE, JSON.stringify(store, null, 1));
61
+ // 保存のついでに死にエントリを掃除する(GET では書き込まないので掃除もしない)
62
+ fs.writeFileSync(DIAG_FILE, JSON.stringify((0, cache_1.pruneMissing)(store), null, 1));
61
63
  }
62
64
  function buildPrompt(name, content, lang) {
63
65
  if (lang === 'ja') {
@@ -45,8 +45,15 @@ exports.attachFlows = attachFlows;
45
45
  const fs = __importStar(require("node:fs"));
46
46
  const os = __importStar(require("node:os"));
47
47
  const path = __importStar(require("node:path"));
48
+ const cache_1 = require("./cache");
48
49
  const summary_1 = require("./summary");
49
50
  const FLOW_FILE = path.join(os.homedir(), '.cache', 'skills-viewer', 'flows.json');
51
+ /*
52
+ * 抽出スキーマの世代。branches.to(ループ/スキップ)追加で 2。
53
+ * 旧世代キャッシュも表示には使い続ける(フローチャート描画は to 無しでも成立し、
54
+ * ループ矢印だけ出ない)が、再抽出時は stale 扱いして新スキーマで作り直す。
55
+ */
56
+ const FLOW_SCHEMA_V = 2;
50
57
  function loadFlows() {
51
58
  try {
52
59
  return JSON.parse(fs.readFileSync(FLOW_FILE, 'utf8'));
@@ -57,7 +64,8 @@ function loadFlows() {
57
64
  }
58
65
  function saveFlows(store) {
59
66
  fs.mkdirSync(path.dirname(FLOW_FILE), { recursive: true });
60
- fs.writeFileSync(FLOW_FILE, JSON.stringify(store, null, 1));
67
+ // 保存のついでに死にエントリを掃除する(GET では書き込まないので掃除もしない)
68
+ fs.writeFileSync(FLOW_FILE, JSON.stringify((0, cache_1.pruneMissing)(store), null, 1));
61
69
  }
62
70
  function buildPrompt(name, content, lang) {
63
71
  if (lang === 'ja') {
@@ -65,12 +73,13 @@ function buildPrompt(name, content, lang) {
65
73
  '{"steps": [{"title": "ステップ名(10字程度)", "detail": "何をするか(25字程度)",\n' +
66
74
  ' "calls": ["このステップで起動/委譲する他の skill・コマンド名"],\n' +
67
75
  ' "gate": "human" | "auto" | null,\n' +
68
- ' "branches": [{"when": "分岐条件(15字程度)", "then": "その場合の挙動(20字程度)"}]}]}\n\n' +
76
+ ' "branches": [{"when": "分岐条件(15字程度)", "then": "その場合の挙動(20字程度)", "to": 行き先ステップ番号}]}]}\n\n' +
69
77
  '制約:\n' +
70
78
  '- steps は実行順に 4〜8 個(単純な skill なら少なくてよい)\n' +
71
79
  '- gate は人間の確認/承認を待つステップだけ "human"(自動で進むなら "auto"、該当なしは null)\n' +
72
80
  '- calls は本文に実際に登場する名前のみ(幻覚禁止)\n' +
73
- '- branches は中断・フォールバック等の分岐だけ(無ければ省略)\n\n' +
81
+ '- branches は中断・フォールバック等の分岐だけ(無ければ省略)。when は「テスト失敗」のような判定できる条件文にする\n' +
82
+ '- to は分岐が別ステップへ移るときだけ 1 始まりの番号で(リトライ/ループで前へ戻る場合が典型)。単なる中断・終了なら省略\n\n' +
74
83
  '# skill: ' +
75
84
  name +
76
85
  '\n\n' +
@@ -80,12 +89,13 @@ function buildPrompt(name, content, lang) {
80
89
  '{"steps": [{"title": "step name (2-4 words)", "detail": "what it does (about 10 words)",\n' +
81
90
  ' "calls": ["other skill/command names this step invokes or delegates to"],\n' +
82
91
  ' "gate": "human" | "auto" | null,\n' +
83
- ' "branches": [{"when": "branch condition (about 5 words)", "then": "behavior in that case (about 7 words)"}]}]}\n\n' +
92
+ ' "branches": [{"when": "branch condition (about 5 words)", "then": "behavior in that case (about 7 words)", "to": target step number}]}]}\n\n' +
84
93
  'Constraints:\n' +
85
94
  '- 4 to 8 steps in execution order (fewer is fine for simple skills)\n' +
86
95
  '- gate is "human" ONLY for steps that wait for human confirmation/approval ("auto" if it proceeds automatically, null otherwise)\n' +
87
96
  '- calls may contain only names that actually appear in the body (no hallucination)\n' +
88
- '- branches only for aborts / fallbacks / real forks (omit when none)\n\n' +
97
+ '- branches only for aborts / fallbacks / real forks (omit when none); "when" must be a checkable condition like "tests fail"\n' +
98
+ '- 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' +
89
99
  '# skill: ' +
90
100
  name +
91
101
  '\n\n' +
@@ -116,6 +126,7 @@ function parseFlow(text) {
116
126
  .map((b) => ({
117
127
  when: String(b.when).slice(0, 60),
118
128
  then: String(b.then || '').slice(0, 80),
129
+ ...(Number.isInteger(b.to) && b.to >= 1 ? { to: b.to } : {}),
119
130
  }))
120
131
  .slice(0, 4),
121
132
  });
@@ -124,18 +135,30 @@ function parseFlow(text) {
124
135
  }
125
136
  if (!steps.length)
126
137
  throw new Error('no steps in output');
138
+ // to の上限検証は全 step が出揃ってから(範囲外は to だけ捨てて分岐テキストは残す)
139
+ for (const st of steps)
140
+ for (const b of st.branches)
141
+ if (b.to !== undefined && b.to > steps.length)
142
+ delete b.to;
127
143
  return { steps };
128
144
  }
129
145
  async function flowOne(realPath, name, lang, model = 'haiku') {
130
146
  const hash = (0, summary_1.contentHash)(realPath);
131
147
  const store = loadFlows();
132
148
  const cached = store[realPath];
133
- if (cached && cached.hash === hash && cached.lang === lang) {
149
+ if (cached && cached.hash === hash && cached.lang === lang && cached.v === FLOW_SCHEMA_V) {
134
150
  return { steps: cached.steps };
135
151
  }
136
152
  const content = fs.readFileSync(realPath, 'utf8').slice(0, 12000);
137
153
  const result = parseFlow(await (0, summary_1.runClaude)(buildPrompt(name, content, lang), model));
138
- store[realPath] = { ...result, hash, lang, model, generatedAt: new Date().toISOString() };
154
+ store[realPath] = {
155
+ ...result,
156
+ hash,
157
+ lang,
158
+ model,
159
+ v: FLOW_SCHEMA_V,
160
+ generatedAt: new Date().toISOString(),
161
+ };
139
162
  saveFlows(store);
140
163
  return result;
141
164
  }
@@ -46,12 +46,14 @@ const crypto = __importStar(require("node:crypto"));
46
46
  const node_child_process_1 = require("node:child_process");
47
47
  const scan_1 = require("./scan");
48
48
  const usage_1 = require("./usage");
49
+ const memory_1 = require("./memory");
49
50
  const summary_1 = require("./summary");
50
51
  const manage_1 = require("./manage");
51
52
  const edit_1 = require("./edit");
52
53
  const diagnose_1 = require("./diagnose");
53
54
  const flow_1 = require("./flow");
54
55
  const groups_1 = require("./groups");
56
+ const memory_triage_1 = require("./memory-triage");
55
57
  const snapshot_1 = require("./snapshot");
56
58
  const errors_1 = require("./errors");
57
59
  const locale_1 = require("./locale");
@@ -134,6 +136,41 @@ function attributeUsage(sections) {
134
136
  }
135
137
  return Object.keys(byDir).length > 0;
136
138
  }
139
+ /*
140
+ * memory の Read(参照)/ Write・Edit(作成・更新)実績を付与する。
141
+ * skill と違って帰属先の解決は不要で、Read の file_path がそのまま実ファイルを指す。
142
+ * usageAvailable は「そのプロジェクトのトランスクリプトがあるか」= エンコード名で始まる
143
+ * ディレクトリ(worktree 分を含む)に jsonl が 1 件以上あるか。false なら Read 列は出さない。
144
+ */
145
+ function attributeMemoryUsage(memory) {
146
+ if (!memory.length)
147
+ return;
148
+ const { byPath, dirsWithTranscripts } = (0, usage_1.scanMemoryUsage)();
149
+ for (const sec of memory) {
150
+ sec.usageAvailable = (0, usage_1.hasTranscripts)(dirsWithTranscripts, sec.id);
151
+ for (const it of sec.items) {
152
+ const u = byPath[it.path];
153
+ if (!u)
154
+ continue;
155
+ if (u.reads > 0) {
156
+ it.useCount = u.reads;
157
+ it.lastUsed = u.lastRead;
158
+ it.dailyUse = u.daily;
159
+ }
160
+ if (u.writes > 0)
161
+ it.writeCount = u.writes;
162
+ }
163
+ }
164
+ }
165
+ /*
166
+ * 実績付きの memory セクション一覧。/api/skills だけでなく /api/memory-triage からも
167
+ * 同じ事実(Read / W-E / usageAvailable)をプロンプトに載せる必要があるので共通化する。
168
+ */
169
+ function memorySections(cwd) {
170
+ const memory = (0, memory_1.scanMemory)(cwd);
171
+ attributeMemoryUsage(memory);
172
+ return memory;
173
+ }
137
174
  function collect(cwd, lang) {
138
175
  const sections = (0, scan_1.scanSections)(cwd, lang);
139
176
  const usageAvailable = attributeUsage(sections);
@@ -162,6 +199,9 @@ function collect(cwd, lang) {
162
199
  (0, flow_1.attachFlows)(sections, lang);
163
200
  const grp = (0, groups_1.attachGroups)(sections, lang);
164
201
  const aiStale = (0, summary_1.staleItems)(sections, lang).length;
202
+ // memory は「呼び出す」ものではないので sections には混ぜず、別配列で同乗させる
203
+ const memory = memorySections(cwd);
204
+ (0, memory_triage_1.attachMemoryTriage)(memory, lang);
165
205
  const targets = [
166
206
  { label: 'user skills', sub: '~/.claude/skills/', path: scan_1.HOME },
167
207
  ...(0, scan_1.listProjects)(cwd)
@@ -178,6 +218,7 @@ function collect(cwd, lang) {
178
218
  changes: (0, snapshot_1.computeChanges)(sections),
179
219
  ...(grp.groups ? { groups: grp.groups } : {}),
180
220
  ...(grp.stale ? { groupsStale: true } : {}),
221
+ ...(memory.length ? { memory } : {}),
181
222
  };
182
223
  }
183
224
  /* DNS rebinding 対策: same-origin GET には Origin が付かないため Host 側も検証する */
@@ -291,6 +332,26 @@ function handleApi(req, res, cwd) {
291
332
  .catch((e) => send(400, (0, errors_1.toErrorBody)(e)));
292
333
  return;
293
334
  }
335
+ if (url.pathname === '/api/memory-triage') {
336
+ // 1 プロジェクト分をまとめて 1 回の claude 呼び出しで棚卸しする(結果は再取得で反映)
337
+ const project = String(data.project || '');
338
+ const sec = memorySections(cwd).find((s) => s.id === project);
339
+ if (!sec)
340
+ throw new errors_1.ApiError('not-found', project);
341
+ const files = Array.isArray(data.files)
342
+ ? data.files.filter((f) => typeof f === 'string')
343
+ : undefined;
344
+ // sections は「CLAUDE.md / skill に既に書いてある」「skill へ昇格」を判定させる文脈。
345
+ // AI を呼ぶときだけ要るので、フルスキャンは関数で渡して遅延させる
346
+ (0, memory_triage_1.triageProject)(sec, lang, model, {
347
+ force: !!data.force,
348
+ files,
349
+ sections: () => (0, scan_1.scanSections)(cwd, lang),
350
+ })
351
+ .then((results) => send(200, { ok: true, results }))
352
+ .catch((e) => send(400, (0, errors_1.toErrorBody)(e)));
353
+ return;
354
+ }
294
355
  if (url.pathname === '/api/summarize') {
295
356
  const real = (0, manage_1.assertReadableMd)(data.src);
296
357
  // refs(関係候補)はスキャン結果から復元する
@@ -332,7 +393,7 @@ function serveStatic(req, res) {
332
393
  }
333
394
  /*
334
395
  * 起動時の1〜2行サマリー(--no-open 運用でも価値が出るように)。
335
- * 前回からの差分 + セッション注入トークン概算 + 未使用件数。失敗しても起動は止めない。
396
+ * 前回からの差分 + セッション注入トークン概算 + 直近未使用の件数。失敗しても起動は止めない。
336
397
  */
337
398
  function printStartupSummary(cwd) {
338
399
  try {
@@ -350,8 +411,8 @@ function printStartupSummary(cwd) {
350
411
  .length
351
412
  : null;
352
413
  console.log((0, locale_1.srvMsg)(`スキル定義のセッション注入 ≈${sessionTokens.toLocaleString()}tok` +
353
- (unused !== null ? ` / 未使用 ${unused} 件` : ''), `Skill definitions inject ≈${sessionTokens.toLocaleString()} tok/session` +
354
- (unused !== null ? ` / ${unused} unused` : '')));
414
+ (unused !== null ? ` / 直近未使用 ${unused} 件` : ''), `Skill definitions inject ≈${sessionTokens.toLocaleString()} tok/session` +
415
+ (unused !== null ? ` / ${unused} with no recent use` : '')));
355
416
  }
356
417
  catch {
357
418
  /* サマリーは補助情報。失敗しても起動を妨げない */