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 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/`) per project with its context cost split into the always-on part (the `MEMORY.md` index line injected into every session) and the pay-per-use part (the body, read on demand), plus Read / Write counts from transcripts, `[[link]]` resolution and backlinks. **AI triage** reads every memory of a project with the claude CLI (one call, split into a few for very large projects) and proposes a destination per memory — keep / shrink / move to CLAUDE.md / move to docs / move to a skill / delete / wrong project — with the reasoning and a paste-ready instruction for Claude Code (it is asked to always cover removing the `MEMORY.md` index line and re-pointing `[[link]]`s). Each memory is first judged for freshness (current / outdated / historical / obsolete) from mechanical signals — dates in the body, missing paths, merged or deleted branches, references to another project — and a _rewrite the body_ verdict covers memories whose gist still holds; the triage also flags when the `MEMORY.md` index line disagrees with the body. For feedback memories the instruction is built from a fixed template, so it reads the same on every model. The viewer never writes to memory: you paste the instruction into Claude Code, which inspects, asks when unsure, and executes after your approval
27
+ - **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
 
@@ -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
- sec.usageAvailable = (0, usage_1.hasTranscripts)(dirsWithTranscripts, sec.id);
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
- (0, memory_triage_1.attachMemoryTriage)(memory, lang);
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);
@@ -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
- /* 読み取り専用は plugin 配下も許可(.claude 配下の .md のみ) */
81
- function assertReadableMd(p) {
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.includes(path.sep + '.claude' + path.sep))
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 配下ならなんでも良い(settings.json 等も含む) */
90
- function assertOpenablePath(p) {
126
+ /* エディタで開くのは .claude 配下(settings.json 等も含む)と自動メモリの置き場配下 */
127
+ function assertOpenablePath(p, cwd) {
91
128
  const real = realpathOrThrow(p);
92
- if (!real.includes(path.sep + '.claude' + path.sep))
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
- * 呼び出し側で自分自身と worktree 関係のプロジェクトは除いて渡す。
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 === p || resolved.startsWith(p + path.sep))
169
- hit.add(path.basename(p));
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
- return [...hit].slice(0, 2);
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;