skills-viewer 0.2.0 → 0.4.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 +7 -2
- package/build/server/index.js +50 -2
- package/build/server/lint.js +57 -0
- package/build/server/scan.js +17 -2
- package/build/server/snapshot.js +114 -0
- package/build/server/usage.js +12 -1
- package/dist/assets/{index-D2xAPQHX.css → index-DBYLrO5r.css} +1 -1
- package/dist/assets/index-DdnpYONk.js +67 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/dist/assets/index-BF9RnD9N.js +0 -67
package/README.md
CHANGED
|
@@ -17,7 +17,11 @@ 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
|
|
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 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
|
+
- **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
|
+
- **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
|
|
24
|
+
- **Usage sparkline** — the detail pane charts the last 30 days of per-day usage
|
|
21
25
|
- **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
26
|
- **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
27
|
- **Detail pane** — master/detail layout with an Overview tab (description, usage, bundled files) and a rendered SKILL.md tab
|
|
@@ -68,7 +72,8 @@ dist/ # prebuilt UI shipped in the npm package (generated by prepack
|
|
|
68
72
|
|
|
69
73
|
## Notes
|
|
70
74
|
|
|
71
|
-
- Usage stats only cover the transcript retention window of Claude Code (`cleanupPeriodDays`, default 30 days)
|
|
75
|
+
- Usage stats only cover the transcript retention window of Claude Code (`cleanupPeriodDays`, default 30 days) — the _unused_ badge has the same limitation
|
|
76
|
+
- Token costs are heuristic estimates (≈4 chars/token for ASCII, ≈1.5 for CJK), not exact tokenizer counts
|
|
72
77
|
- 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
78
|
- 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
79
|
- AI summaries require a logged-in `claude` CLI
|
package/build/server/index.js
CHANGED
|
@@ -48,6 +48,7 @@ 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 snapshot_1 = require("./snapshot");
|
|
51
52
|
const errors_1 = require("./errors");
|
|
52
53
|
const locale_1 = require("./locale");
|
|
53
54
|
const TOKEN = crypto.randomBytes(16).toString('hex');
|
|
@@ -67,6 +68,7 @@ const MIME = {
|
|
|
67
68
|
* 呼び出し元プロジェクト(トランスクリプトのディレクトリ)を特定し、そのプロジェクトの
|
|
68
69
|
* skill → user → plugin → built-in の解決優先順で1つの定義にだけ加算する。
|
|
69
70
|
* 呼び出し元が未知(worktree・削除済みプロジェクト等)の場合は user 以降にフォールバック。
|
|
71
|
+
* 戻り値はトランスクリプトが1件でも存在したか(false なら「未使用」判定は無意味)。
|
|
70
72
|
*/
|
|
71
73
|
function attributeUsage(sections) {
|
|
72
74
|
const byDir = (0, usage_1.scanUsageByDir)();
|
|
@@ -119,12 +121,18 @@ function attributeUsage(sections) {
|
|
|
119
121
|
target.autoCount = (target.autoCount || 0) + u.auto;
|
|
120
122
|
target.useCount = (target.useCount || 0) + u.typed + u.auto;
|
|
121
123
|
target.lastUsed = Math.max(target.lastUsed || 0, u.last);
|
|
124
|
+
if (u.daily) {
|
|
125
|
+
const du = target.dailyUse || (target.dailyUse = {});
|
|
126
|
+
for (const [day, n] of Object.entries(u.daily))
|
|
127
|
+
du[day] = (du[day] || 0) + n;
|
|
128
|
+
}
|
|
122
129
|
}
|
|
123
130
|
}
|
|
131
|
+
return Object.keys(byDir).length > 0;
|
|
124
132
|
}
|
|
125
133
|
function collect(cwd, lang) {
|
|
126
134
|
const sections = (0, scan_1.scanSections)(cwd, lang);
|
|
127
|
-
attributeUsage(sections);
|
|
135
|
+
const usageAvailable = attributeUsage(sections);
|
|
128
136
|
const summaries = (0, summary_1.loadSummaries)();
|
|
129
137
|
for (const sec of sections) {
|
|
130
138
|
for (const it of sec.items) {
|
|
@@ -153,7 +161,15 @@ function collect(cwd, lang) {
|
|
|
153
161
|
.sort((a, b) => path.basename(a).localeCompare(path.basename(b)))
|
|
154
162
|
.map((p) => ({ label: path.basename(p), sub: p, path: p })),
|
|
155
163
|
];
|
|
156
|
-
return {
|
|
164
|
+
return {
|
|
165
|
+
generatedAt: new Date().toISOString(),
|
|
166
|
+
cwd,
|
|
167
|
+
sections,
|
|
168
|
+
targets,
|
|
169
|
+
aiStale,
|
|
170
|
+
usageAvailable,
|
|
171
|
+
changes: (0, snapshot_1.computeChanges)(sections),
|
|
172
|
+
};
|
|
157
173
|
}
|
|
158
174
|
/* DNS rebinding 対策: same-origin GET には Origin が付かないため Host 側も検証する */
|
|
159
175
|
function hostOk(req) {
|
|
@@ -222,6 +238,10 @@ function handleApi(req, res, cwd) {
|
|
|
222
238
|
}
|
|
223
239
|
const lang = langOf(data.lang);
|
|
224
240
|
try {
|
|
241
|
+
if (url.pathname === '/api/changes-ack') {
|
|
242
|
+
(0, snapshot_1.ackChanges)((0, scan_1.scanSections)(cwd, lang));
|
|
243
|
+
return send(200, { ok: true });
|
|
244
|
+
}
|
|
225
245
|
if (url.pathname === '/api/copy')
|
|
226
246
|
return send(200, (0, manage_1.doCopy)(data, cwd));
|
|
227
247
|
if (url.pathname === '/api/delete')
|
|
@@ -269,6 +289,33 @@ function serveStatic(req, res) {
|
|
|
269
289
|
res.writeHead(200, { 'Content-Type': MIME[path.extname(fp)] || 'application/octet-stream' });
|
|
270
290
|
res.end(fs.readFileSync(fp));
|
|
271
291
|
}
|
|
292
|
+
/*
|
|
293
|
+
* 起動時の1〜2行サマリー(--no-open 運用でも価値が出るように)。
|
|
294
|
+
* 前回からの差分 + セッション注入トークン概算 + 未使用件数。失敗しても起動は止めない。
|
|
295
|
+
*/
|
|
296
|
+
function printStartupSummary(cwd) {
|
|
297
|
+
try {
|
|
298
|
+
const data = collect(cwd, locale_1.serverLang);
|
|
299
|
+
const ch = data.changes;
|
|
300
|
+
if (ch) {
|
|
301
|
+
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`));
|
|
302
|
+
}
|
|
303
|
+
const sessionTokens = data.sections
|
|
304
|
+
.filter((s) => s.source !== 'project' || s.isCurrent)
|
|
305
|
+
.flatMap((s) => s.items)
|
|
306
|
+
.reduce((sum, it) => sum + (it.tokens || 0), 0);
|
|
307
|
+
const unused = data.usageAvailable
|
|
308
|
+
? data.sections.flatMap((s) => s.items).filter((it) => it.kind !== 'hook' && !it.useCount)
|
|
309
|
+
.length
|
|
310
|
+
: null;
|
|
311
|
+
console.log((0, locale_1.srvMsg)(`スキル定義のセッション注入 ≈${sessionTokens.toLocaleString()}tok` +
|
|
312
|
+
(unused !== null ? ` / 未使用 ${unused} 件` : ''), `Skill definitions inject ≈${sessionTokens.toLocaleString()} tok/session` +
|
|
313
|
+
(unused !== null ? ` / ${unused} unused` : '')));
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
/* サマリーは補助情報。失敗しても起動を妨げない */
|
|
317
|
+
}
|
|
318
|
+
}
|
|
272
319
|
function start({ port = 4763, open = true, cwd = process.cwd() } = {}, attempt = 0) {
|
|
273
320
|
const server = http.createServer((req, res) => {
|
|
274
321
|
if ((req.url || '').startsWith('/api/'))
|
|
@@ -287,6 +334,7 @@ function start({ port = 4763, open = true, cwd = process.cwd() } = {}, attempt =
|
|
|
287
334
|
server.listen(port, '127.0.0.1', () => {
|
|
288
335
|
const url = `http://127.0.0.1:${port}`;
|
|
289
336
|
console.log(`skills-viewer: ${url} (cwd: ${cwd})`);
|
|
337
|
+
printStartupSummary(cwd);
|
|
290
338
|
console.log((0, locale_1.srvMsg)('Ctrl+C で終了。ページ再読み込みで再スキャンされます。', 'Press Ctrl+C to quit. Reloading the page rescans.'));
|
|
291
339
|
if (open && process.platform === 'darwin')
|
|
292
340
|
(0, node_child_process_1.execFile)('open', [url], () => { });
|
|
@@ -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
|
+
}
|
package/build/server/scan.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
}
|
|
@@ -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/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
|
}
|
|
@@ -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}.chg-banner{display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin:14px 0 -4px;padding:10px 14px;border:1px solid #e3ddd0;border-radius:10px;background:#faf7ef;font-size:12px;color:#57534a}.chg-title{font-weight:700;flex:none}.chg-group{display:inline-flex;align-items:center;gap:6px;flex-wrap:wrap}.chg-label{font-size:10.5px;font-weight:700;padding:2px 8px;border-radius:999px;white-space:nowrap}.chg-label.add{color:#1f8a5b;background:#1f8a5b1a}.chg-label.upd{color:#2a6fdb;background:#2a6fdb1a}.chg-label.del{color:#b0432f;background:#c848301a}.chg-chip{font-family:ui-monospace,Menlo,monospace;font-size:11.5px;padding:2px 8px;border-radius:6px;border:1px solid #ddd8ce;background:#fff;color:#57534a;cursor:pointer}.chg-chip:hover{border-color:#b9b2a3}.chg-chip.gone{cursor:default;text-decoration:line-through;color:#a39d90}.chg-more{font-size:11px;color:#8a8579}.chg-ack{margin-left:auto;flex:none}.spark-wrap{display:flex;align-items:flex-end;gap:10px;margin:2px 0 6px}.spark rect{fill:#e3ddd0}.spark rect.on{fill:#1f8a5b}.spark-label{font-size:10.5px;color:#8a8579;white-space:nowrap}
|