skills-viewer 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -17,7 +17,9 @@ 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
- - **Search / sort** — incremental search over name + description + usage; sort by name, usage count, last used, or updated date
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-only 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
+ - **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
21
23
  - **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
22
24
  - **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
23
25
  - **Detail pane** — master/detail layout with an Overview tab (description, usage, bundled files) and a rendered SKILL.md tab
@@ -68,7 +70,8 @@ dist/ # prebuilt UI shipped in the npm package (generated by prepack
68
70
 
69
71
  ## Notes
70
72
 
71
- - Usage stats only cover the transcript retention window of Claude Code (`cleanupPeriodDays`, default 30 days)
73
+ - Usage stats only cover the transcript retention window of Claude Code (`cleanupPeriodDays`, default 30 days) — the _unused_ badge has the same limitation
74
+ - Token costs are heuristic estimates (≈4 chars/token for ASCII, ≈1.5 for CJK), not exact tokenizer counts
72
75
  - 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
73
76
  - 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
74
77
  - AI summaries require a logged-in `claude` CLI
@@ -67,6 +67,7 @@ const MIME = {
67
67
  * 呼び出し元プロジェクト(トランスクリプトのディレクトリ)を特定し、そのプロジェクトの
68
68
  * skill → user → plugin → built-in の解決優先順で1つの定義にだけ加算する。
69
69
  * 呼び出し元が未知(worktree・削除済みプロジェクト等)の場合は user 以降にフォールバック。
70
+ * 戻り値はトランスクリプトが1件でも存在したか(false なら「未使用」判定は無意味)。
70
71
  */
71
72
  function attributeUsage(sections) {
72
73
  const byDir = (0, usage_1.scanUsageByDir)();
@@ -121,10 +122,11 @@ function attributeUsage(sections) {
121
122
  target.lastUsed = Math.max(target.lastUsed || 0, u.last);
122
123
  }
123
124
  }
125
+ return Object.keys(byDir).length > 0;
124
126
  }
125
127
  function collect(cwd, lang) {
126
128
  const sections = (0, scan_1.scanSections)(cwd, lang);
127
- attributeUsage(sections);
129
+ const usageAvailable = attributeUsage(sections);
128
130
  const summaries = (0, summary_1.loadSummaries)();
129
131
  for (const sec of sections) {
130
132
  for (const it of sec.items) {
@@ -153,7 +155,7 @@ function collect(cwd, lang) {
153
155
  .sort((a, b) => path.basename(a).localeCompare(path.basename(b)))
154
156
  .map((p) => ({ label: path.basename(p), sub: p, path: p })),
155
157
  ];
156
- return { generatedAt: new Date().toISOString(), cwd, sections, targets, aiStale };
158
+ return { generatedAt: new Date().toISOString(), cwd, sections, targets, aiStale, usageAvailable };
157
159
  }
158
160
  /* DNS rebinding 対策: same-origin GET には Origin が付かないため Host 側も検証する */
159
161
  function hostOk(req) {
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ /*
3
+ * description の静的リントと、セッション注入トークン量の概算。
4
+ * Claude Code はスキル選択のために frontmatter の name + description を毎セッション
5
+ * コンテキストへ注入する。lint は「モデルが発動判断に使える description か」の観点で、
6
+ * frontmatter の meta(本文フォールバック前)に対して行う。
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.estimateTokens = estimateTokens;
10
+ exports.lintItem = lintItem;
11
+ /*
12
+ * トークン数の概算(厳密なトークナイザは依存が重いので入れない)。
13
+ * 目安: ASCII ≈ 4 文字/トークン、非 ASCII(日本語等)≈ 1.5 文字/トークン。
14
+ */
15
+ function estimateTokens(text) {
16
+ let ascii = 0;
17
+ let other = 0;
18
+ for (const ch of text) {
19
+ if (ch.charCodeAt(0) < 128)
20
+ ascii++;
21
+ else
22
+ other++;
23
+ }
24
+ return Math.ceil(ascii / 4 + other / 1.5);
25
+ }
26
+ /*
27
+ * 発動条件らしい語句。en は "use when/for …"・条件節、ja は「〜のときに使用」系。
28
+ * command は人間が明示的に打つ起点なので発動条件は要求しない(呼び出し側で除外)。
29
+ */
30
+ const TRIGGER_RE = /\b(use|when|whenever|if|for|before|after|trigger|invoke)\b|使用|使う|使って|用い|とき|時に|場合|際|なら|向け/i;
31
+ const SHORT_LIMIT = 30;
32
+ const LONG_LIMIT = 1024;
33
+ /*
34
+ * meta: frontmatter のパース結果。name はフォールバック解決後の表示名。
35
+ * 戻り値は言語非依存のコード(表示ラベルは web の i18n で解決)。
36
+ */
37
+ function lintItem(meta, name, kind) {
38
+ if (kind === 'hook')
39
+ return [];
40
+ const warnings = [];
41
+ const desc = (meta.description || '').trim();
42
+ if (!desc)
43
+ return ['no-description'];
44
+ if (desc.length < SHORT_LIMIT)
45
+ warnings.push('short-description');
46
+ if (desc.length > LONG_LIMIT)
47
+ warnings.push('long-description');
48
+ // 記号を除いて比較し、name をほぼ繰り返しただけの description を検出
49
+ const norm = (s) => s.toLowerCase().replace(/[^a-z0-9ぁ-んァ-ン一-龠]/g, '');
50
+ if (norm(desc) === norm(name))
51
+ warnings.push('name-echo');
52
+ // skill / agent はモデルが description を見て自動発動するため、発動条件の記述を求める
53
+ if ((kind === 'skill' || kind === 'agent') && !TRIGGER_RE.test(desc)) {
54
+ warnings.push('no-trigger');
55
+ }
56
+ return warnings;
57
+ }
@@ -41,6 +41,7 @@ exports.scanSections = scanSections;
41
41
  const fs = __importStar(require("node:fs"));
42
42
  const os = __importStar(require("node:os"));
43
43
  const path = __importStar(require("node:path"));
44
+ const lint_1 = require("./lint");
44
45
  exports.HOME = os.homedir();
45
46
  /* ---------- frontmatter parsing (minimal YAML: scalars + block scalars) ---------- */
46
47
  function parseFrontmatter(raw) {
@@ -129,8 +130,10 @@ function readSkillDir(dir, nameHint) {
129
130
  return null; // 権限エラー等で読めない skill はスキップ(一覧全体を落とさない)
130
131
  }
131
132
  const { meta, body } = parseFrontmatter(raw);
133
+ const name = meta.name || nameHint;
134
+ const lint = (0, lint_1.lintItem)(meta, name, 'skill');
132
135
  return {
133
- name: meta.name || nameHint,
136
+ name,
134
137
  description: meta.description || firstBodyLine(body),
135
138
  argumentHint: meta['argument-hint'] || '',
136
139
  version: meta.version || '',
@@ -138,6 +141,7 @@ function readSkillDir(dir, nameHint) {
138
141
  path: skillMd,
139
142
  updatedAt: fileMtime(skillMd),
140
143
  files: listFiles(dir).sort(),
144
+ ...(lint.length ? { lint } : {}),
141
145
  _body: body, // 参照抽出用(scanSections で refs 化して破棄)
142
146
  };
143
147
  }
@@ -171,8 +175,10 @@ function scanMdRoot(root, kind) {
171
175
  continue; // 読めないファイルはスキップ(一覧全体を落とさない)
172
176
  }
173
177
  const { meta, body } = parseFrontmatter(raw);
178
+ const name = meta.name || entry.name.replace(/\.md$/, '');
179
+ const lint = (0, lint_1.lintItem)(meta, name, kind);
174
180
  items.push({
175
- name: meta.name || entry.name.replace(/\.md$/, ''),
181
+ name,
176
182
  description: meta.description || firstBodyLine(body),
177
183
  argumentHint: kind === 'command' ? meta['argument-hint'] || '' : '',
178
184
  version: meta.version || '',
@@ -180,6 +186,7 @@ function scanMdRoot(root, kind) {
180
186
  path: fp,
181
187
  updatedAt: fileMtime(fp),
182
188
  files: [entry.name],
189
+ ...(lint.length ? { lint } : {}),
183
190
  _body: body,
184
191
  });
185
192
  }
@@ -440,5 +447,13 @@ function scanSections(cwd, lang = 'en') {
440
447
  },
441
448
  ];
442
449
  attachRefs(sections);
450
+ // name + description は毎セッション注入されるため、その分のトークンを概算しておく。
451
+ // hook は設定エントリ(description 注入なし)なので対象外
452
+ for (const s of sections) {
453
+ for (const it of s.items) {
454
+ if (it.kind !== 'hook')
455
+ it.tokens = (0, lint_1.estimateTokens)(it.name + ': ' + it.description);
456
+ }
457
+ }
443
458
  return sections;
444
459
  }
@@ -1 +1 @@
1
- *{box-sizing:border-box}body{margin:0;background:#faf9f7;color:#1b1a17;font-family:Helvetica Neue,system-ui,-apple-system,Hiragino Sans,sans-serif}button{font-family:inherit}.mono{font-family:ui-monospace,SF Mono,Menlo,monospace}.wrap{margin:0 auto;min-height:100vh}.wrap.fixed{max-width:1200px}.wrap.fixed .grid{grid-template-columns:repeat(3,minmax(0,1fr))}@media(max-width:900px){.wrap.fixed .grid{grid-template-columns:repeat(2,minmax(0,1fr))}}.hd{padding:28px 32px 0}.hd .t-row{display:flex;align-items:baseline;gap:12px}.hd h1{font-size:22px;font-weight:700;letter-spacing:-.01em;margin:0}.hd h1 a{color:#1b1a17;text-decoration:none}.hd h1 a:hover{color:#57534a}.hd .sub{font-size:12px;color:#8a8579}.hd .count{margin-left:auto;font-size:12px;color:#8a8579}.hd input.q{margin-top:16px;width:100%;padding:11px 14px;border:1px solid #ddd8ce;border-radius:10px;font-size:13.5px;background:#fff;color:#1b1a17;outline:none;font-family:inherit}.controls{display:flex;gap:8px;margin-top:12px;flex-wrap:wrap;align-items:center}.chip{padding:6px 14px;border-radius:999px;font-size:12px;cursor:pointer;background:#fff;border:1px solid #ddd8ce;color:#6b6558}.chip.on{background:#1b1a17;border-color:#1b1a17;color:#fff}.grp-label{display:flex;align-items:center;gap:7px;font-size:12px;color:#57534a;cursor:pointer;margin-left:auto}.grp-label input{accent-color:#1b1a17;width:15px;height:15px;cursor:pointer}.ctrl-sep{width:1px;height:18px;background:#ddd8ce;margin:0 4px;align-self:center}.sel{-moz-appearance:none;appearance:none;-webkit-appearance:none;font-family:inherit;font-size:12px;color:#6b6558;background:#fff;border:1px solid #ddd8ce;border-radius:999px;padding:6px 30px 6px 14px;cursor:pointer;outline:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5'%3E%3Cpath d='M0 0l4 5 4-5z' fill='%238a8579'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 12px center}.sel:focus{border-color:#1b1a17}.seg{display:inline-flex;border:1px solid #ddd8ce;border-radius:999px;overflow:hidden;background:#fff}.seg button{border:0;background:transparent;font-family:inherit;font-size:12px;color:#6b6558;padding:6px 13px;cursor:pointer}.seg button+button{border-left:1px solid #efede8}.seg button.on{background:#1b1a17;color:#fff}.set-label{font-size:12px;font-weight:600;color:#57534a;margin-bottom:10px}.set-options{display:flex;flex-direction:column;gap:8px;margin-bottom:20px}.set-option{display:flex;align-items:center;gap:9px;font-size:13px;cursor:pointer}.set-option input{accent-color:#1b1a17}.set-scheme{font-family:ui-monospace,Menlo,monospace;font-size:10.5px;color:#a39d90;margin-left:auto}.set-input{font-family:ui-monospace,Menlo,monospace;font-size:12px;padding:8px 10px;border:1px solid #ddd8ce;border-radius:8px;outline:none;margin-left:24px}.set-input:focus{border-color:#1b1a17}.pbtn.primary{background:#1b1a17;border-color:#1b1a17;color:#fff}.pbtn.primary:hover{background:#3a372f;border-color:#3a372f}.sec-h{display:flex;align-items:center;gap:8px;margin:14px 0 6px;position:sticky;top:0;z-index:5;background:#faf9f7;padding:10px 0 8px}.sec-h .sq{width:8px;height:8px;border-radius:2px;flex:none}.sec-h .lbl{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:#57534a}.sec-h .n{font-size:11px;color:#a39d90}.sec-h .ln{flex:1;height:1px;background:#e8e4dc}.sec-h.sm{margin:8px 0 4px;padding:8px 0 6px}.sec-h.sm .lbl{font-size:10.5px}.grid-pad{padding:6px 32px 32px}.grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.card{background:#fff;border:1px solid #e8e4dc;border-radius:10px;padding:18px 20px;display:flex;flex-direction:column;gap:10px;cursor:pointer;text-align:left;min-width:0}.card:hover{border-color:#b9b2a3;box-shadow:0 2px 8px #1b1a170f}.card .top{display:flex;align-items:baseline;gap:8px}.card .nm{font-size:15px;font-weight:600;color:#1b1a17;word-break:break-all}.card .ver{margin-left:auto;font-size:10.5px;color:#a39d90;font-family:ui-monospace,Menlo,monospace;white-space:nowrap}.card .desc{font-size:13px;line-height:1.65;color:#57534a;margin:0;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}.card .usage{font-family:ui-monospace,Menlo,monospace;font-size:11.5px;background:#f4f1ea;border-radius:6px;padding:8px 10px;color:#6b6558;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.card .meta{display:flex;gap:8px;font-size:10.5px;color:#8a8579}.card .meta span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.card .meta .meta-r{margin-left:auto;flex:none;display:inline-flex;gap:8px;align-items:center}.scope-mini{display:inline-flex;align-items:center;gap:5px;font-size:10px;font-weight:600;letter-spacing:.04em;color:#8a8579;margin-bottom:-4px}.dot5{width:6px;height:6px;border-radius:2px;flex:none}.empty{text-align:center;padding:44px;font-size:13px;color:#8a8579}.ai-mark{color:#b0836a;font-size:11px}.kbadge{font-size:9.5px;font-weight:700;padding:2px 8px;border-radius:999px;background:#f1efe9;color:#8a8579;white-space:nowrap;flex:none;vertical-align:3px;margin-left:8px}.same-actions{margin-left:auto;display:inline-flex;gap:6px}.pbtn.sm{font-size:11px;padding:3px 10px}.pbtn.sm.on{background:#1b1a17;border-color:#1b1a17;color:#fff}.diff-wrap{margin-top:10px}.diff-legend{display:flex;gap:14px;align-items:center;font-size:11px;margin-bottom:6px}.d-del-mark{color:#b0392e}.d-add-mark{color:#1f8a5b}.d-count{color:#8a8579;margin-left:auto}.diff{border:1px solid #e8e4dc;border-radius:8px;overflow-x:auto;background:#fff;font-family:ui-monospace,Menlo,monospace;font-size:11px;line-height:1.7;max-height:480px;overflow-y:auto}.d-line{padding:0 12px;white-space:pre}.d-add{background:#1f8a5b14;color:#16603f}.d-del{background:#b0392e12;color:#8c2d24}.d-skip{padding:2px 12px;color:#a39d90;background:#faf9f7;text-align:center;border-top:1px solid #efede8;border-bottom:1px solid #efede8}.inv-badge{font-size:9.5px;font-weight:700;padding:2.5px 8px;border-radius:999px;white-space:nowrap;flex:none}.inv-human{color:#1f8a5b;background:#1f8a5b1a}.inv-agent{color:#7b5bd6;background:#7b5bd61a}.inv-both{color:#b8862f;background:#c07a1f21}.inv-ai{opacity:.85}.rel-chips{display:flex;flex-wrap:wrap;gap:8px}.rel-chip{display:inline-flex;align-items:center;gap:7px;background:#fff;border:1px solid #ddd8ce;border-radius:999px;padding:7px 12px;cursor:pointer}.rel-chip:hover{background:#f4f1ea}.rel-chip .rt{font-size:9.5px;font-weight:700;text-transform:uppercase;color:#8a8579}.rel-chip .rn{font-family:ui-monospace,Menlo,monospace;font-size:12px;color:#1b1a17}.rel-chip.missing{border-style:dashed;cursor:default}.rel-chip.missing .rn{color:#a39d90}.rel-chip.missing .rm{font-size:9.5px;color:#b0836a}.md-wrap{display:flex;align-items:stretch}.left-col{width:340px;flex:none;border-right:1px solid #eeeae2;padding:8px 20px 28px 32px;position:sticky;top:0;max-height:100vh;overflow-y:auto;overscroll-behavior:contain}.ccard{display:flex;flex-direction:column;gap:4px;width:100%;background:#fff;border:1px solid #e8e4dc;border-radius:10px;padding:12px 14px;cursor:pointer;text-align:left;margin-bottom:8px}.ccard:hover{border-color:#b9b2a3}.ccard.sel{border-color:#1b1a17;box-shadow:0 1px 5px #1b1a171a}.ccard .r1{display:flex;align-items:center;gap:7px}.ccard .dot7{width:7px;height:7px;border-radius:50%;flex:none}.ccard .nm{font-size:13.5px;font-weight:600;color:#1b1a17;word-break:break-all}.ccard .ver{margin-left:auto;font-size:10px;color:#a39d90;font-family:ui-monospace,Menlo,monospace}.ccard .d1{font-size:11.5px;color:#8a8579;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.pane{flex:1;min-width:0;max-width:980px;padding:20px 36px 36px 32px}.back{background:#fff;border:1px solid #ddd8ce;border-radius:999px;font-size:12px;color:#6b6558;padding:7px 14px;cursor:pointer}.back:hover{background:#f4f1ea}.meta-row{display:flex;align-items:center;gap:10px;margin-top:20px;position:relative;flex-wrap:wrap}.badge{font-size:10.5px;font-weight:700;padding:4px 11px;border-radius:999px}.m-ver{font-size:11px;color:#8a8579;font-family:ui-monospace,Menlo,monospace}.m-upd{margin-left:auto;font-size:11px;color:#8a8579}.pbtn{background:#fff;border:1px solid #ddd8ce;border-radius:999px;font-size:12px;color:#6b6558;padding:6px 13px;cursor:pointer}.pbtn:hover{background:#f4f1ea}.pbtn.danger{color:#b0392e;border-color:#e4ccc7}.pbtn.danger:hover{background:#fbf1ef}.d-name{font-size:26px;font-weight:700;color:#1b1a17;margin:14px 0 0;word-break:break-all}.tabs{display:flex;gap:20px;margin-top:18px;border-bottom:1px solid #eeeae2}.tab{background:none;border:0;font-size:13px;color:#8a8579;padding:8px 2px 10px;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px}.tab.on{color:#1b1a17;font-weight:600;border-bottom-color:#1b1a17}.sec-t{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:#8a8579;margin:26px 0 10px}.full-desc{font-size:13.5px;line-height:1.8;color:#57534a;margin:0;white-space:pre-wrap}.ex-block{font-family:ui-monospace,Menlo,monospace;font-size:12px;background:#f4f1ea;border-radius:8px;padding:10px 13px;color:#57534a;margin-bottom:8px;overflow-x:auto;white-space:pre}.f-row{display:flex;align-items:center;gap:9px;border-bottom:1px solid #eeeae2;padding:8px 4px}.f-row .sq6{width:6px;height:6px;border-radius:2px;background:#c5bfb2;flex:none}.f-row .p{font-family:ui-monospace,Menlo,monospace;font-size:12px;color:#57534a;word-break:break-all}.fm-box{font-family:ui-monospace,Menlo,monospace;font-size:10.5px;color:#8a8579;background:#faf9f7;border:1px solid #e8e4dc;border-radius:8px;padding:12px 14px;white-space:pre-wrap;word-break:break-word;margin:18px 0}.md-body{font-size:12.5px;line-height:1.9;color:#3a372f}.md-body h1,.md-body h2{border-bottom:1px solid #eeeae2;padding-bottom:6px}.md-body h1{font-size:19px;margin:26px 0 10px;color:#1b1a17}.md-body h2{font-size:16px;margin:24px 0 10px;color:#1b1a17}.md-body h3{font-size:14px;margin:20px 0 8px;color:#1b1a17}.md-body h4{font-size:13px;margin:16px 0 6px;color:#1b1a17}.md-body p{margin:8px 0}.md-body ul,.md-body ol{margin:8px 0;padding-left:22px}.md-body li{margin:3px 0}.md-body pre{background:#f4f1ea;border-radius:8px;padding:12px 14px;overflow-x:auto;font-family:ui-monospace,Menlo,monospace;font-size:11.5px;line-height:1.6}.md-body code{font-family:ui-monospace,Menlo,monospace;font-size:.9em;background:#f4f1ea;color:#8a5a2b;border-radius:4px;padding:1px 5px}.md-body pre code{background:none;color:inherit;padding:0}.md-body blockquote{border-left:3px solid #ddd8ce;margin:8px 0;padding:2px 0 2px 14px;color:#6b6558}.md-body hr{border:0;border-top:1px solid #eeeae2;margin:18px 0}.md-body a{color:#2a6fdb}.drop{position:absolute;top:calc(100% + 6px);right:0;width:260px;background:#fff;border:1px solid #ddd8ce;border-radius:10px;box-shadow:0 10px 28px #1b1a1724;z-index:30;padding:8px 0;max-height:320px;overflow-y:auto}.drop .dh{font-size:10px;text-transform:uppercase;letter-spacing:.07em;color:#8a8579;padding:6px 14px 4px}.drop .di{display:block;width:100%;text-align:left;background:none;border:0;padding:8px 14px;cursor:pointer}.drop .di:hover{background:#f4f1ea}.drop .di .l1{font-size:12.5px;font-weight:600;color:#1b1a17}.drop .di .l2{font-size:10.5px;color:#a39d90;font-family:ui-monospace,Menlo,monospace;word-break:break-all}.overlay{position:fixed;top:0;right:0;bottom:0;left:0;background:#1b1a1759;z-index:40;display:flex;align-items:center;justify-content:center}.modal{width:390px;max-width:90vw;background:#fff;border-radius:14px;padding:26px 28px}.modal h3{font-size:15px;font-weight:700;margin:0 0 10px;color:#1b1a17}.modal p{font-size:12.5px;line-height:1.7;color:#57534a;margin:0 0 20px}.modal .btns{display:flex;justify-content:flex-end;gap:8px}.modal .del{background:#b0392e;border:1px solid #b0392e;color:#fff;border-radius:999px;font-size:12px;padding:7px 16px;cursor:pointer}.modal .del:hover{background:#962f27}@media(max-width:900px){.grid{grid-template-columns:repeat(2,minmax(0,1fr))}.left-col{display:none}}@media(min-width:1480px){.grid{grid-template-columns:repeat(4,minmax(0,1fr))}}@media(min-width:1960px){.grid{grid-template-columns:repeat(5,minmax(0,1fr))}}
1
+ *{box-sizing:border-box}body{margin:0;background:#faf9f7;color:#1b1a17;font-family:Helvetica Neue,system-ui,-apple-system,Hiragino Sans,sans-serif}button{font-family:inherit}.mono{font-family:ui-monospace,SF Mono,Menlo,monospace}.wrap{margin:0 auto;min-height:100vh}.wrap.fixed{max-width:1200px}.wrap.fixed .grid{grid-template-columns:repeat(3,minmax(0,1fr))}@media(max-width:900px){.wrap.fixed .grid{grid-template-columns:repeat(2,minmax(0,1fr))}}.hd{padding:28px 32px 0}.hd .t-row{display:flex;align-items:baseline;gap:12px}.hd h1{font-size:22px;font-weight:700;letter-spacing:-.01em;margin:0}.hd h1 a{color:#1b1a17;text-decoration:none}.hd h1 a:hover{color:#57534a}.hd .sub{font-size:12px;color:#8a8579}.hd .count{margin-left:auto;font-size:12px;color:#8a8579}.hd input.q{margin-top:16px;width:100%;padding:11px 14px;border:1px solid #ddd8ce;border-radius:10px;font-size:13.5px;background:#fff;color:#1b1a17;outline:none;font-family:inherit}.controls{display:flex;gap:8px;margin-top:12px;flex-wrap:wrap;align-items:center}.chip{padding:6px 14px;border-radius:999px;font-size:12px;cursor:pointer;background:#fff;border:1px solid #ddd8ce;color:#6b6558}.chip.on{background:#1b1a17;border-color:#1b1a17;color:#fff}.grp-label{display:flex;align-items:center;gap:7px;font-size:12px;color:#57534a;cursor:pointer;margin-left:auto}.grp-label input{accent-color:#1b1a17;width:15px;height:15px;cursor:pointer}.ctrl-sep{width:1px;height:18px;background:#ddd8ce;margin:0 4px;align-self:center}.sel{-moz-appearance:none;appearance:none;-webkit-appearance:none;font-family:inherit;font-size:12px;color:#6b6558;background:#fff;border:1px solid #ddd8ce;border-radius:999px;padding:6px 30px 6px 14px;cursor:pointer;outline:none;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='5'%3E%3Cpath d='M0 0l4 5 4-5z' fill='%238a8579'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:right 12px center}.sel:focus{border-color:#1b1a17}.seg{display:inline-flex;border:1px solid #ddd8ce;border-radius:999px;overflow:hidden;background:#fff}.seg button{border:0;background:transparent;font-family:inherit;font-size:12px;color:#6b6558;padding:6px 13px;cursor:pointer}.seg button+button{border-left:1px solid #efede8}.seg button.on{background:#1b1a17;color:#fff}.set-label{font-size:12px;font-weight:600;color:#57534a;margin-bottom:10px}.set-options{display:flex;flex-direction:column;gap:8px;margin-bottom:20px}.set-option{display:flex;align-items:center;gap:9px;font-size:13px;cursor:pointer}.set-option input{accent-color:#1b1a17}.set-scheme{font-family:ui-monospace,Menlo,monospace;font-size:10.5px;color:#a39d90;margin-left:auto}.set-input{font-family:ui-monospace,Menlo,monospace;font-size:12px;padding:8px 10px;border:1px solid #ddd8ce;border-radius:8px;outline:none;margin-left:24px}.set-input:focus{border-color:#1b1a17}.pbtn.primary{background:#1b1a17;border-color:#1b1a17;color:#fff}.pbtn.primary:hover{background:#3a372f;border-color:#3a372f}.sec-h{display:flex;align-items:center;gap:8px;margin:14px 0 6px;position:sticky;top:0;z-index:5;background:#faf9f7;padding:10px 0 8px}.sec-h .sq{width:8px;height:8px;border-radius:2px;flex:none}.sec-h .lbl{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:#57534a}.sec-h .n{font-size:11px;color:#a39d90}.sec-h .ln{flex:1;height:1px;background:#e8e4dc}.sec-h.sm{margin:8px 0 4px;padding:8px 0 6px}.sec-h.sm .lbl{font-size:10.5px}.grid-pad{padding:6px 32px 32px}.grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.card{background:#fff;border:1px solid #e8e4dc;border-radius:10px;padding:18px 20px;display:flex;flex-direction:column;gap:10px;cursor:pointer;text-align:left;min-width:0}.card:hover{border-color:#b9b2a3;box-shadow:0 2px 8px #1b1a170f}.card .top{display:flex;align-items:baseline;gap:8px}.card .nm{font-size:15px;font-weight:600;color:#1b1a17;word-break:break-all}.card .ver{margin-left:auto;font-size:10.5px;color:#a39d90;font-family:ui-monospace,Menlo,monospace;white-space:nowrap}.card .desc{font-size:13px;line-height:1.65;color:#57534a;margin:0;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}.card .usage{font-family:ui-monospace,Menlo,monospace;font-size:11.5px;background:#f4f1ea;border-radius:6px;padding:8px 10px;color:#6b6558;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.card .meta{display:flex;gap:8px;font-size:10.5px;color:#8a8579}.card .meta span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.card .meta .meta-r{margin-left:auto;flex:none;display:inline-flex;gap:8px;align-items:center}.scope-mini{display:inline-flex;align-items:center;gap:5px;font-size:10px;font-weight:600;letter-spacing:.04em;color:#8a8579;margin-bottom:-4px}.dot5{width:6px;height:6px;border-radius:2px;flex:none}.empty{text-align:center;padding:44px;font-size:13px;color:#8a8579}.ai-mark{color:#b0836a;font-size:11px}.kbadge{font-size:9.5px;font-weight:700;padding:2px 8px;border-radius:999px;background:#f1efe9;color:#8a8579;white-space:nowrap;flex:none;vertical-align:3px;margin-left:8px}.same-actions{margin-left:auto;display:inline-flex;gap:6px}.pbtn.sm{font-size:11px;padding:3px 10px}.pbtn.sm.on{background:#1b1a17;border-color:#1b1a17;color:#fff}.diff-wrap{margin-top:10px}.diff-legend{display:flex;gap:14px;align-items:center;font-size:11px;margin-bottom:6px}.d-del-mark{color:#b0392e}.d-add-mark{color:#1f8a5b}.d-count{color:#8a8579;margin-left:auto}.diff{border:1px solid #e8e4dc;border-radius:8px;overflow-x:auto;background:#fff;font-family:ui-monospace,Menlo,monospace;font-size:11px;line-height:1.7;max-height:480px;overflow-y:auto}.d-line{padding:0 12px;white-space:pre}.d-add{background:#1f8a5b14;color:#16603f}.d-del{background:#b0392e12;color:#8c2d24}.d-skip{padding:2px 12px;color:#a39d90;background:#faf9f7;text-align:center;border-top:1px solid #efede8;border-bottom:1px solid #efede8}.inv-badge{font-size:9.5px;font-weight:700;padding:2.5px 8px;border-radius:999px;white-space:nowrap;flex:none}.inv-human{color:#1f8a5b;background:#1f8a5b1a}.inv-agent{color:#7b5bd6;background:#7b5bd61a}.inv-both{color:#b8862f;background:#c07a1f21}.inv-ai{opacity:.85}.rel-chips{display:flex;flex-wrap:wrap;gap:8px}.rel-chip{display:inline-flex;align-items:center;gap:7px;background:#fff;border:1px solid #ddd8ce;border-radius:999px;padding:7px 12px;cursor:pointer}.rel-chip:hover{background:#f4f1ea}.rel-chip .rt{font-size:9.5px;font-weight:700;text-transform:uppercase;color:#8a8579}.rel-chip .rn{font-family:ui-monospace,Menlo,monospace;font-size:12px;color:#1b1a17}.rel-chip.missing{border-style:dashed;cursor:default}.rel-chip.missing .rn{color:#a39d90}.rel-chip.missing .rm{font-size:9.5px;color:#b0836a}.md-wrap{display:flex;align-items:stretch}.left-col{width:340px;flex:none;border-right:1px solid #eeeae2;padding:8px 20px 28px 32px;position:sticky;top:0;max-height:100vh;overflow-y:auto;overscroll-behavior:contain}.ccard{display:flex;flex-direction:column;gap:4px;width:100%;background:#fff;border:1px solid #e8e4dc;border-radius:10px;padding:12px 14px;cursor:pointer;text-align:left;margin-bottom:8px}.ccard:hover{border-color:#b9b2a3}.ccard.sel{border-color:#1b1a17;box-shadow:0 1px 5px #1b1a171a}.ccard .r1{display:flex;align-items:center;gap:7px}.ccard .dot7{width:7px;height:7px;border-radius:50%;flex:none}.ccard .nm{font-size:13.5px;font-weight:600;color:#1b1a17;word-break:break-all}.ccard .ver{margin-left:auto;font-size:10px;color:#a39d90;font-family:ui-monospace,Menlo,monospace}.ccard .d1{font-size:11.5px;color:#8a8579;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.pane{flex:1;min-width:0;max-width:980px;padding:20px 36px 36px 32px}.back{background:#fff;border:1px solid #ddd8ce;border-radius:999px;font-size:12px;color:#6b6558;padding:7px 14px;cursor:pointer}.back:hover{background:#f4f1ea}.meta-row{display:flex;align-items:center;gap:10px;margin-top:20px;position:relative;flex-wrap:wrap}.badge{font-size:10.5px;font-weight:700;padding:4px 11px;border-radius:999px}.m-ver{font-size:11px;color:#8a8579;font-family:ui-monospace,Menlo,monospace}.m-upd{margin-left:auto;font-size:11px;color:#8a8579}.pbtn{background:#fff;border:1px solid #ddd8ce;border-radius:999px;font-size:12px;color:#6b6558;padding:6px 13px;cursor:pointer}.pbtn:hover{background:#f4f1ea}.pbtn.danger{color:#b0392e;border-color:#e4ccc7}.pbtn.danger:hover{background:#fbf1ef}.d-name{font-size:26px;font-weight:700;color:#1b1a17;margin:14px 0 0;word-break:break-all}.tabs{display:flex;gap:20px;margin-top:18px;border-bottom:1px solid #eeeae2}.tab{background:none;border:0;font-size:13px;color:#8a8579;padding:8px 2px 10px;cursor:pointer;border-bottom:2px solid transparent;margin-bottom:-1px}.tab.on{color:#1b1a17;font-weight:600;border-bottom-color:#1b1a17}.sec-t{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.07em;color:#8a8579;margin:26px 0 10px}.full-desc{font-size:13.5px;line-height:1.8;color:#57534a;margin:0;white-space:pre-wrap}.ex-block{font-family:ui-monospace,Menlo,monospace;font-size:12px;background:#f4f1ea;border-radius:8px;padding:10px 13px;color:#57534a;margin-bottom:8px;overflow-x:auto;white-space:pre}.f-row{display:flex;align-items:center;gap:9px;border-bottom:1px solid #eeeae2;padding:8px 4px}.f-row .sq6{width:6px;height:6px;border-radius:2px;background:#c5bfb2;flex:none}.f-row .p{font-family:ui-monospace,Menlo,monospace;font-size:12px;color:#57534a;word-break:break-all}.fm-box{font-family:ui-monospace,Menlo,monospace;font-size:10.5px;color:#8a8579;background:#faf9f7;border:1px solid #e8e4dc;border-radius:8px;padding:12px 14px;white-space:pre-wrap;word-break:break-word;margin:18px 0}.md-body{font-size:12.5px;line-height:1.9;color:#3a372f}.md-body h1,.md-body h2{border-bottom:1px solid #eeeae2;padding-bottom:6px}.md-body h1{font-size:19px;margin:26px 0 10px;color:#1b1a17}.md-body h2{font-size:16px;margin:24px 0 10px;color:#1b1a17}.md-body h3{font-size:14px;margin:20px 0 8px;color:#1b1a17}.md-body h4{font-size:13px;margin:16px 0 6px;color:#1b1a17}.md-body p{margin:8px 0}.md-body ul,.md-body ol{margin:8px 0;padding-left:22px}.md-body li{margin:3px 0}.md-body pre{background:#f4f1ea;border-radius:8px;padding:12px 14px;overflow-x:auto;font-family:ui-monospace,Menlo,monospace;font-size:11.5px;line-height:1.6}.md-body code{font-family:ui-monospace,Menlo,monospace;font-size:.9em;background:#f4f1ea;color:#8a5a2b;border-radius:4px;padding:1px 5px}.md-body pre code{background:none;color:inherit;padding:0}.md-body blockquote{border-left:3px solid #ddd8ce;margin:8px 0;padding:2px 0 2px 14px;color:#6b6558}.md-body hr{border:0;border-top:1px solid #eeeae2;margin:18px 0}.md-body a{color:#2a6fdb}.drop{position:absolute;top:calc(100% + 6px);right:0;width:260px;background:#fff;border:1px solid #ddd8ce;border-radius:10px;box-shadow:0 10px 28px #1b1a1724;z-index:30;padding:8px 0;max-height:320px;overflow-y:auto}.drop .dh{font-size:10px;text-transform:uppercase;letter-spacing:.07em;color:#8a8579;padding:6px 14px 4px}.drop .di{display:block;width:100%;text-align:left;background:none;border:0;padding:8px 14px;cursor:pointer}.drop .di:hover{background:#f4f1ea}.drop .di .l1{font-size:12.5px;font-weight:600;color:#1b1a17}.drop .di .l2{font-size:10.5px;color:#a39d90;font-family:ui-monospace,Menlo,monospace;word-break:break-all}.overlay{position:fixed;top:0;right:0;bottom:0;left:0;background:#1b1a1759;z-index:40;display:flex;align-items:center;justify-content:center}.modal{width:390px;max-width:90vw;background:#fff;border-radius:14px;padding:26px 28px}.modal h3{font-size:15px;font-weight:700;margin:0 0 10px;color:#1b1a17}.modal p{font-size:12.5px;line-height:1.7;color:#57534a;margin:0 0 20px}.modal .btns{display:flex;justify-content:flex-end;gap:8px}.modal .del{background:#b0392e;border:1px solid #b0392e;color:#fff;border-radius:999px;font-size:12px;padding:7px 16px;cursor:pointer}.modal .del:hover{background:#962f27}@media(max-width:900px){.grid{grid-template-columns:repeat(2,minmax(0,1fr))}.left-col{display:none}}@media(min-width:1480px){.grid{grid-template-columns:repeat(4,minmax(0,1fr))}}@media(min-width:1960px){.grid{grid-template-columns:repeat(5,minmax(0,1fr))}}.unused-badge{font-size:9.5px;font-weight:700;padding:2.5px 8px;border-radius:999px;white-space:nowrap;flex:none;color:#5b7285;background:#5b72851f}.warn-badge{font-size:9.5px;font-weight:700;padding:2.5px 8px;border-radius:999px;white-space:nowrap;flex:none;color:#b0432f;background:#c848301a;position:relative;cursor:help}.card .top .top-r{margin-left:auto;display:inline-flex;align-items:center;gap:8px;flex:none}.card .top .top-r .ver{margin-left:0}.card .chips{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin-top:-4px}.card .chips .kbadge{margin-left:0;vertical-align:0}.warn-badge .tip{display:none;position:absolute;top:calc(100% + 6px);right:-6px;z-index:40;width:250px;padding:10px 12px;border-radius:8px;background:#1b1a17;color:#f4f1ea;font-size:11.5px;font-weight:400;line-height:1.6;white-space:normal;text-align:left;box-shadow:0 4px 14px #1b1a1740}.warn-badge:hover .tip{display:block}.warn-badge .tip .tip-t{display:block;font-weight:700;margin-bottom:4px}.warn-badge .tip .tip-line{display:block;margin-top:3px;padding-left:12px;text-indent:-12px}.warn-badge .tip .tip-line:before{content:"· "}.card .meta .tok,.tok-total{font-family:ui-monospace,Menlo,monospace;white-space:nowrap}.hd .tok-total{cursor:help}.sec-h .sec-tok{font-size:11px;color:#a39d90;font-family:ui-monospace,Menlo,monospace;white-space:nowrap;cursor:help}.lint-row{display:flex;gap:8px;align-items:baseline;font-size:13px;line-height:1.65;color:#57534a;padding:2px 0}.lint-row .lint-mark{color:#b0432f;flex:none}