skills-viewer 0.8.0 → 0.8.1
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 +3 -1
- package/build/server/index.js +38 -8
- package/build/server/manage.js +45 -8
- package/build/server/memory-signals.js +18 -5
- package/build/server/memory-triage.js +566 -73
- package/build/server/memory.js +377 -17
- package/build/server/usage.js +43 -13
- package/dist/assets/index-BtXAJZT3.css +1 -0
- package/dist/assets/index-CgQLhTsl.js +93 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/dist/assets/index-CFbG0247.js +0 -84
- package/dist/assets/index-Cx_UH1Vx.css +0 -1
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ npx skills-viewer
|
|
|
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
|
|
27
|
+
- **Memory triage** — a _Memory_ view lists Claude Code's auto memory (`~/.claude/projects/<project>/memory/`, or the directory set by `autoMemoryDirectory`) 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 your user 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
|
|
28
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/`
|
|
29
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
|
|
30
30
|
- **Usage sparkline** — the detail pane charts the last 30 days of per-day usage
|
|
@@ -53,6 +53,7 @@ Run it from a project directory to have that project marked as “current” and
|
|
|
53
53
|
- Binds to `127.0.0.1` only
|
|
54
54
|
- Mutating APIs require a per-run token that other origins cannot read (same-origin policy), and requests with a non-localhost `Origin` are rejected
|
|
55
55
|
- Copy/delete are restricted to `.claude/skills/` / `.claude/commands/` / `.claude/agents/` paths; plugin directories are never written to; deletions go to the OS trash
|
|
56
|
+
- Reads stay inside `~/.claude`, the per-project `.claude` directories and the `autoMemoryDirectory` you configured — a setting that resolves to a filesystem root, your home directory or an ancestor of it is ignored
|
|
56
57
|
|
|
57
58
|
## Development
|
|
58
59
|
|
|
@@ -84,6 +85,7 @@ dist/ # prebuilt UI shipped in the npm package (generated by prepack
|
|
|
84
85
|
- 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
|
|
85
86
|
- AI summaries require a logged-in `claude` CLI
|
|
86
87
|
- 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
|
|
88
|
+
- The Memory view resolves `autoMemoryDirectory` from your user settings and the current project only — project-scope settings of other projects are not read
|
|
87
89
|
|
|
88
90
|
## License
|
|
89
91
|
|
package/build/server/index.js
CHANGED
|
@@ -141,13 +141,16 @@ function attributeUsage(sections) {
|
|
|
141
141
|
* skill と違って帰属先の解決は不要で、Read の file_path がそのまま実ファイルを指す。
|
|
142
142
|
* usageAvailable は「そのプロジェクトのトランスクリプトがあるか」= エンコード名で始まる
|
|
143
143
|
* ディレクトリ(worktree 分を含む)に jsonl が 1 件以上あるか。false なら Read 列は出さない。
|
|
144
|
+
* 判定そのもの(共有ストアの扱いを含む)は usageAvailableFor に集約している。
|
|
144
145
|
*/
|
|
145
146
|
function attributeMemoryUsage(memory) {
|
|
146
147
|
if (!memory.length)
|
|
147
148
|
return;
|
|
148
149
|
const { byPath, dirsWithTranscripts } = (0, usage_1.scanMemoryUsage)();
|
|
149
150
|
for (const sec of memory) {
|
|
150
|
-
|
|
151
|
+
// autoMemoryDirectory の置き場は id が置き場のパス由来なので、transcript の
|
|
152
|
+
// ディレクトリ名(現在のプロジェクトの slug)を別に持っている
|
|
153
|
+
sec.usageAvailable = (0, memory_1.usageAvailableFor)(sec, dirsWithTranscripts);
|
|
151
154
|
for (const it of sec.items) {
|
|
152
155
|
const u = byPath[it.path];
|
|
153
156
|
if (!u)
|
|
@@ -167,11 +170,32 @@ function attributeMemoryUsage(memory) {
|
|
|
167
170
|
* 同じ事実(Read / W-E / usageAvailable)をプロンプトに載せる必要があるので共通化する。
|
|
168
171
|
*/
|
|
169
172
|
function memorySections(cwd) {
|
|
173
|
+
primeMemoryRoots(cwd);
|
|
170
174
|
const memory = (0, memory_1.scanMemory)(cwd);
|
|
171
175
|
attributeMemoryUsage(memory);
|
|
172
176
|
return memory;
|
|
173
177
|
}
|
|
178
|
+
/*
|
|
179
|
+
* この環境の自動メモリ置き場(autoMemoryDirectory)を usage 集計の許可ルートに設定する。
|
|
180
|
+
* skill 集計と memory 集計は同じ transcript キャッシュを共有するので、走査を始める前に
|
|
181
|
+
* 揃えておかないと同じファイルを二度読みすることになる(解決自体は memo 済みで安い)。
|
|
182
|
+
*/
|
|
183
|
+
function primeMemoryRoots(cwd) {
|
|
184
|
+
const auto = (0, memory_1.resolveAutoMemoryDir)(cwd);
|
|
185
|
+
if (!auto) {
|
|
186
|
+
(0, usage_1.setMemoryRoots)([]);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
/*
|
|
190
|
+
* 設定値そのものと、その実パス(異なるときだけ)の両方を許可ルートにする。
|
|
191
|
+
* transcript の file_path が symlink 解決済みで記録される環境があり、設定値だけを
|
|
192
|
+
* 前方一致に使うと、その置き場の Read / Write を丸ごと取り逃すため。
|
|
193
|
+
*/
|
|
194
|
+
const real = (0, memory_1.realDir)(auto.dir);
|
|
195
|
+
(0, usage_1.setMemoryRoots)(real !== auto.dir ? [auto.dir, real] : [auto.dir]);
|
|
196
|
+
}
|
|
174
197
|
function collect(cwd, lang) {
|
|
198
|
+
primeMemoryRoots(cwd);
|
|
175
199
|
const sections = (0, scan_1.scanSections)(cwd, lang);
|
|
176
200
|
const usageAvailable = attributeUsage(sections);
|
|
177
201
|
const summaries = (0, summary_1.loadSummaries)();
|
|
@@ -201,7 +225,10 @@ function collect(cwd, lang) {
|
|
|
201
225
|
const aiStale = (0, summary_1.staleItems)(sections, lang).length;
|
|
202
226
|
// memory は「呼び出す」ものではないので sections には混ぜず、別配列で同乗させる
|
|
203
227
|
const memory = memorySections(cwd);
|
|
204
|
-
|
|
228
|
+
// 共有ストア環境かどうかは cwd から解決した値で判定する(棚卸し側と同じ事実を見る)
|
|
229
|
+
(0, memory_triage_1.attachMemoryTriage)(memory, lang, undefined, {
|
|
230
|
+
sharedEnv: (0, memory_1.resolveAutoMemoryDir)(cwd)?.scope === 'user',
|
|
231
|
+
});
|
|
205
232
|
const targets = [
|
|
206
233
|
{ label: 'user skills', sub: '~/.claude/skills/', path: scan_1.HOME },
|
|
207
234
|
...(0, scan_1.listProjects)(cwd)
|
|
@@ -218,7 +245,7 @@ function collect(cwd, lang) {
|
|
|
218
245
|
changes: (0, snapshot_1.computeChanges)(sections),
|
|
219
246
|
...(grp.groups ? { groups: grp.groups } : {}),
|
|
220
247
|
...(grp.stale ? { groupsStale: true } : {}),
|
|
221
|
-
...(memory.length ? { memory } : {}),
|
|
248
|
+
...(memory.length ? { memory: (0, memory_1.publicMemory)(memory) } : {}),
|
|
222
249
|
};
|
|
223
250
|
}
|
|
224
251
|
/* DNS rebinding 対策: same-origin GET には Origin が付かないため Host 側も検証する */
|
|
@@ -260,7 +287,7 @@ function handleApi(req, res, cwd) {
|
|
|
260
287
|
if (url.pathname === '/api/summary-status')
|
|
261
288
|
return send(200, (0, summary_1.summaryStatus)());
|
|
262
289
|
if (url.pathname === '/api/file') {
|
|
263
|
-
const real = (0, manage_1.assertReadableMd)(url.searchParams.get('src') || '');
|
|
290
|
+
const real = (0, manage_1.assertReadableMd)(url.searchParams.get('src') || '', cwd);
|
|
264
291
|
// mtime は編集画面の競合検出(/api/save の baseMtime)に使う
|
|
265
292
|
return send(200, {
|
|
266
293
|
content: fs.readFileSync(real, 'utf8'),
|
|
@@ -298,7 +325,7 @@ function handleApi(req, res, cwd) {
|
|
|
298
325
|
if (url.pathname === '/api/apply-description')
|
|
299
326
|
return send(200, (0, edit_1.doApplyDescription)(data));
|
|
300
327
|
if (url.pathname === '/api/diagnose') {
|
|
301
|
-
const real = (0, manage_1.assertReadableMd)(data.src);
|
|
328
|
+
const real = (0, manage_1.assertReadableMd)(data.src, cwd);
|
|
302
329
|
const name = data.name || path.basename(path.dirname(real));
|
|
303
330
|
(0, diagnose_1.diagnoseOne)(real, name, lang, model)
|
|
304
331
|
.then((d) => send(200, { ok: true, ...d }))
|
|
@@ -306,7 +333,7 @@ function handleApi(req, res, cwd) {
|
|
|
306
333
|
return;
|
|
307
334
|
}
|
|
308
335
|
if (url.pathname === '/api/flow') {
|
|
309
|
-
const real = (0, manage_1.assertReadableMd)(data.src);
|
|
336
|
+
const real = (0, manage_1.assertReadableMd)(data.src, cwd);
|
|
310
337
|
const name = data.name || path.basename(path.dirname(real));
|
|
311
338
|
(0, flow_1.flowOne)(real, name, lang, model)
|
|
312
339
|
.then((f) => send(200, { ok: true, ...f }))
|
|
@@ -322,7 +349,7 @@ function handleApi(req, res, cwd) {
|
|
|
322
349
|
if (url.pathname === '/api/delete')
|
|
323
350
|
return send(200, (0, manage_1.doDelete)(data));
|
|
324
351
|
if (url.pathname === '/api/open')
|
|
325
|
-
return send(200, (0, manage_1.openInEditor)(data));
|
|
352
|
+
return send(200, (0, manage_1.openInEditor)(data, cwd));
|
|
326
353
|
if (url.pathname === '/api/summarize-all')
|
|
327
354
|
return send(200, (0, summary_1.startSummarizeAll)((0, scan_1.scanSections)(cwd, lang), !!data.force, lang, model));
|
|
328
355
|
if (url.pathname === '/api/group-generate') {
|
|
@@ -347,13 +374,16 @@ function handleApi(req, res, cwd) {
|
|
|
347
374
|
force: !!data.force,
|
|
348
375
|
files,
|
|
349
376
|
sections: () => (0, scan_1.scanSections)(cwd, lang),
|
|
377
|
+
// 置き場の解決は起動ディレクトリ基準。process.cwd() 任せにせず、この
|
|
378
|
+
// リクエストと同じ cwd で解決した値を渡す(スキャン・読み取り許可と同じ事実を見る)
|
|
379
|
+
autoMemory: (0, memory_1.resolveAutoMemoryDir)(cwd),
|
|
350
380
|
})
|
|
351
381
|
.then((results) => send(200, { ok: true, results }))
|
|
352
382
|
.catch((e) => send(400, (0, errors_1.toErrorBody)(e)));
|
|
353
383
|
return;
|
|
354
384
|
}
|
|
355
385
|
if (url.pathname === '/api/summarize') {
|
|
356
|
-
const real = (0, manage_1.assertReadableMd)(data.src);
|
|
386
|
+
const real = (0, manage_1.assertReadableMd)(data.src, cwd);
|
|
357
387
|
// refs(関係候補)はスキャン結果から復元する
|
|
358
388
|
const sections = (0, scan_1.scanSections)(cwd, lang);
|
|
359
389
|
const item = sections.flatMap((s) => s.items).find((x) => x.path === real);
|
package/build/server/manage.js
CHANGED
|
@@ -45,6 +45,7 @@ const os = __importStar(require("node:os"));
|
|
|
45
45
|
const path = __importStar(require("node:path"));
|
|
46
46
|
const node_child_process_1 = require("node:child_process");
|
|
47
47
|
const scan_1 = require("./scan");
|
|
48
|
+
const memory_1 = require("./memory");
|
|
48
49
|
const errors_1 = require("./errors");
|
|
49
50
|
const HOME = os.homedir();
|
|
50
51
|
/* realpath 解決(存在しないパスは not-found に正規化) */
|
|
@@ -77,19 +78,55 @@ function assertManagedPath(p) {
|
|
|
77
78
|
throw new errors_1.ApiError('not-managed-path', real);
|
|
78
79
|
return { real, kind };
|
|
79
80
|
}
|
|
80
|
-
/*
|
|
81
|
-
|
|
81
|
+
/*
|
|
82
|
+
* 自動メモリの置き場(settings の autoMemoryDirectory)の realpath。キャッシュは解決値ごとに 1 回。
|
|
83
|
+
* 設定が有効な環境では memory の実体が .claude の外へ丸ごと移るため、
|
|
84
|
+
* 一覧に出ている本文が読めない(fetchFile・棚卸しモーダル・エディタで開くが全滅する)。
|
|
85
|
+
*/
|
|
86
|
+
const autoRealMemo = new Map();
|
|
87
|
+
function autoMemoryRoot(cwd) {
|
|
88
|
+
const info = (0, memory_1.resolveAutoMemoryDir)(cwd);
|
|
89
|
+
if (!info)
|
|
90
|
+
return null;
|
|
91
|
+
const cached = autoRealMemo.get(info.dir);
|
|
92
|
+
if (cached)
|
|
93
|
+
return cached;
|
|
94
|
+
try {
|
|
95
|
+
const real = fs.realpathSync(info.dir);
|
|
96
|
+
// 解決できたときだけ覚える(置き場がまだ無い時点の失敗を焼き付けない。
|
|
97
|
+
// ディレクトリは後から作られ得るので、次のリクエストで解決し直せるようにする)
|
|
98
|
+
autoRealMemo.set(info.dir, real);
|
|
99
|
+
return real;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/*
|
|
106
|
+
* 解決済み autoMemoryDirectory の配下か。realpath 同士を path.sep 区切りで前方一致させる
|
|
107
|
+
* (symlink 経由のパスで一致が外れないように / `<dir>-other` のような兄弟を巻き込まないように)。
|
|
108
|
+
* `.claude` のルールは緩めず、この許可を足すだけ。
|
|
109
|
+
*/
|
|
110
|
+
function underAutoMemory(real, cwd) {
|
|
111
|
+
const root = autoMemoryRoot(cwd);
|
|
112
|
+
// isUnder はケース非依存 FS で case-fold する(root が `/USERS/…` のとき
|
|
113
|
+
// 実ファイルの realpath `/Users/…` と取り違えないように)
|
|
114
|
+
return !!root && real !== root && (0, memory_1.isUnder)(real, root);
|
|
115
|
+
}
|
|
116
|
+
const underDotClaude = (real) => real.includes(path.sep + '.claude' + path.sep);
|
|
117
|
+
/* 読み取り専用は plugin 配下も許可(.claude 配下 + 自動メモリの置き場配下の .md のみ) */
|
|
118
|
+
function assertReadableMd(p, cwd = process.cwd()) {
|
|
82
119
|
const real = realpathOrThrow(p);
|
|
83
120
|
if (!real.endsWith('.md'))
|
|
84
121
|
throw new errors_1.ApiError('not-md', real);
|
|
85
|
-
if (!real
|
|
122
|
+
if (!underDotClaude(real) && !underAutoMemory(real, cwd))
|
|
86
123
|
throw new errors_1.ApiError('not-readable-path', real);
|
|
87
124
|
return real;
|
|
88
125
|
}
|
|
89
|
-
/* エディタで開くのは .claude
|
|
90
|
-
function assertOpenablePath(p) {
|
|
126
|
+
/* エディタで開くのは .claude 配下(settings.json 等も含む)と自動メモリの置き場配下 */
|
|
127
|
+
function assertOpenablePath(p, cwd) {
|
|
91
128
|
const real = realpathOrThrow(p);
|
|
92
|
-
if (!real
|
|
129
|
+
if (!underDotClaude(real) && !underAutoMemory(real, cwd))
|
|
93
130
|
throw new errors_1.ApiError('not-openable-path', real);
|
|
94
131
|
return real;
|
|
95
132
|
}
|
|
@@ -190,8 +227,8 @@ function detectEditor() {
|
|
|
190
227
|
}
|
|
191
228
|
return (editorCache = { cmd: null });
|
|
192
229
|
}
|
|
193
|
-
function openInEditor({ src }) {
|
|
194
|
-
const real = assertOpenablePath(src);
|
|
230
|
+
function openInEditor({ src }, cwd = process.cwd()) {
|
|
231
|
+
const real = assertOpenablePath(src, cwd);
|
|
195
232
|
const { cmd } = detectEditor();
|
|
196
233
|
if (cmd) {
|
|
197
234
|
(0, node_child_process_1.spawn)(cmd, [real], { detached: true, stdio: 'ignore' }).unref();
|
|
@@ -88,7 +88,7 @@ function latestDate(body, now) {
|
|
|
88
88
|
/*
|
|
89
89
|
* 本文が参照するファイルパスのうち存在しないもの。
|
|
90
90
|
* 対象は「/ を含み、末尾が拡張子つきのファイル名」に限る(URL・ブランチ名・パッケージ名を拾わない)。
|
|
91
|
-
* 相対パスは projectPath 基準。projectPath が無い(
|
|
91
|
+
* 相対パスは projectPath 基準。projectPath が無い(プロジェクト不明)なら絶対パスと ~/ だけを見る。
|
|
92
92
|
*/
|
|
93
93
|
function missingPaths(body, projectPath, home) {
|
|
94
94
|
const out = [];
|
|
@@ -155,7 +155,9 @@ function extractSignals(body, description, projectPath, opts = {}) {
|
|
|
155
155
|
/*
|
|
156
156
|
* 本文が別の登録プロジェクトの配下パス(絶対 / ~/)を指しているか。
|
|
157
157
|
* 「別プロジェクトの話が混入した memory」の機械的な根拠で、置き場所(wrong-project)の判断材料になる。
|
|
158
|
-
*
|
|
158
|
+
* 呼び出し側で自分自身(slug 一致を含む)・worktree 関係・入れ子プロジェクトは除いて渡す。
|
|
159
|
+
* 値はフルパス(basename だと teamA/ai-workspace と teamB/ai-workspace のような
|
|
160
|
+
* 同名プロジェクトを区別できないため。区別が目的なので表示側もフルパスのまま出す)。
|
|
159
161
|
*/
|
|
160
162
|
function otherProjectRefs(body, home, others) {
|
|
161
163
|
if (!others.length)
|
|
@@ -164,12 +166,23 @@ function otherProjectRefs(body, home, others) {
|
|
|
164
166
|
for (const m of body.matchAll(/(~\/[^\s))」'"`<>]+|\/[\w.@-]+(?:\/[\w.@-]+)+)/g)) {
|
|
165
167
|
const raw = m[1].replace(/[/.,:;。、))」]+$/, '');
|
|
166
168
|
const resolved = raw.startsWith('~/') ? path.join(home, raw.slice(2)) : raw;
|
|
169
|
+
/*
|
|
170
|
+
* 1 つのパス参照が採るのは最長一致の 1 件だけ。入れ子で登録されたプロジェクト
|
|
171
|
+
* (親 /a と子 /a/b が両方 ~/.claude.json にある)では、/a/b/x.ts への参照 1 つが
|
|
172
|
+
* 親子 2 件の候補を生み、粗いほうの親が wrong-project の移動先判断を誤らせる。
|
|
173
|
+
*/
|
|
174
|
+
let best = '';
|
|
167
175
|
for (const p of others) {
|
|
168
|
-
if (resolved
|
|
169
|
-
|
|
176
|
+
if (resolved !== p && !resolved.startsWith(p + path.sep))
|
|
177
|
+
continue;
|
|
178
|
+
if (p.length > best.length)
|
|
179
|
+
best = p;
|
|
170
180
|
}
|
|
181
|
+
if (best)
|
|
182
|
+
hit.add(best);
|
|
171
183
|
}
|
|
172
|
-
|
|
184
|
+
// 上限 3: 参照が複数プロジェクトに散っているとき、2 件では正解が落ちることがある(最長一致で 1 参照 1 件になった分の余裕)
|
|
185
|
+
return [...hit].slice(0, 3);
|
|
173
186
|
}
|
|
174
187
|
const WHY_RE = /^\s*(?:\*\*)?Why:?(?:\*\*)?:?\s*/im;
|
|
175
188
|
const HOW_RE = /^\s*(?:\*\*)?How to apply:?(?:\*\*)?:?\s*/im;
|