skills-viewer 0.8.0 → 0.9.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 +38 -19
- package/build/cli.js +0 -0
- package/build/server/claude-md.js +512 -0
- package/build/server/diff.js +156 -0
- package/build/server/index.js +458 -48
- package/build/server/lint.js +8 -0
- package/build/server/manage.js +45 -8
- package/build/server/memory-signals.js +18 -5
- package/build/server/memory-triage.js +569 -73
- package/build/server/memory.js +519 -23
- package/build/server/read-access.js +242 -0
- package/build/server/scan.js +75 -11
- package/build/server/snapshot.js +263 -23
- package/build/server/summary.js +41 -0
- package/build/server/usage.js +77 -15
- package/build/shared/import-refs.js +81 -0
- package/dist/assets/index-D9VG-dm-.css +1 -0
- package/dist/assets/index-YQ2U1bsy.js +96 -0
- package/dist/index.html +16 -2
- package/package.json +16 -17
- package/dist/assets/index-CFbG0247.js +0 -84
- package/dist/assets/index-Cx_UH1Vx.css +0 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# skills-viewer
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Read, search and diagnose your [Claude Code](https://code.claude.com) skills, commands, agents, hooks, auto memory and CLAUDE.md in the browser.
|
|
4
4
|
|
|
5
|
-
Scans every project registered in `~/.claude.json` (plus user scope, plugins and built-ins), and serves a local web UI to explore them — with usage stats, AI-generated summaries, full SKILL.md rendering
|
|
5
|
+
Scans every project registered in `~/.claude.json` (plus user scope, plugins and built-ins), and serves a local web UI to explore them — with usage stats, AI-generated summaries, full SKILL.md rendering and same-name diffs. It reads and explains; changes are handed to Claude Code (as a paste-ready instruction) or to your editor.
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
npx skills-viewer
|
|
@@ -12,31 +12,43 @@ npx skills-viewer
|
|
|
12
12
|
|
|
13
13
|
<!-- 相対パス参照: private リポジトリでも GitHub 上で表示でき、npmjs.com は repository フィールドを元に raw URL へ書き換えるため public 化後は npm でも表示される -->
|
|
14
14
|
|
|
15
|
-

|
|
16
|
+
|
|
17
|
+
<!-- 3 枚は横並びのサムネイル(クリックで原寸)。縦に並べると README が画面 4 枚分伸びるため -->
|
|
18
|
+
<p>
|
|
19
|
+
<a href="assets/skill.png"><img src="assets/skill.png" width="32%" alt="Skill page — summary, facts strip, how it triggers and what it touches"></a>
|
|
20
|
+
<a href="assets/flow.png"><img src="assets/flow.png" width="32%" alt="Flow diagram extracted from the skill body — steps, branches, delegations and a human gate"></a>
|
|
21
|
+
<a href="assets/memory.png"><img src="assets/memory.png" width="32%" alt="Memory page — index line, freshness and triage verdict, then the body"></a>
|
|
22
|
+
</p>
|
|
23
|
+
<sub>Skill page · flow diagram · memory page (click to enlarge). Ledger theme; Console is the dark one.</sub>
|
|
16
24
|
|
|
17
25
|
## Features
|
|
18
26
|
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
27
|
+
- **Home answers three questions** — what changed since you last looked, what this session actually loads, and what is available in it. The list is no longer the first thing you see
|
|
28
|
+
- **What's loaded, and what it costs** — the _session context_ block adds up everything Claude Code reads at the start of a session in the current project: the CLAUDE.md files, the `MEMORY.md` index (against the official 200-line / 25 KB limit) and every name + description (against the skill-listing budget — Claude Code truncates the listing at about 2 % of the context window; the viewer estimates it and defers to `/doctor` for the actual cut). System prompt, MCP tools and hook output are not visible to the viewer and are excluded
|
|
29
|
+
- **CLAUDE.md, all seven layers** — managed policy, `~/.claude/CLAUDE.md`, `CLAUDE.md`, `.claude/CLAUDE.md`, `CLAUDE.local.md`, `.claude/rules/*.md` and ancestor files up to the git root, listed in load order with per-heading token cost and `@import` expanded in place. Layers that do not exist stay on the list as _none_, because what is **not** read is information too
|
|
30
|
+
- **Two themes** — Console (dark, monospace labels, dense rows) and Ledger (light, rows and tables). Follows your OS setting by default and can be pinned in settings; the layout is fluid down to a 900 px minimum
|
|
31
|
+
- **All scopes in one view** — user (`~/.claude/skills`), every project's `.claude/skills` / `.claude/commands`, installed plugins, and Claude Code built-ins. The current project opens first and the rest stay collapsed, so "what is in this repo" is not buried in what applies everywhere
|
|
32
|
+
- **Worktrees** — the project switcher lists main checkouts only, with a worktree count; open a project and pick a worktree from the selector at the top of the page to see what a session started there would load (skills from the worktree itself, memory shared with the main checkout)
|
|
33
|
+
- **Purpose grouping (AI)** — one haiku call classifies everything installed by _when you use it_ into 4–8 groups generated for your environment (planning / building / review / release / … as a role-agnostic guide — a designer's or PM's skills get their own groups). Pick between by-source, by-purpose and single-list orderings in the _all projects_ view; a frontmatter `category:` pins an item to a manual group that takes precedence
|
|
21
34
|
- **Search / sort** — incremental search over name + description + usage; sort by name, usage count, last used, updated date, or token cost
|
|
22
|
-
- **Diagnostics** — a _no recent use_ badge (no recorded use within the transcript retention window) with an all / recent use / no recent use filter, plus static description lint: missing / too-short / too-long descriptions, missing trigger conditions ("use when …") that make auto-invocation unlikely,
|
|
35
|
+
- **Diagnostics** — a _no recent use_ badge (no recorded use within the transcript retention window) with an all / recent use / no recent use filter, plus static description lint: missing / too-short / too-long descriptions, missing trigger conditions ("use when …") that make auto-invocation unlikely, name-echo descriptions, and descriptions that exceed the 1,536-character cap Claude Code applies to the skill listing (the rest is cut, so the trigger should come first)
|
|
23
36
|
- **Token cost** — since every name + description is injected into each session, the estimated token overhead is shown per item, per scope, and as a per-session total for the current project
|
|
24
|
-
- **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
|
|
37
|
+
- **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 (cached by content hash). The result comes with a paste-ready instruction for Claude Code — the target path, what to change (or what to keep if you change it), and the "check first, ask when unsure, execute after approval" steps — so the edit happens where you can review it
|
|
25
38
|
- **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
39
|
- **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
|
|
28
|
-
- **
|
|
29
|
-
- **What's changed** — a banner shows items added / updated / removed since your last launch (baseline advances only when you dismiss it); the CLI prints a one-line summary at startup too
|
|
40
|
+
- **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
|
|
41
|
+
- **What's changed** — the first block on the home screen lists what was added / updated / removed since you last marked it read, in GitHub's diff grammar (`+` / `~` / `−`). Skills, commands, agents, **auto memory and CLAUDE.md** are all tracked, and for files under git the author and date come from `git log` (looked up for the first 40 changed files, within a short time budget; the rest show the date only); the CLI prints a one-line summary at startup too
|
|
30
42
|
- **Usage sparkline** — the detail pane charts the last 30 days of per-day usage
|
|
31
43
|
- **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
|
|
32
44
|
- **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
|
|
33
|
-
- **
|
|
34
|
-
- **Agents & hooks too** — `.claude/agents/*.md` and `hooks` entries from `settings.json` / `settings.local.json` are listed alongside skills with kind badges (
|
|
35
|
-
- **
|
|
45
|
+
- **One page per item** — a single column, read top to bottom: summary, then a strip of facts (usage, when it was added and by whom, same-name definitions, bundled files), then how it triggers, what it touches (delegates and `allowed-tools`, so writes and outbound calls are known without asking the model), then the flow, then the full text. Hooks get a short page of their own (event, matcher, command, and the other hooks in the session)
|
|
46
|
+
- **Agents & hooks too** — `.claude/agents/*.md` and `hooks` entries from `settings.json` / `settings.local.json` are listed alongside skills with kind badges (a hook is a `settings.json` entry, not a file of its own)
|
|
47
|
+
- **Read-only by design** — the viewer never edits, copies or deletes your definitions. It diagnoses and explains, then hands the change over: a paste-ready instruction for Claude Code, or _Open in editor_ for any file it is allowed to read
|
|
36
48
|
- **Same-name diff** — when the same skill name exists in multiple scopes, the detail page lists the other definitions and shows a line diff between them
|
|
37
|
-
- **Open in editor** — via URL scheme (VS Code / Cursor / Zed / Windsurf / custom, configurable in the ⚙ settings modal), or the OS default opener
|
|
49
|
+
- **Open in editor** — via URL scheme (VS Code / Cursor / Zed / Windsurf / custom, configurable in the ⚙ settings modal), or the OS default opener; offered for every file inside the paths the viewer may read, including `settings.json` and not only `.md`
|
|
38
50
|
- **English / 日本語** — UI language auto-detected from the browser and switchable in settings; AI summaries are generated in the selected language (CLI messages follow `LANG`)
|
|
39
|
-
- **URL routing** — `/skills/:id
|
|
51
|
+
- **URL routing** — `/skills/:id`, `/claude-md/:id`, `/memory`, `/memory/:id`, with `?project=<id>|all`, search, sort and filters in query params; the project id is derived from its path, so links are shareable across reloads
|
|
40
52
|
|
|
41
53
|
## Usage
|
|
42
54
|
|
|
@@ -52,7 +64,12 @@ Run it from a project directory to have that project marked as “current” and
|
|
|
52
64
|
|
|
53
65
|
- Binds to `127.0.0.1` only
|
|
54
66
|
- 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
|
-
-
|
|
67
|
+
- **The viewer never modifies your definitions.** It writes only its own state (the "what's changed" baseline and the AI result cache under `~/.cache/skills-viewer/`); the other mutating APIs just start a `claude` CLI call or hand a path to your editor
|
|
68
|
+
- Reads stay inside `~/.claude`, the per-project `.claude` directories, the `autoMemoryDirectory` configured for the current directory or the project you selected (its `.claude/settings.json` / `settings.local.json` are read the same way Claude Code reads them there), and **the CLAUDE.md files Claude Code itself loads for those same two projects** — `CLAUDE.md`, `CLAUDE.local.md`, `.claude/rules/*.md` and ancestor `CLAUDE.md` files up to the git root. That last group sits outside `.claude`, so it is allowed by exact path: only the files the scanner enumerated at those fixed names and places, never an arbitrary `.md`. (Change tracking additionally hashes the CLAUDE.md group of every registered project and its worktrees so the _Changed_ block can list them; those files are read to compute a hash and are never returned to the page or sent anywhere.) The `autoMemoryDirectory` and CLAUDE.md parts follow your selection and nothing else — the `.claude` rule itself is fixed and does not depend on what you select: a project you have registered but are not looking at cannot widen those two, since `autoMemoryDirectory` is read from a committed `.claude/settings.json` that arrives with a cloned repository. Selecting a project is what grants it: the viewer has no separate trust prompt, so its `.claude/settings.json` is read the same way Claude Code reads it once you trust the folder. The project is picked by id from the list the viewer itself enumerated (the registry, the worktrees linked from it, and the main checkouts those worktrees belong to), never by a path sent from the page. A symlinked `CLAUDE.md` is followed, since Claude Code follows it too (`ln -s AGENTS.md CLAUDE.md` is a documented setup). An `autoMemoryDirectory` that resolves to a filesystem root, your home directory or an ancestor of it is ignored. _Open in editor_ is limited to the same paths
|
|
69
|
+
- `@import` inside a CLAUDE.md is followed only within the project or `~/.claude`, up to 4 levels (the official limit) and 4 MiB per file (the size at which Claude Code itself skips a file). A CLAUDE.md arrives with a cloned repository, so `@/etc/hosts` is reported as out of scope and never opened — not even to check whether it exists. Dot-prefixed names and anything under `.git` stay out of reach even inside the project (`.claude` itself is the exception, since it is the boundary), and the boundary is re-checked after symlinks resolve. At most 200 references per file and 500 per scan are followed; when either limit is reached, the home and CLAUDE.md screens say so, since the token totals they show are then short of the rest. A file larger than 4 MiB is listed but shown as not loaded, since Claude Code skips it too
|
|
70
|
+
- Only the bodies under `~/.claude`, the per-project `.claude` directories and the `autoMemoryDirectory` of the current or selected project are ever sent to the `claude` CLI. The CLAUDE.md group sits outside those, so its bodies are displayed but never sent — `CLAUDE.local.md` in particular is usually gitignored and private. The one exception is deliberate and small: memory triage sends the **headings** (never the bodies) of `~/.claude/CLAUDE.md` and of the triaged project's `CLAUDE.md` / `.claude/CLAUDE.md`, so it can spot duplicates and promotion targets (see Notes)
|
|
71
|
+
- On startup the viewer deletes `~/.cache/skills-viewer/backups/`, the one-generation copies the v0.8 in-browser editor used to make. Nothing writes there any more, and it prints a line when it removes them
|
|
72
|
+
- Reading the previous version of a file (for the diff) runs local `git show` inside the same allowed paths, and `git log` supplies the author for changed files. Both are invoked as argument arrays with a timeout, never through a shell, and no network access is involved
|
|
56
73
|
|
|
57
74
|
## Development
|
|
58
75
|
|
|
@@ -69,7 +86,7 @@ Layout:
|
|
|
69
86
|
```
|
|
70
87
|
src/
|
|
71
88
|
├─ cli.ts # bin entry
|
|
72
|
-
├─ server/ # zero-dep Node API (scan / usage / summary /
|
|
89
|
+
├─ server/ # zero-dep Node API (scan / usage / summary / read-access) — TypeScript
|
|
73
90
|
└─ shared/ # types shared between server and web (single source)
|
|
74
91
|
web/ # React + TypeScript + Vite (devDependencies only — bundled into dist/)
|
|
75
92
|
build/ # compiled server JS shipped in the npm package (generated by prepack)
|
|
@@ -81,9 +98,11 @@ dist/ # prebuilt UI shipped in the npm package (generated by prepack
|
|
|
81
98
|
- Usage stats only cover the transcript retention window of Claude Code (`cleanupPeriodDays`, default 30 days) — the _no recent use_ badge and memory Read counts have the same limitation (projects without transcripts show no usage columns at all)
|
|
82
99
|
- Token costs are heuristic estimates (≈4 chars/token for ASCII, ≈1.5 for CJK), not exact tokenizer counts
|
|
83
100
|
- 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
|
|
84
|
-
- The built-in skill list is hardcoded in `server/scan.
|
|
85
|
-
-
|
|
101
|
+
- The built-in skill list is hardcoded in `src/server/scan.ts` (they live inside the Claude Code binary); check `/skills` inside Claude Code for the authoritative list
|
|
102
|
+
- The skill-listing budget shown on the home screen is an estimate: Claude Code caps the listing by characters at about 2 % of the context window, and the viewer compares its token estimate against 2 % of a 200k window (4,000 tokens). It says _likely truncated_, not _truncated_ — run `/doctor` inside Claude Code to see what was actually cut
|
|
103
|
+
- AI features require a logged-in `claude` CLI. Its presence is checked once at startup (`claude --version`); if it is missing, the AI buttons are disabled with a note — install it and restart the viewer to enable them
|
|
86
104
|
- 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
|
|
105
|
+
- The Memory view resolves `autoMemoryDirectory` from your user settings and the project you selected (any project registered in `~/.claude.json`, or a linked worktree of one; its `.claude/settings.json` / `settings.local.json` are read the same way Claude Code reads them there). Only the current directory and the selected project are resolved this way — the settings of the other registered projects are not read, so they cannot widen what the viewer may read
|
|
87
106
|
|
|
88
107
|
## License
|
|
89
108
|
|
package/build/cli.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* CLAUDE.md 群の走査。Claude Code が「毎セッションの最初に読むもの」を注入順に列挙し、
|
|
4
|
+
* 常時コスト(トークン)と本文を返す。ホームの「セッションの文脈」と CLAUDE.md 画面の土台。
|
|
5
|
+
*
|
|
6
|
+
* 公式の読み込み順(https://code.claude.com/docs/en/memory.md、2026-09-08 確認):
|
|
7
|
+
* 1. 管理ポリシー(OS 固定パス)
|
|
8
|
+
* 2. ~/.claude/CLAUDE.md
|
|
9
|
+
* 3. <project>/CLAUDE.md
|
|
10
|
+
* 4. <project>/.claude/CLAUDE.md
|
|
11
|
+
* 5. <project>/CLAUDE.local.md
|
|
12
|
+
* 6. <project>/.claude/rules/*.md(frontmatter に paths: が無いものだけが起動時。有りは遅延)
|
|
13
|
+
* 7. 親ディレクトリの CLAUDE.md(git root まで遡る)
|
|
14
|
+
* サブディレクトリの CLAUDE.md は「そのディレクトリのファイルを読むとき」の遅延ロードなので
|
|
15
|
+
* 常時コストには含めない(この走査の対象外)。
|
|
16
|
+
*
|
|
17
|
+
* HOME/.claude の参照はこのファイルに集約する(計画 11 の claudeDir() へ差し替えやすくするため)。
|
|
18
|
+
*/
|
|
19
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
22
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
23
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
24
|
+
}
|
|
25
|
+
Object.defineProperty(o, k2, desc);
|
|
26
|
+
}) : (function(o, m, k, k2) {
|
|
27
|
+
if (k2 === undefined) k2 = k;
|
|
28
|
+
o[k2] = m[k];
|
|
29
|
+
}));
|
|
30
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
31
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
32
|
+
}) : function(o, v) {
|
|
33
|
+
o["default"] = v;
|
|
34
|
+
});
|
|
35
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
36
|
+
var ownKeys = function(o) {
|
|
37
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
38
|
+
var ar = [];
|
|
39
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
40
|
+
return ar;
|
|
41
|
+
};
|
|
42
|
+
return ownKeys(o);
|
|
43
|
+
};
|
|
44
|
+
return function (mod) {
|
|
45
|
+
if (mod && mod.__esModule) return mod;
|
|
46
|
+
var result = {};
|
|
47
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
48
|
+
__setModuleDefault(result, mod);
|
|
49
|
+
return result;
|
|
50
|
+
};
|
|
51
|
+
})();
|
|
52
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
|
+
exports.userClaudeDir = void 0;
|
|
54
|
+
exports.claudeMdLayers = claudeMdLayers;
|
|
55
|
+
exports.claudeMdPaths = claudeMdPaths;
|
|
56
|
+
exports.claudeMdRefs = claudeMdRefs;
|
|
57
|
+
exports.claudeMdRefsOf = claudeMdRefsOf;
|
|
58
|
+
const fs = __importStar(require("node:fs"));
|
|
59
|
+
const os = __importStar(require("node:os"));
|
|
60
|
+
const path = __importStar(require("node:path"));
|
|
61
|
+
const lint_1 = require("./lint");
|
|
62
|
+
const scan_1 = require("./scan");
|
|
63
|
+
const memory_1 = require("./memory");
|
|
64
|
+
const import_refs_1 = require("../shared/import-refs");
|
|
65
|
+
/*
|
|
66
|
+
* user scope の設定ディレクトリ。HOME/.claude の参照はこのファイルに集約する
|
|
67
|
+
* (計画 11 の CLAUDE_CONFIG_DIR 対応で claudeDir() に差し替える箇所を 1 つに保つため)。
|
|
68
|
+
*/
|
|
69
|
+
const userClaudeDir = (home = os.homedir()) => path.join(home, '.claude');
|
|
70
|
+
exports.userClaudeDir = userClaudeDir;
|
|
71
|
+
/* @import の展開上限(公式仕様: maximum depth of four hops) */
|
|
72
|
+
const MAX_IMPORT_DEPTH = 4;
|
|
73
|
+
/*
|
|
74
|
+
* 1 ファイルの読み取り上限。公式が「Claude Code loads a CLAUDE.md file of up to 4 MiB in full
|
|
75
|
+
* and skips a larger file」と定めているので同じ線に揃える。ここを公式より狭くすると、
|
|
76
|
+
* Claude Code は読んでいるのに viewer は「未読」と出すことになり、常時コストを過少に出す。
|
|
77
|
+
* 7 段の本体にも @import 先にも同じ上限を掛ける(本体だけ無制限だと概算のループが効かない)。
|
|
78
|
+
*/
|
|
79
|
+
const MAX_FILE_BYTES = 4 * 1024 * 1024;
|
|
80
|
+
/*
|
|
81
|
+
* 1 ファイルから拾う参照の件数と、1 回の走査(7 段ぜんぶ)で作る @import 行の総数。
|
|
82
|
+
* 参照 1 件ごとに realpath + stat が走るので、扇形に広い CLAUDE.md で毎リクエスト数千回の
|
|
83
|
+
* fs 呼び出しと数 MB の JSON が出るのを防ぐ(git author に 40 件の予算を置いたのと同じ趣旨)。
|
|
84
|
+
*
|
|
85
|
+
* 総数はファイル単位ではなく**走査全体で共有する**。rules 段は .claude/rules/*.md を
|
|
86
|
+
* 件数の制限なく回すので、ファイル単位の上限だけだと「200 本 × 200 件」で元の木阿弥になる。
|
|
87
|
+
*/
|
|
88
|
+
const MAX_IMPORT_REFS_PER_FILE = 200;
|
|
89
|
+
const MAX_IMPORT_ENTRIES = 500;
|
|
90
|
+
function importScopeOf(root, home) {
|
|
91
|
+
const dirs = [(0, exports.userClaudeDir)(home), root].filter((d) => !!d);
|
|
92
|
+
// realDir は memory.ts の写像(realpath が取れなければ resolve で代用)。
|
|
93
|
+
// 許可の判定はこの 1 本に揃える — 場所ごとに実パスで見るかが変わると許可と判定がずれる
|
|
94
|
+
return {
|
|
95
|
+
roots: [...new Set(dirs.flatMap((d) => [d, (0, memory_1.realDir)(d)]))],
|
|
96
|
+
budget: { left: MAX_IMPORT_ENTRIES, truncated: false },
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/* 管理ポリシーの置き場(OS ごとの固定パス)。本文は返さず存在と概算だけ扱う */
|
|
100
|
+
function managedPolicyPath() {
|
|
101
|
+
if (process.platform === 'darwin')
|
|
102
|
+
return '/Library/Application Support/ClaudeCode/CLAUDE.md';
|
|
103
|
+
if (process.platform === 'win32')
|
|
104
|
+
return 'C:\\Program Files\\ClaudeCode\\CLAUDE.md';
|
|
105
|
+
return '/etc/claude-code/CLAUDE.md';
|
|
106
|
+
}
|
|
107
|
+
/*
|
|
108
|
+
* 本文の読み取り。'too-large' と null(読めない)を言い分ける:
|
|
109
|
+
* 大きすぎるファイルは「存在するが読まれない」ので、段から消すと差分追跡が
|
|
110
|
+
* 「消えた」と誤って出す(claudeMdRefs は段の files から現在のキーを作る)。
|
|
111
|
+
*/
|
|
112
|
+
function readText(fp) {
|
|
113
|
+
try {
|
|
114
|
+
// 公式も 4 MiB 超の CLAUDE.md は読まない(loads … up to 4 MiB in full and skips a larger file)。
|
|
115
|
+
// 読む前に大きさで落とす(毎リクエストの全読みと概算の文字単位ループが効かないように)
|
|
116
|
+
if (fs.statSync(fp).size > MAX_FILE_BYTES)
|
|
117
|
+
return 'too-large';
|
|
118
|
+
return fs.readFileSync(fp, 'utf8');
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return null; // 権限エラー等で読めない段は「無い」扱いにして一覧全体を落とさない
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function mtime(fp) {
|
|
125
|
+
try {
|
|
126
|
+
return fs.statSync(fp).mtime.toISOString();
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return '';
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/*
|
|
133
|
+
* 見出しごとの概算。見出し行から次の見出し行の直前までを 1 節として数えるので、
|
|
134
|
+
* 節の合計は本文全体の概算とほぼ一致する(どの節が重いかを出すための内訳)。
|
|
135
|
+
*/
|
|
136
|
+
function headingsOf(body) {
|
|
137
|
+
const lines = body.split(/\r?\n/);
|
|
138
|
+
const out = [];
|
|
139
|
+
let cur = null;
|
|
140
|
+
for (const line of lines) {
|
|
141
|
+
const h = line.match(/^(#{1,6})\s+(.*)$/);
|
|
142
|
+
if (h) {
|
|
143
|
+
if (cur)
|
|
144
|
+
out.push({ text: cur.text, tokens: (0, lint_1.estimateTokens)(cur.buf.join('\n')) });
|
|
145
|
+
cur = { text: h[2].trim(), buf: [line] };
|
|
146
|
+
}
|
|
147
|
+
else if (cur) {
|
|
148
|
+
cur.buf.push(line);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (cur)
|
|
152
|
+
out.push({ text: cur.text, tokens: (0, lint_1.estimateTokens)(cur.buf.join('\n')) });
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
function resolveImport(ref, fromDir, home) {
|
|
156
|
+
if (ref.startsWith('~/'))
|
|
157
|
+
return path.join(home, ref.slice(2));
|
|
158
|
+
if (path.isAbsolute(ref))
|
|
159
|
+
return ref;
|
|
160
|
+
return path.resolve(fromDir, ref);
|
|
161
|
+
}
|
|
162
|
+
/*
|
|
163
|
+
* @import を深さ優先で展開する。
|
|
164
|
+
* - stack は展開の経路(自分の先祖)。ここに居れば本当の循環
|
|
165
|
+
* - seen は 1 ファイルの走査で数えた実パス。経路が違うのに再登場したのはダイヤモンド参照で、
|
|
166
|
+
* 二重計上を避けるために展開しないが循環ではない(表示の文言を分ける)
|
|
167
|
+
*/
|
|
168
|
+
function expandImports(body, fromDir, home, depth, seen, stack, scope, out) {
|
|
169
|
+
const found = (0, import_refs_1.importRefs)(body, MAX_IMPORT_REFS_PER_FILE);
|
|
170
|
+
// 1 ファイル単位で切った分も「数えていない」ことに変わりはないので同じ印を立てる
|
|
171
|
+
if (found.truncated)
|
|
172
|
+
scope.budget.truncated = true;
|
|
173
|
+
for (const ref of found.refs) {
|
|
174
|
+
if (scope.budget.left <= 0) {
|
|
175
|
+
scope.budget.truncated = true;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
scope.budget.left--;
|
|
179
|
+
const abs = resolveImport(ref, fromDir, home);
|
|
180
|
+
/*
|
|
181
|
+
* 読み取りの境界の外は realpath も stat もしない。CLAUDE.md は clone したリポジトリから
|
|
182
|
+
* 来るファイルなので、@/etc/hosts や @~/.ssh/config を素直に解決すると、本文を読まなくても
|
|
183
|
+
* 「存在するか」「symlink の先はどこか」をブラウザへ返してしまう(覗く材料になる)。
|
|
184
|
+
* 字句で先に落とし、返すのも要求されたパスのまま(解決後のフルパスは出さない)。
|
|
185
|
+
*/
|
|
186
|
+
if (!openableImport(abs, scope)) {
|
|
187
|
+
out.push({ ref, path: abs, exists: false, depth, tokens: 0, skipped: 'out-of-scope' });
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
let real = abs;
|
|
191
|
+
let size = 0;
|
|
192
|
+
let exists;
|
|
193
|
+
try {
|
|
194
|
+
real = fs.realpathSync(abs);
|
|
195
|
+
const st = fs.statSync(real);
|
|
196
|
+
exists = st.isFile();
|
|
197
|
+
size = st.size;
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
exists = false;
|
|
201
|
+
}
|
|
202
|
+
if (!exists) {
|
|
203
|
+
out.push({ ref, path: abs, exists: false, depth, tokens: 0 });
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
// symlink で境界の外へ出ていないか、解決後にもう一度見る
|
|
207
|
+
if (!openableImport(real, scope)) {
|
|
208
|
+
out.push({ ref, path: abs, exists: false, depth, tokens: 0, skipped: 'out-of-scope' });
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (stack.has(real)) {
|
|
212
|
+
out.push({ ref, path: real, exists: true, depth, tokens: 0, skipped: 'cycle' });
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (seen.has(real)) {
|
|
216
|
+
out.push({ ref, path: real, exists: true, depth, tokens: 0, skipped: 'duplicate' });
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
if (depth > MAX_IMPORT_DEPTH) {
|
|
220
|
+
out.push({ ref, path: real, exists: true, depth, tokens: 0, skipped: 'depth' });
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (size > MAX_FILE_BYTES) {
|
|
224
|
+
out.push({ ref, path: real, exists: true, depth, tokens: 0, skipped: 'too-large' });
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
seen.add(real);
|
|
228
|
+
const raw = readText(real);
|
|
229
|
+
if (raw === null) {
|
|
230
|
+
out.push({ ref, path: real, exists: false, depth, tokens: 0 });
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (raw === 'too-large') {
|
|
234
|
+
out.push({ ref, path: real, exists: true, depth, tokens: 0, skipped: 'too-large' });
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
out.push({ ref, path: real, exists: true, depth, tokens: (0, lint_1.estimateTokens)(raw) });
|
|
238
|
+
stack.add(real);
|
|
239
|
+
expandImports(raw, path.dirname(real), home, depth + 1, seen, stack, scope, out);
|
|
240
|
+
stack.delete(real);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/*
|
|
244
|
+
* @import 先として開いてよい範囲。Claude Code 自身が読むのはプロジェクト配下のファイル
|
|
245
|
+
* (@README など)と user scope の設定なので、その 2 つに限る。
|
|
246
|
+
*/
|
|
247
|
+
const inImportScope = (p, scope) => scope.roots.some((r) => (0, memory_1.isUnder)(p, r));
|
|
248
|
+
/*
|
|
249
|
+
* 境界の中でも開かないもの。公式の @import は拡張子を問わない(@package.json も読む)ので
|
|
250
|
+
* .md には絞らないが、秘密が入る場所は外す。判定は**パスの全区切り**を見る:
|
|
251
|
+
* 名前だけを見ると `<project>/.aws/credentials` や `<project>/.ssh/id_rsa` が素通りする。
|
|
252
|
+
*
|
|
253
|
+
* 例外は `.claude` そのもの。境界の定義に使っているディレクトリで、`~/.claude/rules/x.md`
|
|
254
|
+
* のような正当な参照がここを通る。その配下でも `.` で始まるもの(`.credentials.json`)は外す。
|
|
255
|
+
*
|
|
256
|
+
* 本文は返さないので漏れるのは「存在するか」と概算サイズだけだが、`.env` を外した理由
|
|
257
|
+
* (参照されただけで大きさを渡したくない)は `.aws` `.ssh` にそのまま当てはまる。
|
|
258
|
+
*/
|
|
259
|
+
function openableImport(p, scope) {
|
|
260
|
+
if (!inImportScope(p, scope))
|
|
261
|
+
return false;
|
|
262
|
+
return !p
|
|
263
|
+
.split(path.sep)
|
|
264
|
+
.some((seg) => seg.startsWith('.') && seg !== '.claude' && seg !== '.' && seg !== '..');
|
|
265
|
+
}
|
|
266
|
+
/* 1 ファイル分の読み取り。本文を返さない段(管理ポリシー)は withhold で切り替える */
|
|
267
|
+
function readFile(fp, home, scope, opts = {}) {
|
|
268
|
+
const raw = readText(fp);
|
|
269
|
+
if (raw === null)
|
|
270
|
+
return null;
|
|
271
|
+
// 大きすぎて読まなかった段。存在は残し、コストは 0(Claude Code も読み飛ばす)
|
|
272
|
+
if (raw === 'too-large')
|
|
273
|
+
return {
|
|
274
|
+
path: fp,
|
|
275
|
+
ownTokens: 0,
|
|
276
|
+
tokens: 0,
|
|
277
|
+
updatedAt: mtime(fp),
|
|
278
|
+
headings: [],
|
|
279
|
+
imports: [],
|
|
280
|
+
tooLarge: true,
|
|
281
|
+
};
|
|
282
|
+
const ownTokens = (0, lint_1.estimateTokens)(raw);
|
|
283
|
+
const imports = [];
|
|
284
|
+
const seen = new Set();
|
|
285
|
+
const stack = new Set();
|
|
286
|
+
try {
|
|
287
|
+
const self = fs.realpathSync(fp);
|
|
288
|
+
seen.add(self);
|
|
289
|
+
stack.add(self);
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
seen.add(fp);
|
|
293
|
+
stack.add(fp);
|
|
294
|
+
}
|
|
295
|
+
/*
|
|
296
|
+
* 参照は frontmatter を外した本文から拾う。web は frontmatter を別枠で描くので印を置けず、
|
|
297
|
+
* server だけが数えると出現順の突き合わせが 1 つずれる
|
|
298
|
+
* (.claude/rules/*.md の `description: applies to @types/node` のような値で実際に起きる)。
|
|
299
|
+
*/
|
|
300
|
+
expandImports((0, import_refs_1.bodyWithoutFrontmatter)(raw), path.dirname(fp), home, 1, seen, stack, scope, imports);
|
|
301
|
+
const importTokens = imports.reduce((n, im) => n + im.tokens, 0);
|
|
302
|
+
return {
|
|
303
|
+
path: fp,
|
|
304
|
+
ownTokens,
|
|
305
|
+
tokens: ownTokens + importTokens,
|
|
306
|
+
updatedAt: mtime(fp),
|
|
307
|
+
headings: opts.withhold ? [] : headingsOf(raw),
|
|
308
|
+
imports,
|
|
309
|
+
...(opts.withhold ? { bodyWithheld: true } : {}),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
/* 単一ファイルの段。無くても「どこを見たか」を残すため files: [] で返す */
|
|
313
|
+
function singleLayer(kind, fp, home, scope, opts = {}) {
|
|
314
|
+
const f = fs.existsSync(fp) ? readFile(fp, home, scope, opts) : null;
|
|
315
|
+
return { kind, label: fp, files: f ? [f] : [], tokens: f ? f.tokens : 0 };
|
|
316
|
+
}
|
|
317
|
+
/*
|
|
318
|
+
* .claude/rules/*.md。frontmatter に paths: があるものは「そのパスのファイルを読むとき」の
|
|
319
|
+
* 遅延ロードなので常時コストに含めない(件数だけ lazy で残す)。
|
|
320
|
+
* paths: の値は YAML リストで、既存の parseFrontmatter では中身を取れない。
|
|
321
|
+
* ここで要るのは有無だけなので、キーの存在で判定する(真偽値で見ると空文字列に負ける)。
|
|
322
|
+
*/
|
|
323
|
+
function rulesLayer(root, home, scope) {
|
|
324
|
+
const dir = path.join(root, '.claude', 'rules');
|
|
325
|
+
const layer = {
|
|
326
|
+
kind: 'rules',
|
|
327
|
+
label: path.join(dir, '*.md'),
|
|
328
|
+
files: [],
|
|
329
|
+
tokens: 0,
|
|
330
|
+
};
|
|
331
|
+
let names;
|
|
332
|
+
try {
|
|
333
|
+
names = fs
|
|
334
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
335
|
+
.filter((e) => e.isFile() && e.name.endsWith('.md'))
|
|
336
|
+
.map((e) => e.name)
|
|
337
|
+
.sort();
|
|
338
|
+
}
|
|
339
|
+
catch {
|
|
340
|
+
return layer;
|
|
341
|
+
}
|
|
342
|
+
for (const name of names) {
|
|
343
|
+
const fp = path.join(dir, name);
|
|
344
|
+
const raw = readText(fp);
|
|
345
|
+
if (raw === null)
|
|
346
|
+
continue;
|
|
347
|
+
const f = readFile(fp, home, scope);
|
|
348
|
+
if (!f)
|
|
349
|
+
continue;
|
|
350
|
+
const lazy = 'paths' in (0, scan_1.parseFrontmatter)(raw).meta;
|
|
351
|
+
layer.files.push(lazy ? { ...f, lazy: true } : f);
|
|
352
|
+
if (!lazy)
|
|
353
|
+
layer.tokens += f.tokens;
|
|
354
|
+
}
|
|
355
|
+
return layer;
|
|
356
|
+
}
|
|
357
|
+
/*
|
|
358
|
+
* 親ディレクトリの探索範囲。git root まで遡り、ホームとその祖先・ファイルシステムのルートには出ない。
|
|
359
|
+
* git 管理外なら 1 つ上だけ(どこまでも遡ると無関係な親の CLAUDE.md を拾う)。
|
|
360
|
+
* 本文の読み取り(parentLayer)とパスの列挙(claudeMdPaths)で同じ範囲を使う。
|
|
361
|
+
*/
|
|
362
|
+
function parentDirs(root, home) {
|
|
363
|
+
const gitRoot = (0, memory_1.worktreeRootOf)(root);
|
|
364
|
+
const dirs = [];
|
|
365
|
+
let cur = path.dirname(path.resolve(root));
|
|
366
|
+
for (;;) {
|
|
367
|
+
if (cur === path.dirname(cur))
|
|
368
|
+
break; // ファイルシステムのルート
|
|
369
|
+
if (cur === home || (0, memory_1.isUnder)(home, cur))
|
|
370
|
+
break; // ホームとその祖先には出ない
|
|
371
|
+
if (!gitRoot) {
|
|
372
|
+
dirs.push(cur); // git 管理外は 1 つ上だけ
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
// git 配下は境界の判定を先に置く。root 自身が git root(普通のリポジトリを開いた場合)なら
|
|
376
|
+
// 親は範囲外なので 1 件も拾わない
|
|
377
|
+
if (!(0, memory_1.isUnder)(cur, gitRoot))
|
|
378
|
+
break;
|
|
379
|
+
dirs.push(cur);
|
|
380
|
+
if (cur === gitRoot)
|
|
381
|
+
break;
|
|
382
|
+
cur = path.dirname(cur);
|
|
383
|
+
}
|
|
384
|
+
return dirs;
|
|
385
|
+
}
|
|
386
|
+
/*
|
|
387
|
+
* 親ディレクトリの CLAUDE.md。git root まで遡り、ホームやファイルシステムのルートには出ない。
|
|
388
|
+
* git 管理外なら 1 つ上だけ見る(どこまでも遡ると無関係な親の CLAUDE.md を拾う)。
|
|
389
|
+
*/
|
|
390
|
+
function parentLayer(root, home, scope) {
|
|
391
|
+
const layer = {
|
|
392
|
+
kind: 'parent',
|
|
393
|
+
label: path.join(path.dirname(root), 'CLAUDE.md'),
|
|
394
|
+
files: [],
|
|
395
|
+
tokens: 0,
|
|
396
|
+
};
|
|
397
|
+
for (const dir of parentDirs(root, home)) {
|
|
398
|
+
const fp = path.join(dir, 'CLAUDE.md');
|
|
399
|
+
if (!fs.existsSync(fp))
|
|
400
|
+
continue;
|
|
401
|
+
const f = readFile(fp, home, scope);
|
|
402
|
+
if (!f)
|
|
403
|
+
continue;
|
|
404
|
+
layer.files.push(f);
|
|
405
|
+
layer.tokens += f.tokens;
|
|
406
|
+
}
|
|
407
|
+
return layer;
|
|
408
|
+
}
|
|
409
|
+
/*
|
|
410
|
+
* 注入順に 7 段を並べる。無い段も files: [] で残す(「何が読まれていないか」も情報なので、
|
|
411
|
+
* 画面側で「なし」の行として出せるようにする)。
|
|
412
|
+
* root / home はテストから差し替えられるよう引数で受ける。
|
|
413
|
+
*/
|
|
414
|
+
function claudeMdLayers(opts = {}) {
|
|
415
|
+
const home = opts.home ?? os.homedir();
|
|
416
|
+
const root = opts.root ? path.resolve(opts.root) : null;
|
|
417
|
+
const scope = importScopeOf(root, home);
|
|
418
|
+
const layers = [
|
|
419
|
+
singleLayer('managed', opts.managedPath ?? managedPolicyPath(), home, scope, {
|
|
420
|
+
withhold: true,
|
|
421
|
+
}),
|
|
422
|
+
singleLayer('user', path.join((0, exports.userClaudeDir)(home), 'CLAUDE.md'), home, scope),
|
|
423
|
+
];
|
|
424
|
+
if (root) {
|
|
425
|
+
layers.push(singleLayer('project', path.join(root, 'CLAUDE.md'), home, scope), singleLayer('project-dot', path.join(root, '.claude', 'CLAUDE.md'), home, scope), singleLayer('local', path.join(root, 'CLAUDE.local.md'), home, scope), rulesLayer(root, home, scope), parentLayer(root, home, scope));
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
layers,
|
|
429
|
+
tokens: layers.reduce((n, l) => n + l.tokens, 0),
|
|
430
|
+
// 予算切れで打ち切ったなら上限の件数を添える。合計 tok が小さく出た理由を画面が言えるように
|
|
431
|
+
// (件数は画面の文面にも出すので、定数を web 側へ写さずここから渡す)
|
|
432
|
+
// 上限(1 ファイル 200 件 / 走査全体 500 件)のどちらかで切ったら立てる。
|
|
433
|
+
// どちらの上限かは利用者の行動(参照を減らす)を変えないので、値は分けない
|
|
434
|
+
...(scope.budget.truncated ? { importsTruncated: true } : {}),
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
/*
|
|
438
|
+
* 走査が見るファイルのパスだけを列挙する(本文を読まず、トークンも数えない)。
|
|
439
|
+
* 読み取り・エディタ起動の許可判定(read-access.ts)は「このパスか」だけを知りたいので、
|
|
440
|
+
* claudeMdLayers を呼ぶと 1 リクエストごとに全文の読み取りと @import の展開が走ってしまう。
|
|
441
|
+
* 管理ポリシーは本文を返さない段なので含めない(許可の対象外)。
|
|
442
|
+
*/
|
|
443
|
+
function claudeMdPaths(opts = {}) {
|
|
444
|
+
const home = opts.home ?? os.homedir();
|
|
445
|
+
const root = opts.root ? path.resolve(opts.root) : null;
|
|
446
|
+
const out = [path.join((0, exports.userClaudeDir)(home), 'CLAUDE.md')];
|
|
447
|
+
if (root) {
|
|
448
|
+
out.push(path.join(root, 'CLAUDE.md'), path.join(root, '.claude', 'CLAUDE.md'), path.join(root, 'CLAUDE.local.md'));
|
|
449
|
+
const rulesDir = path.join(root, '.claude', 'rules');
|
|
450
|
+
try {
|
|
451
|
+
for (const e of fs.readdirSync(rulesDir, { withFileTypes: true }))
|
|
452
|
+
if (e.isFile() && e.name.endsWith('.md'))
|
|
453
|
+
out.push(path.join(rulesDir, e.name));
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
/* rules ディレクトリが無い環境 */
|
|
457
|
+
}
|
|
458
|
+
for (const dir of parentDirs(root, home))
|
|
459
|
+
out.push(path.join(dir, 'CLAUDE.md'));
|
|
460
|
+
}
|
|
461
|
+
return out.filter((fp) => fs.existsSync(fp));
|
|
462
|
+
}
|
|
463
|
+
/* 差分追跡(snapshot)へ渡す参照。遅延ロードの rules も「変わったら知りたい」ので含める */
|
|
464
|
+
function claudeMdRefs(scan, home = os.homedir()) {
|
|
465
|
+
const userDir = (0, exports.userClaudeDir)(home);
|
|
466
|
+
const out = [];
|
|
467
|
+
for (const layer of scan.layers) {
|
|
468
|
+
// 管理ポリシーは OS が配るもので、利用者の変更対象ではないので追跡しない
|
|
469
|
+
if (layer.kind === 'managed')
|
|
470
|
+
continue;
|
|
471
|
+
for (const f of layer.files) {
|
|
472
|
+
out.push({
|
|
473
|
+
path: f.path,
|
|
474
|
+
exists: true,
|
|
475
|
+
name: path.basename(f.path),
|
|
476
|
+
source: (0, memory_1.isUnder)(f.path, userDir) ? 'user' : 'project',
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
return out;
|
|
481
|
+
}
|
|
482
|
+
/*
|
|
483
|
+
* 差分追跡(snapshot)へ渡す参照。計画 16 Phase D: CLAUDE.md の変化追跡を cwd だけでなく
|
|
484
|
+
* 登録済み全プロジェクトへ広げるための入口。claudeMdRefs(claudeMdLayers(...))と違い、
|
|
485
|
+
* 本文を読んで @import を展開する重い走査(claudeMdLayers)は使わず、パスの存在だけを見る
|
|
486
|
+
* claudeMdPaths を各プロジェクトに回す ── 38 プロジェクトでも existsSync と小さいファイルの
|
|
487
|
+
* hash(呼び出し元の computeChanges が contentHash で取る)で済む軽さを保つため。
|
|
488
|
+
*
|
|
489
|
+
* `~/.claude/CLAUDE.md` は全プロジェクトで共通に列挙され、親ディレクトリの CLAUDE.md も
|
|
490
|
+
* サブディレクトリ登録で重複しうるので、パスで重複を除いてから返す(snapshot のキーは
|
|
491
|
+
* パス由来なので、重複したままだと同じキーへの上書きが起きるだけで実害は無いが、
|
|
492
|
+
* 呼び出し側の件数集計をわかりやすくするために先に潰す)。
|
|
493
|
+
*/
|
|
494
|
+
function claudeMdRefsOf(paths, home = os.homedir()) {
|
|
495
|
+
const userDir = (0, exports.userClaudeDir)(home);
|
|
496
|
+
const seen = new Set();
|
|
497
|
+
const out = [];
|
|
498
|
+
for (const root of paths) {
|
|
499
|
+
for (const fp of claudeMdPaths({ root, home })) {
|
|
500
|
+
if (seen.has(fp))
|
|
501
|
+
continue;
|
|
502
|
+
seen.add(fp);
|
|
503
|
+
out.push({
|
|
504
|
+
path: fp,
|
|
505
|
+
exists: true,
|
|
506
|
+
name: path.basename(fp),
|
|
507
|
+
source: (0, memory_1.isUnder)(fp, userDir) ? 'user' : 'project',
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return out;
|
|
512
|
+
}
|