From 2621bc1389fc6a2b7bbe02a1794754bd8bf346b3 Mon Sep 17 00:00:00 2001 From: Cain-Ish Date: Wed, 3 Jun 2026 18:22:00 +0200 Subject: [PATCH 1/9] docs(spec): SP-2 raw inbox design Per-project raw staging area (~/.second-brain/projects//raw/), markdown-item format with provenance frontmatter, derived (drift-free) work-list, kb-schema 'raw' group, /second-brain:capture producer, session-load backlog banner. Foundation + real producer; SP-3 setup-scan + SP-4 maintainer drain deferred. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/specs/2026-06-03-raw-inbox-design.md | 169 ++++++++++++++++++++++ 1 file changed, 169 insertions(+) create mode 100644 docs/specs/2026-06-03-raw-inbox-design.md diff --git a/docs/specs/2026-06-03-raw-inbox-design.md b/docs/specs/2026-06-03-raw-inbox-design.md new file mode 100644 index 0000000..dda4837 --- /dev/null +++ b/docs/specs/2026-06-03-raw-inbox-design.md @@ -0,0 +1,169 @@ +# SP-2 — Raw Inbox — Design + +**Status:** approved (2026-06-03) +**Vision:** consolidation roadmap — sub-project SP-2 of 6 (SP-0 Four Principles ✓, SP-1 project-scoped serving ✓). +**Scope chosen:** *Foundation + real producer.* SP-2 builds the raw-inbox structure/contract **and** a working manual producer. SP-3 (setup deep-scan, the bulk producer) and SP-4 (maintainer drain raw→nodes) are separate later sub-projects that plug into the contract defined here. + +--- + +## Problem + +Knowledge enters the KB today only as **finished** artifacts: + +- the capture-time **extractor** (Stop hook) writes complete wiki pages; +- **`track`/doc-sources** references the user's *existing* local files in place (read-only registry — SP-1 local-docs), it is not a staging area; +- the **`sources`** wiki category holds finished "external reference material" provenance pages; +- **dream / maintainer** consolidate what is already in the wiki. + +There is nowhere to **drop unprocessed material** — a PDF, a pasted spec, a clipped article, a future setup-scan's output — and hold it until something refines it into proper wiki nodes. That missing staging area is the **raw inbox**. It is the foundation SP-3 fills and SP-4 drains. + +## Goals + +1. A per-project staging area for unprocessed material, with provenance, held **out** of the searchable wiki. +2. A single, drift-free contract for the inbox's location/format/lifecycle, declared in the source of truth (`kb-schema.json`) and read by both the TS and bash sides. +3. A working **manual producer** so a human can fill the inbox end-to-end today (before SP-3/SP-4 land). +4. Lightweight **surfacing** so the user knows there is a backlog to process. + +## Non-goals (explicitly deferred) + +- **SP-3** setup deep-scan (the bulk auto-producer that seeds a project's inbox from a folder walk). +- **SP-4** maintainer **drain**: turning raw items into wiki nodes, auto status transitions, and projecting the raw→node edge. +- URL **auto-fetch** (offline-first: SP-2 records the URL as a pointer). +- Binary **text-extraction** (PDF/OCR). SP-2 records a one-line gist only. + +--- + +## Architecture + +``` +~/.second-brain/projects// +├── PROJECT.md (existing) +├── doc-sources.config.json (existing) +├── doc-sources.json (existing) +└── raw/ ← NEW: this project's raw inbox + ├── 20260603T141500Z-auth-spec.md (sidecar manifest + body) + ├── 20260603T141500Z-auth-spec.pdf (original blob, when binary) + └── 20260603T150210Z-rate-limit-note.md +``` + +Raw lives in the **hot tier, per project** — beside the project's other state, project-scoped by construction (consistent with SP-1), and **never** under `~/knowledge/wiki/` so it cannot pollute `knowledge_search`. + +### Components (well-bounded units) + +| Unit | Responsibility | Depends on | +|---|---|---| +| `kb-schema.json` `raw` group | the one declaration of the inbox structure | — | +| `mcp/src/tools/raw-inbox.ts` (pure) | item id/format helpers, frontmatter (de)serialise, `captureItem`, `listItems`, `setStatus`, `unprocessedCount` | `doc-sources.ts` `hashContent`/`assertSafeSlug` (reuse) | +| `mcp/src/tools/raw-capture-cli.ts` (thin bundle) | deterministic file work the skill calls — no bash/TS logic drift | `raw-inbox.ts` | +| `skills/capture/SKILL.md` | the user-facing producer (`/second-brain:capture`) | `raw-capture-cli` bundle, active-slug resolution | +| `scripts/kb-schema.sh` + `constants/kb-schema.ts` | expose the `raw` group to both sides | `kb-schema.json` | +| `scripts/session-load.sh` | low-priority backlog banner | `raw-inbox` count helper | +| `knowledge_validate` | gentle warning on malformed raw frontmatter | `raw-inbox.ts` parse | + +--- + +## Data model + +### Item file: `raw/.md` + +```yaml +--- +id: 20260603T141500Z-auth-spec # -; sortable + unique +source: /home/me/Downloads/auth-spec.pdf # absolute path | https://… | "paste" +captured_at: 2026-06-03T14:15:00Z # ISO-8601 UTC +captured_by: user # user | setup-scan | dream (closed vocab; forward-compat) +content_type: application/pdf # MIME-ish: text/markdown, text/html, text/uri-list, application/pdf, … +status: unprocessed # unprocessed | processed | discarded +target_node: # OPTIONAL active-wiki slug this item backs (provenance only) +blob: 20260603T141500Z-auth-spec.pdf # OPTIONAL sibling filename when the original is binary +gist: One-line human summary of the item. +--- + + +``` + +**Rules** + +- **id** = `date -u +%Y%m%dT%H%M%SZ` + `-` + a short kebab slug derived from the source filename / first words of paste / URL host. Collision-safe: if the file already exists, append a `-2`, `-3`, … suffix. +- **text / paste / text file** → content_type `text/markdown` (or the file's type); the content goes in the `.md` body; no `blob`. +- **binary file** (anything not detected as text) → copy the original to `raw/.` and set `blob:`; the `.md` is a manifest whose body is the gist only. No parsing in SP-2. +- **bare URL** → content_type `text/uri-list`; body is the URL; `source` is the URL; no fetch. +- **status** is a closed vocabulary `unprocessed | processed | discarded`. SP-2 writes `unprocessed` on capture and `discarded` via `--discard`; `processed` is reserved for SP-4 (the manual contract is documented so SP-4 has a fixed target). +- **target_node** is a plain slug (never a `[[link]]`), optional, recording which existing wiki page the item is evidence for. SP-2 only stores it; the raw→node **edge** is projected later by SP-4 through a sanctioned writer (SP-2 never writes graph edges). + +### Work-list = derived, never stored + +The backlog/work-list is **computed** by scanning `raw/*.md` for `status: unprocessed` (the same derive-don't-store discipline as `kb-ai-block-candidates.sh`). There is **no** separate index file — status lives only in each item's frontmatter, so the two can never disagree. This honours the "single source of truth" value. + +### kb-schema.json addition + +```json +"raw": { + "dir": "raw", + "tier": "project", + "statuses": ["unprocessed", "processed", "discarded"], + "searchable": false +} +``` + +Read by `mcp/src/constants/kb-schema.ts` (esbuild-inlined) and `scripts/kb-schema.sh` (jq → `SB_RAW_*` vars), per the established dual-reader pattern. `test-kb-schema.sh` asserts both sides see it. + +--- + +## Producer — `/second-brain:capture` + +A new **user-invocable** skill (`user-invocable: true`, `disable-model-invocation: true`, like `import-host`). It maps an argument to a `raw-capture-cli` action and reports the result. + +``` +/second-brain:capture # copy a file into the active project's inbox +/second-brain:capture # record a URL pointer (no fetch) +/second-brain:capture --paste # capture piped/pasted text from stdin +/second-brain:capture --node … # also set target_node (pre-attach to a wiki page) +/second-brain:capture --list # list this project's inbox items (id, status, gist) +/second-brain:capture --discard # mark an item discarded +``` + +Behaviour: + +1. Resolve the active project slug via the existing pin (`~/.second-brain/.active-session-slug`, `assertSafeSlug`). If no active project, report and stop (capture is project-scoped). +2. `captureItem` (in `raw-inbox.ts`): classify the source (file/url/paste), stamp provenance, copy the blob via node `fs` (portable — not `cp`), write the `.md`, create `raw/` if absent. +3. **Idempotent**: if an item with the same `source` content hash already exists and is `unprocessed`, do not duplicate — report the existing id (`hashContent` reuse). +4. Print the new (or existing) id and the current `unprocessedCount`. + +`raw-capture-cli` is a thin bundle so the skill carries no logic; all classification/file work is in the tested pure module. + +--- + +## Surfacing + lifecycle + +- **Backlog banner.** `session-load.sh` adds one low-priority line — `raw: N unprocessed item(s) — /second-brain:capture --list` — for the **active project** only, gated by `SB_RAW_INBOX=off`. The count comes from a cheap scan of the project's `raw/`. Mirrors the existing `conflicts.jsonl` banner pattern (advisory, never blocks). +- **Directory creation.** `raw/` is created **on demand** by `captureItem` (`mkdir -p`); `ensure-dirs.sh` is intentionally **not** modified, so projects that never capture get no empty `raw/` litter. +- **Out of search.** Raw items are never indexed by `knowledge_search` (they are unprocessed and not under `wiki/`). They surface only as the count and via `--list`. +- **Drain (SP-4, deferred).** The maintainer will read the derived unprocessed work-list, turn each item into wiki node(s) (or attach to `target_node`), set `status: processed`, and project the raw→node edge. SP-2 fixes the contract (location, fields, status vocab) so SP-4 has a stable target. + +## Error handling + +- Missing/!active project → capture refuses with a clear message (no global inbox). +- Unsafe slug → `assertSafeSlug` throws (reused). +- Unreadable source file → report and skip; never partially write an item. +- Malformed raw frontmatter encountered later → `knowledge_validate` emits a **gentle warning** (raw is messy by nature; never an error, never autofixed-away). +- All file ops fail-safe: write to a temp name then atomic rename (the `doc-sources` write pattern), so an interrupted capture leaves no half-item. + +## Cross-platform + +mawk-safe bash (no shell-var interpolation into awk; `-v` + coercion); portable (`stat -c||-f`, `timeout||gtimeout`, no `mapfile`, no `grep -P`); file copy through node `fs` so Windows backslash paths are handled by the existing `toBashPath` where a bash→node path crossing occurs. + +## Testing (TDD) + +| Test | Covers | +|---|---| +| `raw-inbox.test.ts` (vitest) | id format + collision suffix; capture of file (blob+sidecar), paste (body), URL (uri-list pointer); `hashContent` dedup idempotency; `assertSafeSlug` guard; `target_node` set; `setStatus` transitions; `unprocessedCount` | +| `test-raw-capture.sh` (bash) | skill→CLI end-to-end: capture a temp file → item appears with correct frontmatter → re-capture is idempotent → `--list` shows it → `--discard` flips status → backlog count | +| `test-kb-schema.sh` (extend) | the `raw` group is visible to **both** `kb-schema.sh` (`SB_RAW_*`) and `constants/kb-schema.ts` | +| `test-raw-inbox-banner.sh` (bash) | `session-load` prints the unprocessed count; `SB_RAW_INBOX=off` suppresses it; zero items → no line | +| validate case | malformed raw frontmatter → gentle warning, not error | + +## Versioning + +Plugin patch bump + migration row (additive, no state migration — `raw/` appears lazily on first capture). MCP server minor bump justified by `knowledge_validate` gaining raw-frontmatter awareness + the new `raw-capture-cli` bundle (no new MCP server *tool* is registered — capture rides a standalone CLI bundle, like `knowledge-search-cli`). Back-compat: with no captures and `SB_RAW_INBOX` unset, behaviour is unchanged. From 483e0abc5f2f71c39a8fb3dd83af0de2b52297d6 Mon Sep 17 00:00:00 2001 From: Cain-Ish Date: Wed, 3 Jun 2026 18:44:49 +0200 Subject: [PATCH 2/9] docs(plan): SP-2 raw inbox implementation plan Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/plans/2026-06-03-raw-inbox.md | 838 +++++++++++++++++++++++++++++ 1 file changed, 838 insertions(+) create mode 100644 docs/plans/2026-06-03-raw-inbox.md diff --git a/docs/plans/2026-06-03-raw-inbox.md b/docs/plans/2026-06-03-raw-inbox.md new file mode 100644 index 0000000..ba5e794 --- /dev/null +++ b/docs/plans/2026-06-03-raw-inbox.md @@ -0,0 +1,838 @@ +# SP-2 Raw Inbox Implementation Plan + +> **For agentic workers:** Implement this plan task-by-task following TDD. Steps use checkbox (`- [ ]`) syntax for tracking. See `second-brain:test-driven-development` and `second-brain:verification-before-completion`. + +**Goal:** Give the KB a per-project raw inbox (`~/.second-brain/projects//raw/`) where unprocessed material is dropped with provenance, held out of `knowledge_search`, surfaced as a backlog, and fillable today via a `/second-brain:capture` producer. + +**Architecture:** A `raw` group is declared once in `kb-schema.json` (read by both TS and bash). A pure `raw-inbox.ts` module does all item logic (id, capture, list, status, count) reusing `doc-sources.ts` helpers; a thin `raw-capture-cli` bundle exposes it; the `capture` skill drives the CLI; `session-load.sh` surfaces an unprocessed-count banner. No separate index file — the work-list is derived by scanning item frontmatter. + +**Tech Stack:** TypeScript (esbuild-bundled ESM, Node 20), vitest, POSIX bash (mawk-safe, macOS/Git-Bash portable), jq. + +**Spec:** `docs/specs/2026-06-03-raw-inbox-design.md` + +**One deliberate refinement vs. spec:** the spec said "`knowledge_validate` gains a gentle warning for malformed raw frontmatter." `knowledge_validate` is **wiki-scoped** (`~/knowledge/wiki`), while raw lives in the hot tier — coupling it across tiers is wrong. Instead, raw owns its own validation: `listItems` flags malformed items (`malformed: true`) and `/second-brain:capture --list` surfaces a `(malformed)` marker. This realizes the spec's "gentle warning" within the raw group (per the heterogeneous-groups contract) without polluting the wiki validator. + +--- + +## File Structure + +| File | Responsibility | Action | +|---|---|---| +| `kb-schema.json` | declare the `raw` group (one source of truth) | Modify | +| `mcp/src/constants/kb-schema.ts` | expose `RAW_*` to TS | Modify | +| `scripts/kb-schema.sh` | expose `SB_RAW_*` to bash | Modify | +| `tests/test-kb-schema.sh` | assert both readers see `raw` | Modify | +| `mcp/src/tools/doc-sources.ts` | export `assertSafeSlug` for reuse | Modify (1 line) | +| `mcp/src/tools/raw-inbox.ts` | pure item logic (id, capture, list, status, count, parse) | Create | +| `mcp/src/tools/raw-inbox.test.ts` | vitest for the module | Create | +| `mcp/src/tools/raw-capture-cli.ts` | thin CLI bundle the skill calls | Create | +| `mcp/package.json` | register the esbuild bundle | Modify | +| `skills/capture/SKILL.md` | `/second-brain:capture` producer | Create | +| `tests/test-raw-capture.sh` | skill→CLI end-to-end | Create | +| `scripts/session-load.sh` | backlog banner | Modify | +| `tests/test-raw-inbox-banner.sh` | banner + `SB_RAW_INBOX=off` | Create | + +--- + +## Task 1: `raw` group in the source of truth + +**Files:** +- Modify: `kb-schema.json` +- Modify: `mcp/src/constants/kb-schema.ts` +- Modify: `scripts/kb-schema.sh` +- Test: `tests/test-kb-schema.sh` + +- [ ] **Step 1: Write the failing test.** Append to `tests/test-kb-schema.sh`, just before its final summary/exit (find the last `echo`/exit; insert above it): + +```bash +# --- SP-2: the `raw` group is visible to both readers --- +RAW_DIR_JSON=$(jq -r '.raw.dir' "$ROOT/kb-schema.json") +[ "$RAW_DIR_JSON" = "raw" ] || { echo "FAIL: kb-schema.json .raw.dir != raw (got '$RAW_DIR_JSON')"; exit 1; } +# bash reader exposes it +( set -e; source "$ROOT/scripts/kb-schema.sh"; [ "$SB_RAW_DIR" = "raw" ] ) \ + || { echo "FAIL: kb-schema.sh did not export SB_RAW_DIR=raw"; exit 1; } +# TS reader exposes it (compiled constant) +RAW_TS=$(cd "$ROOT/mcp" && node --input-type=module -e \ + 'import { RAW_DIR, RAW_STATUSES } from "./dist/constants/kb-schema.js"; process.stdout.write(RAW_DIR + ":" + RAW_STATUSES.join(","))' 2>/dev/null) +[ "$RAW_TS" = "raw:unprocessed,processed,discarded" ] \ + || { echo "FAIL: kb-schema.ts RAW_DIR/RAW_STATUSES wrong (got '$RAW_TS')"; exit 1; } +echo "PASS: raw group visible to json + bash + ts readers" +``` + +> Note: `$ROOT` is already defined at the top of `test-kb-schema.sh` (repo root). If the file uses a different root var, match it. The TS check imports the **built** `dist/` — so this test depends on Step 4's build (it will SKIP-fail until built; that's expected in Step 2). + +- [ ] **Step 2: Run it to verify it fails.** + +Run: `bash tests/test-kb-schema.sh` +Expected: FAIL at `.raw.dir != raw` (the key doesn't exist yet → jq prints `null`). + +- [ ] **Step 3: Add the `raw` group to `kb-schema.json`.** Insert after the `"forget_protection": { … }` block (add a comma after its closing brace): + +```json + "raw": { + "dir": "raw", + "tier": "project", + "statuses": ["unprocessed", "processed", "discarded"], + "searchable": false + } +``` + +- [ ] **Step 4: Expose it in both readers.** In `mcp/src/constants/kb-schema.ts`, add after the `FORGET_DISCOUNTED` export: + +```typescript +/** Raw inbox group: per-project staging for unprocessed material (SP-2). Never searched. */ +export const RAW_DIR: string = schema.raw.dir; +export const RAW_STATUSES: readonly string[] = schema.raw.statuses; +``` + +In `scripts/kb-schema.sh`, inside the `if command -v jq … ; then` block, add two more reads and extend the `export` line: + +```bash + SB_RAW_DIR=$(jq -r '.raw.dir' "$_SB_KB_SCHEMA" 2>/dev/null) + SB_RAW_STATUSES=$(jq -r '.raw.statuses | join(" ")' "$_SB_KB_SCHEMA" 2>/dev/null) +``` + +and append `SB_RAW_DIR SB_RAW_STATUSES` to the existing `export …` statement. + +- [ ] **Step 5: Build + run to verify pass.** + +Run: `cd mcp && npm run build && cd .. && bash tests/test-kb-schema.sh` +Expected: `PASS: raw group visible to json + bash + ts readers` and the file's existing assertions still pass. + +- [ ] **Step 6: Commit.** + +```bash +git add kb-schema.json mcp/src/constants/kb-schema.ts scripts/kb-schema.sh tests/test-kb-schema.sh mcp/dist +git commit -m "feat(kb): declare the raw-inbox group in the source of truth (SP-2 Task 1)" +``` + +--- + +## Task 2: `raw-inbox.ts` pure module + +**Files:** +- Modify: `mcp/src/tools/doc-sources.ts` (export `assertSafeSlug`) +- Create: `mcp/src/tools/raw-inbox.ts` +- Test: `mcp/src/tools/raw-inbox.test.ts` + +- [ ] **Step 1: Export `assertSafeSlug`.** In `mcp/src/tools/doc-sources.ts`, change `function assertSafeSlug(` to `export function assertSafeSlug(` (line ~38). + +- [ ] **Step 2: Write the failing test.** Create `mcp/src/tools/raw-inbox.test.ts`: + +```typescript +import { describe, it, expect, beforeEach } from 'vitest'; +import { promises as fs } from 'fs'; +import { join } from 'path'; +import { tmpdir } from 'os'; +import { captureItem, listItems, setStatus, unprocessedCount, rawDir } from './raw-inbox.js'; + +async function brain(): Promise<{ brainDir: string; slug: string }> { + const brainDir = await fs.mkdtemp(join(tmpdir(), 'raw-')); + const slug = 'alpha'; + await fs.mkdir(join(brainDir, 'projects', slug), { recursive: true }); + return { brainDir, slug }; +} +const NOW = '2026-06-03T14:15:00Z'; + +describe('raw-inbox', () => { + it('captures pasted text as a markdown item with provenance frontmatter', async () => { + const { brainDir, slug } = await brain(); + const r = await captureItem({ brainDir, slug, kind: 'paste', source: 'paste', + content: 'a rate-limit note', now: NOW }); + expect(r.duplicate).toBe(false); + expect(r.id).toMatch(/^20260603T141500Z-/); + const items = await listItems(brainDir, slug); + expect(items).toHaveLength(1); + expect(items[0].status).toBe('unprocessed'); + expect(items[0].content_type).toBe('text/markdown'); + expect(items[0].captured_by).toBe('user'); + expect(items[0].body).toContain('a rate-limit note'); + }); + + it('captures a URL as an offline pointer (no fetch), content_type text/uri-list', async () => { + const { brainDir, slug } = await brain(); + const r = await captureItem({ brainDir, slug, kind: 'url', + source: 'https://example.com/spec', content: 'https://example.com/spec', now: NOW }); + const items = await listItems(brainDir, slug); + expect(items[0].content_type).toBe('text/uri-list'); + expect(items[0].source).toBe('https://example.com/spec'); + expect(items[0].body).toContain('https://example.com/spec'); + expect(r.id).toContain('example-com'); + }); + + it('captures a text file into the body; a binary file into a blob sidecar', async () => { + const { brainDir, slug } = await brain(); + const txt = join(brainDir, 'note.md'); + await fs.writeFile(txt, '# Title\nbody text'); + await captureItem({ brainDir, slug, kind: 'file', source: txt, now: NOW }); + const bin = join(brainDir, 'pic.bin'); + await fs.writeFile(bin, Buffer.from([0x00, 0x01, 0x02, 0x00])); + const rb = await captureItem({ brainDir, slug, kind: 'file', source: bin, now: '2026-06-03T14:16:00Z' }); + const items = await listItems(brainDir, slug); + const text = items.find(i => i.source === txt)!; + expect(text.blob).toBeUndefined(); + expect(text.body).toContain('body text'); + const binItem = items.find(i => i.source === bin)!; + expect(binItem.blob).toBe(`${rb.id}.bin`); + // the blob file exists next to the manifest + await expect(fs.access(join(rawDir(brainDir, slug), `${rb.id}.bin`))).resolves.toBeUndefined(); + }); + + it('is idempotent: re-capturing identical content returns the existing unprocessed item', async () => { + const { brainDir, slug } = await brain(); + const a = await captureItem({ brainDir, slug, kind: 'paste', source: 'paste', content: 'same', now: NOW }); + const b = await captureItem({ brainDir, slug, kind: 'paste', source: 'paste', content: 'same', now: '2026-06-03T15:00:00Z' }); + expect(b.duplicate).toBe(true); + expect(b.id).toBe(a.id); + expect(await unprocessedCount(brainDir, slug)).toBe(1); + }); + + it('records an optional target_node and supports status transitions + count', async () => { + const { brainDir, slug } = await brain(); + const r = await captureItem({ brainDir, slug, kind: 'paste', source: 'paste', + content: 'evidence', targetNode: 'auth-design', now: NOW }); + expect((await listItems(brainDir, slug))[0].target_node).toBe('auth-design'); + expect(await unprocessedCount(brainDir, slug)).toBe(1); + expect(await setStatus(brainDir, slug, r.id, 'discarded')).toBe(true); + expect(await unprocessedCount(brainDir, slug)).toBe(0); + expect((await listItems(brainDir, slug))[0].status).toBe('discarded'); + }); + + it('rejects an unsafe slug', async () => { + const { brainDir } = await brain(); + await expect(captureItem({ brainDir, slug: '../escape', kind: 'paste', source: 'paste', + content: 'x', now: NOW })).rejects.toThrow(); + }); + + it('flags a malformed item (missing frontmatter) without throwing', async () => { + const { brainDir, slug } = await brain(); + await fs.writeFile(join(rawDir(brainDir, slug), 'broken.md'), 'no frontmatter here'); + const items = await listItems(brainDir, slug); + const broken = items.find(i => i.id === 'broken')!; + expect(broken.malformed).toBe(true); + // malformed items still count toward the unprocessed backlog (so they are visible) + expect(await unprocessedCount(brainDir, slug)).toBe(1); + }); +}); +``` + +- [ ] **Step 3: Run it to verify it fails.** + +Run: `cd mcp && npx vitest run raw-inbox.test.ts` +Expected: FAIL — `Cannot find module './raw-inbox.js'`. + +- [ ] **Step 4: Implement `mcp/src/tools/raw-inbox.ts`.** + +```typescript +import { promises as fs } from 'fs'; +import { join, basename, extname } from 'path'; +import { hashContent, assertSafeSlug } from './doc-sources.js'; + +export type RawStatus = 'unprocessed' | 'processed' | 'discarded'; +export type CapturedBy = 'user' | 'setup-scan' | 'dream'; + +export interface RawItem { + id: string; + source: string; + captured_at: string; + captured_by: CapturedBy; + content_type: string; + status: RawStatus; + target_node?: string; + blob?: string; + hash: string; + gist: string; + body: string; + malformed?: boolean; +} + +export interface CaptureInput { + brainDir: string; + slug: string; + source: string; // file path, url, or 'paste' + kind: 'file' | 'url' | 'paste'; + content?: string; // paste text / url string; for files it is read from disk + targetNode?: string; + capturedBy?: CapturedBy; + now?: string; // ISO timestamp; injectable for deterministic tests +} + +export function rawDir(brainDir: string, slug: string): string { + return join(brainDir, 'projects', slug, 'raw'); +} + +/** kebab slug from arbitrary text: lowercase, non-alnum→'-', collapse, trim, cap length. */ +function slugify(text: string): string { + const s = text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40); + return s || 'item'; +} + +/** ISO '2026-06-03T14:15:00Z' → compact sortable '20260603T141500Z'. */ +function compactStamp(iso: string): string { + return iso.replace(/[-:]/g, '').replace(/\.\d+Z$/, 'Z'); +} + +function isBinary(buf: Buffer): boolean { + const n = Math.min(buf.length, 8192); + for (let i = 0; i < n; i++) if (buf[i] === 0) return true; + return false; +} + +function contentTypeForFile(path: string, binary: boolean): string { + const ext = extname(path).toLowerCase(); + if (binary) return ext === '.pdf' ? 'application/pdf' : 'application/octet-stream'; + return ext === '.md' || ext === '.markdown' ? 'text/markdown' : 'text/plain'; +} + +function serialize(item: RawItem): string { + const fm: string[] = ['---']; + fm.push(`id: ${item.id}`); + fm.push(`source: ${item.source}`); + fm.push(`captured_at: ${item.captured_at}`); + fm.push(`captured_by: ${item.captured_by}`); + fm.push(`content_type: ${item.content_type}`); + fm.push(`status: ${item.status}`); + if (item.target_node) fm.push(`target_node: ${item.target_node}`); + if (item.blob) fm.push(`blob: ${item.blob}`); + fm.push(`hash: ${item.hash}`); + fm.push(`gist: ${item.gist.replace(/\n/g, ' ')}`); + fm.push('---', '', item.body, ''); + return fm.join('\n'); +} + +/** Tolerant flat-frontmatter parse. id comes from the filename (authoritative). */ +function parse(content: string, id: string): RawItem { + const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/); + const base: RawItem = { + id, source: '', captured_at: '', captured_by: 'user', content_type: '', + status: 'unprocessed', hash: '', gist: '', body: '', + }; + if (!m) { return { ...base, body: content, malformed: true }; } + const [, fmText, body] = m; + const get = (k: string): string | undefined => { + const mm = fmText.match(new RegExp(`^${k}:[ \\t]*(.*)$`, 'm')); + return mm ? mm[1].trim() : undefined; + }; + const status = (get('status') ?? '') as RawStatus; + const validStatus = status === 'unprocessed' || status === 'processed' || status === 'discarded'; + const item: RawItem = { + id, + source: get('source') ?? '', + captured_at: get('captured_at') ?? '', + captured_by: (get('captured_by') as CapturedBy) ?? 'user', + content_type: get('content_type') ?? '', + status: validStatus ? status : 'unprocessed', + target_node: get('target_node') || undefined, + blob: get('blob') || undefined, + hash: get('hash') ?? '', + gist: get('gist') ?? '', + body: body.trim(), + }; + // Malformed = missing the fields a well-formed capture always writes, or a bad status. + if (!item.source || !item.captured_at || !item.content_type || !validStatus) item.malformed = true; + return item; +} + +async function readItems(brainDir: string, slug: string): Promise { + const dir = rawDir(brainDir, slug); + let names: string[] = []; + try { names = (await fs.readdir(dir)).filter(n => n.endsWith('.md')); } catch { return []; } + const items: RawItem[] = []; + for (const name of names.sort()) { + try { + const content = await fs.readFile(join(dir, name), 'utf-8'); + items.push(parse(content, name.replace(/\.md$/, ''))); + } catch { /* skip unreadable */ } + } + return items; +} + +export async function listItems(brainDir: string, slug: string): Promise { + assertSafeSlug(slug); + return readItems(brainDir, slug); +} + +export async function unprocessedCount(brainDir: string, slug: string): Promise { + assertSafeSlug(slug); + const items = await readItems(brainDir, slug); + // malformed items count as unprocessed so they stay visible in the backlog. + return items.filter(i => i.status === 'unprocessed' || i.malformed).length; +} + +export async function setStatus(brainDir: string, slug: string, id: string, status: RawStatus): Promise { + assertSafeSlug(slug); + const file = join(rawDir(brainDir, slug), `${id}.md`); + let content: string; + try { content = await fs.readFile(file, 'utf-8'); } catch { return false; } + const next = /^status:[ \t]*.*$/m.test(content) + ? content.replace(/^status:[ \t]*.*$/m, `status: ${status}`) + : content.replace(/^---\r?\n/, `---\nstatus: ${status}\n`); + const tmp = `${file}.tmp`; + await fs.writeFile(tmp, next); + await fs.rename(tmp, file); // atomic + return true; +} + +export async function captureItem(input: CaptureInput): Promise<{ id: string; duplicate: boolean; unprocessed: number }> { + assertSafeSlug(input.slug); + const dir = rawDir(input.brainDir, input.slug); + const now = input.now ?? new Date().toISOString(); + const capturedBy = input.capturedBy ?? 'user'; + + // Resolve content + blob + content_type by kind. + let body = ''; + let blobBuf: Buffer | undefined; + let blobExt = ''; + let contentType = ''; + let gistSeed = ''; + let hashInput = ''; + + if (input.kind === 'paste') { + body = input.content ?? ''; + contentType = 'text/markdown'; + gistSeed = body; + hashInput = `paste:${body}`; + } else if (input.kind === 'url') { + const url = input.content ?? input.source; + body = url; + contentType = 'text/uri-list'; + gistSeed = url; + hashInput = `url:${url}`; + } else { + const buf = await fs.readFile(input.source); + const binary = isBinary(buf); + contentType = contentTypeForFile(input.source, binary); + hashInput = `file:${hashContent(buf.toString('binary'))}`; + if (binary) { + blobBuf = buf; + blobExt = extname(input.source) || '.bin'; + gistSeed = basename(input.source); + body = `(binary ${contentType}; original captured as the sibling blob) — ${basename(input.source)}`; + } else { + body = buf.toString('utf-8'); + gistSeed = body; + } + } + + const hash = hashContent(hashInput); + + // Dedup: an existing UNPROCESSED item with the same hash → return it. + const existing = (await readItems(input.brainDir, input.slug)) + .find(i => i.hash === hash && i.status === 'unprocessed'); + if (existing) { + return { id: existing.id, duplicate: true, unprocessed: await unprocessedCount(input.brainDir, input.slug) }; + } + + // Unique id (collision suffix). + await fs.mkdir(dir, { recursive: true }); + const sourceSlug = input.kind === 'url' ? slugify(input.content ?? input.source) + : input.kind === 'file' ? slugify(basename(input.source)) + : slugify(body); + const baseId = `${compactStamp(now)}-${sourceSlug}`; + let id = baseId; + for (let n = 2; ; n++) { + try { await fs.access(join(dir, `${id}.md`)); id = `${baseId}-${n}`; } catch { break; } + } + + const blob = blobBuf ? `${id}${blobExt}` : undefined; + if (blobBuf && blob) { + const btmp = join(dir, `${blob}.tmp`); + await fs.writeFile(btmp, blobBuf); + await fs.rename(btmp, join(dir, blob)); + } + + const gist = gistSeed.replace(/^#\s*/, '').split('\n').map(l => l.trim()).find(Boolean)?.slice(0, 120) ?? ''; + const item: RawItem = { + id, source: input.source, captured_at: now, captured_by: capturedBy, + content_type: contentType, status: 'unprocessed', + target_node: input.targetNode, blob, hash, gist, body, + }; + const file = join(dir, `${id}.md`); + const tmp = `${file}.tmp`; + await fs.writeFile(tmp, serialize(item)); + await fs.rename(tmp, file); + + return { id, duplicate: false, unprocessed: await unprocessedCount(input.brainDir, input.slug) }; +} +``` + +- [ ] **Step 5: Run the tests to verify they pass.** + +Run: `cd mcp && npx vitest run raw-inbox.test.ts` +Expected: PASS (7 tests). If the binary-`blob` test fails on the sidecar filename, confirm `blobExt` includes the leading dot (`extname` returns `.bin`). + +- [ ] **Step 6: Run the full vitest suite (no regressions from the `assertSafeSlug` export).** + +Run: `cd mcp && npx vitest run` +Expected: all green. + +- [ ] **Step 7: Commit.** + +```bash +git add mcp/src/tools/doc-sources.ts mcp/src/tools/raw-inbox.ts mcp/src/tools/raw-inbox.test.ts +git commit -m "feat(kb): raw-inbox pure module — capture/list/status/count (SP-2 Task 2)" +``` + +--- + +## Task 3: `raw-capture-cli` thin bundle + +**Files:** +- Create: `mcp/src/tools/raw-capture-cli.ts` +- Modify: `mcp/package.json` (esbuild registration) + +- [ ] **Step 1: Write the CLI.** Create `mcp/src/tools/raw-capture-cli.ts`: + +```typescript +import { homedir } from 'os'; +import { join, basename } from 'path'; +import { existsSync, readFileSync, statSync } from 'fs'; +import { captureItem, listItems, setStatus, unprocessedCount } from './raw-inbox.js'; + +function resolveSlug(brainDir: string): string | undefined { + if (process.env.SB_ACTIVE_SLUG) return process.env.SB_ACTIVE_SLUG; + try { + const pin = readFileSync(join(brainDir, '.active-session-slug'), 'utf-8').trim(); + if (pin && existsSync(join(brainDir, 'projects', pin, 'PROJECT.md'))) return pin; + } catch { /* no pin */ } + const base = basename(process.cwd()); + return base && base !== '/' && base !== '.' ? base : undefined; +} + +/** Pull `--node ` out of argv; return the rest + the node value. */ +function takeNode(args: string[]): { rest: string[]; node?: string } { + const i = args.indexOf('--node'); + if (i >= 0 && args[i + 1]) return { rest: [...args.slice(0, i), ...args.slice(i + 2)], node: args[i + 1] }; + return { rest: args }; +} + +async function main(): Promise { + const brainDir = process.env.BRAIN_DIR || join(homedir(), '.second-brain'); + const slug = resolveSlug(brainDir); + if (!slug) { console.log('capture: could not resolve the active project (no slug). cd into a project.'); return; } + + const action = process.argv[2]; + const { rest, node } = takeNode(process.argv.slice(3)); + + try { + if (action === 'list') { + const items = await listItems(brainDir, slug); + const open = items.filter(i => i.status === 'unprocessed' || i.malformed).length; + console.log(`Raw inbox for ${slug} — ${items.length} item(s), ${open} unprocessed:`); + for (const i of items) { + console.log(` - ${i.id} [${i.malformed ? 'malformed' : i.status}] ${i.gist || i.source}`); + } + if (items.length === 0) console.log(' (empty — capture something, e.g. /second-brain:capture ./notes.md)'); + } else if (action === 'discard') { + const id = rest[0]; + if (!id) { console.log('usage: capture --discard '); return; } + console.log(await setStatus(brainDir, slug, id, 'discarded') + ? `Discarded ${id}.` : `No raw item with id ${id}.`); + } else if (action === 'paste') { + const content = readFileSync(0, 'utf-8'); // stdin + if (!content.trim()) { console.log('capture: nothing on stdin.'); return; } + const r = await captureItem({ brainDir, slug, kind: 'paste', source: 'paste', content, targetNode: node }); + console.log(`${r.duplicate ? 'Already captured' : 'Captured'} ${r.id} — ${r.unprocessed} unprocessed.`); + } else if (action === 'capture') { + const src = rest[0]; + if (!src) { console.log('usage: capture [--node ] | capture paste'); return; } + let kind: 'file' | 'url' | 'paste'; let content: string | undefined; + if (/^https?:\/\//i.test(src)) { kind = 'url'; content = src; } + else if (existsSync(src) && statSync(src).isFile()) { kind = 'file'; } + else { kind = 'paste'; content = src; } // inline text fallback + const r = await captureItem({ brainDir, slug, kind, source: src, content, targetNode: node }); + console.log(`${r.duplicate ? 'Already captured' : 'Captured'} ${r.id} (${kind}) — ${r.unprocessed} unprocessed.`); + } else { + const n = await unprocessedCount(brainDir, slug); + console.log(`usage: capture | capture paste | capture --list | capture --discard (${n} unprocessed)`); + } + } catch (e) { + console.log(`capture error: ${e instanceof Error ? e.message : String(e)}`); + } +} + +main(); +``` + +> The skill maps `--list` → `list` and `--discard` → `discard` (Task 4). `readFileSync(0, …)` reads fd 0 (stdin). + +- [ ] **Step 2: Register the esbuild bundle.** In `mcp/package.json`, the `"bundle"` script is a chain of `esbuild … && esbuild …`. Append one more (mirror the `ai-block-render-cli` entry exactly), at the end of the chain: + +``` + && esbuild src/tools/raw-capture-cli.ts --bundle --platform=node --target=node20 --format=esm --external:@huggingface/transformers --outfile=dist/tools/raw-capture-cli.bundle.js +``` + +- [ ] **Step 3: Build to verify it compiles + bundles.** + +Run: `cd mcp && npm run build` +Expected: build succeeds; `ls dist/tools/raw-capture-cli.bundle.js` exists. + +- [ ] **Step 4: Smoke-test the CLI by hand.** + +Run: +```bash +T=$(mktemp -d); mkdir -p "$T/projects/demo"; : > "$T/projects/demo/PROJECT.md" +BRAIN_DIR="$T" SB_ACTIVE_SLUG=demo node mcp/dist/tools/raw-capture-cli.bundle.js capture "a quick note" +BRAIN_DIR="$T" SB_ACTIVE_SLUG=demo node mcp/dist/tools/raw-capture-cli.bundle.js list +``` +Expected: `Captured (paste) — 1 unprocessed.` then a `list` showing the item. Clean up: `rm -rf "$T"`. + +- [ ] **Step 5: Commit.** + +```bash +git add mcp/src/tools/raw-capture-cli.ts mcp/package.json mcp/dist +git commit -m "feat(kb): raw-capture-cli bundle (SP-2 Task 3)" +``` + +--- + +## Task 4: `/second-brain:capture` skill + end-to-end test + +**Files:** +- Create: `skills/capture/SKILL.md` +- Test: `tests/test-raw-capture.sh` + +- [ ] **Step 1: Write the failing bash test.** Create `tests/test-raw-capture.sh`: + +```bash +#!/bin/bash +# End-to-end: the capture CLI (which the skill drives) ingests material into a project's +# raw inbox, is idempotent, lists items (flagging malformed), and discards. +set -u +ROOT="$(cd "$(dirname "$0")"/.. && pwd)" +CLI="$ROOT/mcp/dist/tools/raw-capture-cli.bundle.js" +SKILL="$ROOT/skills/capture/SKILL.md" +fail(){ echo "FAIL: $1"; exit 1; }; pass(){ echo "PASS: $1"; } + +[ -f "$SKILL" ] || fail "skills/capture/SKILL.md missing" +grep -q 'raw-capture-cli.bundle.js' "$SKILL" || fail "capture skill does not invoke the raw-capture CLI" +grep -q 'user-invocable: true' "$SKILL" || fail "capture skill not user-invocable" +pass "capture skill present and wired" + +command -v node >/dev/null 2>&1 || { echo "SKIP: node"; echo; echo "ALL PASS"; exit 0; } +[ -f "$CLI" ] || { echo "SKIP: CLI bundle not built"; echo; echo "ALL PASS"; exit 0; } + +T=$(mktemp -d); export BRAIN_DIR="$T" SB_ACTIVE_SLUG=demo +mkdir -p "$T/projects/demo"; : > "$T/projects/demo/PROJECT.md" +run(){ node "$CLI" "$@"; } + +out=$(run capture "rate limit design note") +echo "$out" | grep -q 'Captured .* — 1 unprocessed' || fail "first capture not reported ($out)" +pass "capture creates an unprocessed item" + +out=$(run capture "rate limit design note") +echo "$out" | grep -q 'Already captured' || fail "re-capture not idempotent ($out)" +pass "identical re-capture is idempotent" + +RAW="$T/projects/demo/raw" +[ "$(ls "$RAW"/*.md | wc -l)" -eq 1 ] || fail "expected exactly 1 item after idempotent re-capture" +grep -q '^status: unprocessed$' "$RAW"/*.md || fail "item missing status: unprocessed" +grep -q '^content_type: text/markdown$' "$RAW"/*.md || fail "item missing content_type" + +printf 'not a real item\n' > "$RAW/broken.md" +run list | grep -q 'broken .*malformed' || fail "--list did not flag the malformed item" +pass "list flags malformed items" + +ID=$(ls "$RAW" | grep -v broken | sed 's/\.md$//' | head -1) +run discard "$ID" | grep -q "Discarded $ID" || fail "discard did not report" +grep -q '^status: discarded$' "$RAW/$ID.md" || fail "status not flipped to discarded" +pass "discard flips status" + +rm -rf "$T" +echo; echo "ALL PASS" +``` + +- [ ] **Step 2: Run it to verify it fails.** + +Run: `bash tests/test-raw-capture.sh` +Expected: FAIL — `skills/capture/SKILL.md missing`. + +- [ ] **Step 3: Write the skill.** Create `skills/capture/SKILL.md`: + +```markdown +--- +name: capture +description: Drop unprocessed material (a file, a URL, or pasted text) into the current project's raw inbox for later refinement into wiki notes. Usage: /second-brain:capture | "" | --list | --discard | --node . +user-invocable: true +disable-model-invocation: true +allowed-tools: Read Bash(node *) Bash(test *) Bash(cat *) Bash(basename *) +--- + +# /second-brain:capture — raw inbox producer + +Hold unprocessed material in the active project's raw inbox +(`~/.second-brain/projects//raw/`) until the maintainer refines it into wiki +nodes. Raw items are **not** searched — they are a staging area, surfaced as a +backlog count at session start. + +Map the user's argument to a `raw-capture-cli` action and run it. The CLI resolves +the active project, stamps provenance, copies blobs, and dedups by content hash. + +```bash +CLI="${CLAUDE_PLUGIN_ROOT}/mcp/dist/tools/raw-capture-cli.bundle.js" +``` + +- `` or `` or inline `"text"` → `node "$CLI" capture [--node ]` +- pasted/piped text → `node "$CLI" paste [--node ]` (reads stdin) +- `--list` → `node "$CLI" list` +- `--discard ` → `node "$CLI" discard ` + +`--node ` records that the item is evidence for an existing wiki page +(provenance only — the link is projected later when the maintainer processes it). + +Relay the CLI's output. If it reports "could not resolve the active project", tell +the user to `cd` into their project directory first. +``` + +- [ ] **Step 4: Run the test to verify it passes.** + +Run: `bash tests/test-raw-capture.sh` +Expected: `ALL PASS`. + +- [ ] **Step 5: Commit.** + +```bash +git add skills/capture/SKILL.md tests/test-raw-capture.sh +git commit -m "feat(kb): /second-brain:capture producer skill (SP-2 Task 4)" +``` + +--- + +## Task 5: session-load backlog banner + +**Files:** +- Modify: `scripts/session-load.sh` +- Test: `tests/test-raw-inbox-banner.sh` + +- [ ] **Step 1: Write the failing test.** Create `tests/test-raw-inbox-banner.sh`: + +```bash +#!/bin/bash +# session-load surfaces a raw-inbox backlog banner for the active project, gated by SB_RAW_INBOX. +set -u +ROOT="$(cd "$(dirname "$0")"/.. && pwd)" +SL="$ROOT/scripts/session-load.sh" +fail(){ echo "FAIL: $1"; exit 1; }; pass(){ echo "PASS: $1"; } + +grep -q 'SB_RAW_INBOX' "$SL" || fail "session-load.sh has no SB_RAW_INBOX gate" +grep -q 'raw' "$SL" || fail "session-load.sh has no raw-inbox banner block" +pass "session-load references the raw-inbox banner + kill switch" + +# Extract and run just the banner block's counting logic in isolation, to prove it counts +# unprocessed items. (The block is guarded by SB_RAW_INBOX != off and count > 0.) +T=$(mktemp -d); RAW="$T/projects/demo/raw"; mkdir -p "$RAW" +mk(){ printf -- '---\nstatus: %s\n---\nx\n' "$1" > "$RAW/$2.md"; } +mk unprocessed a; mk unprocessed b; mk discarded c +N=$(grep -rl '^status: unprocessed$' "$RAW" 2>/dev/null | wc -l | tr -d ' ') +[ "$N" = "2" ] || fail "expected the documented count expression to yield 2 (got $N)" +pass "the banner count expression counts unprocessed items (2)" +rm -rf "$T" +echo; echo "ALL PASS" +``` + +- [ ] **Step 2: Run it to verify it fails.** + +Run: `bash tests/test-raw-inbox-banner.sh` +Expected: FAIL — `session-load.sh has no SB_RAW_INBOX gate`. + +- [ ] **Step 3: Add the banner to `scripts/session-load.sh`.** Insert this block immediately AFTER the existing wiki-enrichment block (after the `fi` that closes the `if [ -n "${PROJ_KW// /}" ]; then` block around line 411 — i.e. after its wiki-hits `sb_append`). It reuses `$slug`, `$BRAIN_DIR`, and `sb_append`: + +```bash +# SP-2: raw-inbox backlog — surface how many unprocessed items await processing for this project. +if [ "${SB_RAW_INBOX:-on}" != "off" ]; then + RAW_DIR_PATH="$BRAIN_DIR/projects/$slug/raw" + if [ -d "$RAW_DIR_PATH" ]; then + RAW_N=$(grep -rl '^status: unprocessed$' "$RAW_DIR_PATH" 2>/dev/null | wc -l | tr -d ' ') + if [ "${RAW_N:-0}" -gt 0 ]; then + sb_append "$(printf '## ⓘ raw inbox — %s unprocessed item(s)\nRun `/second-brain:capture --list` to review; the maintainer refines them into notes.\n\n' "$RAW_N")" \ + "raw-inbox-banner" 250 + fi + fi +fi +``` + +> `grep -rl '^status: unprocessed$'` matches the exact frontmatter line the module writes — mawk-free, portable. `tr -d ' '` strips `wc` padding (macOS). The `250` priority slots it alongside the other advisory banners. + +- [ ] **Step 4: Run the test to verify it passes.** + +Run: `bash tests/test-raw-inbox-banner.sh` +Expected: `ALL PASS`. + +- [ ] **Step 5: Commit.** + +```bash +git add scripts/session-load.sh tests/test-raw-inbox-banner.sh +git commit -m "feat(kb): session-load raw-inbox backlog banner (SP-2 Task 5)" +``` + +--- + +## Task 6: Ship — gate, version bump, PR + +**Files:** +- Modify: `.claude-plugin/plugin.json`, `.claude-plugin/marketplace.json`, `mcp/src/server.ts`, `skills/upgrade/SKILL.md` + +- [ ] **Step 1: Branch (if not already on a feature branch).** + +```bash +git rev-parse --abbrev-ref HEAD # if 'main', branch: +git checkout -b feat/sp2-raw-inbox +``` + +> If you implemented Tasks 1–5 directly on `main`, instead create the branch first and cherry-pick, or (simpler) do all of SP-2 on `feat/sp2-raw-inbox` from the start. + +- [ ] **Step 2: Build + full suite (the gate).** + +```bash +cd mcp && npm run build && cd .. +bash tests/run-all.sh +``` +Expected: `ALL GREEN` (includes the new `test-mcp-typecheck`, `test-raw-capture`, `test-raw-inbox-banner`, `test-kb-schema`). + +- [ ] **Step 3: Deep-review gate.** Run `second-brain:code-review-deep --base main`. Triage findings; fix any confirmed (≥70) with TDD; re-run until clean. + +- [ ] **Step 4: Version bump + migration row.** + - `.claude-plugin/plugin.json` + `.claude-plugin/marketplace.json`: `0.24.10` → `0.24.11`. + - `mcp/src/server.ts`: `version: "2.6.4"` → `"2.6.5"`. + - `skills/upgrade/SKILL.md`: add a `**0.24.11**` row above `**0.24.10**` describing the raw inbox (per-project `raw/`, `/second-brain:capture`, derived work-list, `kb-schema` raw group, `session-load` banner `SB_RAW_INBOX=off`; SP-3/SP-4 deferred; additive, no state migration). + +- [ ] **Step 5: Rebuild (server version embedded in bundle) + verify lockstep + migration-row test.** + +```bash +cd mcp && npm run build && cd .. +bash scripts/validate-plugin.sh +bash tests/test-upgrade-migration-row.sh +``` +Expected: `OK: all plugin files valid` + `PASS: upgrade migration row present for 0.24.11`. + +- [ ] **Step 6: Commit + PR + merge.** + +```bash +git add -A +git commit -m "chore(release): raw inbox (SP-2) — bump 0.24.11 + migration row" +git push -u origin feat/sp2-raw-inbox +gh pr create --base main --title "feat(kb): raw inbox (SP-2)" --body "" +gh pr merge --merge --delete-branch +git checkout main && git pull --ff-only origin main +``` + +--- + +## Self-Review + +**1. Spec coverage:** +- Storage per-project hot tier → Task 2 `rawDir` + capture. ✓ +- Item format + provenance frontmatter + blob sidecar + URL pointer → Task 2. ✓ +- Derived work-list (no index) → Task 2 `unprocessedCount`/`listItems` scan; Task 5 grep. ✓ +- `kb-schema` raw group dual-reader → Task 1. ✓ +- `/second-brain:capture` (+ `--node`/`--list`/`--discard`, idempotent) → Tasks 3–4. ✓ +- Backlog banner + `SB_RAW_INBOX=off` → Task 5. ✓ +- "Out of search" → raw is never under `wiki/` and no search code touches it (nothing to add; verified by omission). ✓ +- Malformed-frontmatter gentle warning → realized as `malformed` flag + `--list` marker (Tasks 2/4), not `knowledge_validate` (documented deviation). ✓ +- Cross-OS (node `fs` copy, mawk-safe bash) → Tasks 2/5. ✓ +- Versioning → Task 6. ✓ +- Deferred SP-3/SP-4/fetch/extraction → not built. ✓ + +**2. Placeholder scan:** PR `--body ""` in Task 6 Step 6 is the one spot to fill at ship time (write a real summary, mirroring the SP-1 PR). No other placeholders; all code blocks complete. + +**3. Type consistency:** `captureItem`/`listItems`/`setStatus`/`unprocessedCount`/`rawDir` signatures match between `raw-inbox.ts` (Task 2), its test (Task 2), and the CLI (Task 3). `RawStatus` values (`unprocessed|processed|discarded`) match `kb-schema.json` (Task 1). The CLI actions (`capture|paste|list|discard`) match the skill's mapping (Task 4) and the bash test (Task 4). From 57c9847aa1ec8a782a0aa6dd88cf6a7e3cbcab44 Mon Sep 17 00:00:00 2001 From: Cain-Ish Date: Wed, 3 Jun 2026 18:48:14 +0200 Subject: [PATCH 3/9] feat(kb): declare the raw-inbox group in the source of truth (SP-2 Task 1) Co-Authored-By: Claude Opus 4.8 (1M context) --- kb-schema.json | 6 ++++++ mcp/dist/constants/kb-schema.d.ts | 3 +++ mcp/dist/constants/kb-schema.d.ts.map | 2 +- mcp/dist/constants/kb-schema.js | 3 +++ mcp/dist/constants/kb-schema.js.map | 2 +- mcp/dist/constants/kb-schema.test.js | 4 ++++ mcp/dist/constants/kb-schema.test.js.map | 2 +- mcp/dist/server.bundle.js | 8 ++++++++ mcp/dist/tools/knowledge-reindex.bundle.js | 8 ++++++++ mcp/dist/tools/knowledge-validate.bundle.js | 8 ++++++++ mcp/src/constants/kb-schema.test.ts | 4 ++++ mcp/src/constants/kb-schema.ts | 4 ++++ scripts/kb-schema.sh | 5 ++++- tests/test-kb-schema.sh | 6 ++++++ 14 files changed, 61 insertions(+), 4 deletions(-) diff --git a/kb-schema.json b/kb-schema.json index 61de85c..f1ebcb5 100644 --- a/kb-schema.json +++ b/kb-schema.json @@ -8,5 +8,11 @@ "forget_protection": { "protected": ["learnings", "decisions", "concepts", "security", "themes", "projects"], "discounted": ["entities", "sources", "issues"] + }, + "raw": { + "dir": "raw", + "tier": "project", + "statuses": ["unprocessed", "processed", "discarded"], + "searchable": false } } diff --git a/mcp/dist/constants/kb-schema.d.ts b/mcp/dist/constants/kb-schema.d.ts index a0a3823..2587128 100644 --- a/mcp/dist/constants/kb-schema.d.ts +++ b/mcp/dist/constants/kb-schema.d.ts @@ -5,6 +5,9 @@ export declare const EDGE_TYPES: readonly string[]; export declare const PROJECT_SECTIONS: readonly string[]; export declare const FORGET_PROTECTED: readonly string[]; export declare const FORGET_DISCOUNTED: readonly string[]; +/** Raw inbox group: per-project staging for unprocessed material (SP-2). Never searched. */ +export declare const RAW_DIR: string; +export declare const RAW_STATUSES: readonly string[]; /** Wiki categories that hold authored content (have a directory, are scaffolded + write-guarded). */ export declare const CONTENT_CATEGORIES: readonly string[]; /** Every recognized wiki category, including the generated MOC dirs (projects/, themes/). */ diff --git a/mcp/dist/constants/kb-schema.d.ts.map b/mcp/dist/constants/kb-schema.d.ts.map index cffab0f..b00ace5 100644 --- a/mcp/dist/constants/kb-schema.d.ts.map +++ b/mcp/dist/constants/kb-schema.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"kb-schema.d.ts","sourceRoot":"","sources":["../../src/constants/kb-schema.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,gBAAgB,EAAE,SAAS,MAAM,EAA4B,CAAC;AAC3E,eAAO,MAAM,kBAAkB,EAAE,SAAS,MAAM,EAA8B,CAAC;AAC/E,eAAO,MAAM,cAAc,EAAE,SAAS,MAAM,EAA0B,CAAC;AACvE,eAAO,MAAM,UAAU,EAAE,SAAS,MAAM,EAAsB,CAAC;AAC/D,eAAO,MAAM,gBAAgB,EAAE,SAAS,MAAM,EAA4B,CAAC;AAC3E,eAAO,MAAM,gBAAgB,EAAE,SAAS,MAAM,EAAuC,CAAC;AACtF,eAAO,MAAM,iBAAiB,EAAE,SAAS,MAAM,EAAwC,CAAC;AAExF,qGAAqG;AACrG,eAAO,MAAM,kBAAkB,EAAE,SAAS,MAAM,EAAiD,CAAC;AAClG,6FAA6F;AAC7F,eAAO,MAAM,cAAc,EAAE,SAAS,MAAM,EAA+C,CAAC;AAE5F,iEAAiE;AACjE,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAyC;AAC7F,6FAA6F;AAC7F,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAuC"} \ No newline at end of file +{"version":3,"file":"kb-schema.d.ts","sourceRoot":"","sources":["../../src/constants/kb-schema.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,gBAAgB,EAAE,SAAS,MAAM,EAA4B,CAAC;AAC3E,eAAO,MAAM,kBAAkB,EAAE,SAAS,MAAM,EAA8B,CAAC;AAC/E,eAAO,MAAM,cAAc,EAAE,SAAS,MAAM,EAA0B,CAAC;AACvE,eAAO,MAAM,UAAU,EAAE,SAAS,MAAM,EAAsB,CAAC;AAC/D,eAAO,MAAM,gBAAgB,EAAE,SAAS,MAAM,EAA4B,CAAC;AAC3E,eAAO,MAAM,gBAAgB,EAAE,SAAS,MAAM,EAAuC,CAAC;AACtF,eAAO,MAAM,iBAAiB,EAAE,SAAS,MAAM,EAAwC,CAAC;AAExF,4FAA4F;AAC5F,eAAO,MAAM,OAAO,EAAE,MAAuB,CAAC;AAC9C,eAAO,MAAM,YAAY,EAAE,SAAS,MAAM,EAAwB,CAAC;AAEnE,qGAAqG;AACrG,eAAO,MAAM,kBAAkB,EAAE,SAAS,MAAM,EAAiD,CAAC;AAClG,6FAA6F;AAC7F,eAAO,MAAM,cAAc,EAAE,SAAS,MAAM,EAA+C,CAAC;AAE5F,iEAAiE;AACjE,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAyC;AAC7F,6FAA6F;AAC7F,wBAAgB,cAAc,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAuC"} \ No newline at end of file diff --git a/mcp/dist/constants/kb-schema.js b/mcp/dist/constants/kb-schema.js index f26da25..98e3065 100644 --- a/mcp/dist/constants/kb-schema.js +++ b/mcp/dist/constants/kb-schema.js @@ -10,6 +10,9 @@ export const EDGE_TYPES = schema.edge_types; export const PROJECT_SECTIONS = schema.project_sections; export const FORGET_PROTECTED = schema.forget_protection.protected; export const FORGET_DISCOUNTED = schema.forget_protection.discounted; +/** Raw inbox group: per-project staging for unprocessed material (SP-2). Never searched. */ +export const RAW_DIR = schema.raw.dir; +export const RAW_STATUSES = schema.raw.statuses; /** Wiki categories that hold authored content (have a directory, are scaffolded + write-guarded). */ export const CONTENT_CATEGORIES = [...STRUCTURED_TYPES, ...UNSTRUCTURED_TYPES]; /** Every recognized wiki category, including the generated MOC dirs (projects/, themes/). */ diff --git a/mcp/dist/constants/kb-schema.js.map b/mcp/dist/constants/kb-schema.js.map index 0639110..c9d3c44 100644 --- a/mcp/dist/constants/kb-schema.js.map +++ b/mcp/dist/constants/kb-schema.js.map @@ -1 +1 @@ -{"version":3,"file":"kb-schema.js","sourceRoot":"","sources":["../../src/constants/kb-schema.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,iGAAiG;AACjG,kGAAkG;AAClG,4FAA4F;AAC5F,OAAO,MAAM,MAAM,yBAAyB,CAAC;AAE7C,MAAM,CAAC,MAAM,gBAAgB,GAAsB,MAAM,CAAC,gBAAgB,CAAC;AAC3E,MAAM,CAAC,MAAM,kBAAkB,GAAsB,MAAM,CAAC,kBAAkB,CAAC;AAC/E,MAAM,CAAC,MAAM,cAAc,GAAsB,MAAM,CAAC,cAAc,CAAC;AACvE,MAAM,CAAC,MAAM,UAAU,GAAsB,MAAM,CAAC,UAAU,CAAC;AAC/D,MAAM,CAAC,MAAM,gBAAgB,GAAsB,MAAM,CAAC,gBAAgB,CAAC;AAC3E,MAAM,CAAC,MAAM,gBAAgB,GAAsB,MAAM,CAAC,iBAAiB,CAAC,SAAS,CAAC;AACtF,MAAM,CAAC,MAAM,iBAAiB,GAAsB,MAAM,CAAC,iBAAiB,CAAC,UAAU,CAAC;AAExF,qGAAqG;AACrG,MAAM,CAAC,MAAM,kBAAkB,GAAsB,CAAC,GAAG,gBAAgB,EAAE,GAAG,kBAAkB,CAAC,CAAC;AAClG,6FAA6F;AAC7F,MAAM,CAAC,MAAM,cAAc,GAAsB,CAAC,GAAG,kBAAkB,EAAE,GAAG,cAAc,CAAC,CAAC;AAE5F,iEAAiE;AACjE,MAAM,UAAU,gBAAgB,CAAC,CAAS,IAAa,OAAO,gBAAgB,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC7F,6FAA6F;AAC7F,MAAM,UAAU,cAAc,CAAC,CAAS,IAAa,OAAO,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC"} \ No newline at end of file +{"version":3,"file":"kb-schema.js","sourceRoot":"","sources":["../../src/constants/kb-schema.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,iGAAiG;AACjG,kGAAkG;AAClG,4FAA4F;AAC5F,OAAO,MAAM,MAAM,yBAAyB,CAAC;AAE7C,MAAM,CAAC,MAAM,gBAAgB,GAAsB,MAAM,CAAC,gBAAgB,CAAC;AAC3E,MAAM,CAAC,MAAM,kBAAkB,GAAsB,MAAM,CAAC,kBAAkB,CAAC;AAC/E,MAAM,CAAC,MAAM,cAAc,GAAsB,MAAM,CAAC,cAAc,CAAC;AACvE,MAAM,CAAC,MAAM,UAAU,GAAsB,MAAM,CAAC,UAAU,CAAC;AAC/D,MAAM,CAAC,MAAM,gBAAgB,GAAsB,MAAM,CAAC,gBAAgB,CAAC;AAC3E,MAAM,CAAC,MAAM,gBAAgB,GAAsB,MAAM,CAAC,iBAAiB,CAAC,SAAS,CAAC;AACtF,MAAM,CAAC,MAAM,iBAAiB,GAAsB,MAAM,CAAC,iBAAiB,CAAC,UAAU,CAAC;AAExF,4FAA4F;AAC5F,MAAM,CAAC,MAAM,OAAO,GAAW,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC;AAC9C,MAAM,CAAC,MAAM,YAAY,GAAsB,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;AAEnE,qGAAqG;AACrG,MAAM,CAAC,MAAM,kBAAkB,GAAsB,CAAC,GAAG,gBAAgB,EAAE,GAAG,kBAAkB,CAAC,CAAC;AAClG,6FAA6F;AAC7F,MAAM,CAAC,MAAM,cAAc,GAAsB,CAAC,GAAG,kBAAkB,EAAE,GAAG,cAAc,CAAC,CAAC;AAE5F,iEAAiE;AACjE,MAAM,UAAU,gBAAgB,CAAC,CAAS,IAAa,OAAO,gBAAgB,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC7F,6FAA6F;AAC7F,MAAM,UAAU,cAAc,CAAC,CAAS,IAAa,OAAO,cAAc,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC"} \ No newline at end of file diff --git a/mcp/dist/constants/kb-schema.test.js b/mcp/dist/constants/kb-schema.test.js index f839e3b..867e1fd 100644 --- a/mcp/dist/constants/kb-schema.test.js +++ b/mcp/dist/constants/kb-schema.test.js @@ -14,6 +14,10 @@ describe('kb-schema — single source of truth (TS side)', () => { expect(kb.FORGET_PROTECTED).toEqual(json.forget_protection.protected); expect(kb.FORGET_DISCOUNTED).toEqual(json.forget_protection.discounted); }); + it('SP-2: raw group derives verbatim from kb-schema.json', () => { + expect(kb.RAW_DIR).toEqual(json.raw.dir); + expect(kb.RAW_STATUSES).toEqual(json.raw.statuses); + }); it('derived category sets are correct', () => { expect(kb.CONTENT_CATEGORIES).toEqual([...json.structured_types, ...json.unstructured_types]); expect(kb.ALL_CATEGORIES).toEqual([...json.structured_types, ...json.unstructured_types, ...json.generated_dirs]); diff --git a/mcp/dist/constants/kb-schema.test.js.map b/mcp/dist/constants/kb-schema.test.js.map index b4fbc97..4c4049d 100644 --- a/mcp/dist/constants/kb-schema.test.js.map +++ b/mcp/dist/constants/kb-schema.test.js.map @@ -1 +1 @@ -{"version":3,"file":"kb-schema.test.js","sourceRoot":"","sources":["../../src/constants/kb-schema.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,IAAI,CAAC;AAClC,OAAO,KAAK,EAAE,MAAM,gBAAgB,CAAC;AACrC,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAAE,UAAU,IAAI,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAEzE,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,GAAG,CAAC,yBAAyB,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;AAEpG,QAAQ,CAAC,8CAA8C,EAAE,GAAG,EAAE;IAC5D,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;QACvD,MAAM,CAAC,EAAE,CAAC,gBAAgB,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;QAC3D,MAAM,CAAC,EAAE,CAAC,kBAAkB,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAC;QAC/D,MAAM,CAAC,EAAE,CAAC,cAAc,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QACvD,MAAM,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAC/C,MAAM,CAAC,EAAE,CAAC,gBAAgB,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;QAC3D,MAAM,CAAC,EAAE,CAAC,gBAAgB,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,iBAAiB,CAAC,SAAS,CAAC,CAAC;QACtE,MAAM,CAAC,EAAE,CAAC,iBAAiB,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,iBAAiB,CAAC,UAAU,CAAC,CAAC;IAC1E,CAAC,CAAC,CAAC;IACH,EAAE,CAAC,mCAAmC,EAAE,GAAG,EAAE;QAC3C,MAAM,CAAC,EAAE,CAAC,kBAAkB,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,gBAAgB,EAAE,GAAG,IAAI,CAAC,kBAAkB,CAAC,CAAC,CAAC;QAC9F,MAAM,CAAC,EAAE,CAAC,cAAc,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,gBAAgB,EAAE,GAAG,IAAI,CAAC,kBAAkB,EAAE,GAAG,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC;IACpH,CAAC,CAAC,CAAC;IACH,EAAE,CAAC,8BAA8B,EAAE,GAAG,EAAE;QACtC,MAAM,CAAC,EAAE,CAAC,gBAAgB,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACpD,MAAM,CAAC,EAAE,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACjD,MAAM,CAAC,EAAE,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACjD,MAAM,CAAC,EAAE,CAAC,cAAc,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACrD,CAAC,CAAC,CAAC;IACH,EAAE,CAAC,6EAA6E,EAAE,GAAG,EAAE;QACrF,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,CAAC,gBAAgB,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IACxF,CAAC,CAAC,CAAC;IACH,EAAE,CAAC,+DAA+D,EAAE,GAAG,EAAE;QACvE,MAAM,CAAC,CAAC,GAAG,gBAAgB,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IACzD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"} \ No newline at end of file +{"version":3,"file":"kb-schema.test.js","sourceRoot":"","sources":["../../src/constants/kb-schema.test.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/mcp/dist/server.bundle.js b/mcp/dist/server.bundle.js index 38a50b2..9aa9b7e 100644 --- a/mcp/dist/server.bundle.js +++ b/mcp/dist/server.bundle.js @@ -28212,6 +28212,12 @@ var kb_schema_default = { forget_protection: { protected: ["learnings", "decisions", "concepts", "security", "themes", "projects"], discounted: ["entities", "sources", "issues"] + }, + raw: { + dir: "raw", + tier: "project", + statuses: ["unprocessed", "processed", "discarded"], + searchable: false } }; @@ -28223,6 +28229,8 @@ var EDGE_TYPES2 = kb_schema_default.edge_types; var PROJECT_SECTIONS = kb_schema_default.project_sections; var FORGET_PROTECTED = kb_schema_default.forget_protection.protected; var FORGET_DISCOUNTED = kb_schema_default.forget_protection.discounted; +var RAW_DIR = kb_schema_default.raw.dir; +var RAW_STATUSES = kb_schema_default.raw.statuses; var CONTENT_CATEGORIES = [...STRUCTURED_TYPES, ...UNSTRUCTURED_TYPES]; var ALL_CATEGORIES = [...CONTENT_CATEGORIES, ...GENERATED_DIRS]; diff --git a/mcp/dist/tools/knowledge-reindex.bundle.js b/mcp/dist/tools/knowledge-reindex.bundle.js index bf2f5e6..a82b8a8 100644 --- a/mcp/dist/tools/knowledge-reindex.bundle.js +++ b/mcp/dist/tools/knowledge-reindex.bundle.js @@ -6291,6 +6291,12 @@ var kb_schema_default = { forget_protection: { protected: ["learnings", "decisions", "concepts", "security", "themes", "projects"], discounted: ["entities", "sources", "issues"] + }, + raw: { + dir: "raw", + tier: "project", + statuses: ["unprocessed", "processed", "discarded"], + searchable: false } }; @@ -6302,6 +6308,8 @@ var EDGE_TYPES2 = kb_schema_default.edge_types; var PROJECT_SECTIONS = kb_schema_default.project_sections; var FORGET_PROTECTED = kb_schema_default.forget_protection.protected; var FORGET_DISCOUNTED = kb_schema_default.forget_protection.discounted; +var RAW_DIR = kb_schema_default.raw.dir; +var RAW_STATUSES = kb_schema_default.raw.statuses; var CONTENT_CATEGORIES = [...STRUCTURED_TYPES, ...UNSTRUCTURED_TYPES]; var ALL_CATEGORIES = [...CONTENT_CATEGORIES, ...GENERATED_DIRS]; diff --git a/mcp/dist/tools/knowledge-validate.bundle.js b/mcp/dist/tools/knowledge-validate.bundle.js index 6ea097d..4e24ade 100644 --- a/mcp/dist/tools/knowledge-validate.bundle.js +++ b/mcp/dist/tools/knowledge-validate.bundle.js @@ -6208,6 +6208,12 @@ var kb_schema_default = { forget_protection: { protected: ["learnings", "decisions", "concepts", "security", "themes", "projects"], discounted: ["entities", "sources", "issues"] + }, + raw: { + dir: "raw", + tier: "project", + statuses: ["unprocessed", "processed", "discarded"], + searchable: false } }; @@ -6219,6 +6225,8 @@ var EDGE_TYPES = kb_schema_default.edge_types; var PROJECT_SECTIONS = kb_schema_default.project_sections; var FORGET_PROTECTED = kb_schema_default.forget_protection.protected; var FORGET_DISCOUNTED = kb_schema_default.forget_protection.discounted; +var RAW_DIR = kb_schema_default.raw.dir; +var RAW_STATUSES = kb_schema_default.raw.statuses; var CONTENT_CATEGORIES = [...STRUCTURED_TYPES, ...UNSTRUCTURED_TYPES]; var ALL_CATEGORIES = [...CONTENT_CATEGORIES, ...GENERATED_DIRS]; diff --git a/mcp/src/constants/kb-schema.test.ts b/mcp/src/constants/kb-schema.test.ts index 2825d13..f53161f 100644 --- a/mcp/src/constants/kb-schema.test.ts +++ b/mcp/src/constants/kb-schema.test.ts @@ -16,6 +16,10 @@ describe('kb-schema — single source of truth (TS side)', () => { expect(kb.FORGET_PROTECTED).toEqual(json.forget_protection.protected); expect(kb.FORGET_DISCOUNTED).toEqual(json.forget_protection.discounted); }); + it('SP-2: raw group derives verbatim from kb-schema.json', () => { + expect(kb.RAW_DIR).toEqual(json.raw.dir); + expect(kb.RAW_STATUSES).toEqual(json.raw.statuses); + }); it('derived category sets are correct', () => { expect(kb.CONTENT_CATEGORIES).toEqual([...json.structured_types, ...json.unstructured_types]); expect(kb.ALL_CATEGORIES).toEqual([...json.structured_types, ...json.unstructured_types, ...json.generated_dirs]); diff --git a/mcp/src/constants/kb-schema.ts b/mcp/src/constants/kb-schema.ts index 9e2520a..874ca97 100644 --- a/mcp/src/constants/kb-schema.ts +++ b/mcp/src/constants/kb-schema.ts @@ -12,6 +12,10 @@ export const PROJECT_SECTIONS: readonly string[] = schema.project_sections; export const FORGET_PROTECTED: readonly string[] = schema.forget_protection.protected; export const FORGET_DISCOUNTED: readonly string[] = schema.forget_protection.discounted; +/** Raw inbox group: per-project staging for unprocessed material (SP-2). Never searched. */ +export const RAW_DIR: string = schema.raw.dir; +export const RAW_STATUSES: readonly string[] = schema.raw.statuses; + /** Wiki categories that hold authored content (have a directory, are scaffolded + write-guarded). */ export const CONTENT_CATEGORIES: readonly string[] = [...STRUCTURED_TYPES, ...UNSTRUCTURED_TYPES]; /** Every recognized wiki category, including the generated MOC dirs (projects/, themes/). */ diff --git a/scripts/kb-schema.sh b/scripts/kb-schema.sh index 25c1a85..602dada 100644 --- a/scripts/kb-schema.sh +++ b/scripts/kb-schema.sh @@ -14,6 +14,9 @@ if command -v jq >/dev/null 2>&1 && [ -f "$_SB_KB_SCHEMA" ]; then SB_EDGE_TYPES=$(jq -r '.edge_types | join(" ")' "$_SB_KB_SCHEMA" 2>/dev/null) SB_FORGET_PROTECTED=$(jq -r '.forget_protection.protected | join(" ")' "$_SB_KB_SCHEMA" 2>/dev/null) SB_FORGET_DISCOUNTED=$(jq -r '.forget_protection.discounted | join(" ")' "$_SB_KB_SCHEMA" 2>/dev/null) + SB_RAW_DIR=$(jq -r '.raw.dir' "$_SB_KB_SCHEMA" 2>/dev/null) + SB_RAW_STATUSES=$(jq -r '.raw.statuses | join(" ")' "$_SB_KB_SCHEMA" 2>/dev/null) export SB_STRUCTURED_TYPES SB_UNSTRUCTURED_TYPES SB_GENERATED_DIRS SB_CONTENT_CATEGORIES \ - SB_ALL_CATEGORIES SB_EDGE_TYPES SB_FORGET_PROTECTED SB_FORGET_DISCOUNTED + SB_ALL_CATEGORIES SB_EDGE_TYPES SB_FORGET_PROTECTED SB_FORGET_DISCOUNTED \ + SB_RAW_DIR SB_RAW_STATUSES fi diff --git a/tests/test-kb-schema.sh b/tests/test-kb-schema.sh index e9c67bc..024d34f 100755 --- a/tests/test-kb-schema.sh +++ b/tests/test-kb-schema.sh @@ -31,4 +31,10 @@ H=$(grep -rnE 'for [a-z]+ in learnings decisions entities issues concepts securi [ -z "$H" ] && pass "no hardcoded six-type loop (all use \$SB_STRUCTURED_TYPES)" \ || fail "hardcoded six-type loop found (source kb-schema.sh + use \$SB_STRUCTURED_TYPES):" "$H" +# --- SP-2: the `raw` group is visible to the json + bash readers (TS side in kb-schema.test.ts) --- +[ "$(jq -r '.raw.dir' "$M")" = "raw" ] || fail "kb-schema.json .raw.dir != raw" +[ "$SB_RAW_DIR" = "raw" ] || fail "kb-schema.sh did not export SB_RAW_DIR=raw (got '$SB_RAW_DIR')" +[ "$SB_RAW_STATUSES" = "$(jq -r '.raw.statuses|join(" ")' "$M")" ] || fail "SB_RAW_STATUSES != manifest" +pass "raw group visible to json + bash readers" + echo; echo "ALL PASS" From 3707568aa5adaa4c16c92c1b7eb10c9fd05661bc Mon Sep 17 00:00:00 2001 From: Cain-Ish Date: Wed, 3 Jun 2026 18:49:48 +0200 Subject: [PATCH 4/9] =?UTF-8?q?feat(kb):=20raw-inbox=20pure=20module=20?= =?UTF-8?q?=E2=80=94=20capture/list/status/count=20(SP-2=20Task=202)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- mcp/src/tools/doc-sources.ts | 2 +- mcp/src/tools/raw-inbox.test.ts | 93 +++++++++++++ mcp/src/tools/raw-inbox.ts | 230 ++++++++++++++++++++++++++++++++ 3 files changed, 324 insertions(+), 1 deletion(-) create mode 100644 mcp/src/tools/raw-inbox.test.ts create mode 100644 mcp/src/tools/raw-inbox.ts diff --git a/mcp/src/tools/doc-sources.ts b/mcp/src/tools/doc-sources.ts index 141b075..466173a 100644 --- a/mcp/src/tools/doc-sources.ts +++ b/mcp/src/tools/doc-sources.ts @@ -35,7 +35,7 @@ export interface DocEntry { const JUNK_DIRS = new Set(['node_modules', '.git', '.venv', 'venv', '.next', 'dist', 'build']); -function assertSafeSlug(slug: string): void { +export function assertSafeSlug(slug: string): void { if (!slug || /[\\/]|\.\./.test(slug)) { throw new Error(`unsafe slug: ${JSON.stringify(slug)}`); } diff --git a/mcp/src/tools/raw-inbox.test.ts b/mcp/src/tools/raw-inbox.test.ts new file mode 100644 index 0000000..37ed85e --- /dev/null +++ b/mcp/src/tools/raw-inbox.test.ts @@ -0,0 +1,93 @@ +import { describe, it, expect } from 'vitest'; +import { promises as fs } from 'fs'; +import { join } from 'path'; +import { tmpdir } from 'os'; +import { captureItem, listItems, setStatus, unprocessedCount, rawDir } from './raw-inbox.js'; + +async function brain(): Promise<{ brainDir: string; slug: string }> { + const brainDir = await fs.mkdtemp(join(tmpdir(), 'raw-')); + const slug = 'alpha'; + await fs.mkdir(join(brainDir, 'projects', slug), { recursive: true }); + return { brainDir, slug }; +} +const NOW = '2026-06-03T14:15:00Z'; + +describe('raw-inbox', () => { + it('captures pasted text as a markdown item with provenance frontmatter', async () => { + const { brainDir, slug } = await brain(); + const r = await captureItem({ brainDir, slug, kind: 'paste', source: 'paste', + content: 'a rate-limit note', now: NOW }); + expect(r.duplicate).toBe(false); + expect(r.id).toMatch(/^20260603T141500Z-/); + const items = await listItems(brainDir, slug); + expect(items).toHaveLength(1); + expect(items[0].status).toBe('unprocessed'); + expect(items[0].content_type).toBe('text/markdown'); + expect(items[0].captured_by).toBe('user'); + expect(items[0].body).toContain('a rate-limit note'); + }); + + it('captures a URL as an offline pointer (no fetch), content_type text/uri-list', async () => { + const { brainDir, slug } = await brain(); + const r = await captureItem({ brainDir, slug, kind: 'url', + source: 'https://example.com/spec', content: 'https://example.com/spec', now: NOW }); + const items = await listItems(brainDir, slug); + expect(items[0].content_type).toBe('text/uri-list'); + expect(items[0].source).toBe('https://example.com/spec'); + expect(items[0].body).toContain('https://example.com/spec'); + expect(r.id).toContain('example-com'); + }); + + it('captures a text file into the body; a binary file into a blob sidecar', async () => { + const { brainDir, slug } = await brain(); + const txt = join(brainDir, 'note.md'); + await fs.writeFile(txt, '# Title\nbody text'); + await captureItem({ brainDir, slug, kind: 'file', source: txt, now: NOW }); + const bin = join(brainDir, 'pic.bin'); + await fs.writeFile(bin, Buffer.from([0x00, 0x01, 0x02, 0x00])); + const rb = await captureItem({ brainDir, slug, kind: 'file', source: bin, now: '2026-06-03T14:16:00Z' }); + const items = await listItems(brainDir, slug); + const text = items.find(i => i.source === txt)!; + expect(text.blob).toBeUndefined(); + expect(text.body).toContain('body text'); + const binItem = items.find(i => i.source === bin)!; + expect(binItem.blob).toBe(`${rb.id}.bin`); + await expect(fs.access(join(rawDir(brainDir, slug), `${rb.id}.bin`))).resolves.toBeUndefined(); + }); + + it('is idempotent: re-capturing identical content returns the existing unprocessed item', async () => { + const { brainDir, slug } = await brain(); + const a = await captureItem({ brainDir, slug, kind: 'paste', source: 'paste', content: 'same', now: NOW }); + const b = await captureItem({ brainDir, slug, kind: 'paste', source: 'paste', content: 'same', now: '2026-06-03T15:00:00Z' }); + expect(b.duplicate).toBe(true); + expect(b.id).toBe(a.id); + expect(await unprocessedCount(brainDir, slug)).toBe(1); + }); + + it('records an optional target_node and supports status transitions + count', async () => { + const { brainDir, slug } = await brain(); + const r = await captureItem({ brainDir, slug, kind: 'paste', source: 'paste', + content: 'evidence', targetNode: 'auth-design', now: NOW }); + expect((await listItems(brainDir, slug))[0].target_node).toBe('auth-design'); + expect(await unprocessedCount(brainDir, slug)).toBe(1); + expect(await setStatus(brainDir, slug, r.id, 'discarded')).toBe(true); + expect(await unprocessedCount(brainDir, slug)).toBe(0); + expect((await listItems(brainDir, slug))[0].status).toBe('discarded'); + }); + + it('rejects an unsafe slug', async () => { + const { brainDir } = await brain(); + await expect(captureItem({ brainDir, slug: '../escape', kind: 'paste', source: 'paste', + content: 'x', now: NOW })).rejects.toThrow(); + }); + + it('flags a malformed item (missing frontmatter) without throwing', async () => { + const { brainDir, slug } = await brain(); + await fs.mkdir(rawDir(brainDir, slug), { recursive: true }); + await fs.writeFile(join(rawDir(brainDir, slug), 'broken.md'), 'no frontmatter here'); + const items = await listItems(brainDir, slug); + const broken = items.find(i => i.id === 'broken')!; + expect(broken.malformed).toBe(true); + expect(await unprocessedCount(brainDir, slug)).toBe(1); + }); +}); diff --git a/mcp/src/tools/raw-inbox.ts b/mcp/src/tools/raw-inbox.ts new file mode 100644 index 0000000..ee30280 --- /dev/null +++ b/mcp/src/tools/raw-inbox.ts @@ -0,0 +1,230 @@ +import { promises as fs } from 'fs'; +import { join, basename, extname } from 'path'; +import { hashContent, assertSafeSlug } from './doc-sources.js'; + +export type RawStatus = 'unprocessed' | 'processed' | 'discarded'; +export type CapturedBy = 'user' | 'setup-scan' | 'dream'; + +export interface RawItem { + id: string; + source: string; + captured_at: string; + captured_by: CapturedBy; + content_type: string; + status: RawStatus; + target_node?: string; + blob?: string; + hash: string; + gist: string; + body: string; + malformed?: boolean; +} + +export interface CaptureInput { + brainDir: string; + slug: string; + source: string; // file path, url, or 'paste' + kind: 'file' | 'url' | 'paste'; + content?: string; // paste text / url string; for files it is read from disk + targetNode?: string; + capturedBy?: CapturedBy; + now?: string; // ISO timestamp; injectable for deterministic tests +} + +export function rawDir(brainDir: string, slug: string): string { + return join(brainDir, 'projects', slug, 'raw'); +} + +/** kebab slug from arbitrary text: lowercase, non-alnum→'-', collapse, trim, cap length. */ +function slugify(text: string): string { + const s = text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40); + return s || 'item'; +} + +/** ISO '2026-06-03T14:15:00Z' → compact sortable '20260603T141500Z'. */ +function compactStamp(iso: string): string { + return iso.replace(/[-:]/g, '').replace(/\.\d+Z$/, 'Z'); +} + +function isBinary(buf: Buffer): boolean { + const n = Math.min(buf.length, 8192); + for (let i = 0; i < n; i++) if (buf[i] === 0) return true; + return false; +} + +function contentTypeForFile(path: string, binary: boolean): string { + const ext = extname(path).toLowerCase(); + if (binary) return ext === '.pdf' ? 'application/pdf' : 'application/octet-stream'; + return ext === '.md' || ext === '.markdown' ? 'text/markdown' : 'text/plain'; +} + +function serialize(item: RawItem): string { + const fm: string[] = ['---']; + fm.push(`id: ${item.id}`); + fm.push(`source: ${item.source}`); + fm.push(`captured_at: ${item.captured_at}`); + fm.push(`captured_by: ${item.captured_by}`); + fm.push(`content_type: ${item.content_type}`); + fm.push(`status: ${item.status}`); + if (item.target_node) fm.push(`target_node: ${item.target_node}`); + if (item.blob) fm.push(`blob: ${item.blob}`); + fm.push(`hash: ${item.hash}`); + fm.push(`gist: ${item.gist.replace(/\n/g, ' ')}`); + fm.push('---', '', item.body, ''); + return fm.join('\n'); +} + +/** Tolerant flat-frontmatter parse. id comes from the filename (authoritative). */ +function parse(content: string, id: string): RawItem { + const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/); + const base: RawItem = { + id, source: '', captured_at: '', captured_by: 'user', content_type: '', + status: 'unprocessed', hash: '', gist: '', body: '', + }; + if (!m) { return { ...base, body: content, malformed: true }; } + const [, fmText, body] = m; + const get = (k: string): string | undefined => { + const mm = fmText.match(new RegExp(`^${k}:[ \\t]*(.*)$`, 'm')); + return mm ? mm[1].trim() : undefined; + }; + const status = (get('status') ?? '') as RawStatus; + const validStatus = status === 'unprocessed' || status === 'processed' || status === 'discarded'; + const item: RawItem = { + id, + source: get('source') ?? '', + captured_at: get('captured_at') ?? '', + captured_by: (get('captured_by') as CapturedBy) ?? 'user', + content_type: get('content_type') ?? '', + status: validStatus ? status : 'unprocessed', + target_node: get('target_node') || undefined, + blob: get('blob') || undefined, + hash: get('hash') ?? '', + gist: get('gist') ?? '', + body: body.trim(), + }; + // Malformed = missing the fields a well-formed capture always writes, or a bad status. + if (!item.source || !item.captured_at || !item.content_type || !validStatus) item.malformed = true; + return item; +} + +async function readItems(brainDir: string, slug: string): Promise { + const dir = rawDir(brainDir, slug); + let names: string[] = []; + try { names = (await fs.readdir(dir)).filter(n => n.endsWith('.md')); } catch { return []; } + const items: RawItem[] = []; + for (const name of names.sort()) { + try { + const content = await fs.readFile(join(dir, name), 'utf-8'); + items.push(parse(content, name.replace(/\.md$/, ''))); + } catch { /* skip unreadable */ } + } + return items; +} + +export async function listItems(brainDir: string, slug: string): Promise { + assertSafeSlug(slug); + return readItems(brainDir, slug); +} + +export async function unprocessedCount(brainDir: string, slug: string): Promise { + assertSafeSlug(slug); + const items = await readItems(brainDir, slug); + // malformed items count as unprocessed so they stay visible in the backlog. + return items.filter(i => i.status === 'unprocessed' || i.malformed).length; +} + +export async function setStatus(brainDir: string, slug: string, id: string, status: RawStatus): Promise { + assertSafeSlug(slug); + const file = join(rawDir(brainDir, slug), `${id}.md`); + let content: string; + try { content = await fs.readFile(file, 'utf-8'); } catch { return false; } + const next = /^status:[ \t]*.*$/m.test(content) + ? content.replace(/^status:[ \t]*.*$/m, `status: ${status}`) + : content.replace(/^---\r?\n/, `---\nstatus: ${status}\n`); + const tmp = `${file}.tmp`; + await fs.writeFile(tmp, next); + await fs.rename(tmp, file); // atomic + return true; +} + +export async function captureItem(input: CaptureInput): Promise<{ id: string; duplicate: boolean; unprocessed: number }> { + assertSafeSlug(input.slug); + const dir = rawDir(input.brainDir, input.slug); + const now = input.now ?? new Date().toISOString(); + const capturedBy = input.capturedBy ?? 'user'; + + // Resolve content + blob + content_type by kind. + let body = ''; + let blobBuf: Buffer | undefined; + let blobExt = ''; + let contentType = ''; + let gistSeed = ''; + let hashInput = ''; + + if (input.kind === 'paste') { + body = input.content ?? ''; + contentType = 'text/markdown'; + gistSeed = body; + hashInput = `paste:${body}`; + } else if (input.kind === 'url') { + const url = input.content ?? input.source; + body = url; + contentType = 'text/uri-list'; + gistSeed = url; + hashInput = `url:${url}`; + } else { + const buf = await fs.readFile(input.source); + const binary = isBinary(buf); + contentType = contentTypeForFile(input.source, binary); + hashInput = `file:${hashContent(buf.toString('binary'))}`; + if (binary) { + blobBuf = buf; + blobExt = extname(input.source) || '.bin'; + gistSeed = basename(input.source); + body = `(binary ${contentType}; original captured as the sibling blob) — ${basename(input.source)}`; + } else { + body = buf.toString('utf-8'); + gistSeed = body; + } + } + + const hash = hashContent(hashInput); + + // Dedup: an existing UNPROCESSED item with the same hash → return it. + const existing = (await readItems(input.brainDir, input.slug)) + .find(i => i.hash === hash && i.status === 'unprocessed'); + if (existing) { + return { id: existing.id, duplicate: true, unprocessed: await unprocessedCount(input.brainDir, input.slug) }; + } + + // Unique id (collision suffix). + await fs.mkdir(dir, { recursive: true }); + const sourceSlug = input.kind === 'url' ? slugify(input.content ?? input.source) + : input.kind === 'file' ? slugify(basename(input.source)) + : slugify(body); + const baseId = `${compactStamp(now)}-${sourceSlug}`; + let id = baseId; + for (let n = 2; ; n++) { + try { await fs.access(join(dir, `${id}.md`)); id = `${baseId}-${n}`; } catch { break; } + } + + const blob = blobBuf ? `${id}${blobExt}` : undefined; + if (blobBuf && blob) { + const btmp = join(dir, `${blob}.tmp`); + await fs.writeFile(btmp, blobBuf); + await fs.rename(btmp, join(dir, blob)); + } + + const gist = gistSeed.replace(/^#\s*/, '').split('\n').map(l => l.trim()).find(Boolean)?.slice(0, 120) ?? ''; + const item: RawItem = { + id, source: input.source, captured_at: now, captured_by: capturedBy, + content_type: contentType, status: 'unprocessed', + target_node: input.targetNode, blob, hash, gist, body, + }; + const file = join(dir, `${id}.md`); + const tmp = `${file}.tmp`; + await fs.writeFile(tmp, serialize(item)); + await fs.rename(tmp, file); + + return { id, duplicate: false, unprocessed: await unprocessedCount(input.brainDir, input.slug) }; +} From ee7e4ba047a190e50165d6b724c726df471ebaee Mon Sep 17 00:00:00 2001 From: Cain-Ish Date: Wed, 3 Jun 2026 19:13:51 +0200 Subject: [PATCH 5/9] feat(kb): raw-capture-cli bundle (SP-2 Task 3) Co-Authored-By: Claude Opus 4.8 (1M context) --- mcp/dist/tools/doc-sources.d.ts | 1 + mcp/dist/tools/doc-sources.d.ts.map | 2 +- mcp/dist/tools/doc-sources.js | 2 +- mcp/dist/tools/doc-sources.js.map | 2 +- mcp/dist/tools/raw-capture-cli.bundle.js | 6360 ++++++++++++++++++++++ mcp/dist/tools/raw-capture-cli.d.ts | 2 + mcp/dist/tools/raw-capture-cli.d.ts.map | 1 + mcp/dist/tools/raw-capture-cli.js | 94 + mcp/dist/tools/raw-capture-cli.js.map | 1 + mcp/dist/tools/raw-inbox.d.ts | 36 + mcp/dist/tools/raw-inbox.d.ts.map | 1 + mcp/dist/tools/raw-inbox.js | 210 + mcp/dist/tools/raw-inbox.js.map | 1 + mcp/dist/tools/raw-inbox.test.d.ts | 2 + mcp/dist/tools/raw-inbox.test.d.ts.map | 1 + mcp/dist/tools/raw-inbox.test.js | 86 + mcp/dist/tools/raw-inbox.test.js.map | 1 + mcp/package.json | 2 +- mcp/src/tools/raw-capture-cli.ts | 68 + 19 files changed, 6869 insertions(+), 4 deletions(-) create mode 100644 mcp/dist/tools/raw-capture-cli.bundle.js create mode 100644 mcp/dist/tools/raw-capture-cli.d.ts create mode 100644 mcp/dist/tools/raw-capture-cli.d.ts.map create mode 100644 mcp/dist/tools/raw-capture-cli.js create mode 100644 mcp/dist/tools/raw-capture-cli.js.map create mode 100644 mcp/dist/tools/raw-inbox.d.ts create mode 100644 mcp/dist/tools/raw-inbox.d.ts.map create mode 100644 mcp/dist/tools/raw-inbox.js create mode 100644 mcp/dist/tools/raw-inbox.js.map create mode 100644 mcp/dist/tools/raw-inbox.test.d.ts create mode 100644 mcp/dist/tools/raw-inbox.test.d.ts.map create mode 100644 mcp/dist/tools/raw-inbox.test.js create mode 100644 mcp/dist/tools/raw-inbox.test.js.map create mode 100644 mcp/src/tools/raw-capture-cli.ts diff --git a/mcp/dist/tools/doc-sources.d.ts b/mcp/dist/tools/doc-sources.d.ts index ca0d073..b69d278 100644 --- a/mcp/dist/tools/doc-sources.d.ts +++ b/mcp/dist/tools/doc-sources.d.ts @@ -16,6 +16,7 @@ export interface DocEntry { mtime: string; size: number; } +export declare function assertSafeSlug(slug: string): void; export declare function readConfig(brainDir: string, slug: string): Promise; export declare function scanLocations(projectRoot: string, locations: string[]): Promise; export interface DocRegistry { diff --git a/mcp/dist/tools/doc-sources.d.ts.map b/mcp/dist/tools/doc-sources.d.ts.map index 74b3180..86c544e 100644 --- a/mcp/dist/tools/doc-sources.d.ts.map +++ b/mcp/dist/tools/doc-sources.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"doc-sources.d.ts","sourceRoot":"","sources":["../../src/tools/doc-sources.ts"],"names":[],"mappings":"AAMA,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,yFAAyF;AACzF,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAWnD;AAED,wDAAwD;AACxD,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAEzD;AAED,MAAM,WAAW,eAAe;IAAG,SAAS,EAAE,MAAM,EAAE,CAAC;CAAE;AACzD,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IACpD,QAAQ,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;CAC/D;AAUD,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAKzF;AAgBD,wBAAsB,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CA6BjG;AAED,MAAM,WAAW,WAAW;IAAG,YAAY,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,QAAQ,EAAE,CAAC;CAAE;AAM5F,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAG9F;AAED;;qDAEqD;AACrD,wBAAsB,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAW7G;AAgBD,wBAAsB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAGrF;AAED,wBAAsB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,SAAS,EAAE,MAAM,EAAE,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC,CAWpI;AAED,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,SAAS,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAQzI"} \ No newline at end of file +{"version":3,"file":"doc-sources.d.ts","sourceRoot":"","sources":["../../src/tools/doc-sources.ts"],"names":[],"mappings":"AAMA,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,yFAAyF;AACzF,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAWnD;AAED,wDAAwD;AACxD,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAEzD;AAED,MAAM,WAAW,eAAe;IAAG,SAAS,EAAE,MAAM,EAAE,CAAC;CAAE;AACzD,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IACpD,QAAQ,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;CAC/D;AAID,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAIjD;AAED,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAKzF;AAgBD,wBAAsB,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CA6BjG;AAED,MAAM,WAAW,WAAW;IAAG,YAAY,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,QAAQ,EAAE,CAAC;CAAE;AAM5F,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAG9F;AAED;;qDAEqD;AACrD,wBAAsB,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAW7G;AAgBD,wBAAsB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAGrF;AAED,wBAAsB,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,SAAS,EAAE,MAAM,EAAE,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAAC,CAWpI;AAED,wBAAsB,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,SAAS,EAAE,MAAM,EAAE,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAC,CAQzI"} \ No newline at end of file diff --git a/mcp/dist/tools/doc-sources.js b/mcp/dist/tools/doc-sources.js index debc24e..cfa3eda 100644 --- a/mcp/dist/tools/doc-sources.js +++ b/mcp/dist/tools/doc-sources.js @@ -26,7 +26,7 @@ export function extractHeadings(content) { return content.split('\n').map((l) => l.trim()).filter((l) => /^#{2,3}\s+\S/.test(l)); } const JUNK_DIRS = new Set(['node_modules', '.git', '.venv', 'venv', '.next', 'dist', 'build']); -function assertSafeSlug(slug) { +export function assertSafeSlug(slug) { if (!slug || /[\\/]|\.\./.test(slug)) { throw new Error(`unsafe slug: ${JSON.stringify(slug)}`); } diff --git a/mcp/dist/tools/doc-sources.js.map b/mcp/dist/tools/doc-sources.js.map index 194fdf8..0cc6928 100644 --- a/mcp/dist/tools/doc-sources.js.map +++ b/mcp/dist/tools/doc-sources.js.map @@ -1 +1 @@ -{"version":3,"file":"doc-sources.js","sourceRoot":"","sources":["../../src/tools/doc-sources.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"doc-sources.js","sourceRoot":"","sources":["../../src/tools/doc-sources.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/mcp/dist/tools/raw-capture-cli.bundle.js b/mcp/dist/tools/raw-capture-cli.bundle.js new file mode 100644 index 0000000..605fa71 --- /dev/null +++ b/mcp/dist/tools/raw-capture-cli.bundle.js @@ -0,0 +1,6360 @@ +// src/tools/raw-capture-cli.ts +import { homedir } from "os"; +import { join as join2, basename as basename2 } from "path"; +import { existsSync, readFileSync, statSync } from "fs"; + +// src/tools/raw-inbox.ts +import { promises as fs } from "fs"; +import { join, basename, extname } from "path"; + +// src/tools/doc-sources.ts +import { createHash } from "crypto"; + +// node_modules/balanced-match/dist/esm/index.js +var balanced = (a, b, str) => { + const ma = a instanceof RegExp ? maybeMatch(a, str) : a; + const mb = b instanceof RegExp ? maybeMatch(b, str) : b; + const r = ma !== null && mb != null && range(ma, mb, str); + return r && { + start: r[0], + end: r[1], + pre: str.slice(0, r[0]), + body: str.slice(r[0] + ma.length, r[1]), + post: str.slice(r[1] + mb.length) + }; +}; +var maybeMatch = (reg, str) => { + const m = str.match(reg); + return m ? m[0] : null; +}; +var range = (a, b, str) => { + let begs, beg, left, right = void 0, result; + let ai = str.indexOf(a); + let bi = str.indexOf(b, ai + 1); + let i = ai; + if (ai >= 0 && bi > 0) { + if (a === b) { + return [ai, bi]; + } + begs = []; + left = str.length; + while (i >= 0 && !result) { + if (i === ai) { + begs.push(i); + ai = str.indexOf(a, i + 1); + } else if (begs.length === 1) { + const r = begs.pop(); + if (r !== void 0) + result = [r, bi]; + } else { + beg = begs.pop(); + if (beg !== void 0 && beg < left) { + left = beg; + right = bi; + } + bi = str.indexOf(b, i + 1); + } + i = ai < bi && ai >= 0 ? ai : bi; + } + if (begs.length && right !== void 0) { + result = [left, right]; + } + } + return result; +}; + +// node_modules/brace-expansion/dist/esm/index.js +var escSlash = "\0SLASH" + Math.random() + "\0"; +var escOpen = "\0OPEN" + Math.random() + "\0"; +var escClose = "\0CLOSE" + Math.random() + "\0"; +var escComma = "\0COMMA" + Math.random() + "\0"; +var escPeriod = "\0PERIOD" + Math.random() + "\0"; +var escSlashPattern = new RegExp(escSlash, "g"); +var escOpenPattern = new RegExp(escOpen, "g"); +var escClosePattern = new RegExp(escClose, "g"); +var escCommaPattern = new RegExp(escComma, "g"); +var escPeriodPattern = new RegExp(escPeriod, "g"); +var slashPattern = /\\\\/g; +var openPattern = /\\{/g; +var closePattern = /\\}/g; +var commaPattern = /\\,/g; +var periodPattern = /\\\./g; +var EXPANSION_MAX = 1e5; +function numeric(str) { + return !isNaN(str) ? parseInt(str, 10) : str.charCodeAt(0); +} +function escapeBraces(str) { + return str.replace(slashPattern, escSlash).replace(openPattern, escOpen).replace(closePattern, escClose).replace(commaPattern, escComma).replace(periodPattern, escPeriod); +} +function unescapeBraces(str) { + return str.replace(escSlashPattern, "\\").replace(escOpenPattern, "{").replace(escClosePattern, "}").replace(escCommaPattern, ",").replace(escPeriodPattern, "."); +} +function parseCommaParts(str) { + if (!str) { + return [""]; + } + const parts = []; + const m = balanced("{", "}", str); + if (!m) { + return str.split(","); + } + const { pre, body, post } = m; + const p = pre.split(","); + p[p.length - 1] += "{" + body + "}"; + const postParts = parseCommaParts(post); + if (post.length) { + ; + p[p.length - 1] += postParts.shift(); + p.push.apply(p, postParts); + } + parts.push.apply(parts, p); + return parts; +} +function expand(str, options = {}) { + if (!str) { + return []; + } + const { max = EXPANSION_MAX } = options; + if (str.slice(0, 2) === "{}") { + str = "\\{\\}" + str.slice(2); + } + return expand_(escapeBraces(str), max, true).map(unescapeBraces); +} +function embrace(str) { + return "{" + str + "}"; +} +function isPadded(el) { + return /^-?0\d/.test(el); +} +function lte(i, y) { + return i <= y; +} +function gte(i, y) { + return i >= y; +} +function expand_(str, max, isTop) { + const expansions = []; + const m = balanced("{", "}", str); + if (!m) + return [str]; + const pre = m.pre; + const post = m.post.length ? expand_(m.post, max, false) : [""]; + if (/\$$/.test(m.pre)) { + for (let k = 0; k < post.length && k < max; k++) { + const expansion = pre + "{" + m.body + "}" + post[k]; + expansions.push(expansion); + } + } else { + const isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body); + const isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body); + const isSequence = isNumericSequence || isAlphaSequence; + const isOptions = m.body.indexOf(",") >= 0; + if (!isSequence && !isOptions) { + if (m.post.match(/,(?!,).*\}/)) { + str = m.pre + "{" + m.body + escClose + m.post; + return expand_(str, max, true); + } + return [str]; + } + let n; + if (isSequence) { + n = m.body.split(/\.\./); + } else { + n = parseCommaParts(m.body); + if (n.length === 1 && n[0] !== void 0) { + n = expand_(n[0], max, false).map(embrace); + if (n.length === 1) { + return post.map((p) => m.pre + n[0] + p); + } + } + } + let N; + if (isSequence && n[0] !== void 0 && n[1] !== void 0) { + const x = numeric(n[0]); + const y = numeric(n[1]); + const width = Math.max(n[0].length, n[1].length); + let incr = n.length === 3 && n[2] !== void 0 ? Math.max(Math.abs(numeric(n[2])), 1) : 1; + let test = lte; + const reverse = y < x; + if (reverse) { + incr *= -1; + test = gte; + } + const pad = n.some(isPadded); + N = []; + for (let i = x; test(i, y); i += incr) { + let c; + if (isAlphaSequence) { + c = String.fromCharCode(i); + if (c === "\\") { + c = ""; + } + } else { + c = String(i); + if (pad) { + const need = width - c.length; + if (need > 0) { + const z = new Array(need + 1).join("0"); + if (i < 0) { + c = "-" + z + c.slice(1); + } else { + c = z + c; + } + } + } + } + N.push(c); + } + } else { + N = []; + for (let j2 = 0; j2 < n.length; j2++) { + N.push.apply(N, expand_(n[j2], max, false)); + } + } + for (let j2 = 0; j2 < N.length; j2++) { + for (let k = 0; k < post.length && expansions.length < max; k++) { + const expansion = pre + N[j2] + post[k]; + if (!isTop || isSequence || expansion) { + expansions.push(expansion); + } + } + } + } + return expansions; +} + +// node_modules/minimatch/dist/esm/assert-valid-pattern.js +var MAX_PATTERN_LENGTH = 1024 * 64; +var assertValidPattern = (pattern) => { + if (typeof pattern !== "string") { + throw new TypeError("invalid pattern"); + } + if (pattern.length > MAX_PATTERN_LENGTH) { + throw new TypeError("pattern is too long"); + } +}; + +// node_modules/minimatch/dist/esm/brace-expressions.js +var posixClasses = { + "[:alnum:]": ["\\p{L}\\p{Nl}\\p{Nd}", true], + "[:alpha:]": ["\\p{L}\\p{Nl}", true], + "[:ascii:]": ["\\x00-\\x7f", false], + "[:blank:]": ["\\p{Zs}\\t", true], + "[:cntrl:]": ["\\p{Cc}", true], + "[:digit:]": ["\\p{Nd}", true], + "[:graph:]": ["\\p{Z}\\p{C}", true, true], + "[:lower:]": ["\\p{Ll}", true], + "[:print:]": ["\\p{C}", true], + "[:punct:]": ["\\p{P}", true], + "[:space:]": ["\\p{Z}\\t\\r\\n\\v\\f", true], + "[:upper:]": ["\\p{Lu}", true], + "[:word:]": ["\\p{L}\\p{Nl}\\p{Nd}\\p{Pc}", true], + "[:xdigit:]": ["A-Fa-f0-9", false] +}; +var braceEscape = (s) => s.replace(/[[\]\\-]/g, "\\$&"); +var regexpEscape = (s) => s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); +var rangesToString = (ranges) => ranges.join(""); +var parseClass = (glob2, position) => { + const pos = position; + if (glob2.charAt(pos) !== "[") { + throw new Error("not in a brace expression"); + } + const ranges = []; + const negs = []; + let i = pos + 1; + let sawStart = false; + let uflag = false; + let escaping = false; + let negate = false; + let endPos = pos; + let rangeStart = ""; + WHILE: while (i < glob2.length) { + const c = glob2.charAt(i); + if ((c === "!" || c === "^") && i === pos + 1) { + negate = true; + i++; + continue; + } + if (c === "]" && sawStart && !escaping) { + endPos = i + 1; + break; + } + sawStart = true; + if (c === "\\") { + if (!escaping) { + escaping = true; + i++; + continue; + } + } + if (c === "[" && !escaping) { + for (const [cls, [unip, u3, neg]] of Object.entries(posixClasses)) { + if (glob2.startsWith(cls, i)) { + if (rangeStart) { + return ["$.", false, glob2.length - pos, true]; + } + i += cls.length; + if (neg) + negs.push(unip); + else + ranges.push(unip); + uflag = uflag || u3; + continue WHILE; + } + } + } + escaping = false; + if (rangeStart) { + if (c > rangeStart) { + ranges.push(braceEscape(rangeStart) + "-" + braceEscape(c)); + } else if (c === rangeStart) { + ranges.push(braceEscape(c)); + } + rangeStart = ""; + i++; + continue; + } + if (glob2.startsWith("-]", i + 1)) { + ranges.push(braceEscape(c + "-")); + i += 2; + continue; + } + if (glob2.startsWith("-", i + 1)) { + rangeStart = c; + i += 2; + continue; + } + ranges.push(braceEscape(c)); + i++; + } + if (endPos < i) { + return ["", false, 0, false]; + } + if (!ranges.length && !negs.length) { + return ["$.", false, glob2.length - pos, true]; + } + if (negs.length === 0 && ranges.length === 1 && /^\\?.$/.test(ranges[0]) && !negate) { + const r = ranges[0].length === 2 ? ranges[0].slice(-1) : ranges[0]; + return [regexpEscape(r), false, endPos - pos, false]; + } + const sranges = "[" + (negate ? "^" : "") + rangesToString(ranges) + "]"; + const snegs = "[" + (negate ? "" : "^") + rangesToString(negs) + "]"; + const comb = ranges.length && negs.length ? "(" + sranges + "|" + snegs + ")" : ranges.length ? sranges : snegs; + return [comb, uflag, endPos - pos, true]; +}; + +// node_modules/minimatch/dist/esm/unescape.js +var unescape = (s, { windowsPathsNoEscape = false, magicalBraces = true } = {}) => { + if (magicalBraces) { + return windowsPathsNoEscape ? s.replace(/\[([^/\\])\]/g, "$1") : s.replace(/((?!\\).|^)\[([^/\\])\]/g, "$1$2").replace(/\\([^/])/g, "$1"); + } + return windowsPathsNoEscape ? s.replace(/\[([^/\\{}])\]/g, "$1") : s.replace(/((?!\\).|^)\[([^/\\{}])\]/g, "$1$2").replace(/\\([^/{}])/g, "$1"); +}; + +// node_modules/minimatch/dist/esm/ast.js +var _a; +var types = /* @__PURE__ */ new Set(["!", "?", "+", "*", "@"]); +var isExtglobType = (c) => types.has(c); +var isExtglobAST = (c) => isExtglobType(c.type); +var adoptionMap = /* @__PURE__ */ new Map([ + ["!", ["@"]], + ["?", ["?", "@"]], + ["@", ["@"]], + ["*", ["*", "+", "?", "@"]], + ["+", ["+", "@"]] +]); +var adoptionWithSpaceMap = /* @__PURE__ */ new Map([ + ["!", ["?"]], + ["@", ["?"]], + ["+", ["?", "*"]] +]); +var adoptionAnyMap = /* @__PURE__ */ new Map([ + ["!", ["?", "@"]], + ["?", ["?", "@"]], + ["@", ["?", "@"]], + ["*", ["*", "+", "?", "@"]], + ["+", ["+", "@", "?", "*"]] +]); +var usurpMap = /* @__PURE__ */ new Map([ + ["!", /* @__PURE__ */ new Map([["!", "@"]])], + [ + "?", + /* @__PURE__ */ new Map([ + ["*", "*"], + ["+", "*"] + ]) + ], + [ + "@", + /* @__PURE__ */ new Map([ + ["!", "!"], + ["?", "?"], + ["@", "@"], + ["*", "*"], + ["+", "+"] + ]) + ], + [ + "+", + /* @__PURE__ */ new Map([ + ["?", "*"], + ["*", "*"] + ]) + ] +]); +var startNoTraversal = "(?!(?:^|/)\\.\\.?(?:$|/))"; +var startNoDot = "(?!\\.)"; +var addPatternStart = /* @__PURE__ */ new Set(["[", "."]); +var justDots = /* @__PURE__ */ new Set(["..", "."]); +var reSpecials = new Set("().*{}+?[]^$\\!"); +var regExpEscape = (s) => s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); +var qmark = "[^/]"; +var star = qmark + "*?"; +var starNoEmpty = qmark + "+?"; +var ID = 0; +var AST = class { + type; + #root; + #hasMagic; + #uflag = false; + #parts = []; + #parent; + #parentIndex; + #negs; + #filledNegs = false; + #options; + #toString; + // set to true if it's an extglob with no children + // (which really means one child of '') + #emptyExt = false; + id = ++ID; + get depth() { + return (this.#parent?.depth ?? -1) + 1; + } + [/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() { + return { + "@@type": "AST", + id: this.id, + type: this.type, + root: this.#root.id, + parent: this.#parent?.id, + depth: this.depth, + partsLength: this.#parts.length, + parts: this.#parts + }; + } + constructor(type, parent, options = {}) { + this.type = type; + if (type) + this.#hasMagic = true; + this.#parent = parent; + this.#root = this.#parent ? this.#parent.#root : this; + this.#options = this.#root === this ? options : this.#root.#options; + this.#negs = this.#root === this ? [] : this.#root.#negs; + if (type === "!" && !this.#root.#filledNegs) + this.#negs.push(this); + this.#parentIndex = this.#parent ? this.#parent.#parts.length : 0; + } + get hasMagic() { + if (this.#hasMagic !== void 0) + return this.#hasMagic; + for (const p of this.#parts) { + if (typeof p === "string") + continue; + if (p.type || p.hasMagic) + return this.#hasMagic = true; + } + return this.#hasMagic; + } + // reconstructs the pattern + toString() { + return this.#toString !== void 0 ? this.#toString : !this.type ? this.#toString = this.#parts.map((p) => String(p)).join("") : this.#toString = this.type + "(" + this.#parts.map((p) => String(p)).join("|") + ")"; + } + #fillNegs() { + if (this !== this.#root) + throw new Error("should only call on root"); + if (this.#filledNegs) + return this; + this.toString(); + this.#filledNegs = true; + let n; + while (n = this.#negs.pop()) { + if (n.type !== "!") + continue; + let p = n; + let pp = p.#parent; + while (pp) { + for (let i = p.#parentIndex + 1; !pp.type && i < pp.#parts.length; i++) { + for (const part of n.#parts) { + if (typeof part === "string") { + throw new Error("string part in extglob AST??"); + } + part.copyIn(pp.#parts[i]); + } + } + p = pp; + pp = p.#parent; + } + } + return this; + } + push(...parts) { + for (const p of parts) { + if (p === "") + continue; + if (typeof p !== "string" && !(p instanceof _a && p.#parent === this)) { + throw new Error("invalid part: " + p); + } + this.#parts.push(p); + } + } + toJSON() { + const ret = this.type === null ? this.#parts.slice().map((p) => typeof p === "string" ? p : p.toJSON()) : [this.type, ...this.#parts.map((p) => p.toJSON())]; + if (this.isStart() && !this.type) + ret.unshift([]); + if (this.isEnd() && (this === this.#root || this.#root.#filledNegs && this.#parent?.type === "!")) { + ret.push({}); + } + return ret; + } + isStart() { + if (this.#root === this) + return true; + if (!this.#parent?.isStart()) + return false; + if (this.#parentIndex === 0) + return true; + const p = this.#parent; + for (let i = 0; i < this.#parentIndex; i++) { + const pp = p.#parts[i]; + if (!(pp instanceof _a && pp.type === "!")) { + return false; + } + } + return true; + } + isEnd() { + if (this.#root === this) + return true; + if (this.#parent?.type === "!") + return true; + if (!this.#parent?.isEnd()) + return false; + if (!this.type) + return this.#parent?.isEnd(); + const pl = this.#parent ? this.#parent.#parts.length : 0; + return this.#parentIndex === pl - 1; + } + copyIn(part) { + if (typeof part === "string") + this.push(part); + else + this.push(part.clone(this)); + } + clone(parent) { + const c = new _a(this.type, parent); + for (const p of this.#parts) { + c.copyIn(p); + } + return c; + } + static #parseAST(str, ast, pos, opt, extDepth) { + const maxDepth = opt.maxExtglobRecursion ?? 2; + let escaping = false; + let inBrace = false; + let braceStart = -1; + let braceNeg = false; + if (ast.type === null) { + let i2 = pos; + let acc2 = ""; + while (i2 < str.length) { + const c = str.charAt(i2++); + if (escaping || c === "\\") { + escaping = !escaping; + acc2 += c; + continue; + } + if (inBrace) { + if (i2 === braceStart + 1) { + if (c === "^" || c === "!") { + braceNeg = true; + } + } else if (c === "]" && !(i2 === braceStart + 2 && braceNeg)) { + inBrace = false; + } + acc2 += c; + continue; + } else if (c === "[") { + inBrace = true; + braceStart = i2; + braceNeg = false; + acc2 += c; + continue; + } + const doRecurse = !opt.noext && isExtglobType(c) && str.charAt(i2) === "(" && extDepth <= maxDepth; + if (doRecurse) { + ast.push(acc2); + acc2 = ""; + const ext2 = new _a(c, ast); + i2 = _a.#parseAST(str, ext2, i2, opt, extDepth + 1); + ast.push(ext2); + continue; + } + acc2 += c; + } + ast.push(acc2); + return i2; + } + let i = pos + 1; + let part = new _a(null, ast); + const parts = []; + let acc = ""; + while (i < str.length) { + const c = str.charAt(i++); + if (escaping || c === "\\") { + escaping = !escaping; + acc += c; + continue; + } + if (inBrace) { + if (i === braceStart + 1) { + if (c === "^" || c === "!") { + braceNeg = true; + } + } else if (c === "]" && !(i === braceStart + 2 && braceNeg)) { + inBrace = false; + } + acc += c; + continue; + } else if (c === "[") { + inBrace = true; + braceStart = i; + braceNeg = false; + acc += c; + continue; + } + const doRecurse = !opt.noext && isExtglobType(c) && str.charAt(i) === "(" && /* c8 ignore start - the maxDepth is sufficient here */ + (extDepth <= maxDepth || ast && ast.#canAdoptType(c)); + if (doRecurse) { + const depthAdd = ast && ast.#canAdoptType(c) ? 0 : 1; + part.push(acc); + acc = ""; + const ext2 = new _a(c, part); + part.push(ext2); + i = _a.#parseAST(str, ext2, i, opt, extDepth + depthAdd); + continue; + } + if (c === "|") { + part.push(acc); + acc = ""; + parts.push(part); + part = new _a(null, ast); + continue; + } + if (c === ")") { + if (acc === "" && ast.#parts.length === 0) { + ast.#emptyExt = true; + } + part.push(acc); + acc = ""; + ast.push(...parts, part); + return i; + } + acc += c; + } + ast.type = null; + ast.#hasMagic = void 0; + ast.#parts = [str.substring(pos - 1)]; + return i; + } + #canAdoptWithSpace(child) { + return this.#canAdopt(child, adoptionWithSpaceMap); + } + #canAdopt(child, map = adoptionMap) { + if (!child || typeof child !== "object" || child.type !== null || child.#parts.length !== 1 || this.type === null) { + return false; + } + const gc = child.#parts[0]; + if (!gc || typeof gc !== "object" || gc.type === null) { + return false; + } + return this.#canAdoptType(gc.type, map); + } + #canAdoptType(c, map = adoptionAnyMap) { + return !!map.get(this.type)?.includes(c); + } + #adoptWithSpace(child, index) { + const gc = child.#parts[0]; + const blank = new _a(null, gc, this.options); + blank.#parts.push(""); + gc.push(blank); + this.#adopt(child, index); + } + #adopt(child, index) { + const gc = child.#parts[0]; + this.#parts.splice(index, 1, ...gc.#parts); + for (const p of gc.#parts) { + if (typeof p === "object") + p.#parent = this; + } + this.#toString = void 0; + } + #canUsurpType(c) { + const m = usurpMap.get(this.type); + return !!m?.has(c); + } + #canUsurp(child) { + if (!child || typeof child !== "object" || child.type !== null || child.#parts.length !== 1 || this.type === null || this.#parts.length !== 1) { + return false; + } + const gc = child.#parts[0]; + if (!gc || typeof gc !== "object" || gc.type === null) { + return false; + } + return this.#canUsurpType(gc.type); + } + #usurp(child) { + const m = usurpMap.get(this.type); + const gc = child.#parts[0]; + const nt = m?.get(gc.type); + if (!nt) + return false; + this.#parts = gc.#parts; + for (const p of this.#parts) { + if (typeof p === "object") { + p.#parent = this; + } + } + this.type = nt; + this.#toString = void 0; + this.#emptyExt = false; + } + static fromGlob(pattern, options = {}) { + const ast = new _a(null, void 0, options); + _a.#parseAST(pattern, ast, 0, options, 0); + return ast; + } + // returns the regular expression if there's magic, or the unescaped + // string if not. + toMMPattern() { + if (this !== this.#root) + return this.#root.toMMPattern(); + const glob2 = this.toString(); + const [re, body, hasMagic2, uflag] = this.toRegExpSource(); + const anyMagic = hasMagic2 || this.#hasMagic || this.#options.nocase && !this.#options.nocaseMagicOnly && glob2.toUpperCase() !== glob2.toLowerCase(); + if (!anyMagic) { + return body; + } + const flags = (this.#options.nocase ? "i" : "") + (uflag ? "u" : ""); + return Object.assign(new RegExp(`^${re}$`, flags), { + _src: re, + _glob: glob2 + }); + } + get options() { + return this.#options; + } + // returns the string match, the regexp source, whether there's magic + // in the regexp (so a regular expression is required) and whether or + // not the uflag is needed for the regular expression (for posix classes) + // TODO: instead of injecting the start/end at this point, just return + // the BODY of the regexp, along with the start/end portions suitable + // for binding the start/end in either a joined full-path makeRe context + // (where we bind to (^|/), or a standalone matchPart context (where + // we bind to ^, and not /). Otherwise slashes get duped! + // + // In part-matching mode, the start is: + // - if not isStart: nothing + // - if traversal possible, but not allowed: ^(?!\.\.?$) + // - if dots allowed or not possible: ^ + // - if dots possible and not allowed: ^(?!\.) + // end is: + // - if not isEnd(): nothing + // - else: $ + // + // In full-path matching mode, we put the slash at the START of the + // pattern, so start is: + // - if first pattern: same as part-matching mode + // - if not isStart(): nothing + // - if traversal possible, but not allowed: /(?!\.\.?(?:$|/)) + // - if dots allowed or not possible: / + // - if dots possible and not allowed: /(?!\.) + // end is: + // - if last pattern, same as part-matching mode + // - else nothing + // + // Always put the (?:$|/) on negated tails, though, because that has to be + // there to bind the end of the negated pattern portion, and it's easier to + // just stick it in now rather than try to inject it later in the middle of + // the pattern. + // + // We can just always return the same end, and leave it up to the caller + // to know whether it's going to be used joined or in parts. + // And, if the start is adjusted slightly, can do the same there: + // - if not isStart: nothing + // - if traversal possible, but not allowed: (?:/|^)(?!\.\.?$) + // - if dots allowed or not possible: (?:/|^) + // - if dots possible and not allowed: (?:/|^)(?!\.) + // + // But it's better to have a simpler binding without a conditional, for + // performance, so probably better to return both start options. + // + // Then the caller just ignores the end if it's not the first pattern, + // and the start always gets applied. + // + // But that's always going to be $ if it's the ending pattern, or nothing, + // so the caller can just attach $ at the end of the pattern when building. + // + // So the todo is: + // - better detect what kind of start is needed + // - return both flavors of starting pattern + // - attach $ at the end of the pattern when creating the actual RegExp + // + // Ah, but wait, no, that all only applies to the root when the first pattern + // is not an extglob. If the first pattern IS an extglob, then we need all + // that dot prevention biz to live in the extglob portions, because eg + // +(*|.x*) can match .xy but not .yx. + // + // So, return the two flavors if it's #root and the first child is not an + // AST, otherwise leave it to the child AST to handle it, and there, + // use the (?:^|/) style of start binding. + // + // Even simplified further: + // - Since the start for a join is eg /(?!\.) and the start for a part + // is ^(?!\.), we can just prepend (?!\.) to the pattern (either root + // or start or whatever) and prepend ^ or / at the Regexp construction. + toRegExpSource(allowDot) { + const dot = allowDot ?? !!this.#options.dot; + if (this.#root === this) { + this.#flatten(); + this.#fillNegs(); + } + if (!isExtglobAST(this)) { + const noEmpty = this.isStart() && this.isEnd() && !this.#parts.some((s) => typeof s !== "string"); + const src = this.#parts.map((p) => { + const [re, _, hasMagic2, uflag] = typeof p === "string" ? _a.#parseGlob(p, this.#hasMagic, noEmpty) : p.toRegExpSource(allowDot); + this.#hasMagic = this.#hasMagic || hasMagic2; + this.#uflag = this.#uflag || uflag; + return re; + }).join(""); + let start2 = ""; + if (this.isStart()) { + if (typeof this.#parts[0] === "string") { + const dotTravAllowed = this.#parts.length === 1 && justDots.has(this.#parts[0]); + if (!dotTravAllowed) { + const aps = addPatternStart; + const needNoTrav = ( + // dots are allowed, and the pattern starts with [ or . + dot && aps.has(src.charAt(0)) || // the pattern starts with \., and then [ or . + src.startsWith("\\.") && aps.has(src.charAt(2)) || // the pattern starts with \.\., and then [ or . + src.startsWith("\\.\\.") && aps.has(src.charAt(4)) + ); + const needNoDot = !dot && !allowDot && aps.has(src.charAt(0)); + start2 = needNoTrav ? startNoTraversal : needNoDot ? startNoDot : ""; + } + } + } + let end = ""; + if (this.isEnd() && this.#root.#filledNegs && this.#parent?.type === "!") { + end = "(?:$|\\/)"; + } + const final2 = start2 + src + end; + return [ + final2, + unescape(src), + this.#hasMagic = !!this.#hasMagic, + this.#uflag + ]; + } + const repeated = this.type === "*" || this.type === "+"; + const start = this.type === "!" ? "(?:(?!(?:" : "(?:"; + let body = this.#partsToRegExp(dot); + if (this.isStart() && this.isEnd() && !body && this.type !== "!") { + const s = this.toString(); + const me = this; + me.#parts = [s]; + me.type = null; + me.#hasMagic = void 0; + return [s, unescape(this.toString()), false, false]; + } + let bodyDotAllowed = !repeated || allowDot || dot || !startNoDot ? "" : this.#partsToRegExp(true); + if (bodyDotAllowed === body) { + bodyDotAllowed = ""; + } + if (bodyDotAllowed) { + body = `(?:${body})(?:${bodyDotAllowed})*?`; + } + let final = ""; + if (this.type === "!" && this.#emptyExt) { + final = (this.isStart() && !dot ? startNoDot : "") + starNoEmpty; + } else { + const close = this.type === "!" ? ( + // !() must match something,but !(x) can match '' + "))" + (this.isStart() && !dot && !allowDot ? startNoDot : "") + star + ")" + ) : this.type === "@" ? ")" : this.type === "?" ? ")?" : this.type === "+" && bodyDotAllowed ? ")" : this.type === "*" && bodyDotAllowed ? `)?` : `)${this.type}`; + final = start + body + close; + } + return [ + final, + unescape(body), + this.#hasMagic = !!this.#hasMagic, + this.#uflag + ]; + } + #flatten() { + if (!isExtglobAST(this)) { + for (const p of this.#parts) { + if (typeof p === "object") { + p.#flatten(); + } + } + } else { + let iterations = 0; + let done = false; + do { + done = true; + for (let i = 0; i < this.#parts.length; i++) { + const c = this.#parts[i]; + if (typeof c === "object") { + c.#flatten(); + if (this.#canAdopt(c)) { + done = false; + this.#adopt(c, i); + } else if (this.#canAdoptWithSpace(c)) { + done = false; + this.#adoptWithSpace(c, i); + } else if (this.#canUsurp(c)) { + done = false; + this.#usurp(c); + } + } + } + } while (!done && ++iterations < 10); + } + this.#toString = void 0; + } + #partsToRegExp(dot) { + return this.#parts.map((p) => { + if (typeof p === "string") { + throw new Error("string type in extglob ast??"); + } + const [re, _, _hasMagic, uflag] = p.toRegExpSource(dot); + this.#uflag = this.#uflag || uflag; + return re; + }).filter((p) => !(this.isStart() && this.isEnd()) || !!p).join("|"); + } + static #parseGlob(glob2, hasMagic2, noEmpty = false) { + let escaping = false; + let re = ""; + let uflag = false; + let inStar = false; + for (let i = 0; i < glob2.length; i++) { + const c = glob2.charAt(i); + if (escaping) { + escaping = false; + re += (reSpecials.has(c) ? "\\" : "") + c; + continue; + } + if (c === "*") { + if (inStar) + continue; + inStar = true; + re += noEmpty && /^[*]+$/.test(glob2) ? starNoEmpty : star; + hasMagic2 = true; + continue; + } else { + inStar = false; + } + if (c === "\\") { + if (i === glob2.length - 1) { + re += "\\\\"; + } else { + escaping = true; + } + continue; + } + if (c === "[") { + const [src, needUflag, consumed, magic] = parseClass(glob2, i); + if (consumed) { + re += src; + uflag = uflag || needUflag; + i += consumed - 1; + hasMagic2 = hasMagic2 || magic; + continue; + } + } + if (c === "?") { + re += qmark; + hasMagic2 = true; + continue; + } + re += regExpEscape(c); + } + return [re, unescape(glob2), !!hasMagic2, uflag]; + } +}; +_a = AST; + +// node_modules/minimatch/dist/esm/escape.js +var escape = (s, { windowsPathsNoEscape = false, magicalBraces = false } = {}) => { + if (magicalBraces) { + return windowsPathsNoEscape ? s.replace(/[?*()[\]{}]/g, "[$&]") : s.replace(/[?*()[\]\\{}]/g, "\\$&"); + } + return windowsPathsNoEscape ? s.replace(/[?*()[\]]/g, "[$&]") : s.replace(/[?*()[\]\\]/g, "\\$&"); +}; + +// node_modules/minimatch/dist/esm/index.js +var minimatch = (p, pattern, options = {}) => { + assertValidPattern(pattern); + if (!options.nocomment && pattern.charAt(0) === "#") { + return false; + } + return new Minimatch(pattern, options).match(p); +}; +var starDotExtRE = /^\*+([^+@!?*[(]*)$/; +var starDotExtTest = (ext2) => (f) => !f.startsWith(".") && f.endsWith(ext2); +var starDotExtTestDot = (ext2) => (f) => f.endsWith(ext2); +var starDotExtTestNocase = (ext2) => { + ext2 = ext2.toLowerCase(); + return (f) => !f.startsWith(".") && f.toLowerCase().endsWith(ext2); +}; +var starDotExtTestNocaseDot = (ext2) => { + ext2 = ext2.toLowerCase(); + return (f) => f.toLowerCase().endsWith(ext2); +}; +var starDotStarRE = /^\*+\.\*+$/; +var starDotStarTest = (f) => !f.startsWith(".") && f.includes("."); +var starDotStarTestDot = (f) => f !== "." && f !== ".." && f.includes("."); +var dotStarRE = /^\.\*+$/; +var dotStarTest = (f) => f !== "." && f !== ".." && f.startsWith("."); +var starRE = /^\*+$/; +var starTest = (f) => f.length !== 0 && !f.startsWith("."); +var starTestDot = (f) => f.length !== 0 && f !== "." && f !== ".."; +var qmarksRE = /^\?+([^+@!?*[(]*)?$/; +var qmarksTestNocase = ([$0, ext2 = ""]) => { + const noext = qmarksTestNoExt([$0]); + if (!ext2) + return noext; + ext2 = ext2.toLowerCase(); + return (f) => noext(f) && f.toLowerCase().endsWith(ext2); +}; +var qmarksTestNocaseDot = ([$0, ext2 = ""]) => { + const noext = qmarksTestNoExtDot([$0]); + if (!ext2) + return noext; + ext2 = ext2.toLowerCase(); + return (f) => noext(f) && f.toLowerCase().endsWith(ext2); +}; +var qmarksTestDot = ([$0, ext2 = ""]) => { + const noext = qmarksTestNoExtDot([$0]); + return !ext2 ? noext : (f) => noext(f) && f.endsWith(ext2); +}; +var qmarksTest = ([$0, ext2 = ""]) => { + const noext = qmarksTestNoExt([$0]); + return !ext2 ? noext : (f) => noext(f) && f.endsWith(ext2); +}; +var qmarksTestNoExt = ([$0]) => { + const len = $0.length; + return (f) => f.length === len && !f.startsWith("."); +}; +var qmarksTestNoExtDot = ([$0]) => { + const len = $0.length; + return (f) => f.length === len && f !== "." && f !== ".."; +}; +var defaultPlatform = typeof process === "object" && process ? typeof process.env === "object" && process.env && process.env.__MINIMATCH_TESTING_PLATFORM__ || process.platform : "posix"; +var path = { + win32: { sep: "\\" }, + posix: { sep: "/" } +}; +var sep = defaultPlatform === "win32" ? path.win32.sep : path.posix.sep; +minimatch.sep = sep; +var GLOBSTAR = /* @__PURE__ */ Symbol("globstar **"); +minimatch.GLOBSTAR = GLOBSTAR; +var qmark2 = "[^/]"; +var star2 = qmark2 + "*?"; +var twoStarDot = "(?:(?!(?:\\/|^)(?:\\.{1,2})($|\\/)).)*?"; +var twoStarNoDot = "(?:(?!(?:\\/|^)\\.).)*?"; +var filter = (pattern, options = {}) => (p) => minimatch(p, pattern, options); +minimatch.filter = filter; +var ext = (a, b = {}) => Object.assign({}, a, b); +var defaults = (def) => { + if (!def || typeof def !== "object" || !Object.keys(def).length) { + return minimatch; + } + const orig = minimatch; + const m = (p, pattern, options = {}) => orig(p, pattern, ext(def, options)); + return Object.assign(m, { + Minimatch: class Minimatch extends orig.Minimatch { + constructor(pattern, options = {}) { + super(pattern, ext(def, options)); + } + static defaults(options) { + return orig.defaults(ext(def, options)).Minimatch; + } + }, + AST: class AST extends orig.AST { + /* c8 ignore start */ + constructor(type, parent, options = {}) { + super(type, parent, ext(def, options)); + } + /* c8 ignore stop */ + static fromGlob(pattern, options = {}) { + return orig.AST.fromGlob(pattern, ext(def, options)); + } + }, + unescape: (s, options = {}) => orig.unescape(s, ext(def, options)), + escape: (s, options = {}) => orig.escape(s, ext(def, options)), + filter: (pattern, options = {}) => orig.filter(pattern, ext(def, options)), + defaults: (options) => orig.defaults(ext(def, options)), + makeRe: (pattern, options = {}) => orig.makeRe(pattern, ext(def, options)), + braceExpand: (pattern, options = {}) => orig.braceExpand(pattern, ext(def, options)), + match: (list, pattern, options = {}) => orig.match(list, pattern, ext(def, options)), + sep: orig.sep, + GLOBSTAR + }); +}; +minimatch.defaults = defaults; +var braceExpand = (pattern, options = {}) => { + assertValidPattern(pattern); + if (options.nobrace || !/\{(?:(?!\{).)*\}/.test(pattern)) { + return [pattern]; + } + return expand(pattern, { max: options.braceExpandMax }); +}; +minimatch.braceExpand = braceExpand; +var makeRe = (pattern, options = {}) => new Minimatch(pattern, options).makeRe(); +minimatch.makeRe = makeRe; +var match = (list, pattern, options = {}) => { + const mm = new Minimatch(pattern, options); + list = list.filter((f) => mm.match(f)); + if (mm.options.nonull && !list.length) { + list.push(pattern); + } + return list; +}; +minimatch.match = match; +var globMagic = /[?*]|[+@!]\(.*?\)|\[|\]/; +var regExpEscape2 = (s) => s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); +var Minimatch = class { + options; + set; + pattern; + windowsPathsNoEscape; + nonegate; + negate; + comment; + empty; + preserveMultipleSlashes; + partial; + globSet; + globParts; + nocase; + isWindows; + platform; + windowsNoMagicRoot; + maxGlobstarRecursion; + regexp; + constructor(pattern, options = {}) { + assertValidPattern(pattern); + options = options || {}; + this.options = options; + this.maxGlobstarRecursion = options.maxGlobstarRecursion ?? 200; + this.pattern = pattern; + this.platform = options.platform || defaultPlatform; + this.isWindows = this.platform === "win32"; + const awe = "allowWindowsEscape"; + this.windowsPathsNoEscape = !!options.windowsPathsNoEscape || options[awe] === false; + if (this.windowsPathsNoEscape) { + this.pattern = this.pattern.replace(/\\/g, "/"); + } + this.preserveMultipleSlashes = !!options.preserveMultipleSlashes; + this.regexp = null; + this.negate = false; + this.nonegate = !!options.nonegate; + this.comment = false; + this.empty = false; + this.partial = !!options.partial; + this.nocase = !!this.options.nocase; + this.windowsNoMagicRoot = options.windowsNoMagicRoot !== void 0 ? options.windowsNoMagicRoot : !!(this.isWindows && this.nocase); + this.globSet = []; + this.globParts = []; + this.set = []; + this.make(); + } + hasMagic() { + if (this.options.magicalBraces && this.set.length > 1) { + return true; + } + for (const pattern of this.set) { + for (const part of pattern) { + if (typeof part !== "string") + return true; + } + } + return false; + } + debug(..._) { + } + make() { + const pattern = this.pattern; + const options = this.options; + if (!options.nocomment && pattern.charAt(0) === "#") { + this.comment = true; + return; + } + if (!pattern) { + this.empty = true; + return; + } + this.parseNegate(); + this.globSet = [...new Set(this.braceExpand())]; + if (options.debug) { + this.debug = (...args) => console.error(...args); + } + this.debug(this.pattern, this.globSet); + const rawGlobParts = this.globSet.map((s) => this.slashSplit(s)); + this.globParts = this.preprocess(rawGlobParts); + this.debug(this.pattern, this.globParts); + let set = this.globParts.map((s, _, __) => { + if (this.isWindows && this.windowsNoMagicRoot) { + const isUNC = s[0] === "" && s[1] === "" && (s[2] === "?" || !globMagic.test(s[2])) && !globMagic.test(s[3]); + const isDrive = /^[a-z]:/i.test(s[0]); + if (isUNC) { + return [ + ...s.slice(0, 4), + ...s.slice(4).map((ss) => this.parse(ss)) + ]; + } else if (isDrive) { + return [s[0], ...s.slice(1).map((ss) => this.parse(ss))]; + } + } + return s.map((ss) => this.parse(ss)); + }); + this.debug(this.pattern, set); + this.set = set.filter((s) => s.indexOf(false) === -1); + if (this.isWindows) { + for (let i = 0; i < this.set.length; i++) { + const p = this.set[i]; + if (p[0] === "" && p[1] === "" && this.globParts[i][2] === "?" && typeof p[3] === "string" && /^[a-z]:$/i.test(p[3])) { + p[2] = "?"; + } + } + } + this.debug(this.pattern, this.set); + } + // various transforms to equivalent pattern sets that are + // faster to process in a filesystem walk. The goal is to + // eliminate what we can, and push all ** patterns as far + // to the right as possible, even if it increases the number + // of patterns that we have to process. + preprocess(globParts) { + if (this.options.noglobstar) { + for (const partset of globParts) { + for (let j2 = 0; j2 < partset.length; j2++) { + if (partset[j2] === "**") { + partset[j2] = "*"; + } + } + } + } + const { optimizationLevel = 1 } = this.options; + if (optimizationLevel >= 2) { + globParts = this.firstPhasePreProcess(globParts); + globParts = this.secondPhasePreProcess(globParts); + } else if (optimizationLevel >= 1) { + globParts = this.levelOneOptimize(globParts); + } else { + globParts = this.adjascentGlobstarOptimize(globParts); + } + return globParts; + } + // just get rid of adjascent ** portions + adjascentGlobstarOptimize(globParts) { + return globParts.map((parts) => { + let gs = -1; + while (-1 !== (gs = parts.indexOf("**", gs + 1))) { + let i = gs; + while (parts[i + 1] === "**") { + i++; + } + if (i !== gs) { + parts.splice(gs, i - gs); + } + } + return parts; + }); + } + // get rid of adjascent ** and resolve .. portions + levelOneOptimize(globParts) { + return globParts.map((parts) => { + parts = parts.reduce((set, part) => { + const prev = set[set.length - 1]; + if (part === "**" && prev === "**") { + return set; + } + if (part === "..") { + if (prev && prev !== ".." && prev !== "." && prev !== "**") { + set.pop(); + return set; + } + } + set.push(part); + return set; + }, []); + return parts.length === 0 ? [""] : parts; + }); + } + levelTwoFileOptimize(parts) { + if (!Array.isArray(parts)) { + parts = this.slashSplit(parts); + } + let didSomething = false; + do { + didSomething = false; + if (!this.preserveMultipleSlashes) { + for (let i = 1; i < parts.length - 1; i++) { + const p = parts[i]; + if (i === 1 && p === "" && parts[0] === "") + continue; + if (p === "." || p === "") { + didSomething = true; + parts.splice(i, 1); + i--; + } + } + if (parts[0] === "." && parts.length === 2 && (parts[1] === "." || parts[1] === "")) { + didSomething = true; + parts.pop(); + } + } + let dd = 0; + while (-1 !== (dd = parts.indexOf("..", dd + 1))) { + const p = parts[dd - 1]; + if (p && p !== "." && p !== ".." && p !== "**" && !(this.isWindows && /^[a-z]:$/i.test(p))) { + didSomething = true; + parts.splice(dd - 1, 2); + dd -= 2; + } + } + } while (didSomething); + return parts.length === 0 ? [""] : parts; + } + // First phase: single-pattern processing + //
 is 1 or more portions
+  //  is 1 or more portions
+  // 

is any portion other than ., .., '', or ** + // is . or '' + // + // **/.. is *brutal* for filesystem walking performance, because + // it effectively resets the recursive walk each time it occurs, + // and ** cannot be reduced out by a .. pattern part like a regexp + // or most strings (other than .., ., and '') can be. + // + //

/**/../

/

/ -> {

/../

/

/,

/**/

/

/} + //

// -> 
/
+  // 
/

/../ ->

/
+  // **/**/ -> **/
+  //
+  // **/*/ -> */**/ <== not valid because ** doesn't follow
+  // this WOULD be allowed if ** did follow symlinks, or * didn't
+  firstPhasePreProcess(globParts) {
+    let didSomething = false;
+    do {
+      didSomething = false;
+      for (let parts of globParts) {
+        let gs = -1;
+        while (-1 !== (gs = parts.indexOf("**", gs + 1))) {
+          let gss = gs;
+          while (parts[gss + 1] === "**") {
+            gss++;
+          }
+          if (gss > gs) {
+            parts.splice(gs + 1, gss - gs);
+          }
+          let next = parts[gs + 1];
+          const p = parts[gs + 2];
+          const p2 = parts[gs + 3];
+          if (next !== "..")
+            continue;
+          if (!p || p === "." || p === ".." || !p2 || p2 === "." || p2 === "..") {
+            continue;
+          }
+          didSomething = true;
+          parts.splice(gs, 1);
+          const other = parts.slice(0);
+          other[gs] = "**";
+          globParts.push(other);
+          gs--;
+        }
+        if (!this.preserveMultipleSlashes) {
+          for (let i = 1; i < parts.length - 1; i++) {
+            const p = parts[i];
+            if (i === 1 && p === "" && parts[0] === "")
+              continue;
+            if (p === "." || p === "") {
+              didSomething = true;
+              parts.splice(i, 1);
+              i--;
+            }
+          }
+          if (parts[0] === "." && parts.length === 2 && (parts[1] === "." || parts[1] === "")) {
+            didSomething = true;
+            parts.pop();
+          }
+        }
+        let dd = 0;
+        while (-1 !== (dd = parts.indexOf("..", dd + 1))) {
+          const p = parts[dd - 1];
+          if (p && p !== "." && p !== ".." && p !== "**") {
+            didSomething = true;
+            const needDot = dd === 1 && parts[dd + 1] === "**";
+            const splin = needDot ? ["."] : [];
+            parts.splice(dd - 1, 2, ...splin);
+            if (parts.length === 0)
+              parts.push("");
+            dd -= 2;
+          }
+        }
+      }
+    } while (didSomething);
+    return globParts;
+  }
+  // second phase: multi-pattern dedupes
+  // {
/*/,
/

/} ->

/*/
+  // {
/,
/} -> 
/
+  // {
/**/,
/} -> 
/**/
+  //
+  // {
/**/,
/**/

/} ->

/**/
+  // ^-- not valid because ** doens't follow symlinks
+  secondPhasePreProcess(globParts) {
+    for (let i = 0; i < globParts.length - 1; i++) {
+      for (let j2 = i + 1; j2 < globParts.length; j2++) {
+        const matched = this.partsMatch(globParts[i], globParts[j2], !this.preserveMultipleSlashes);
+        if (matched) {
+          globParts[i] = [];
+          globParts[j2] = matched;
+          break;
+        }
+      }
+    }
+    return globParts.filter((gs) => gs.length);
+  }
+  partsMatch(a, b, emptyGSMatch = false) {
+    let ai = 0;
+    let bi = 0;
+    let result = [];
+    let which = "";
+    while (ai < a.length && bi < b.length) {
+      if (a[ai] === b[bi]) {
+        result.push(which === "b" ? b[bi] : a[ai]);
+        ai++;
+        bi++;
+      } else if (emptyGSMatch && a[ai] === "**" && b[bi] === a[ai + 1]) {
+        result.push(a[ai]);
+        ai++;
+      } else if (emptyGSMatch && b[bi] === "**" && a[ai] === b[bi + 1]) {
+        result.push(b[bi]);
+        bi++;
+      } else if (a[ai] === "*" && b[bi] && (this.options.dot || !b[bi].startsWith(".")) && b[bi] !== "**") {
+        if (which === "b")
+          return false;
+        which = "a";
+        result.push(a[ai]);
+        ai++;
+        bi++;
+      } else if (b[bi] === "*" && a[ai] && (this.options.dot || !a[ai].startsWith(".")) && a[ai] !== "**") {
+        if (which === "a")
+          return false;
+        which = "b";
+        result.push(b[bi]);
+        ai++;
+        bi++;
+      } else {
+        return false;
+      }
+    }
+    return a.length === b.length && result;
+  }
+  parseNegate() {
+    if (this.nonegate)
+      return;
+    const pattern = this.pattern;
+    let negate = false;
+    let negateOffset = 0;
+    for (let i = 0; i < pattern.length && pattern.charAt(i) === "!"; i++) {
+      negate = !negate;
+      negateOffset++;
+    }
+    if (negateOffset)
+      this.pattern = pattern.slice(negateOffset);
+    this.negate = negate;
+  }
+  // set partial to true to test if, for example,
+  // "/a/b" matches the start of "/*/b/*/d"
+  // Partial means, if you run out of file before you run
+  // out of pattern, then that's fine, as long as all
+  // the parts match.
+  matchOne(file, pattern, partial = false) {
+    let fileStartIndex = 0;
+    let patternStartIndex = 0;
+    if (this.isWindows) {
+      const fileDrive = typeof file[0] === "string" && /^[a-z]:$/i.test(file[0]);
+      const fileUNC = !fileDrive && file[0] === "" && file[1] === "" && file[2] === "?" && /^[a-z]:$/i.test(file[3]);
+      const patternDrive = typeof pattern[0] === "string" && /^[a-z]:$/i.test(pattern[0]);
+      const patternUNC = !patternDrive && pattern[0] === "" && pattern[1] === "" && pattern[2] === "?" && typeof pattern[3] === "string" && /^[a-z]:$/i.test(pattern[3]);
+      const fdi = fileUNC ? 3 : fileDrive ? 0 : void 0;
+      const pdi = patternUNC ? 3 : patternDrive ? 0 : void 0;
+      if (typeof fdi === "number" && typeof pdi === "number") {
+        const [fd, pd] = [
+          file[fdi],
+          pattern[pdi]
+        ];
+        if (fd.toLowerCase() === pd.toLowerCase()) {
+          pattern[pdi] = fd;
+          patternStartIndex = pdi;
+          fileStartIndex = fdi;
+        }
+      }
+    }
+    const { optimizationLevel = 1 } = this.options;
+    if (optimizationLevel >= 2) {
+      file = this.levelTwoFileOptimize(file);
+    }
+    if (pattern.includes(GLOBSTAR)) {
+      return this.#matchGlobstar(file, pattern, partial, fileStartIndex, patternStartIndex);
+    }
+    return this.#matchOne(file, pattern, partial, fileStartIndex, patternStartIndex);
+  }
+  #matchGlobstar(file, pattern, partial, fileIndex, patternIndex) {
+    const firstgs = pattern.indexOf(GLOBSTAR, patternIndex);
+    const lastgs = pattern.lastIndexOf(GLOBSTAR);
+    const [head, body, tail] = partial ? [
+      pattern.slice(patternIndex, firstgs),
+      pattern.slice(firstgs + 1),
+      []
+    ] : [
+      pattern.slice(patternIndex, firstgs),
+      pattern.slice(firstgs + 1, lastgs),
+      pattern.slice(lastgs + 1)
+    ];
+    if (head.length) {
+      const fileHead = file.slice(fileIndex, fileIndex + head.length);
+      if (!this.#matchOne(fileHead, head, partial, 0, 0)) {
+        return false;
+      }
+      fileIndex += head.length;
+      patternIndex += head.length;
+    }
+    let fileTailMatch = 0;
+    if (tail.length) {
+      if (tail.length + fileIndex > file.length)
+        return false;
+      let tailStart = file.length - tail.length;
+      if (this.#matchOne(file, tail, partial, tailStart, 0)) {
+        fileTailMatch = tail.length;
+      } else {
+        if (file[file.length - 1] !== "" || fileIndex + tail.length === file.length) {
+          return false;
+        }
+        tailStart--;
+        if (!this.#matchOne(file, tail, partial, tailStart, 0)) {
+          return false;
+        }
+        fileTailMatch = tail.length + 1;
+      }
+    }
+    if (!body.length) {
+      let sawSome = !!fileTailMatch;
+      for (let i2 = fileIndex; i2 < file.length - fileTailMatch; i2++) {
+        const f = String(file[i2]);
+        sawSome = true;
+        if (f === "." || f === ".." || !this.options.dot && f.startsWith(".")) {
+          return false;
+        }
+      }
+      return partial || sawSome;
+    }
+    const bodySegments = [[[], 0]];
+    let currentBody = bodySegments[0];
+    let nonGsParts = 0;
+    const nonGsPartsSums = [0];
+    for (const b of body) {
+      if (b === GLOBSTAR) {
+        nonGsPartsSums.push(nonGsParts);
+        currentBody = [[], 0];
+        bodySegments.push(currentBody);
+      } else {
+        currentBody[0].push(b);
+        nonGsParts++;
+      }
+    }
+    let i = bodySegments.length - 1;
+    const fileLength = file.length - fileTailMatch;
+    for (const b of bodySegments) {
+      b[1] = fileLength - (nonGsPartsSums[i--] + b[0].length);
+    }
+    return !!this.#matchGlobStarBodySections(file, bodySegments, fileIndex, 0, partial, 0, !!fileTailMatch);
+  }
+  // return false for "nope, not matching"
+  // return null for "not matching, cannot keep trying"
+  #matchGlobStarBodySections(file, bodySegments, fileIndex, bodyIndex, partial, globStarDepth, sawTail) {
+    const bs = bodySegments[bodyIndex];
+    if (!bs) {
+      for (let i = fileIndex; i < file.length; i++) {
+        sawTail = true;
+        const f = file[i];
+        if (f === "." || f === ".." || !this.options.dot && f.startsWith(".")) {
+          return false;
+        }
+      }
+      return sawTail;
+    }
+    const [body, after] = bs;
+    while (fileIndex <= after) {
+      const m = this.#matchOne(file.slice(0, fileIndex + body.length), body, partial, fileIndex, 0);
+      if (m && globStarDepth < this.maxGlobstarRecursion) {
+        const sub = this.#matchGlobStarBodySections(file, bodySegments, fileIndex + body.length, bodyIndex + 1, partial, globStarDepth + 1, sawTail);
+        if (sub !== false) {
+          return sub;
+        }
+      }
+      const f = file[fileIndex];
+      if (f === "." || f === ".." || !this.options.dot && f.startsWith(".")) {
+        return false;
+      }
+      fileIndex++;
+    }
+    return partial || null;
+  }
+  #matchOne(file, pattern, partial, fileIndex, patternIndex) {
+    let fi;
+    let pi;
+    let pl;
+    let fl;
+    for (fi = fileIndex, pi = patternIndex, fl = file.length, pl = pattern.length; fi < fl && pi < pl; fi++, pi++) {
+      this.debug("matchOne loop");
+      let p = pattern[pi];
+      let f = file[fi];
+      this.debug(pattern, p, f);
+      if (p === false || p === GLOBSTAR) {
+        return false;
+      }
+      let hit;
+      if (typeof p === "string") {
+        hit = f === p;
+        this.debug("string match", p, f, hit);
+      } else {
+        hit = p.test(f);
+        this.debug("pattern match", p, f, hit);
+      }
+      if (!hit)
+        return false;
+    }
+    if (fi === fl && pi === pl) {
+      return true;
+    } else if (fi === fl) {
+      return partial;
+    } else if (pi === pl) {
+      return fi === fl - 1 && file[fi] === "";
+    } else {
+      throw new Error("wtf?");
+    }
+  }
+  braceExpand() {
+    return braceExpand(this.pattern, this.options);
+  }
+  parse(pattern) {
+    assertValidPattern(pattern);
+    const options = this.options;
+    if (pattern === "**")
+      return GLOBSTAR;
+    if (pattern === "")
+      return "";
+    let m;
+    let fastTest = null;
+    if (m = pattern.match(starRE)) {
+      fastTest = options.dot ? starTestDot : starTest;
+    } else if (m = pattern.match(starDotExtRE)) {
+      fastTest = (options.nocase ? options.dot ? starDotExtTestNocaseDot : starDotExtTestNocase : options.dot ? starDotExtTestDot : starDotExtTest)(m[1]);
+    } else if (m = pattern.match(qmarksRE)) {
+      fastTest = (options.nocase ? options.dot ? qmarksTestNocaseDot : qmarksTestNocase : options.dot ? qmarksTestDot : qmarksTest)(m);
+    } else if (m = pattern.match(starDotStarRE)) {
+      fastTest = options.dot ? starDotStarTestDot : starDotStarTest;
+    } else if (m = pattern.match(dotStarRE)) {
+      fastTest = dotStarTest;
+    }
+    const re = AST.fromGlob(pattern, this.options).toMMPattern();
+    if (fastTest && typeof re === "object") {
+      Reflect.defineProperty(re, "test", { value: fastTest });
+    }
+    return re;
+  }
+  makeRe() {
+    if (this.regexp || this.regexp === false)
+      return this.regexp;
+    const set = this.set;
+    if (!set.length) {
+      this.regexp = false;
+      return this.regexp;
+    }
+    const options = this.options;
+    const twoStar = options.noglobstar ? star2 : options.dot ? twoStarDot : twoStarNoDot;
+    const flags = new Set(options.nocase ? ["i"] : []);
+    let re = set.map((pattern) => {
+      const pp = pattern.map((p) => {
+        if (p instanceof RegExp) {
+          for (const f of p.flags.split(""))
+            flags.add(f);
+        }
+        return typeof p === "string" ? regExpEscape2(p) : p === GLOBSTAR ? GLOBSTAR : p._src;
+      });
+      pp.forEach((p, i) => {
+        const next = pp[i + 1];
+        const prev = pp[i - 1];
+        if (p !== GLOBSTAR || prev === GLOBSTAR) {
+          return;
+        }
+        if (prev === void 0) {
+          if (next !== void 0 && next !== GLOBSTAR) {
+            pp[i + 1] = "(?:\\/|" + twoStar + "\\/)?" + next;
+          } else {
+            pp[i] = twoStar;
+          }
+        } else if (next === void 0) {
+          pp[i - 1] = prev + "(?:\\/|\\/" + twoStar + ")?";
+        } else if (next !== GLOBSTAR) {
+          pp[i - 1] = prev + "(?:\\/|\\/" + twoStar + "\\/)" + next;
+          pp[i + 1] = GLOBSTAR;
+        }
+      });
+      const filtered = pp.filter((p) => p !== GLOBSTAR);
+      if (this.partial && filtered.length >= 1) {
+        const prefixes = [];
+        for (let i = 1; i <= filtered.length; i++) {
+          prefixes.push(filtered.slice(0, i).join("/"));
+        }
+        return "(?:" + prefixes.join("|") + ")";
+      }
+      return filtered.join("/");
+    }).join("|");
+    const [open, close] = set.length > 1 ? ["(?:", ")"] : ["", ""];
+    re = "^" + open + re + close + "$";
+    if (this.partial) {
+      re = "^(?:\\/|" + open + re.slice(1, -1) + close + ")$";
+    }
+    if (this.negate)
+      re = "^(?!" + re + ").+$";
+    try {
+      this.regexp = new RegExp(re, [...flags].join(""));
+    } catch {
+      this.regexp = false;
+    }
+    return this.regexp;
+  }
+  slashSplit(p) {
+    if (this.preserveMultipleSlashes) {
+      return p.split("/");
+    } else if (this.isWindows && /^\/\/[^/]+/.test(p)) {
+      return ["", ...p.split(/\/+/)];
+    } else {
+      return p.split(/\/+/);
+    }
+  }
+  match(f, partial = this.partial) {
+    this.debug("match", f, this.pattern);
+    if (this.comment) {
+      return false;
+    }
+    if (this.empty) {
+      return f === "";
+    }
+    if (f === "/" && partial) {
+      return true;
+    }
+    const options = this.options;
+    if (this.isWindows) {
+      f = f.split("\\").join("/");
+    }
+    const ff = this.slashSplit(f);
+    this.debug(this.pattern, "split", ff);
+    const set = this.set;
+    this.debug(this.pattern, "set", set);
+    let filename = ff[ff.length - 1];
+    if (!filename) {
+      for (let i = ff.length - 2; !filename && i >= 0; i--) {
+        filename = ff[i];
+      }
+    }
+    for (const pattern of set) {
+      let file = ff;
+      if (options.matchBase && pattern.length === 1) {
+        file = [filename];
+      }
+      const hit = this.matchOne(file, pattern, partial);
+      if (hit) {
+        if (options.flipNegate) {
+          return true;
+        }
+        return !this.negate;
+      }
+    }
+    if (options.flipNegate) {
+      return false;
+    }
+    return this.negate;
+  }
+  static defaults(def) {
+    return minimatch.defaults(def).Minimatch;
+  }
+};
+minimatch.AST = AST;
+minimatch.Minimatch = Minimatch;
+minimatch.escape = escape;
+minimatch.unescape = unescape;
+
+// node_modules/glob/dist/esm/glob.js
+import { fileURLToPath as fileURLToPath2 } from "node:url";
+
+// node_modules/lru-cache/dist/esm/node/index.min.js
+import { tracingChannel as j, channel as I } from "node:diagnostics_channel";
+var S = I("lru-cache:metrics");
+var W = j("lru-cache");
+var D = () => S.hasSubscribers || W.hasSubscribers;
+var G = typeof performance == "object" && performance && typeof performance.now == "function" ? performance : Date;
+var M = /* @__PURE__ */ new Set();
+var C = typeof process == "object" && process ? process : {};
+var P = (u3, e, t, i) => {
+  typeof C.emitWarning == "function" ? C.emitWarning(u3, e, t, i) : console.error(`[${t}] ${e}: ${u3}`);
+};
+var H = (u3) => !M.has(u3);
+var F = (u3) => !!u3 && u3 === Math.floor(u3) && u3 > 0 && isFinite(u3);
+var U = (u3) => F(u3) ? u3 <= Math.pow(2, 8) ? Uint8Array : u3 <= Math.pow(2, 16) ? Uint16Array : u3 <= Math.pow(2, 32) ? Uint32Array : u3 <= Number.MAX_SAFE_INTEGER ? O : null : null;
+var O = class extends Array {
+  constructor(e) {
+    super(e), this.fill(0);
+  }
+};
+var R = class u {
+  heap;
+  length;
+  static #o = false;
+  static create(e) {
+    let t = U(e);
+    if (!t) return [];
+    u.#o = true;
+    let i = new u(e, t);
+    return u.#o = false, i;
+  }
+  constructor(e, t) {
+    if (!u.#o) throw new TypeError("instantiate Stack using Stack.create(n)");
+    this.heap = new t(e), this.length = 0;
+  }
+  push(e) {
+    this.heap[this.length++] = e;
+  }
+  pop() {
+    return this.heap[--this.length];
+  }
+};
+var L = class u2 {
+  #o;
+  #u;
+  #w;
+  #D;
+  #S;
+  #M;
+  #U;
+  #m;
+  get perf() {
+    return this.#m;
+  }
+  ttl;
+  ttlResolution;
+  ttlAutopurge;
+  updateAgeOnGet;
+  updateAgeOnHas;
+  allowStale;
+  noDisposeOnSet;
+  noUpdateTTL;
+  maxEntrySize;
+  sizeCalculation;
+  noDeleteOnFetchRejection;
+  noDeleteOnStaleGet;
+  allowStaleOnFetchAbort;
+  allowStaleOnFetchRejection;
+  ignoreFetchAbort;
+  #n;
+  #b;
+  #s;
+  #i;
+  #t;
+  #a;
+  #c;
+  #l;
+  #h;
+  #y;
+  #r;
+  #_;
+  #F;
+  #d;
+  #g;
+  #T;
+  #W;
+  #f;
+  #j;
+  static unsafeExposeInternals(e) {
+    return { starts: e.#F, ttls: e.#d, autopurgeTimers: e.#g, sizes: e.#_, keyMap: e.#s, keyList: e.#i, valList: e.#t, next: e.#a, prev: e.#c, get head() {
+      return e.#l;
+    }, get tail() {
+      return e.#h;
+    }, free: e.#y, isBackgroundFetch: (t) => e.#e(t), backgroundFetch: (t, i, s, n) => e.#P(t, i, s, n), moveToTail: (t) => e.#L(t), indexes: (t) => e.#A(t), rindexes: (t) => e.#z(t), isStale: (t) => e.#p(t) };
+  }
+  get max() {
+    return this.#o;
+  }
+  get maxSize() {
+    return this.#u;
+  }
+  get calculatedSize() {
+    return this.#b;
+  }
+  get size() {
+    return this.#n;
+  }
+  get fetchMethod() {
+    return this.#M;
+  }
+  get memoMethod() {
+    return this.#U;
+  }
+  get dispose() {
+    return this.#w;
+  }
+  get onInsert() {
+    return this.#D;
+  }
+  get disposeAfter() {
+    return this.#S;
+  }
+  constructor(e) {
+    let { max: t = 0, ttl: i, ttlResolution: s = 1, ttlAutopurge: n, updateAgeOnGet: o, updateAgeOnHas: r, allowStale: h, dispose: l, onInsert: c, disposeAfter: f, noDisposeOnSet: g, noUpdateTTL: p, maxSize: T = 0, maxEntrySize: w = 0, sizeCalculation: y, fetchMethod: a, memoMethod: m, noDeleteOnFetchRejection: _, noDeleteOnStaleGet: b, allowStaleOnFetchRejection: d, allowStaleOnFetchAbort: A, ignoreFetchAbort: z, perf: x } = e;
+    if (x !== void 0 && typeof x?.now != "function") throw new TypeError("perf option must have a now() method if specified");
+    if (this.#m = x ?? G, t !== 0 && !F(t)) throw new TypeError("max option must be a nonnegative integer");
+    let v = t ? U(t) : Array;
+    if (!v) throw new Error("invalid max value: " + t);
+    if (this.#o = t, this.#u = T, this.maxEntrySize = w || this.#u, this.sizeCalculation = y, this.sizeCalculation) {
+      if (!this.#u && !this.maxEntrySize) throw new TypeError("cannot set sizeCalculation without setting maxSize or maxEntrySize");
+      if (typeof this.sizeCalculation != "function") throw new TypeError("sizeCalculation set to non-function");
+    }
+    if (m !== void 0 && typeof m != "function") throw new TypeError("memoMethod must be a function if defined");
+    if (this.#U = m, a !== void 0 && typeof a != "function") throw new TypeError("fetchMethod must be a function if specified");
+    if (this.#M = a, this.#W = !!a, this.#s = /* @__PURE__ */ new Map(), this.#i = Array.from({ length: t }).fill(void 0), this.#t = Array.from({ length: t }).fill(void 0), this.#a = new v(t), this.#c = new v(t), this.#l = 0, this.#h = 0, this.#y = R.create(t), this.#n = 0, this.#b = 0, typeof l == "function" && (this.#w = l), typeof c == "function" && (this.#D = c), typeof f == "function" ? (this.#S = f, this.#r = []) : (this.#S = void 0, this.#r = void 0), this.#T = !!this.#w, this.#j = !!this.#D, this.#f = !!this.#S, this.noDisposeOnSet = !!g, this.noUpdateTTL = !!p, this.noDeleteOnFetchRejection = !!_, this.allowStaleOnFetchRejection = !!d, this.allowStaleOnFetchAbort = !!A, this.ignoreFetchAbort = !!z, this.maxEntrySize !== 0) {
+      if (this.#u !== 0 && !F(this.#u)) throw new TypeError("maxSize must be a positive integer if specified");
+      if (!F(this.maxEntrySize)) throw new TypeError("maxEntrySize must be a positive integer if specified");
+      this.#X();
+    }
+    if (this.allowStale = !!h, this.noDeleteOnStaleGet = !!b, this.updateAgeOnGet = !!o, this.updateAgeOnHas = !!r, this.ttlResolution = F(s) || s === 0 ? s : 1, this.ttlAutopurge = !!n, this.ttl = i || 0, this.ttl) {
+      if (!F(this.ttl)) throw new TypeError("ttl must be a positive integer if specified");
+      this.#H();
+    }
+    if (this.#o === 0 && this.ttl === 0 && this.#u === 0) throw new TypeError("At least one of max, maxSize, or ttl is required");
+    if (!this.ttlAutopurge && !this.#o && !this.#u) {
+      let E = "LRU_CACHE_UNBOUNDED";
+      H(E) && (M.add(E), P("TTL caching without ttlAutopurge, max, or maxSize can result in unbounded memory consumption.", "UnboundedCacheWarning", E, u2));
+    }
+  }
+  getRemainingTTL(e) {
+    return this.#s.has(e) ? 1 / 0 : 0;
+  }
+  #H() {
+    let e = new O(this.#o), t = new O(this.#o);
+    this.#d = e, this.#F = t;
+    let i = this.ttlAutopurge ? Array.from({ length: this.#o }) : void 0;
+    this.#g = i, this.#N = (r, h, l = this.#m.now()) => {
+      t[r] = h !== 0 ? l : 0, e[r] = h, s(r, h);
+    }, this.#x = (r) => {
+      t[r] = e[r] !== 0 ? this.#m.now() : 0, s(r, e[r]);
+    };
+    let s = this.ttlAutopurge ? (r, h) => {
+      if (i?.[r] && (clearTimeout(i[r]), i[r] = void 0), h && h !== 0 && i) {
+        let l = setTimeout(() => {
+          this.#p(r) && this.#v(this.#i[r], "expire");
+        }, h + 1);
+        l.unref && l.unref(), i[r] = l;
+      }
+    } : () => {
+    };
+    this.#E = (r, h) => {
+      if (e[h]) {
+        let l = e[h], c = t[h];
+        if (!l || !c) return;
+        r.ttl = l, r.start = c, r.now = n || o();
+        let f = r.now - c;
+        r.remainingTTL = l - f;
+      }
+    };
+    let n = 0, o = () => {
+      let r = this.#m.now();
+      if (this.ttlResolution > 0) {
+        n = r;
+        let h = setTimeout(() => n = 0, this.ttlResolution);
+        h.unref && h.unref();
+      }
+      return r;
+    };
+    this.getRemainingTTL = (r) => {
+      let h = this.#s.get(r);
+      if (h === void 0) return 0;
+      let l = e[h], c = t[h];
+      if (!l || !c) return 1 / 0;
+      let f = (n || o()) - c;
+      return l - f;
+    }, this.#p = (r) => {
+      let h = t[r], l = e[r];
+      return !!l && !!h && (n || o()) - h > l;
+    };
+  }
+  #x = () => {
+  };
+  #E = () => {
+  };
+  #N = () => {
+  };
+  #p = () => false;
+  #X() {
+    let e = new O(this.#o);
+    this.#b = 0, this.#_ = e, this.#R = (t) => {
+      this.#b -= e[t], e[t] = 0;
+    }, this.#k = (t, i, s, n) => {
+      if (this.#e(i)) return 0;
+      if (!F(s)) if (n) {
+        if (typeof n != "function") throw new TypeError("sizeCalculation must be a function");
+        if (s = n(i, t), !F(s)) throw new TypeError("sizeCalculation return invalid (expect positive integer)");
+      } else throw new TypeError("invalid size value (must be positive integer). When maxSize or maxEntrySize is used, sizeCalculation or size must be set.");
+      return s;
+    }, this.#I = (t, i, s) => {
+      if (e[t] = i, this.#u) {
+        let n = this.#u - e[t];
+        for (; this.#b > n; ) this.#G(true);
+      }
+      this.#b += e[t], s && (s.entrySize = i, s.totalCalculatedSize = this.#b);
+    };
+  }
+  #R = (e) => {
+  };
+  #I = (e, t, i) => {
+  };
+  #k = (e, t, i, s) => {
+    if (i || s) throw new TypeError("cannot set size without setting maxSize or maxEntrySize on cache");
+    return 0;
+  };
+  *#A({ allowStale: e = this.allowStale } = {}) {
+    if (this.#n) for (let t = this.#h; this.#V(t) && ((e || !this.#p(t)) && (yield t), t !== this.#l); ) t = this.#c[t];
+  }
+  *#z({ allowStale: e = this.allowStale } = {}) {
+    if (this.#n) for (let t = this.#l; this.#V(t) && ((e || !this.#p(t)) && (yield t), t !== this.#h); ) t = this.#a[t];
+  }
+  #V(e) {
+    return e !== void 0 && this.#s.get(this.#i[e]) === e;
+  }
+  *entries() {
+    for (let e of this.#A()) this.#t[e] !== void 0 && this.#i[e] !== void 0 && !this.#e(this.#t[e]) && (yield [this.#i[e], this.#t[e]]);
+  }
+  *rentries() {
+    for (let e of this.#z()) this.#t[e] !== void 0 && this.#i[e] !== void 0 && !this.#e(this.#t[e]) && (yield [this.#i[e], this.#t[e]]);
+  }
+  *keys() {
+    for (let e of this.#A()) {
+      let t = this.#i[e];
+      t !== void 0 && !this.#e(this.#t[e]) && (yield t);
+    }
+  }
+  *rkeys() {
+    for (let e of this.#z()) {
+      let t = this.#i[e];
+      t !== void 0 && !this.#e(this.#t[e]) && (yield t);
+    }
+  }
+  *values() {
+    for (let e of this.#A()) this.#t[e] !== void 0 && !this.#e(this.#t[e]) && (yield this.#t[e]);
+  }
+  *rvalues() {
+    for (let e of this.#z()) this.#t[e] !== void 0 && !this.#e(this.#t[e]) && (yield this.#t[e]);
+  }
+  [Symbol.iterator]() {
+    return this.entries();
+  }
+  [Symbol.toStringTag] = "LRUCache";
+  find(e, t = {}) {
+    for (let i of this.#A()) {
+      let s = this.#t[i], n = this.#e(s) ? s.__staleWhileFetching : s;
+      if (n !== void 0 && e(n, this.#i[i], this)) return this.#C(this.#i[i], t);
+    }
+  }
+  forEach(e, t = this) {
+    for (let i of this.#A()) {
+      let s = this.#t[i], n = this.#e(s) ? s.__staleWhileFetching : s;
+      n !== void 0 && e.call(t, n, this.#i[i], this);
+    }
+  }
+  rforEach(e, t = this) {
+    for (let i of this.#z()) {
+      let s = this.#t[i], n = this.#e(s) ? s.__staleWhileFetching : s;
+      n !== void 0 && e.call(t, n, this.#i[i], this);
+    }
+  }
+  purgeStale() {
+    let e = false;
+    for (let t of this.#z({ allowStale: true })) this.#p(t) && (this.#v(this.#i[t], "expire"), e = true);
+    return e;
+  }
+  info(e) {
+    let t = this.#s.get(e);
+    if (t === void 0) return;
+    let i = this.#t[t], s = this.#e(i) ? i.__staleWhileFetching : i;
+    if (s === void 0) return;
+    let n = { value: s };
+    if (this.#d && this.#F) {
+      let o = this.#d[t], r = this.#F[t];
+      if (o && r) {
+        let h = o - (this.#m.now() - r);
+        n.ttl = h, n.start = Date.now();
+      }
+    }
+    return this.#_ && (n.size = this.#_[t]), n;
+  }
+  dump() {
+    let e = [];
+    for (let t of this.#A({ allowStale: true })) {
+      let i = this.#i[t], s = this.#t[t], n = this.#e(s) ? s.__staleWhileFetching : s;
+      if (n === void 0 || i === void 0) continue;
+      let o = { value: n };
+      if (this.#d && this.#F) {
+        o.ttl = this.#d[t];
+        let r = this.#m.now() - this.#F[t];
+        o.start = Math.floor(Date.now() - r);
+      }
+      this.#_ && (o.size = this.#_[t]), e.unshift([i, o]);
+    }
+    return e;
+  }
+  load(e) {
+    this.clear();
+    for (let [t, i] of e) {
+      if (i.start) {
+        let s = Date.now() - i.start;
+        i.start = this.#m.now() - s;
+      }
+      this.#O(t, i.value, i);
+    }
+  }
+  set(e, t, i = {}) {
+    let { status: s = S.hasSubscribers ? {} : void 0 } = i;
+    i.status = s, s && (s.op = "set", s.key = e, t !== void 0 && (s.value = t));
+    let n = this.#O(e, t, i);
+    return s && S.hasSubscribers && S.publish(s), n;
+  }
+  #O(e, t, i = {}) {
+    let { ttl: s = this.ttl, start: n, noDisposeOnSet: o = this.noDisposeOnSet, sizeCalculation: r = this.sizeCalculation, status: h } = i;
+    if (t === void 0) return h && (h.set = "deleted"), this.delete(e), this;
+    let { noUpdateTTL: l = this.noUpdateTTL } = i;
+    h && !this.#e(t) && (h.value = t);
+    let c = this.#k(e, t, i.size || 0, r, h);
+    if (this.maxEntrySize && c > this.maxEntrySize) return this.#v(e, "set"), h && (h.set = "miss", h.maxEntrySizeExceeded = true), this;
+    let f = this.#n === 0 ? void 0 : this.#s.get(e);
+    if (f === void 0) f = this.#n === 0 ? this.#h : this.#y.length !== 0 ? this.#y.pop() : this.#n === this.#o ? this.#G(false) : this.#n, this.#i[f] = e, this.#t[f] = t, this.#s.set(e, f), this.#a[this.#h] = f, this.#c[f] = this.#h, this.#h = f, this.#n++, this.#I(f, c, h), h && (h.set = "add"), l = false, this.#j && this.#D?.(t, e, "add");
+    else {
+      this.#L(f);
+      let g = this.#t[f];
+      if (t !== g) {
+        if (this.#W && this.#e(g)) {
+          g.__abortController.abort(new Error("replaced"));
+          let { __staleWhileFetching: p } = g;
+          p !== void 0 && !o && (this.#T && this.#w?.(p, e, "set"), this.#f && this.#r?.push([p, e, "set"]));
+        } else o || (this.#T && this.#w?.(g, e, "set"), this.#f && this.#r?.push([g, e, "set"]));
+        if (this.#R(f), this.#I(f, c, h), this.#t[f] = t, h) {
+          h.set = "replace";
+          let p = g && this.#e(g) ? g.__staleWhileFetching : g;
+          p !== void 0 && (h.oldValue = p);
+        }
+      } else h && (h.set = "update");
+      this.#j && this.onInsert?.(t, e, t === g ? "update" : "replace");
+    }
+    if (s !== 0 && !this.#d && this.#H(), this.#d && (l || this.#N(f, s, n), h && this.#E(h, f)), !o && this.#f && this.#r) {
+      let g = this.#r, p;
+      for (; p = g?.shift(); ) this.#S?.(...p);
+    }
+    return this;
+  }
+  pop() {
+    try {
+      for (; this.#n; ) {
+        let e = this.#t[this.#l];
+        if (this.#G(true), this.#e(e)) {
+          if (e.__staleWhileFetching) return e.__staleWhileFetching;
+        } else if (e !== void 0) return e;
+      }
+    } finally {
+      if (this.#f && this.#r) {
+        let e = this.#r, t;
+        for (; t = e?.shift(); ) this.#S?.(...t);
+      }
+    }
+  }
+  #G(e) {
+    let t = this.#l, i = this.#i[t], s = this.#t[t];
+    return this.#W && this.#e(s) ? s.__abortController.abort(new Error("evicted")) : (this.#T || this.#f) && (this.#T && this.#w?.(s, i, "evict"), this.#f && this.#r?.push([s, i, "evict"])), this.#R(t), this.#g?.[t] && (clearTimeout(this.#g[t]), this.#g[t] = void 0), e && (this.#i[t] = void 0, this.#t[t] = void 0, this.#y.push(t)), this.#n === 1 ? (this.#l = this.#h = 0, this.#y.length = 0) : this.#l = this.#a[t], this.#s.delete(i), this.#n--, t;
+  }
+  has(e, t = {}) {
+    let { status: i = S.hasSubscribers ? {} : void 0 } = t;
+    t.status = i, i && (i.op = "has", i.key = e);
+    let s = this.#Y(e, t);
+    return S.hasSubscribers && S.publish(i), s;
+  }
+  #Y(e, t = {}) {
+    let { updateAgeOnHas: i = this.updateAgeOnHas, status: s } = t, n = this.#s.get(e);
+    if (n !== void 0) {
+      let o = this.#t[n];
+      if (this.#e(o) && o.__staleWhileFetching === void 0) return false;
+      if (this.#p(n)) s && (s.has = "stale", this.#E(s, n));
+      else return i && this.#x(n), s && (s.has = "hit", this.#E(s, n)), true;
+    } else s && (s.has = "miss");
+    return false;
+  }
+  peek(e, t = {}) {
+    let { status: i = D() ? {} : void 0 } = t;
+    i && (i.op = "peek", i.key = e), t.status = i;
+    let s = this.#J(e, t);
+    return S.hasSubscribers && S.publish(i), s;
+  }
+  #J(e, t) {
+    let { status: i, allowStale: s = this.allowStale } = t, n = this.#s.get(e);
+    if (n === void 0 || !s && this.#p(n)) {
+      i && (i.peek = n === void 0 ? "miss" : "stale");
+      return;
+    }
+    let o = this.#t[n], r = this.#e(o) ? o.__staleWhileFetching : o;
+    return i && (r !== void 0 ? (i.peek = "hit", i.value = r) : i.peek = "miss"), r;
+  }
+  #P(e, t, i, s) {
+    let n = t === void 0 ? void 0 : this.#t[t];
+    if (this.#e(n)) return n;
+    let o = new AbortController(), { signal: r } = i;
+    r?.addEventListener("abort", () => o.abort(r.reason), { signal: o.signal });
+    let h = { signal: o.signal, options: i, context: s }, l = (w, y = false) => {
+      let { aborted: a } = o.signal, m = i.ignoreFetchAbort && w !== void 0, _ = i.ignoreFetchAbort || !!(i.allowStaleOnFetchAbort && w !== void 0);
+      if (i.status && (a && !y ? (i.status.fetchAborted = true, i.status.fetchError = o.signal.reason, m && (i.status.fetchAbortIgnored = true)) : i.status.fetchResolved = true), a && !m && !y) return f(o.signal.reason, _);
+      let b = p, d = this.#t[t];
+      return (d === p || d === void 0 && m && y) && (w === void 0 ? b.__staleWhileFetching !== void 0 ? this.#t[t] = b.__staleWhileFetching : this.#v(e, "fetch") : (i.status && (i.status.fetchUpdated = true), this.#O(e, w, h.options))), w;
+    }, c = (w) => (i.status && (i.status.fetchRejected = true, i.status.fetchError = w), f(w, false)), f = (w, y) => {
+      let { aborted: a } = o.signal, m = a && i.allowStaleOnFetchAbort, _ = m || i.allowStaleOnFetchRejection, b = _ || i.noDeleteOnFetchRejection, d = p;
+      if (this.#t[t] === p && (!b || !y && d.__staleWhileFetching === void 0 ? this.#v(e, "fetch") : m || (this.#t[t] = d.__staleWhileFetching)), _) return i.status && d.__staleWhileFetching !== void 0 && (i.status.returnedStale = true), d.__staleWhileFetching;
+      if (d.__returned === d) throw w;
+    }, g = (w, y) => {
+      let a = this.#M?.(e, n, h);
+      a && a instanceof Promise && a.then((m) => w(m === void 0 ? void 0 : m), y), o.signal.addEventListener("abort", () => {
+        (!i.ignoreFetchAbort || i.allowStaleOnFetchAbort) && (w(void 0), i.allowStaleOnFetchAbort && (w = (m) => l(m, true)));
+      });
+    };
+    i.status && (i.status.fetchDispatched = true);
+    let p = new Promise(g).then(l, c), T = Object.assign(p, { __abortController: o, __staleWhileFetching: n, __returned: void 0 });
+    return t === void 0 ? (this.#O(e, T, { ...h.options, status: void 0 }), t = this.#s.get(e)) : this.#t[t] = T, T;
+  }
+  #e(e) {
+    if (!this.#W) return false;
+    let t = e;
+    return !!t && t instanceof Promise && t.hasOwnProperty("__staleWhileFetching") && t.__abortController instanceof AbortController;
+  }
+  fetch(e, t = {}) {
+    let i = W.hasSubscribers, { status: s = D() ? {} : void 0 } = t;
+    t.status = s, s && t.context && (s.context = t.context);
+    let n = this.#B(e, t);
+    return s && D() && i && (s.trace = true, W.tracePromise(() => n, s).catch(() => {
+    })), n;
+  }
+  async #B(e, t = {}) {
+    let { allowStale: i = this.allowStale, updateAgeOnGet: s = this.updateAgeOnGet, noDeleteOnStaleGet: n = this.noDeleteOnStaleGet, ttl: o = this.ttl, noDisposeOnSet: r = this.noDisposeOnSet, size: h = 0, sizeCalculation: l = this.sizeCalculation, noUpdateTTL: c = this.noUpdateTTL, noDeleteOnFetchRejection: f = this.noDeleteOnFetchRejection, allowStaleOnFetchRejection: g = this.allowStaleOnFetchRejection, ignoreFetchAbort: p = this.ignoreFetchAbort, allowStaleOnFetchAbort: T = this.allowStaleOnFetchAbort, context: w, forceRefresh: y = false, status: a, signal: m } = t;
+    if (a && (a.op = "fetch", a.key = e, y && (a.forceRefresh = true)), !this.#W) return a && (a.fetch = "get"), this.#C(e, { allowStale: i, updateAgeOnGet: s, noDeleteOnStaleGet: n, status: a });
+    let _ = { allowStale: i, updateAgeOnGet: s, noDeleteOnStaleGet: n, ttl: o, noDisposeOnSet: r, size: h, sizeCalculation: l, noUpdateTTL: c, noDeleteOnFetchRejection: f, allowStaleOnFetchRejection: g, allowStaleOnFetchAbort: T, ignoreFetchAbort: p, status: a, signal: m }, b = this.#s.get(e);
+    if (b === void 0) {
+      a && (a.fetch = "miss");
+      let d = this.#P(e, b, _, w);
+      return d.__returned = d;
+    } else {
+      let d = this.#t[b];
+      if (this.#e(d)) {
+        let E = i && d.__staleWhileFetching !== void 0;
+        return a && (a.fetch = "inflight", E && (a.returnedStale = true)), E ? d.__staleWhileFetching : d.__returned = d;
+      }
+      let A = this.#p(b);
+      if (!y && !A) return a && (a.fetch = "hit"), this.#L(b), s && this.#x(b), a && this.#E(a, b), d;
+      let z = this.#P(e, b, _, w), v = z.__staleWhileFetching !== void 0 && i;
+      return a && (a.fetch = A ? "stale" : "refresh", v && A && (a.returnedStale = true)), v ? z.__staleWhileFetching : z.__returned = z;
+    }
+  }
+  forceFetch(e, t = {}) {
+    let i = W.hasSubscribers, { status: s = D() ? {} : void 0 } = t;
+    t.status = s, s && t.context && (s.context = t.context);
+    let n = this.#K(e, t);
+    return s && D() && i && (s.trace = true, W.tracePromise(() => n, s).catch(() => {
+    })), n;
+  }
+  async #K(e, t = {}) {
+    let i = await this.#B(e, t);
+    if (i === void 0) throw new Error("fetch() returned undefined");
+    return i;
+  }
+  memo(e, t = {}) {
+    let { status: i = S.hasSubscribers ? {} : void 0 } = t;
+    t.status = i, i && (i.op = "memo", i.key = e, t.context && (i.context = t.context));
+    let s = this.#Q(e, t);
+    return i && (i.value = s), S.hasSubscribers && S.publish(i), s;
+  }
+  #Q(e, t = {}) {
+    let i = this.#U;
+    if (!i) throw new Error("no memoMethod provided to constructor");
+    let { context: s, status: n, forceRefresh: o, ...r } = t;
+    n && o && (n.forceRefresh = true);
+    let h = this.#C(e, r), l = o || h === void 0;
+    if (n && (n.memo = l ? "miss" : "hit", l || (n.value = h)), !l) return h;
+    let c = i(e, h, { options: r, context: s });
+    return n && (n.value = c), this.#O(e, c, r), c;
+  }
+  get(e, t = {}) {
+    let { status: i = S.hasSubscribers ? {} : void 0 } = t;
+    t.status = i, i && (i.op = "get", i.key = e);
+    let s = this.#C(e, t);
+    return i && (s !== void 0 && (i.value = s), S.hasSubscribers && S.publish(i)), s;
+  }
+  #C(e, t = {}) {
+    let { allowStale: i = this.allowStale, updateAgeOnGet: s = this.updateAgeOnGet, noDeleteOnStaleGet: n = this.noDeleteOnStaleGet, status: o } = t, r = this.#s.get(e);
+    if (r === void 0) {
+      o && (o.get = "miss");
+      return;
+    }
+    let h = this.#t[r], l = this.#e(h);
+    return o && this.#E(o, r), this.#p(r) ? l ? (o && (o.get = "stale-fetching"), i && h.__staleWhileFetching !== void 0 ? (o && (o.returnedStale = true), h.__staleWhileFetching) : void 0) : (n || this.#v(e, "expire"), o && (o.get = "stale"), i ? (o && (o.returnedStale = true), h) : void 0) : (o && (o.get = l ? "fetching" : "hit"), this.#L(r), s && this.#x(r), l ? h.__staleWhileFetching : h);
+  }
+  #$(e, t) {
+    this.#c[t] = e, this.#a[e] = t;
+  }
+  #L(e) {
+    e !== this.#h && (e === this.#l ? this.#l = this.#a[e] : this.#$(this.#c[e], this.#a[e]), this.#$(this.#h, e), this.#h = e);
+  }
+  delete(e) {
+    return this.#v(e, "delete");
+  }
+  #v(e, t) {
+    S.hasSubscribers && S.publish({ op: "delete", delete: t, key: e });
+    let i = false;
+    if (this.#n !== 0) {
+      let s = this.#s.get(e);
+      if (s !== void 0) if (this.#g?.[s] && (clearTimeout(this.#g?.[s]), this.#g[s] = void 0), i = true, this.#n === 1) this.#q(t);
+      else {
+        this.#R(s);
+        let n = this.#t[s];
+        if (this.#e(n) ? n.__abortController.abort(new Error("deleted")) : (this.#T || this.#f) && (this.#T && this.#w?.(n, e, t), this.#f && this.#r?.push([n, e, t])), this.#s.delete(e), this.#i[s] = void 0, this.#t[s] = void 0, s === this.#h) this.#h = this.#c[s];
+        else if (s === this.#l) this.#l = this.#a[s];
+        else {
+          let o = this.#c[s];
+          this.#a[o] = this.#a[s];
+          let r = this.#a[s];
+          this.#c[r] = this.#c[s];
+        }
+        this.#n--, this.#y.push(s);
+      }
+    }
+    if (this.#f && this.#r?.length) {
+      let s = this.#r, n;
+      for (; n = s?.shift(); ) this.#S?.(...n);
+    }
+    return i;
+  }
+  clear() {
+    return this.#q("delete");
+  }
+  #q(e) {
+    for (let t of this.#z({ allowStale: true })) {
+      let i = this.#t[t];
+      if (this.#e(i)) i.__abortController.abort(new Error("deleted"));
+      else {
+        let s = this.#i[t];
+        this.#T && this.#w?.(i, s, e), this.#f && this.#r?.push([i, s, e]);
+      }
+    }
+    if (this.#s.clear(), this.#t.fill(void 0), this.#i.fill(void 0), this.#d && this.#F) {
+      this.#d.fill(0), this.#F.fill(0);
+      for (let t of this.#g ?? []) t !== void 0 && clearTimeout(t);
+      this.#g?.fill(void 0);
+    }
+    if (this.#_ && this.#_.fill(0), this.#l = 0, this.#h = 0, this.#y.length = 0, this.#b = 0, this.#n = 0, this.#f && this.#r) {
+      let t = this.#r, i;
+      for (; i = t?.shift(); ) this.#S?.(...i);
+    }
+  }
+};
+
+// node_modules/path-scurry/dist/esm/index.js
+import { posix, win32 } from "node:path";
+import { fileURLToPath } from "node:url";
+import { lstatSync, readdir as readdirCB, readdirSync, readlinkSync, realpathSync as rps } from "fs";
+import * as actualFS from "node:fs";
+import { lstat, readdir, readlink, realpath } from "node:fs/promises";
+
+// node_modules/minipass/dist/esm/index.js
+import { EventEmitter } from "node:events";
+import Stream from "node:stream";
+import { StringDecoder } from "node:string_decoder";
+var proc = typeof process === "object" && process ? process : {
+  stdout: null,
+  stderr: null
+};
+var isStream = (s) => !!s && typeof s === "object" && (s instanceof Minipass || s instanceof Stream || isReadable(s) || isWritable(s));
+var isReadable = (s) => !!s && typeof s === "object" && s instanceof EventEmitter && typeof s.pipe === "function" && // node core Writable streams have a pipe() method, but it throws
+s.pipe !== Stream.Writable.prototype.pipe;
+var isWritable = (s) => !!s && typeof s === "object" && s instanceof EventEmitter && typeof s.write === "function" && typeof s.end === "function";
+var EOF = /* @__PURE__ */ Symbol("EOF");
+var MAYBE_EMIT_END = /* @__PURE__ */ Symbol("maybeEmitEnd");
+var EMITTED_END = /* @__PURE__ */ Symbol("emittedEnd");
+var EMITTING_END = /* @__PURE__ */ Symbol("emittingEnd");
+var EMITTED_ERROR = /* @__PURE__ */ Symbol("emittedError");
+var CLOSED = /* @__PURE__ */ Symbol("closed");
+var READ = /* @__PURE__ */ Symbol("read");
+var FLUSH = /* @__PURE__ */ Symbol("flush");
+var FLUSHCHUNK = /* @__PURE__ */ Symbol("flushChunk");
+var ENCODING = /* @__PURE__ */ Symbol("encoding");
+var DECODER = /* @__PURE__ */ Symbol("decoder");
+var FLOWING = /* @__PURE__ */ Symbol("flowing");
+var PAUSED = /* @__PURE__ */ Symbol("paused");
+var RESUME = /* @__PURE__ */ Symbol("resume");
+var BUFFER = /* @__PURE__ */ Symbol("buffer");
+var PIPES = /* @__PURE__ */ Symbol("pipes");
+var BUFFERLENGTH = /* @__PURE__ */ Symbol("bufferLength");
+var BUFFERPUSH = /* @__PURE__ */ Symbol("bufferPush");
+var BUFFERSHIFT = /* @__PURE__ */ Symbol("bufferShift");
+var OBJECTMODE = /* @__PURE__ */ Symbol("objectMode");
+var DESTROYED = /* @__PURE__ */ Symbol("destroyed");
+var ERROR = /* @__PURE__ */ Symbol("error");
+var EMITDATA = /* @__PURE__ */ Symbol("emitData");
+var EMITEND = /* @__PURE__ */ Symbol("emitEnd");
+var EMITEND2 = /* @__PURE__ */ Symbol("emitEnd2");
+var ASYNC = /* @__PURE__ */ Symbol("async");
+var ABORT = /* @__PURE__ */ Symbol("abort");
+var ABORTED = /* @__PURE__ */ Symbol("aborted");
+var SIGNAL = /* @__PURE__ */ Symbol("signal");
+var DATALISTENERS = /* @__PURE__ */ Symbol("dataListeners");
+var DISCARDED = /* @__PURE__ */ Symbol("discarded");
+var defer = (fn) => Promise.resolve().then(fn);
+var nodefer = (fn) => fn();
+var isEndish = (ev) => ev === "end" || ev === "finish" || ev === "prefinish";
+var isArrayBufferLike = (b) => b instanceof ArrayBuffer || !!b && typeof b === "object" && b.constructor && b.constructor.name === "ArrayBuffer" && b.byteLength >= 0;
+var isArrayBufferView = (b) => !Buffer.isBuffer(b) && ArrayBuffer.isView(b);
+var Pipe = class {
+  src;
+  dest;
+  opts;
+  ondrain;
+  constructor(src, dest, opts) {
+    this.src = src;
+    this.dest = dest;
+    this.opts = opts;
+    this.ondrain = () => src[RESUME]();
+    this.dest.on("drain", this.ondrain);
+  }
+  unpipe() {
+    this.dest.removeListener("drain", this.ondrain);
+  }
+  // only here for the prototype
+  /* c8 ignore start */
+  proxyErrors(_er) {
+  }
+  /* c8 ignore stop */
+  end() {
+    this.unpipe();
+    if (this.opts.end)
+      this.dest.end();
+  }
+};
+var PipeProxyErrors = class extends Pipe {
+  unpipe() {
+    this.src.removeListener("error", this.proxyErrors);
+    super.unpipe();
+  }
+  constructor(src, dest, opts) {
+    super(src, dest, opts);
+    this.proxyErrors = (er) => this.dest.emit("error", er);
+    src.on("error", this.proxyErrors);
+  }
+};
+var isObjectModeOptions = (o) => !!o.objectMode;
+var isEncodingOptions = (o) => !o.objectMode && !!o.encoding && o.encoding !== "buffer";
+var Minipass = class extends EventEmitter {
+  [FLOWING] = false;
+  [PAUSED] = false;
+  [PIPES] = [];
+  [BUFFER] = [];
+  [OBJECTMODE];
+  [ENCODING];
+  [ASYNC];
+  [DECODER];
+  [EOF] = false;
+  [EMITTED_END] = false;
+  [EMITTING_END] = false;
+  [CLOSED] = false;
+  [EMITTED_ERROR] = null;
+  [BUFFERLENGTH] = 0;
+  [DESTROYED] = false;
+  [SIGNAL];
+  [ABORTED] = false;
+  [DATALISTENERS] = 0;
+  [DISCARDED] = false;
+  /**
+   * true if the stream can be written
+   */
+  writable = true;
+  /**
+   * true if the stream can be read
+   */
+  readable = true;
+  /**
+   * If `RType` is Buffer, then options do not need to be provided.
+   * Otherwise, an options object must be provided to specify either
+   * {@link Minipass.SharedOptions.objectMode} or
+   * {@link Minipass.SharedOptions.encoding}, as appropriate.
+   */
+  constructor(...args) {
+    const options = args[0] || {};
+    super();
+    if (options.objectMode && typeof options.encoding === "string") {
+      throw new TypeError("Encoding and objectMode may not be used together");
+    }
+    if (isObjectModeOptions(options)) {
+      this[OBJECTMODE] = true;
+      this[ENCODING] = null;
+    } else if (isEncodingOptions(options)) {
+      this[ENCODING] = options.encoding;
+      this[OBJECTMODE] = false;
+    } else {
+      this[OBJECTMODE] = false;
+      this[ENCODING] = null;
+    }
+    this[ASYNC] = !!options.async;
+    this[DECODER] = this[ENCODING] ? new StringDecoder(this[ENCODING]) : null;
+    if (options && options.debugExposeBuffer === true) {
+      Object.defineProperty(this, "buffer", { get: () => this[BUFFER] });
+    }
+    if (options && options.debugExposePipes === true) {
+      Object.defineProperty(this, "pipes", { get: () => this[PIPES] });
+    }
+    const { signal } = options;
+    if (signal) {
+      this[SIGNAL] = signal;
+      if (signal.aborted) {
+        this[ABORT]();
+      } else {
+        signal.addEventListener("abort", () => this[ABORT]());
+      }
+    }
+  }
+  /**
+   * The amount of data stored in the buffer waiting to be read.
+   *
+   * For Buffer strings, this will be the total byte length.
+   * For string encoding streams, this will be the string character length,
+   * according to JavaScript's `string.length` logic.
+   * For objectMode streams, this is a count of the items waiting to be
+   * emitted.
+   */
+  get bufferLength() {
+    return this[BUFFERLENGTH];
+  }
+  /**
+   * The `BufferEncoding` currently in use, or `null`
+   */
+  get encoding() {
+    return this[ENCODING];
+  }
+  /**
+   * @deprecated - This is a read only property
+   */
+  set encoding(_enc) {
+    throw new Error("Encoding must be set at instantiation time");
+  }
+  /**
+   * @deprecated - Encoding may only be set at instantiation time
+   */
+  setEncoding(_enc) {
+    throw new Error("Encoding must be set at instantiation time");
+  }
+  /**
+   * True if this is an objectMode stream
+   */
+  get objectMode() {
+    return this[OBJECTMODE];
+  }
+  /**
+   * @deprecated - This is a read-only property
+   */
+  set objectMode(_om) {
+    throw new Error("objectMode must be set at instantiation time");
+  }
+  /**
+   * true if this is an async stream
+   */
+  get ["async"]() {
+    return this[ASYNC];
+  }
+  /**
+   * Set to true to make this stream async.
+   *
+   * Once set, it cannot be unset, as this would potentially cause incorrect
+   * behavior.  Ie, a sync stream can be made async, but an async stream
+   * cannot be safely made sync.
+   */
+  set ["async"](a) {
+    this[ASYNC] = this[ASYNC] || !!a;
+  }
+  // drop everything and get out of the flow completely
+  [ABORT]() {
+    this[ABORTED] = true;
+    this.emit("abort", this[SIGNAL]?.reason);
+    this.destroy(this[SIGNAL]?.reason);
+  }
+  /**
+   * True if the stream has been aborted.
+   */
+  get aborted() {
+    return this[ABORTED];
+  }
+  /**
+   * No-op setter. Stream aborted status is set via the AbortSignal provided
+   * in the constructor options.
+   */
+  set aborted(_) {
+  }
+  write(chunk, encoding, cb) {
+    if (this[ABORTED])
+      return false;
+    if (this[EOF])
+      throw new Error("write after end");
+    if (this[DESTROYED]) {
+      this.emit("error", Object.assign(new Error("Cannot call write after a stream was destroyed"), { code: "ERR_STREAM_DESTROYED" }));
+      return true;
+    }
+    if (typeof encoding === "function") {
+      cb = encoding;
+      encoding = "utf8";
+    }
+    if (!encoding)
+      encoding = "utf8";
+    const fn = this[ASYNC] ? defer : nodefer;
+    if (!this[OBJECTMODE] && !Buffer.isBuffer(chunk)) {
+      if (isArrayBufferView(chunk)) {
+        chunk = Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
+      } else if (isArrayBufferLike(chunk)) {
+        chunk = Buffer.from(chunk);
+      } else if (typeof chunk !== "string") {
+        throw new Error("Non-contiguous data written to non-objectMode stream");
+      }
+    }
+    if (this[OBJECTMODE]) {
+      if (this[FLOWING] && this[BUFFERLENGTH] !== 0)
+        this[FLUSH](true);
+      if (this[FLOWING])
+        this.emit("data", chunk);
+      else
+        this[BUFFERPUSH](chunk);
+      if (this[BUFFERLENGTH] !== 0)
+        this.emit("readable");
+      if (cb)
+        fn(cb);
+      return this[FLOWING];
+    }
+    if (!chunk.length) {
+      if (this[BUFFERLENGTH] !== 0)
+        this.emit("readable");
+      if (cb)
+        fn(cb);
+      return this[FLOWING];
+    }
+    if (typeof chunk === "string" && // unless it is a string already ready for us to use
+    !(encoding === this[ENCODING] && !this[DECODER]?.lastNeed)) {
+      chunk = Buffer.from(chunk, encoding);
+    }
+    if (Buffer.isBuffer(chunk) && this[ENCODING]) {
+      chunk = this[DECODER].write(chunk);
+    }
+    if (this[FLOWING] && this[BUFFERLENGTH] !== 0)
+      this[FLUSH](true);
+    if (this[FLOWING])
+      this.emit("data", chunk);
+    else
+      this[BUFFERPUSH](chunk);
+    if (this[BUFFERLENGTH] !== 0)
+      this.emit("readable");
+    if (cb)
+      fn(cb);
+    return this[FLOWING];
+  }
+  /**
+   * Low-level explicit read method.
+   *
+   * In objectMode, the argument is ignored, and one item is returned if
+   * available.
+   *
+   * `n` is the number of bytes (or in the case of encoding streams,
+   * characters) to consume. If `n` is not provided, then the entire buffer
+   * is returned, or `null` is returned if no data is available.
+   *
+   * If `n` is greater that the amount of data in the internal buffer,
+   * then `null` is returned.
+   */
+  read(n) {
+    if (this[DESTROYED])
+      return null;
+    this[DISCARDED] = false;
+    if (this[BUFFERLENGTH] === 0 || n === 0 || n && n > this[BUFFERLENGTH]) {
+      this[MAYBE_EMIT_END]();
+      return null;
+    }
+    if (this[OBJECTMODE])
+      n = null;
+    if (this[BUFFER].length > 1 && !this[OBJECTMODE]) {
+      this[BUFFER] = [
+        this[ENCODING] ? this[BUFFER].join("") : Buffer.concat(this[BUFFER], this[BUFFERLENGTH])
+      ];
+    }
+    const ret = this[READ](n || null, this[BUFFER][0]);
+    this[MAYBE_EMIT_END]();
+    return ret;
+  }
+  [READ](n, chunk) {
+    if (this[OBJECTMODE])
+      this[BUFFERSHIFT]();
+    else {
+      const c = chunk;
+      if (n === c.length || n === null)
+        this[BUFFERSHIFT]();
+      else if (typeof c === "string") {
+        this[BUFFER][0] = c.slice(n);
+        chunk = c.slice(0, n);
+        this[BUFFERLENGTH] -= n;
+      } else {
+        this[BUFFER][0] = c.subarray(n);
+        chunk = c.subarray(0, n);
+        this[BUFFERLENGTH] -= n;
+      }
+    }
+    this.emit("data", chunk);
+    if (!this[BUFFER].length && !this[EOF])
+      this.emit("drain");
+    return chunk;
+  }
+  end(chunk, encoding, cb) {
+    if (typeof chunk === "function") {
+      cb = chunk;
+      chunk = void 0;
+    }
+    if (typeof encoding === "function") {
+      cb = encoding;
+      encoding = "utf8";
+    }
+    if (chunk !== void 0)
+      this.write(chunk, encoding);
+    if (cb)
+      this.once("end", cb);
+    this[EOF] = true;
+    this.writable = false;
+    if (this[FLOWING] || !this[PAUSED])
+      this[MAYBE_EMIT_END]();
+    return this;
+  }
+  // don't let the internal resume be overwritten
+  [RESUME]() {
+    if (this[DESTROYED])
+      return;
+    if (!this[DATALISTENERS] && !this[PIPES].length) {
+      this[DISCARDED] = true;
+    }
+    this[PAUSED] = false;
+    this[FLOWING] = true;
+    this.emit("resume");
+    if (this[BUFFER].length)
+      this[FLUSH]();
+    else if (this[EOF])
+      this[MAYBE_EMIT_END]();
+    else
+      this.emit("drain");
+  }
+  /**
+   * Resume the stream if it is currently in a paused state
+   *
+   * If called when there are no pipe destinations or `data` event listeners,
+   * this will place the stream in a "discarded" state, where all data will
+   * be thrown away. The discarded state is removed if a pipe destination or
+   * data handler is added, if pause() is called, or if any synchronous or
+   * asynchronous iteration is started.
+   */
+  resume() {
+    return this[RESUME]();
+  }
+  /**
+   * Pause the stream
+   */
+  pause() {
+    this[FLOWING] = false;
+    this[PAUSED] = true;
+    this[DISCARDED] = false;
+  }
+  /**
+   * true if the stream has been forcibly destroyed
+   */
+  get destroyed() {
+    return this[DESTROYED];
+  }
+  /**
+   * true if the stream is currently in a flowing state, meaning that
+   * any writes will be immediately emitted.
+   */
+  get flowing() {
+    return this[FLOWING];
+  }
+  /**
+   * true if the stream is currently in a paused state
+   */
+  get paused() {
+    return this[PAUSED];
+  }
+  [BUFFERPUSH](chunk) {
+    if (this[OBJECTMODE])
+      this[BUFFERLENGTH] += 1;
+    else
+      this[BUFFERLENGTH] += chunk.length;
+    this[BUFFER].push(chunk);
+  }
+  [BUFFERSHIFT]() {
+    if (this[OBJECTMODE])
+      this[BUFFERLENGTH] -= 1;
+    else
+      this[BUFFERLENGTH] -= this[BUFFER][0].length;
+    return this[BUFFER].shift();
+  }
+  [FLUSH](noDrain = false) {
+    do {
+    } while (this[FLUSHCHUNK](this[BUFFERSHIFT]()) && this[BUFFER].length);
+    if (!noDrain && !this[BUFFER].length && !this[EOF])
+      this.emit("drain");
+  }
+  [FLUSHCHUNK](chunk) {
+    this.emit("data", chunk);
+    return this[FLOWING];
+  }
+  /**
+   * Pipe all data emitted by this stream into the destination provided.
+   *
+   * Triggers the flow of data.
+   */
+  pipe(dest, opts) {
+    if (this[DESTROYED])
+      return dest;
+    this[DISCARDED] = false;
+    const ended = this[EMITTED_END];
+    opts = opts || {};
+    if (dest === proc.stdout || dest === proc.stderr)
+      opts.end = false;
+    else
+      opts.end = opts.end !== false;
+    opts.proxyErrors = !!opts.proxyErrors;
+    if (ended) {
+      if (opts.end)
+        dest.end();
+    } else {
+      this[PIPES].push(!opts.proxyErrors ? new Pipe(this, dest, opts) : new PipeProxyErrors(this, dest, opts));
+      if (this[ASYNC])
+        defer(() => this[RESUME]());
+      else
+        this[RESUME]();
+    }
+    return dest;
+  }
+  /**
+   * Fully unhook a piped destination stream.
+   *
+   * If the destination stream was the only consumer of this stream (ie,
+   * there are no other piped destinations or `'data'` event listeners)
+   * then the flow of data will stop until there is another consumer or
+   * {@link Minipass#resume} is explicitly called.
+   */
+  unpipe(dest) {
+    const p = this[PIPES].find((p2) => p2.dest === dest);
+    if (p) {
+      if (this[PIPES].length === 1) {
+        if (this[FLOWING] && this[DATALISTENERS] === 0) {
+          this[FLOWING] = false;
+        }
+        this[PIPES] = [];
+      } else
+        this[PIPES].splice(this[PIPES].indexOf(p), 1);
+      p.unpipe();
+    }
+  }
+  /**
+   * Alias for {@link Minipass#on}
+   */
+  addListener(ev, handler) {
+    return this.on(ev, handler);
+  }
+  /**
+   * Mostly identical to `EventEmitter.on`, with the following
+   * behavior differences to prevent data loss and unnecessary hangs:
+   *
+   * - Adding a 'data' event handler will trigger the flow of data
+   *
+   * - Adding a 'readable' event handler when there is data waiting to be read
+   *   will cause 'readable' to be emitted immediately.
+   *
+   * - Adding an 'endish' event handler ('end', 'finish', etc.) which has
+   *   already passed will cause the event to be emitted immediately and all
+   *   handlers removed.
+   *
+   * - Adding an 'error' event handler after an error has been emitted will
+   *   cause the event to be re-emitted immediately with the error previously
+   *   raised.
+   */
+  on(ev, handler) {
+    const ret = super.on(ev, handler);
+    if (ev === "data") {
+      this[DISCARDED] = false;
+      this[DATALISTENERS]++;
+      if (!this[PIPES].length && !this[FLOWING]) {
+        this[RESUME]();
+      }
+    } else if (ev === "readable" && this[BUFFERLENGTH] !== 0) {
+      super.emit("readable");
+    } else if (isEndish(ev) && this[EMITTED_END]) {
+      super.emit(ev);
+      this.removeAllListeners(ev);
+    } else if (ev === "error" && this[EMITTED_ERROR]) {
+      const h = handler;
+      if (this[ASYNC])
+        defer(() => h.call(this, this[EMITTED_ERROR]));
+      else
+        h.call(this, this[EMITTED_ERROR]);
+    }
+    return ret;
+  }
+  /**
+   * Alias for {@link Minipass#off}
+   */
+  removeListener(ev, handler) {
+    return this.off(ev, handler);
+  }
+  /**
+   * Mostly identical to `EventEmitter.off`
+   *
+   * If a 'data' event handler is removed, and it was the last consumer
+   * (ie, there are no pipe destinations or other 'data' event listeners),
+   * then the flow of data will stop until there is another consumer or
+   * {@link Minipass#resume} is explicitly called.
+   */
+  off(ev, handler) {
+    const ret = super.off(ev, handler);
+    if (ev === "data") {
+      this[DATALISTENERS] = this.listeners("data").length;
+      if (this[DATALISTENERS] === 0 && !this[DISCARDED] && !this[PIPES].length) {
+        this[FLOWING] = false;
+      }
+    }
+    return ret;
+  }
+  /**
+   * Mostly identical to `EventEmitter.removeAllListeners`
+   *
+   * If all 'data' event handlers are removed, and they were the last consumer
+   * (ie, there are no pipe destinations), then the flow of data will stop
+   * until there is another consumer or {@link Minipass#resume} is explicitly
+   * called.
+   */
+  removeAllListeners(ev) {
+    const ret = super.removeAllListeners(ev);
+    if (ev === "data" || ev === void 0) {
+      this[DATALISTENERS] = 0;
+      if (!this[DISCARDED] && !this[PIPES].length) {
+        this[FLOWING] = false;
+      }
+    }
+    return ret;
+  }
+  /**
+   * true if the 'end' event has been emitted
+   */
+  get emittedEnd() {
+    return this[EMITTED_END];
+  }
+  [MAYBE_EMIT_END]() {
+    if (!this[EMITTING_END] && !this[EMITTED_END] && !this[DESTROYED] && this[BUFFER].length === 0 && this[EOF]) {
+      this[EMITTING_END] = true;
+      this.emit("end");
+      this.emit("prefinish");
+      this.emit("finish");
+      if (this[CLOSED])
+        this.emit("close");
+      this[EMITTING_END] = false;
+    }
+  }
+  /**
+   * Mostly identical to `EventEmitter.emit`, with the following
+   * behavior differences to prevent data loss and unnecessary hangs:
+   *
+   * If the stream has been destroyed, and the event is something other
+   * than 'close' or 'error', then `false` is returned and no handlers
+   * are called.
+   *
+   * If the event is 'end', and has already been emitted, then the event
+   * is ignored. If the stream is in a paused or non-flowing state, then
+   * the event will be deferred until data flow resumes. If the stream is
+   * async, then handlers will be called on the next tick rather than
+   * immediately.
+   *
+   * If the event is 'close', and 'end' has not yet been emitted, then
+   * the event will be deferred until after 'end' is emitted.
+   *
+   * If the event is 'error', and an AbortSignal was provided for the stream,
+   * and there are no listeners, then the event is ignored, matching the
+   * behavior of node core streams in the presense of an AbortSignal.
+   *
+   * If the event is 'finish' or 'prefinish', then all listeners will be
+   * removed after emitting the event, to prevent double-firing.
+   */
+  emit(ev, ...args) {
+    const data = args[0];
+    if (ev !== "error" && ev !== "close" && ev !== DESTROYED && this[DESTROYED]) {
+      return false;
+    } else if (ev === "data") {
+      return !this[OBJECTMODE] && !data ? false : this[ASYNC] ? (defer(() => this[EMITDATA](data)), true) : this[EMITDATA](data);
+    } else if (ev === "end") {
+      return this[EMITEND]();
+    } else if (ev === "close") {
+      this[CLOSED] = true;
+      if (!this[EMITTED_END] && !this[DESTROYED])
+        return false;
+      const ret2 = super.emit("close");
+      this.removeAllListeners("close");
+      return ret2;
+    } else if (ev === "error") {
+      this[EMITTED_ERROR] = data;
+      super.emit(ERROR, data);
+      const ret2 = !this[SIGNAL] || this.listeners("error").length ? super.emit("error", data) : false;
+      this[MAYBE_EMIT_END]();
+      return ret2;
+    } else if (ev === "resume") {
+      const ret2 = super.emit("resume");
+      this[MAYBE_EMIT_END]();
+      return ret2;
+    } else if (ev === "finish" || ev === "prefinish") {
+      const ret2 = super.emit(ev);
+      this.removeAllListeners(ev);
+      return ret2;
+    }
+    const ret = super.emit(ev, ...args);
+    this[MAYBE_EMIT_END]();
+    return ret;
+  }
+  [EMITDATA](data) {
+    for (const p of this[PIPES]) {
+      if (p.dest.write(data) === false)
+        this.pause();
+    }
+    const ret = this[DISCARDED] ? false : super.emit("data", data);
+    this[MAYBE_EMIT_END]();
+    return ret;
+  }
+  [EMITEND]() {
+    if (this[EMITTED_END])
+      return false;
+    this[EMITTED_END] = true;
+    this.readable = false;
+    return this[ASYNC] ? (defer(() => this[EMITEND2]()), true) : this[EMITEND2]();
+  }
+  [EMITEND2]() {
+    if (this[DECODER]) {
+      const data = this[DECODER].end();
+      if (data) {
+        for (const p of this[PIPES]) {
+          p.dest.write(data);
+        }
+        if (!this[DISCARDED])
+          super.emit("data", data);
+      }
+    }
+    for (const p of this[PIPES]) {
+      p.end();
+    }
+    const ret = super.emit("end");
+    this.removeAllListeners("end");
+    return ret;
+  }
+  /**
+   * Return a Promise that resolves to an array of all emitted data once
+   * the stream ends.
+   */
+  async collect() {
+    const buf = Object.assign([], {
+      dataLength: 0
+    });
+    if (!this[OBJECTMODE])
+      buf.dataLength = 0;
+    const p = this.promise();
+    this.on("data", (c) => {
+      buf.push(c);
+      if (!this[OBJECTMODE])
+        buf.dataLength += c.length;
+    });
+    await p;
+    return buf;
+  }
+  /**
+   * Return a Promise that resolves to the concatenation of all emitted data
+   * once the stream ends.
+   *
+   * Not allowed on objectMode streams.
+   */
+  async concat() {
+    if (this[OBJECTMODE]) {
+      throw new Error("cannot concat in objectMode");
+    }
+    const buf = await this.collect();
+    return this[ENCODING] ? buf.join("") : Buffer.concat(buf, buf.dataLength);
+  }
+  /**
+   * Return a void Promise that resolves once the stream ends.
+   */
+  async promise() {
+    return new Promise((resolve, reject) => {
+      this.on(DESTROYED, () => reject(new Error("stream destroyed")));
+      this.on("error", (er) => reject(er));
+      this.on("end", () => resolve());
+    });
+  }
+  /**
+   * Asynchronous `for await of` iteration.
+   *
+   * This will continue emitting all chunks until the stream terminates.
+   */
+  [Symbol.asyncIterator]() {
+    this[DISCARDED] = false;
+    let stopped = false;
+    const stop = async () => {
+      this.pause();
+      stopped = true;
+      return { value: void 0, done: true };
+    };
+    const next = () => {
+      if (stopped)
+        return stop();
+      const res = this.read();
+      if (res !== null)
+        return Promise.resolve({ done: false, value: res });
+      if (this[EOF])
+        return stop();
+      let resolve;
+      let reject;
+      const onerr = (er) => {
+        this.off("data", ondata);
+        this.off("end", onend);
+        this.off(DESTROYED, ondestroy);
+        stop();
+        reject(er);
+      };
+      const ondata = (value) => {
+        this.off("error", onerr);
+        this.off("end", onend);
+        this.off(DESTROYED, ondestroy);
+        this.pause();
+        resolve({ value, done: !!this[EOF] });
+      };
+      const onend = () => {
+        this.off("error", onerr);
+        this.off("data", ondata);
+        this.off(DESTROYED, ondestroy);
+        stop();
+        resolve({ done: true, value: void 0 });
+      };
+      const ondestroy = () => onerr(new Error("stream destroyed"));
+      return new Promise((res2, rej) => {
+        reject = rej;
+        resolve = res2;
+        this.once(DESTROYED, ondestroy);
+        this.once("error", onerr);
+        this.once("end", onend);
+        this.once("data", ondata);
+      });
+    };
+    return {
+      next,
+      throw: stop,
+      return: stop,
+      [Symbol.asyncIterator]() {
+        return this;
+      },
+      [Symbol.asyncDispose]: async () => {
+      }
+    };
+  }
+  /**
+   * Synchronous `for of` iteration.
+   *
+   * The iteration will terminate when the internal buffer runs out, even
+   * if the stream has not yet terminated.
+   */
+  [Symbol.iterator]() {
+    this[DISCARDED] = false;
+    let stopped = false;
+    const stop = () => {
+      this.pause();
+      this.off(ERROR, stop);
+      this.off(DESTROYED, stop);
+      this.off("end", stop);
+      stopped = true;
+      return { done: true, value: void 0 };
+    };
+    const next = () => {
+      if (stopped)
+        return stop();
+      const value = this.read();
+      return value === null ? stop() : { done: false, value };
+    };
+    this.once("end", stop);
+    this.once(ERROR, stop);
+    this.once(DESTROYED, stop);
+    return {
+      next,
+      throw: stop,
+      return: stop,
+      [Symbol.iterator]() {
+        return this;
+      },
+      [Symbol.dispose]: () => {
+      }
+    };
+  }
+  /**
+   * Destroy a stream, preventing it from being used for any further purpose.
+   *
+   * If the stream has a `close()` method, then it will be called on
+   * destruction.
+   *
+   * After destruction, any attempt to write data, read data, or emit most
+   * events will be ignored.
+   *
+   * If an error argument is provided, then it will be emitted in an
+   * 'error' event.
+   */
+  destroy(er) {
+    if (this[DESTROYED]) {
+      if (er)
+        this.emit("error", er);
+      else
+        this.emit(DESTROYED);
+      return this;
+    }
+    this[DESTROYED] = true;
+    this[DISCARDED] = true;
+    this[BUFFER].length = 0;
+    this[BUFFERLENGTH] = 0;
+    const wc = this;
+    if (typeof wc.close === "function" && !this[CLOSED])
+      wc.close();
+    if (er)
+      this.emit("error", er);
+    else
+      this.emit(DESTROYED);
+    return this;
+  }
+  /**
+   * Alias for {@link isStream}
+   *
+   * Former export location, maintained for backwards compatibility.
+   *
+   * @deprecated
+   */
+  static get isStream() {
+    return isStream;
+  }
+};
+
+// node_modules/path-scurry/dist/esm/index.js
+var realpathSync = rps.native;
+var defaultFS = {
+  lstatSync,
+  readdir: readdirCB,
+  readdirSync,
+  readlinkSync,
+  realpathSync,
+  promises: {
+    lstat,
+    readdir,
+    readlink,
+    realpath
+  }
+};
+var fsFromOption = (fsOption) => !fsOption || fsOption === defaultFS || fsOption === actualFS ? defaultFS : {
+  ...defaultFS,
+  ...fsOption,
+  promises: {
+    ...defaultFS.promises,
+    ...fsOption.promises || {}
+  }
+};
+var uncDriveRegexp = /^\\\\\?\\([a-z]:)\\?$/i;
+var uncToDrive = (rootPath) => rootPath.replace(/\//g, "\\").replace(uncDriveRegexp, "$1\\");
+var eitherSep = /[\\\/]/;
+var UNKNOWN = 0;
+var IFIFO = 1;
+var IFCHR = 2;
+var IFDIR = 4;
+var IFBLK = 6;
+var IFREG = 8;
+var IFLNK = 10;
+var IFSOCK = 12;
+var IFMT = 15;
+var IFMT_UNKNOWN = ~IFMT;
+var READDIR_CALLED = 16;
+var LSTAT_CALLED = 32;
+var ENOTDIR = 64;
+var ENOENT = 128;
+var ENOREADLINK = 256;
+var ENOREALPATH = 512;
+var ENOCHILD = ENOTDIR | ENOENT | ENOREALPATH;
+var TYPEMASK = 1023;
+var entToType = (s) => s.isFile() ? IFREG : s.isDirectory() ? IFDIR : s.isSymbolicLink() ? IFLNK : s.isCharacterDevice() ? IFCHR : s.isBlockDevice() ? IFBLK : s.isSocket() ? IFSOCK : s.isFIFO() ? IFIFO : UNKNOWN;
+var normalizeCache = new L({ max: 2 ** 12 });
+var normalize = (s) => {
+  const c = normalizeCache.get(s);
+  if (c)
+    return c;
+  const n = s.normalize("NFKD");
+  normalizeCache.set(s, n);
+  return n;
+};
+var normalizeNocaseCache = new L({ max: 2 ** 12 });
+var normalizeNocase = (s) => {
+  const c = normalizeNocaseCache.get(s);
+  if (c)
+    return c;
+  const n = normalize(s.toLowerCase());
+  normalizeNocaseCache.set(s, n);
+  return n;
+};
+var ResolveCache = class extends L {
+  constructor() {
+    super({ max: 256 });
+  }
+};
+var ChildrenCache = class extends L {
+  constructor(maxSize = 16 * 1024) {
+    super({
+      maxSize,
+      // parent + children
+      sizeCalculation: (a) => a.length + 1
+    });
+  }
+};
+var setAsCwd = /* @__PURE__ */ Symbol("PathScurry setAsCwd");
+var PathBase = class {
+  /**
+   * the basename of this path
+   *
+   * **Important**: *always* test the path name against any test string
+   * usingthe {@link isNamed} method, and not by directly comparing this
+   * string. Otherwise, unicode path strings that the system sees as identical
+   * will not be properly treated as the same path, leading to incorrect
+   * behavior and possible security issues.
+   */
+  name;
+  /**
+   * the Path entry corresponding to the path root.
+   *
+   * @internal
+   */
+  root;
+  /**
+   * All roots found within the current PathScurry family
+   *
+   * @internal
+   */
+  roots;
+  /**
+   * a reference to the parent path, or undefined in the case of root entries
+   *
+   * @internal
+   */
+  parent;
+  /**
+   * boolean indicating whether paths are compared case-insensitively
+   * @internal
+   */
+  nocase;
+  /**
+   * boolean indicating that this path is the current working directory
+   * of the PathScurry collection that contains it.
+   */
+  isCWD = false;
+  // potential default fs override
+  #fs;
+  // Stats fields
+  #dev;
+  get dev() {
+    return this.#dev;
+  }
+  #mode;
+  get mode() {
+    return this.#mode;
+  }
+  #nlink;
+  get nlink() {
+    return this.#nlink;
+  }
+  #uid;
+  get uid() {
+    return this.#uid;
+  }
+  #gid;
+  get gid() {
+    return this.#gid;
+  }
+  #rdev;
+  get rdev() {
+    return this.#rdev;
+  }
+  #blksize;
+  get blksize() {
+    return this.#blksize;
+  }
+  #ino;
+  get ino() {
+    return this.#ino;
+  }
+  #size;
+  get size() {
+    return this.#size;
+  }
+  #blocks;
+  get blocks() {
+    return this.#blocks;
+  }
+  #atimeMs;
+  get atimeMs() {
+    return this.#atimeMs;
+  }
+  #mtimeMs;
+  get mtimeMs() {
+    return this.#mtimeMs;
+  }
+  #ctimeMs;
+  get ctimeMs() {
+    return this.#ctimeMs;
+  }
+  #birthtimeMs;
+  get birthtimeMs() {
+    return this.#birthtimeMs;
+  }
+  #atime;
+  get atime() {
+    return this.#atime;
+  }
+  #mtime;
+  get mtime() {
+    return this.#mtime;
+  }
+  #ctime;
+  get ctime() {
+    return this.#ctime;
+  }
+  #birthtime;
+  get birthtime() {
+    return this.#birthtime;
+  }
+  #matchName;
+  #depth;
+  #fullpath;
+  #fullpathPosix;
+  #relative;
+  #relativePosix;
+  #type;
+  #children;
+  #linkTarget;
+  #realpath;
+  /**
+   * This property is for compatibility with the Dirent class as of
+   * Node v20, where Dirent['parentPath'] refers to the path of the
+   * directory that was passed to readdir. For root entries, it's the path
+   * to the entry itself.
+   */
+  get parentPath() {
+    return (this.parent || this).fullpath();
+  }
+  /* c8 ignore start */
+  /**
+   * Deprecated alias for Dirent['parentPath'] Somewhat counterintuitively,
+   * this property refers to the *parent* path, not the path object itself.
+   *
+   * @deprecated
+   */
+  get path() {
+    return this.parentPath;
+  }
+  /* c8 ignore stop */
+  /**
+   * Do not create new Path objects directly.  They should always be accessed
+   * via the PathScurry class or other methods on the Path class.
+   *
+   * @internal
+   */
+  constructor(name, type = UNKNOWN, root, roots, nocase, children, opts) {
+    this.name = name;
+    this.#matchName = nocase ? normalizeNocase(name) : normalize(name);
+    this.#type = type & TYPEMASK;
+    this.nocase = nocase;
+    this.roots = roots;
+    this.root = root || this;
+    this.#children = children;
+    this.#fullpath = opts.fullpath;
+    this.#relative = opts.relative;
+    this.#relativePosix = opts.relativePosix;
+    this.parent = opts.parent;
+    if (this.parent) {
+      this.#fs = this.parent.#fs;
+    } else {
+      this.#fs = fsFromOption(opts.fs);
+    }
+  }
+  /**
+   * Returns the depth of the Path object from its root.
+   *
+   * For example, a path at `/foo/bar` would have a depth of 2.
+   */
+  depth() {
+    if (this.#depth !== void 0)
+      return this.#depth;
+    if (!this.parent)
+      return this.#depth = 0;
+    return this.#depth = this.parent.depth() + 1;
+  }
+  /**
+   * @internal
+   */
+  childrenCache() {
+    return this.#children;
+  }
+  /**
+   * Get the Path object referenced by the string path, resolved from this Path
+   */
+  resolve(path2) {
+    if (!path2) {
+      return this;
+    }
+    const rootPath = this.getRootString(path2);
+    const dir = path2.substring(rootPath.length);
+    const dirParts = dir.split(this.splitSep);
+    const result = rootPath ? this.getRoot(rootPath).#resolveParts(dirParts) : this.#resolveParts(dirParts);
+    return result;
+  }
+  #resolveParts(dirParts) {
+    let p = this;
+    for (const part of dirParts) {
+      p = p.child(part);
+    }
+    return p;
+  }
+  /**
+   * Returns the cached children Path objects, if still available.  If they
+   * have fallen out of the cache, then returns an empty array, and resets the
+   * READDIR_CALLED bit, so that future calls to readdir() will require an fs
+   * lookup.
+   *
+   * @internal
+   */
+  children() {
+    const cached = this.#children.get(this);
+    if (cached) {
+      return cached;
+    }
+    const children = Object.assign([], { provisional: 0 });
+    this.#children.set(this, children);
+    this.#type &= ~READDIR_CALLED;
+    return children;
+  }
+  /**
+   * Resolves a path portion and returns or creates the child Path.
+   *
+   * Returns `this` if pathPart is `''` or `'.'`, or `parent` if pathPart is
+   * `'..'`.
+   *
+   * This should not be called directly.  If `pathPart` contains any path
+   * separators, it will lead to unsafe undefined behavior.
+   *
+   * Use `Path.resolve()` instead.
+   *
+   * @internal
+   */
+  child(pathPart, opts) {
+    if (pathPart === "" || pathPart === ".") {
+      return this;
+    }
+    if (pathPart === "..") {
+      return this.parent || this;
+    }
+    const children = this.children();
+    const name = this.nocase ? normalizeNocase(pathPart) : normalize(pathPart);
+    for (const p of children) {
+      if (p.#matchName === name) {
+        return p;
+      }
+    }
+    const s = this.parent ? this.sep : "";
+    const fullpath = this.#fullpath ? this.#fullpath + s + pathPart : void 0;
+    const pchild = this.newChild(pathPart, UNKNOWN, {
+      ...opts,
+      parent: this,
+      fullpath
+    });
+    if (!this.canReaddir()) {
+      pchild.#type |= ENOENT;
+    }
+    children.push(pchild);
+    return pchild;
+  }
+  /**
+   * The relative path from the cwd. If it does not share an ancestor with
+   * the cwd, then this ends up being equivalent to the fullpath()
+   */
+  relative() {
+    if (this.isCWD)
+      return "";
+    if (this.#relative !== void 0) {
+      return this.#relative;
+    }
+    const name = this.name;
+    const p = this.parent;
+    if (!p) {
+      return this.#relative = this.name;
+    }
+    const pv = p.relative();
+    return pv + (!pv || !p.parent ? "" : this.sep) + name;
+  }
+  /**
+   * The relative path from the cwd, using / as the path separator.
+   * If it does not share an ancestor with
+   * the cwd, then this ends up being equivalent to the fullpathPosix()
+   * On posix systems, this is identical to relative().
+   */
+  relativePosix() {
+    if (this.sep === "/")
+      return this.relative();
+    if (this.isCWD)
+      return "";
+    if (this.#relativePosix !== void 0)
+      return this.#relativePosix;
+    const name = this.name;
+    const p = this.parent;
+    if (!p) {
+      return this.#relativePosix = this.fullpathPosix();
+    }
+    const pv = p.relativePosix();
+    return pv + (!pv || !p.parent ? "" : "/") + name;
+  }
+  /**
+   * The fully resolved path string for this Path entry
+   */
+  fullpath() {
+    if (this.#fullpath !== void 0) {
+      return this.#fullpath;
+    }
+    const name = this.name;
+    const p = this.parent;
+    if (!p) {
+      return this.#fullpath = this.name;
+    }
+    const pv = p.fullpath();
+    const fp = pv + (!p.parent ? "" : this.sep) + name;
+    return this.#fullpath = fp;
+  }
+  /**
+   * On platforms other than windows, this is identical to fullpath.
+   *
+   * On windows, this is overridden to return the forward-slash form of the
+   * full UNC path.
+   */
+  fullpathPosix() {
+    if (this.#fullpathPosix !== void 0)
+      return this.#fullpathPosix;
+    if (this.sep === "/")
+      return this.#fullpathPosix = this.fullpath();
+    if (!this.parent) {
+      const p2 = this.fullpath().replace(/\\/g, "/");
+      if (/^[a-z]:\//i.test(p2)) {
+        return this.#fullpathPosix = `//?/${p2}`;
+      } else {
+        return this.#fullpathPosix = p2;
+      }
+    }
+    const p = this.parent;
+    const pfpp = p.fullpathPosix();
+    const fpp = pfpp + (!pfpp || !p.parent ? "" : "/") + this.name;
+    return this.#fullpathPosix = fpp;
+  }
+  /**
+   * Is the Path of an unknown type?
+   *
+   * Note that we might know *something* about it if there has been a previous
+   * filesystem operation, for example that it does not exist, or is not a
+   * link, or whether it has child entries.
+   */
+  isUnknown() {
+    return (this.#type & IFMT) === UNKNOWN;
+  }
+  isType(type) {
+    return this[`is${type}`]();
+  }
+  getType() {
+    return this.isUnknown() ? "Unknown" : this.isDirectory() ? "Directory" : this.isFile() ? "File" : this.isSymbolicLink() ? "SymbolicLink" : this.isFIFO() ? "FIFO" : this.isCharacterDevice() ? "CharacterDevice" : this.isBlockDevice() ? "BlockDevice" : (
+      /* c8 ignore start */
+      this.isSocket() ? "Socket" : "Unknown"
+    );
+  }
+  /**
+   * Is the Path a regular file?
+   */
+  isFile() {
+    return (this.#type & IFMT) === IFREG;
+  }
+  /**
+   * Is the Path a directory?
+   */
+  isDirectory() {
+    return (this.#type & IFMT) === IFDIR;
+  }
+  /**
+   * Is the path a character device?
+   */
+  isCharacterDevice() {
+    return (this.#type & IFMT) === IFCHR;
+  }
+  /**
+   * Is the path a block device?
+   */
+  isBlockDevice() {
+    return (this.#type & IFMT) === IFBLK;
+  }
+  /**
+   * Is the path a FIFO pipe?
+   */
+  isFIFO() {
+    return (this.#type & IFMT) === IFIFO;
+  }
+  /**
+   * Is the path a socket?
+   */
+  isSocket() {
+    return (this.#type & IFMT) === IFSOCK;
+  }
+  /**
+   * Is the path a symbolic link?
+   */
+  isSymbolicLink() {
+    return (this.#type & IFLNK) === IFLNK;
+  }
+  /**
+   * Return the entry if it has been subject of a successful lstat, or
+   * undefined otherwise.
+   *
+   * Does not read the filesystem, so an undefined result *could* simply
+   * mean that we haven't called lstat on it.
+   */
+  lstatCached() {
+    return this.#type & LSTAT_CALLED ? this : void 0;
+  }
+  /**
+   * Return the cached link target if the entry has been the subject of a
+   * successful readlink, or undefined otherwise.
+   *
+   * Does not read the filesystem, so an undefined result *could* just mean we
+   * don't have any cached data. Only use it if you are very sure that a
+   * readlink() has been called at some point.
+   */
+  readlinkCached() {
+    return this.#linkTarget;
+  }
+  /**
+   * Returns the cached realpath target if the entry has been the subject
+   * of a successful realpath, or undefined otherwise.
+   *
+   * Does not read the filesystem, so an undefined result *could* just mean we
+   * don't have any cached data. Only use it if you are very sure that a
+   * realpath() has been called at some point.
+   */
+  realpathCached() {
+    return this.#realpath;
+  }
+  /**
+   * Returns the cached child Path entries array if the entry has been the
+   * subject of a successful readdir(), or [] otherwise.
+   *
+   * Does not read the filesystem, so an empty array *could* just mean we
+   * don't have any cached data. Only use it if you are very sure that a
+   * readdir() has been called recently enough to still be valid.
+   */
+  readdirCached() {
+    const children = this.children();
+    return children.slice(0, children.provisional);
+  }
+  /**
+   * Return true if it's worth trying to readlink.  Ie, we don't (yet) have
+   * any indication that readlink will definitely fail.
+   *
+   * Returns false if the path is known to not be a symlink, if a previous
+   * readlink failed, or if the entry does not exist.
+   */
+  canReadlink() {
+    if (this.#linkTarget)
+      return true;
+    if (!this.parent)
+      return false;
+    const ifmt = this.#type & IFMT;
+    return !(ifmt !== UNKNOWN && ifmt !== IFLNK || this.#type & ENOREADLINK || this.#type & ENOENT);
+  }
+  /**
+   * Return true if readdir has previously been successfully called on this
+   * path, indicating that cachedReaddir() is likely valid.
+   */
+  calledReaddir() {
+    return !!(this.#type & READDIR_CALLED);
+  }
+  /**
+   * Returns true if the path is known to not exist. That is, a previous lstat
+   * or readdir failed to verify its existence when that would have been
+   * expected, or a parent entry was marked either enoent or enotdir.
+   */
+  isENOENT() {
+    return !!(this.#type & ENOENT);
+  }
+  /**
+   * Return true if the path is a match for the given path name.  This handles
+   * case sensitivity and unicode normalization.
+   *
+   * Note: even on case-sensitive systems, it is **not** safe to test the
+   * equality of the `.name` property to determine whether a given pathname
+   * matches, due to unicode normalization mismatches.
+   *
+   * Always use this method instead of testing the `path.name` property
+   * directly.
+   */
+  isNamed(n) {
+    return !this.nocase ? this.#matchName === normalize(n) : this.#matchName === normalizeNocase(n);
+  }
+  /**
+   * Return the Path object corresponding to the target of a symbolic link.
+   *
+   * If the Path is not a symbolic link, or if the readlink call fails for any
+   * reason, `undefined` is returned.
+   *
+   * Result is cached, and thus may be outdated if the filesystem is mutated.
+   */
+  async readlink() {
+    const target = this.#linkTarget;
+    if (target) {
+      return target;
+    }
+    if (!this.canReadlink()) {
+      return void 0;
+    }
+    if (!this.parent) {
+      return void 0;
+    }
+    try {
+      const read = await this.#fs.promises.readlink(this.fullpath());
+      const linkTarget = (await this.parent.realpath())?.resolve(read);
+      if (linkTarget) {
+        return this.#linkTarget = linkTarget;
+      }
+    } catch (er) {
+      this.#readlinkFail(er.code);
+      return void 0;
+    }
+  }
+  /**
+   * Synchronous {@link PathBase.readlink}
+   */
+  readlinkSync() {
+    const target = this.#linkTarget;
+    if (target) {
+      return target;
+    }
+    if (!this.canReadlink()) {
+      return void 0;
+    }
+    if (!this.parent) {
+      return void 0;
+    }
+    try {
+      const read = this.#fs.readlinkSync(this.fullpath());
+      const linkTarget = this.parent.realpathSync()?.resolve(read);
+      if (linkTarget) {
+        return this.#linkTarget = linkTarget;
+      }
+    } catch (er) {
+      this.#readlinkFail(er.code);
+      return void 0;
+    }
+  }
+  #readdirSuccess(children) {
+    this.#type |= READDIR_CALLED;
+    for (let p = children.provisional; p < children.length; p++) {
+      const c = children[p];
+      if (c)
+        c.#markENOENT();
+    }
+  }
+  #markENOENT() {
+    if (this.#type & ENOENT)
+      return;
+    this.#type = (this.#type | ENOENT) & IFMT_UNKNOWN;
+    this.#markChildrenENOENT();
+  }
+  #markChildrenENOENT() {
+    const children = this.children();
+    children.provisional = 0;
+    for (const p of children) {
+      p.#markENOENT();
+    }
+  }
+  #markENOREALPATH() {
+    this.#type |= ENOREALPATH;
+    this.#markENOTDIR();
+  }
+  // save the information when we know the entry is not a dir
+  #markENOTDIR() {
+    if (this.#type & ENOTDIR)
+      return;
+    let t = this.#type;
+    if ((t & IFMT) === IFDIR)
+      t &= IFMT_UNKNOWN;
+    this.#type = t | ENOTDIR;
+    this.#markChildrenENOENT();
+  }
+  #readdirFail(code = "") {
+    if (code === "ENOTDIR" || code === "EPERM") {
+      this.#markENOTDIR();
+    } else if (code === "ENOENT") {
+      this.#markENOENT();
+    } else {
+      this.children().provisional = 0;
+    }
+  }
+  #lstatFail(code = "") {
+    if (code === "ENOTDIR") {
+      const p = this.parent;
+      p.#markENOTDIR();
+    } else if (code === "ENOENT") {
+      this.#markENOENT();
+    }
+  }
+  #readlinkFail(code = "") {
+    let ter = this.#type;
+    ter |= ENOREADLINK;
+    if (code === "ENOENT")
+      ter |= ENOENT;
+    if (code === "EINVAL" || code === "UNKNOWN") {
+      ter &= IFMT_UNKNOWN;
+    }
+    this.#type = ter;
+    if (code === "ENOTDIR" && this.parent) {
+      this.parent.#markENOTDIR();
+    }
+  }
+  #readdirAddChild(e, c) {
+    return this.#readdirMaybePromoteChild(e, c) || this.#readdirAddNewChild(e, c);
+  }
+  #readdirAddNewChild(e, c) {
+    const type = entToType(e);
+    const child = this.newChild(e.name, type, { parent: this });
+    const ifmt = child.#type & IFMT;
+    if (ifmt !== IFDIR && ifmt !== IFLNK && ifmt !== UNKNOWN) {
+      child.#type |= ENOTDIR;
+    }
+    c.unshift(child);
+    c.provisional++;
+    return child;
+  }
+  #readdirMaybePromoteChild(e, c) {
+    for (let p = c.provisional; p < c.length; p++) {
+      const pchild = c[p];
+      const name = this.nocase ? normalizeNocase(e.name) : normalize(e.name);
+      if (name !== pchild.#matchName) {
+        continue;
+      }
+      return this.#readdirPromoteChild(e, pchild, p, c);
+    }
+  }
+  #readdirPromoteChild(e, p, index, c) {
+    const v = p.name;
+    p.#type = p.#type & IFMT_UNKNOWN | entToType(e);
+    if (v !== e.name)
+      p.name = e.name;
+    if (index !== c.provisional) {
+      if (index === c.length - 1)
+        c.pop();
+      else
+        c.splice(index, 1);
+      c.unshift(p);
+    }
+    c.provisional++;
+    return p;
+  }
+  /**
+   * Call lstat() on this Path, and update all known information that can be
+   * determined.
+   *
+   * Note that unlike `fs.lstat()`, the returned value does not contain some
+   * information, such as `mode`, `dev`, `nlink`, and `ino`.  If that
+   * information is required, you will need to call `fs.lstat` yourself.
+   *
+   * If the Path refers to a nonexistent file, or if the lstat call fails for
+   * any reason, `undefined` is returned.  Otherwise the updated Path object is
+   * returned.
+   *
+   * Results are cached, and thus may be out of date if the filesystem is
+   * mutated.
+   */
+  async lstat() {
+    if ((this.#type & ENOENT) === 0) {
+      try {
+        this.#applyStat(await this.#fs.promises.lstat(this.fullpath()));
+        return this;
+      } catch (er) {
+        this.#lstatFail(er.code);
+      }
+    }
+  }
+  /**
+   * synchronous {@link PathBase.lstat}
+   */
+  lstatSync() {
+    if ((this.#type & ENOENT) === 0) {
+      try {
+        this.#applyStat(this.#fs.lstatSync(this.fullpath()));
+        return this;
+      } catch (er) {
+        this.#lstatFail(er.code);
+      }
+    }
+  }
+  #applyStat(st) {
+    const { atime, atimeMs, birthtime, birthtimeMs, blksize, blocks, ctime, ctimeMs, dev, gid, ino, mode, mtime, mtimeMs, nlink, rdev, size, uid } = st;
+    this.#atime = atime;
+    this.#atimeMs = atimeMs;
+    this.#birthtime = birthtime;
+    this.#birthtimeMs = birthtimeMs;
+    this.#blksize = blksize;
+    this.#blocks = blocks;
+    this.#ctime = ctime;
+    this.#ctimeMs = ctimeMs;
+    this.#dev = dev;
+    this.#gid = gid;
+    this.#ino = ino;
+    this.#mode = mode;
+    this.#mtime = mtime;
+    this.#mtimeMs = mtimeMs;
+    this.#nlink = nlink;
+    this.#rdev = rdev;
+    this.#size = size;
+    this.#uid = uid;
+    const ifmt = entToType(st);
+    this.#type = this.#type & IFMT_UNKNOWN | ifmt | LSTAT_CALLED;
+    if (ifmt !== UNKNOWN && ifmt !== IFDIR && ifmt !== IFLNK) {
+      this.#type |= ENOTDIR;
+    }
+  }
+  #onReaddirCB = [];
+  #readdirCBInFlight = false;
+  #callOnReaddirCB(children) {
+    this.#readdirCBInFlight = false;
+    const cbs = this.#onReaddirCB.slice();
+    this.#onReaddirCB.length = 0;
+    cbs.forEach((cb) => cb(null, children));
+  }
+  /**
+   * Standard node-style callback interface to get list of directory entries.
+   *
+   * If the Path cannot or does not contain any children, then an empty array
+   * is returned.
+   *
+   * Results are cached, and thus may be out of date if the filesystem is
+   * mutated.
+   *
+   * @param cb The callback called with (er, entries).  Note that the `er`
+   * param is somewhat extraneous, as all readdir() errors are handled and
+   * simply result in an empty set of entries being returned.
+   * @param allowZalgo Boolean indicating that immediately known results should
+   * *not* be deferred with `queueMicrotask`. Defaults to `false`. Release
+   * zalgo at your peril, the dark pony lord is devious and unforgiving.
+   */
+  readdirCB(cb, allowZalgo = false) {
+    if (!this.canReaddir()) {
+      if (allowZalgo)
+        cb(null, []);
+      else
+        queueMicrotask(() => cb(null, []));
+      return;
+    }
+    const children = this.children();
+    if (this.calledReaddir()) {
+      const c = children.slice(0, children.provisional);
+      if (allowZalgo)
+        cb(null, c);
+      else
+        queueMicrotask(() => cb(null, c));
+      return;
+    }
+    this.#onReaddirCB.push(cb);
+    if (this.#readdirCBInFlight) {
+      return;
+    }
+    this.#readdirCBInFlight = true;
+    const fullpath = this.fullpath();
+    this.#fs.readdir(fullpath, { withFileTypes: true }, (er, entries) => {
+      if (er) {
+        this.#readdirFail(er.code);
+        children.provisional = 0;
+      } else {
+        for (const e of entries) {
+          this.#readdirAddChild(e, children);
+        }
+        this.#readdirSuccess(children);
+      }
+      this.#callOnReaddirCB(children.slice(0, children.provisional));
+      return;
+    });
+  }
+  #asyncReaddirInFlight;
+  /**
+   * Return an array of known child entries.
+   *
+   * If the Path cannot or does not contain any children, then an empty array
+   * is returned.
+   *
+   * Results are cached, and thus may be out of date if the filesystem is
+   * mutated.
+   */
+  async readdir() {
+    if (!this.canReaddir()) {
+      return [];
+    }
+    const children = this.children();
+    if (this.calledReaddir()) {
+      return children.slice(0, children.provisional);
+    }
+    const fullpath = this.fullpath();
+    if (this.#asyncReaddirInFlight) {
+      await this.#asyncReaddirInFlight;
+    } else {
+      let resolve = () => {
+      };
+      this.#asyncReaddirInFlight = new Promise((res) => resolve = res);
+      try {
+        for (const e of await this.#fs.promises.readdir(fullpath, {
+          withFileTypes: true
+        })) {
+          this.#readdirAddChild(e, children);
+        }
+        this.#readdirSuccess(children);
+      } catch (er) {
+        this.#readdirFail(er.code);
+        children.provisional = 0;
+      }
+      this.#asyncReaddirInFlight = void 0;
+      resolve();
+    }
+    return children.slice(0, children.provisional);
+  }
+  /**
+   * synchronous {@link PathBase.readdir}
+   */
+  readdirSync() {
+    if (!this.canReaddir()) {
+      return [];
+    }
+    const children = this.children();
+    if (this.calledReaddir()) {
+      return children.slice(0, children.provisional);
+    }
+    const fullpath = this.fullpath();
+    try {
+      for (const e of this.#fs.readdirSync(fullpath, {
+        withFileTypes: true
+      })) {
+        this.#readdirAddChild(e, children);
+      }
+      this.#readdirSuccess(children);
+    } catch (er) {
+      this.#readdirFail(er.code);
+      children.provisional = 0;
+    }
+    return children.slice(0, children.provisional);
+  }
+  canReaddir() {
+    if (this.#type & ENOCHILD)
+      return false;
+    const ifmt = IFMT & this.#type;
+    if (!(ifmt === UNKNOWN || ifmt === IFDIR || ifmt === IFLNK)) {
+      return false;
+    }
+    return true;
+  }
+  shouldWalk(dirs, walkFilter) {
+    return (this.#type & IFDIR) === IFDIR && !(this.#type & ENOCHILD) && !dirs.has(this) && (!walkFilter || walkFilter(this));
+  }
+  /**
+   * Return the Path object corresponding to path as resolved
+   * by realpath(3).
+   *
+   * If the realpath call fails for any reason, `undefined` is returned.
+   *
+   * Result is cached, and thus may be outdated if the filesystem is mutated.
+   * On success, returns a Path object.
+   */
+  async realpath() {
+    if (this.#realpath)
+      return this.#realpath;
+    if ((ENOREALPATH | ENOREADLINK | ENOENT) & this.#type)
+      return void 0;
+    try {
+      const rp = await this.#fs.promises.realpath(this.fullpath());
+      return this.#realpath = this.resolve(rp);
+    } catch (_) {
+      this.#markENOREALPATH();
+    }
+  }
+  /**
+   * Synchronous {@link realpath}
+   */
+  realpathSync() {
+    if (this.#realpath)
+      return this.#realpath;
+    if ((ENOREALPATH | ENOREADLINK | ENOENT) & this.#type)
+      return void 0;
+    try {
+      const rp = this.#fs.realpathSync(this.fullpath());
+      return this.#realpath = this.resolve(rp);
+    } catch (_) {
+      this.#markENOREALPATH();
+    }
+  }
+  /**
+   * Internal method to mark this Path object as the scurry cwd,
+   * called by {@link PathScurry#chdir}
+   *
+   * @internal
+   */
+  [setAsCwd](oldCwd) {
+    if (oldCwd === this)
+      return;
+    oldCwd.isCWD = false;
+    this.isCWD = true;
+    const changed = /* @__PURE__ */ new Set([]);
+    let rp = [];
+    let p = this;
+    while (p && p.parent) {
+      changed.add(p);
+      p.#relative = rp.join(this.sep);
+      p.#relativePosix = rp.join("/");
+      p = p.parent;
+      rp.push("..");
+    }
+    p = oldCwd;
+    while (p && p.parent && !changed.has(p)) {
+      p.#relative = void 0;
+      p.#relativePosix = void 0;
+      p = p.parent;
+    }
+  }
+};
+var PathWin32 = class _PathWin32 extends PathBase {
+  /**
+   * Separator for generating path strings.
+   */
+  sep = "\\";
+  /**
+   * Separator for parsing path strings.
+   */
+  splitSep = eitherSep;
+  /**
+   * Do not create new Path objects directly.  They should always be accessed
+   * via the PathScurry class or other methods on the Path class.
+   *
+   * @internal
+   */
+  constructor(name, type = UNKNOWN, root, roots, nocase, children, opts) {
+    super(name, type, root, roots, nocase, children, opts);
+  }
+  /**
+   * @internal
+   */
+  newChild(name, type = UNKNOWN, opts = {}) {
+    return new _PathWin32(name, type, this.root, this.roots, this.nocase, this.childrenCache(), opts);
+  }
+  /**
+   * @internal
+   */
+  getRootString(path2) {
+    return win32.parse(path2).root;
+  }
+  /**
+   * @internal
+   */
+  getRoot(rootPath) {
+    rootPath = uncToDrive(rootPath.toUpperCase());
+    if (rootPath === this.root.name) {
+      return this.root;
+    }
+    for (const [compare, root] of Object.entries(this.roots)) {
+      if (this.sameRoot(rootPath, compare)) {
+        return this.roots[rootPath] = root;
+      }
+    }
+    return this.roots[rootPath] = new PathScurryWin32(rootPath, this).root;
+  }
+  /**
+   * @internal
+   */
+  sameRoot(rootPath, compare = this.root.name) {
+    rootPath = rootPath.toUpperCase().replace(/\//g, "\\").replace(uncDriveRegexp, "$1\\");
+    return rootPath === compare;
+  }
+};
+var PathPosix = class _PathPosix extends PathBase {
+  /**
+   * separator for parsing path strings
+   */
+  splitSep = "/";
+  /**
+   * separator for generating path strings
+   */
+  sep = "/";
+  /**
+   * Do not create new Path objects directly.  They should always be accessed
+   * via the PathScurry class or other methods on the Path class.
+   *
+   * @internal
+   */
+  constructor(name, type = UNKNOWN, root, roots, nocase, children, opts) {
+    super(name, type, root, roots, nocase, children, opts);
+  }
+  /**
+   * @internal
+   */
+  getRootString(path2) {
+    return path2.startsWith("/") ? "/" : "";
+  }
+  /**
+   * @internal
+   */
+  getRoot(_rootPath) {
+    return this.root;
+  }
+  /**
+   * @internal
+   */
+  newChild(name, type = UNKNOWN, opts = {}) {
+    return new _PathPosix(name, type, this.root, this.roots, this.nocase, this.childrenCache(), opts);
+  }
+};
+var PathScurryBase = class {
+  /**
+   * The root Path entry for the current working directory of this Scurry
+   */
+  root;
+  /**
+   * The string path for the root of this Scurry's current working directory
+   */
+  rootPath;
+  /**
+   * A collection of all roots encountered, referenced by rootPath
+   */
+  roots;
+  /**
+   * The Path entry corresponding to this PathScurry's current working directory.
+   */
+  cwd;
+  #resolveCache;
+  #resolvePosixCache;
+  #children;
+  /**
+   * Perform path comparisons case-insensitively.
+   *
+   * Defaults true on Darwin and Windows systems, false elsewhere.
+   */
+  nocase;
+  #fs;
+  /**
+   * This class should not be instantiated directly.
+   *
+   * Use PathScurryWin32, PathScurryDarwin, PathScurryPosix, or PathScurry
+   *
+   * @internal
+   */
+  constructor(cwd = process.cwd(), pathImpl, sep2, { nocase, childrenCacheSize = 16 * 1024, fs: fs2 = defaultFS } = {}) {
+    this.#fs = fsFromOption(fs2);
+    if (cwd instanceof URL || cwd.startsWith("file://")) {
+      cwd = fileURLToPath(cwd);
+    }
+    const cwdPath = pathImpl.resolve(cwd);
+    this.roots = /* @__PURE__ */ Object.create(null);
+    this.rootPath = this.parseRootPath(cwdPath);
+    this.#resolveCache = new ResolveCache();
+    this.#resolvePosixCache = new ResolveCache();
+    this.#children = new ChildrenCache(childrenCacheSize);
+    const split = cwdPath.substring(this.rootPath.length).split(sep2);
+    if (split.length === 1 && !split[0]) {
+      split.pop();
+    }
+    if (nocase === void 0) {
+      throw new TypeError("must provide nocase setting to PathScurryBase ctor");
+    }
+    this.nocase = nocase;
+    this.root = this.newRoot(this.#fs);
+    this.roots[this.rootPath] = this.root;
+    let prev = this.root;
+    let len = split.length - 1;
+    const joinSep = pathImpl.sep;
+    let abs = this.rootPath;
+    let sawFirst = false;
+    for (const part of split) {
+      const l = len--;
+      prev = prev.child(part, {
+        relative: new Array(l).fill("..").join(joinSep),
+        relativePosix: new Array(l).fill("..").join("/"),
+        fullpath: abs += (sawFirst ? "" : joinSep) + part
+      });
+      sawFirst = true;
+    }
+    this.cwd = prev;
+  }
+  /**
+   * Get the depth of a provided path, string, or the cwd
+   */
+  depth(path2 = this.cwd) {
+    if (typeof path2 === "string") {
+      path2 = this.cwd.resolve(path2);
+    }
+    return path2.depth();
+  }
+  /**
+   * Return the cache of child entries.  Exposed so subclasses can create
+   * child Path objects in a platform-specific way.
+   *
+   * @internal
+   */
+  childrenCache() {
+    return this.#children;
+  }
+  /**
+   * Resolve one or more path strings to a resolved string
+   *
+   * Same interface as require('path').resolve.
+   *
+   * Much faster than path.resolve() when called multiple times for the same
+   * path, because the resolved Path objects are cached.  Much slower
+   * otherwise.
+   */
+  resolve(...paths) {
+    let r = "";
+    for (let i = paths.length - 1; i >= 0; i--) {
+      const p = paths[i];
+      if (!p || p === ".")
+        continue;
+      r = r ? `${p}/${r}` : p;
+      if (this.isAbsolute(p)) {
+        break;
+      }
+    }
+    const cached = this.#resolveCache.get(r);
+    if (cached !== void 0) {
+      return cached;
+    }
+    const result = this.cwd.resolve(r).fullpath();
+    this.#resolveCache.set(r, result);
+    return result;
+  }
+  /**
+   * Resolve one or more path strings to a resolved string, returning
+   * the posix path.  Identical to .resolve() on posix systems, but on
+   * windows will return a forward-slash separated UNC path.
+   *
+   * Same interface as require('path').resolve.
+   *
+   * Much faster than path.resolve() when called multiple times for the same
+   * path, because the resolved Path objects are cached.  Much slower
+   * otherwise.
+   */
+  resolvePosix(...paths) {
+    let r = "";
+    for (let i = paths.length - 1; i >= 0; i--) {
+      const p = paths[i];
+      if (!p || p === ".")
+        continue;
+      r = r ? `${p}/${r}` : p;
+      if (this.isAbsolute(p)) {
+        break;
+      }
+    }
+    const cached = this.#resolvePosixCache.get(r);
+    if (cached !== void 0) {
+      return cached;
+    }
+    const result = this.cwd.resolve(r).fullpathPosix();
+    this.#resolvePosixCache.set(r, result);
+    return result;
+  }
+  /**
+   * find the relative path from the cwd to the supplied path string or entry
+   */
+  relative(entry = this.cwd) {
+    if (typeof entry === "string") {
+      entry = this.cwd.resolve(entry);
+    }
+    return entry.relative();
+  }
+  /**
+   * find the relative path from the cwd to the supplied path string or
+   * entry, using / as the path delimiter, even on Windows.
+   */
+  relativePosix(entry = this.cwd) {
+    if (typeof entry === "string") {
+      entry = this.cwd.resolve(entry);
+    }
+    return entry.relativePosix();
+  }
+  /**
+   * Return the basename for the provided string or Path object
+   */
+  basename(entry = this.cwd) {
+    if (typeof entry === "string") {
+      entry = this.cwd.resolve(entry);
+    }
+    return entry.name;
+  }
+  /**
+   * Return the dirname for the provided string or Path object
+   */
+  dirname(entry = this.cwd) {
+    if (typeof entry === "string") {
+      entry = this.cwd.resolve(entry);
+    }
+    return (entry.parent || entry).fullpath();
+  }
+  async readdir(entry = this.cwd, opts = {
+    withFileTypes: true
+  }) {
+    if (typeof entry === "string") {
+      entry = this.cwd.resolve(entry);
+    } else if (!(entry instanceof PathBase)) {
+      opts = entry;
+      entry = this.cwd;
+    }
+    const { withFileTypes } = opts;
+    if (!entry.canReaddir()) {
+      return [];
+    } else {
+      const p = await entry.readdir();
+      return withFileTypes ? p : p.map((e) => e.name);
+    }
+  }
+  readdirSync(entry = this.cwd, opts = {
+    withFileTypes: true
+  }) {
+    if (typeof entry === "string") {
+      entry = this.cwd.resolve(entry);
+    } else if (!(entry instanceof PathBase)) {
+      opts = entry;
+      entry = this.cwd;
+    }
+    const { withFileTypes = true } = opts;
+    if (!entry.canReaddir()) {
+      return [];
+    } else if (withFileTypes) {
+      return entry.readdirSync();
+    } else {
+      return entry.readdirSync().map((e) => e.name);
+    }
+  }
+  /**
+   * Call lstat() on the string or Path object, and update all known
+   * information that can be determined.
+   *
+   * Note that unlike `fs.lstat()`, the returned value does not contain some
+   * information, such as `mode`, `dev`, `nlink`, and `ino`.  If that
+   * information is required, you will need to call `fs.lstat` yourself.
+   *
+   * If the Path refers to a nonexistent file, or if the lstat call fails for
+   * any reason, `undefined` is returned.  Otherwise the updated Path object is
+   * returned.
+   *
+   * Results are cached, and thus may be out of date if the filesystem is
+   * mutated.
+   */
+  async lstat(entry = this.cwd) {
+    if (typeof entry === "string") {
+      entry = this.cwd.resolve(entry);
+    }
+    return entry.lstat();
+  }
+  /**
+   * synchronous {@link PathScurryBase.lstat}
+   */
+  lstatSync(entry = this.cwd) {
+    if (typeof entry === "string") {
+      entry = this.cwd.resolve(entry);
+    }
+    return entry.lstatSync();
+  }
+  async readlink(entry = this.cwd, { withFileTypes } = {
+    withFileTypes: false
+  }) {
+    if (typeof entry === "string") {
+      entry = this.cwd.resolve(entry);
+    } else if (!(entry instanceof PathBase)) {
+      withFileTypes = entry.withFileTypes;
+      entry = this.cwd;
+    }
+    const e = await entry.readlink();
+    return withFileTypes ? e : e?.fullpath();
+  }
+  readlinkSync(entry = this.cwd, { withFileTypes } = {
+    withFileTypes: false
+  }) {
+    if (typeof entry === "string") {
+      entry = this.cwd.resolve(entry);
+    } else if (!(entry instanceof PathBase)) {
+      withFileTypes = entry.withFileTypes;
+      entry = this.cwd;
+    }
+    const e = entry.readlinkSync();
+    return withFileTypes ? e : e?.fullpath();
+  }
+  async realpath(entry = this.cwd, { withFileTypes } = {
+    withFileTypes: false
+  }) {
+    if (typeof entry === "string") {
+      entry = this.cwd.resolve(entry);
+    } else if (!(entry instanceof PathBase)) {
+      withFileTypes = entry.withFileTypes;
+      entry = this.cwd;
+    }
+    const e = await entry.realpath();
+    return withFileTypes ? e : e?.fullpath();
+  }
+  realpathSync(entry = this.cwd, { withFileTypes } = {
+    withFileTypes: false
+  }) {
+    if (typeof entry === "string") {
+      entry = this.cwd.resolve(entry);
+    } else if (!(entry instanceof PathBase)) {
+      withFileTypes = entry.withFileTypes;
+      entry = this.cwd;
+    }
+    const e = entry.realpathSync();
+    return withFileTypes ? e : e?.fullpath();
+  }
+  async walk(entry = this.cwd, opts = {}) {
+    if (typeof entry === "string") {
+      entry = this.cwd.resolve(entry);
+    } else if (!(entry instanceof PathBase)) {
+      opts = entry;
+      entry = this.cwd;
+    }
+    const { withFileTypes = true, follow = false, filter: filter2, walkFilter } = opts;
+    const results = [];
+    if (!filter2 || filter2(entry)) {
+      results.push(withFileTypes ? entry : entry.fullpath());
+    }
+    const dirs = /* @__PURE__ */ new Set();
+    const walk = (dir, cb) => {
+      dirs.add(dir);
+      dir.readdirCB((er, entries) => {
+        if (er) {
+          return cb(er);
+        }
+        let len = entries.length;
+        if (!len)
+          return cb();
+        const next = () => {
+          if (--len === 0) {
+            cb();
+          }
+        };
+        for (const e of entries) {
+          if (!filter2 || filter2(e)) {
+            results.push(withFileTypes ? e : e.fullpath());
+          }
+          if (follow && e.isSymbolicLink()) {
+            e.realpath().then((r) => r?.isUnknown() ? r.lstat() : r).then((r) => r?.shouldWalk(dirs, walkFilter) ? walk(r, next) : next());
+          } else {
+            if (e.shouldWalk(dirs, walkFilter)) {
+              walk(e, next);
+            } else {
+              next();
+            }
+          }
+        }
+      }, true);
+    };
+    const start = entry;
+    return new Promise((res, rej) => {
+      walk(start, (er) => {
+        if (er)
+          return rej(er);
+        res(results);
+      });
+    });
+  }
+  walkSync(entry = this.cwd, opts = {}) {
+    if (typeof entry === "string") {
+      entry = this.cwd.resolve(entry);
+    } else if (!(entry instanceof PathBase)) {
+      opts = entry;
+      entry = this.cwd;
+    }
+    const { withFileTypes = true, follow = false, filter: filter2, walkFilter } = opts;
+    const results = [];
+    if (!filter2 || filter2(entry)) {
+      results.push(withFileTypes ? entry : entry.fullpath());
+    }
+    const dirs = /* @__PURE__ */ new Set([entry]);
+    for (const dir of dirs) {
+      const entries = dir.readdirSync();
+      for (const e of entries) {
+        if (!filter2 || filter2(e)) {
+          results.push(withFileTypes ? e : e.fullpath());
+        }
+        let r = e;
+        if (e.isSymbolicLink()) {
+          if (!(follow && (r = e.realpathSync())))
+            continue;
+          if (r.isUnknown())
+            r.lstatSync();
+        }
+        if (r.shouldWalk(dirs, walkFilter)) {
+          dirs.add(r);
+        }
+      }
+    }
+    return results;
+  }
+  /**
+   * Support for `for await`
+   *
+   * Alias for {@link PathScurryBase.iterate}
+   *
+   * Note: As of Node 19, this is very slow, compared to other methods of
+   * walking.  Consider using {@link PathScurryBase.stream} if memory overhead
+   * and backpressure are concerns, or {@link PathScurryBase.walk} if not.
+   */
+  [Symbol.asyncIterator]() {
+    return this.iterate();
+  }
+  iterate(entry = this.cwd, options = {}) {
+    if (typeof entry === "string") {
+      entry = this.cwd.resolve(entry);
+    } else if (!(entry instanceof PathBase)) {
+      options = entry;
+      entry = this.cwd;
+    }
+    return this.stream(entry, options)[Symbol.asyncIterator]();
+  }
+  /**
+   * Iterating over a PathScurry performs a synchronous walk.
+   *
+   * Alias for {@link PathScurryBase.iterateSync}
+   */
+  [Symbol.iterator]() {
+    return this.iterateSync();
+  }
+  *iterateSync(entry = this.cwd, opts = {}) {
+    if (typeof entry === "string") {
+      entry = this.cwd.resolve(entry);
+    } else if (!(entry instanceof PathBase)) {
+      opts = entry;
+      entry = this.cwd;
+    }
+    const { withFileTypes = true, follow = false, filter: filter2, walkFilter } = opts;
+    if (!filter2 || filter2(entry)) {
+      yield withFileTypes ? entry : entry.fullpath();
+    }
+    const dirs = /* @__PURE__ */ new Set([entry]);
+    for (const dir of dirs) {
+      const entries = dir.readdirSync();
+      for (const e of entries) {
+        if (!filter2 || filter2(e)) {
+          yield withFileTypes ? e : e.fullpath();
+        }
+        let r = e;
+        if (e.isSymbolicLink()) {
+          if (!(follow && (r = e.realpathSync())))
+            continue;
+          if (r.isUnknown())
+            r.lstatSync();
+        }
+        if (r.shouldWalk(dirs, walkFilter)) {
+          dirs.add(r);
+        }
+      }
+    }
+  }
+  stream(entry = this.cwd, opts = {}) {
+    if (typeof entry === "string") {
+      entry = this.cwd.resolve(entry);
+    } else if (!(entry instanceof PathBase)) {
+      opts = entry;
+      entry = this.cwd;
+    }
+    const { withFileTypes = true, follow = false, filter: filter2, walkFilter } = opts;
+    const results = new Minipass({ objectMode: true });
+    if (!filter2 || filter2(entry)) {
+      results.write(withFileTypes ? entry : entry.fullpath());
+    }
+    const dirs = /* @__PURE__ */ new Set();
+    const queue = [entry];
+    let processing = 0;
+    const process2 = () => {
+      let paused = false;
+      while (!paused) {
+        const dir = queue.shift();
+        if (!dir) {
+          if (processing === 0)
+            results.end();
+          return;
+        }
+        processing++;
+        dirs.add(dir);
+        const onReaddir = (er, entries, didRealpaths = false) => {
+          if (er)
+            return results.emit("error", er);
+          if (follow && !didRealpaths) {
+            const promises = [];
+            for (const e of entries) {
+              if (e.isSymbolicLink()) {
+                promises.push(e.realpath().then((r) => r?.isUnknown() ? r.lstat() : r));
+              }
+            }
+            if (promises.length) {
+              Promise.all(promises).then(() => onReaddir(null, entries, true));
+              return;
+            }
+          }
+          for (const e of entries) {
+            if (e && (!filter2 || filter2(e))) {
+              if (!results.write(withFileTypes ? e : e.fullpath())) {
+                paused = true;
+              }
+            }
+          }
+          processing--;
+          for (const e of entries) {
+            const r = e.realpathCached() || e;
+            if (r.shouldWalk(dirs, walkFilter)) {
+              queue.push(r);
+            }
+          }
+          if (paused && !results.flowing) {
+            results.once("drain", process2);
+          } else if (!sync2) {
+            process2();
+          }
+        };
+        let sync2 = true;
+        dir.readdirCB(onReaddir, true);
+        sync2 = false;
+      }
+    };
+    process2();
+    return results;
+  }
+  streamSync(entry = this.cwd, opts = {}) {
+    if (typeof entry === "string") {
+      entry = this.cwd.resolve(entry);
+    } else if (!(entry instanceof PathBase)) {
+      opts = entry;
+      entry = this.cwd;
+    }
+    const { withFileTypes = true, follow = false, filter: filter2, walkFilter } = opts;
+    const results = new Minipass({ objectMode: true });
+    const dirs = /* @__PURE__ */ new Set();
+    if (!filter2 || filter2(entry)) {
+      results.write(withFileTypes ? entry : entry.fullpath());
+    }
+    const queue = [entry];
+    let processing = 0;
+    const process2 = () => {
+      let paused = false;
+      while (!paused) {
+        const dir = queue.shift();
+        if (!dir) {
+          if (processing === 0)
+            results.end();
+          return;
+        }
+        processing++;
+        dirs.add(dir);
+        const entries = dir.readdirSync();
+        for (const e of entries) {
+          if (!filter2 || filter2(e)) {
+            if (!results.write(withFileTypes ? e : e.fullpath())) {
+              paused = true;
+            }
+          }
+        }
+        processing--;
+        for (const e of entries) {
+          let r = e;
+          if (e.isSymbolicLink()) {
+            if (!(follow && (r = e.realpathSync())))
+              continue;
+            if (r.isUnknown())
+              r.lstatSync();
+          }
+          if (r.shouldWalk(dirs, walkFilter)) {
+            queue.push(r);
+          }
+        }
+      }
+      if (paused && !results.flowing)
+        results.once("drain", process2);
+    };
+    process2();
+    return results;
+  }
+  chdir(path2 = this.cwd) {
+    const oldCwd = this.cwd;
+    this.cwd = typeof path2 === "string" ? this.cwd.resolve(path2) : path2;
+    this.cwd[setAsCwd](oldCwd);
+  }
+};
+var PathScurryWin32 = class extends PathScurryBase {
+  /**
+   * separator for generating path strings
+   */
+  sep = "\\";
+  constructor(cwd = process.cwd(), opts = {}) {
+    const { nocase = true } = opts;
+    super(cwd, win32, "\\", { ...opts, nocase });
+    this.nocase = nocase;
+    for (let p = this.cwd; p; p = p.parent) {
+      p.nocase = this.nocase;
+    }
+  }
+  /**
+   * @internal
+   */
+  parseRootPath(dir) {
+    return win32.parse(dir).root.toUpperCase();
+  }
+  /**
+   * @internal
+   */
+  newRoot(fs2) {
+    return new PathWin32(this.rootPath, IFDIR, void 0, this.roots, this.nocase, this.childrenCache(), { fs: fs2 });
+  }
+  /**
+   * Return true if the provided path string is an absolute path
+   */
+  isAbsolute(p) {
+    return p.startsWith("/") || p.startsWith("\\") || /^[a-z]:(\/|\\)/i.test(p);
+  }
+};
+var PathScurryPosix = class extends PathScurryBase {
+  /**
+   * separator for generating path strings
+   */
+  sep = "/";
+  constructor(cwd = process.cwd(), opts = {}) {
+    const { nocase = false } = opts;
+    super(cwd, posix, "/", { ...opts, nocase });
+    this.nocase = nocase;
+  }
+  /**
+   * @internal
+   */
+  parseRootPath(_dir) {
+    return "/";
+  }
+  /**
+   * @internal
+   */
+  newRoot(fs2) {
+    return new PathPosix(this.rootPath, IFDIR, void 0, this.roots, this.nocase, this.childrenCache(), { fs: fs2 });
+  }
+  /**
+   * Return true if the provided path string is an absolute path
+   */
+  isAbsolute(p) {
+    return p.startsWith("/");
+  }
+};
+var PathScurryDarwin = class extends PathScurryPosix {
+  constructor(cwd = process.cwd(), opts = {}) {
+    const { nocase = true } = opts;
+    super(cwd, { ...opts, nocase });
+  }
+};
+var Path = process.platform === "win32" ? PathWin32 : PathPosix;
+var PathScurry = process.platform === "win32" ? PathScurryWin32 : process.platform === "darwin" ? PathScurryDarwin : PathScurryPosix;
+
+// node_modules/glob/dist/esm/pattern.js
+var isPatternList = (pl) => pl.length >= 1;
+var isGlobList = (gl) => gl.length >= 1;
+var Pattern = class _Pattern {
+  #patternList;
+  #globList;
+  #index;
+  length;
+  #platform;
+  #rest;
+  #globString;
+  #isDrive;
+  #isUNC;
+  #isAbsolute;
+  #followGlobstar = true;
+  constructor(patternList, globList, index, platform) {
+    if (!isPatternList(patternList)) {
+      throw new TypeError("empty pattern list");
+    }
+    if (!isGlobList(globList)) {
+      throw new TypeError("empty glob list");
+    }
+    if (globList.length !== patternList.length) {
+      throw new TypeError("mismatched pattern list and glob list lengths");
+    }
+    this.length = patternList.length;
+    if (index < 0 || index >= this.length) {
+      throw new TypeError("index out of range");
+    }
+    this.#patternList = patternList;
+    this.#globList = globList;
+    this.#index = index;
+    this.#platform = platform;
+    if (this.#index === 0) {
+      if (this.isUNC()) {
+        const [p0, p1, p2, p3, ...prest] = this.#patternList;
+        const [g0, g1, g2, g3, ...grest] = this.#globList;
+        if (prest[0] === "") {
+          prest.shift();
+          grest.shift();
+        }
+        const p = [p0, p1, p2, p3, ""].join("/");
+        const g = [g0, g1, g2, g3, ""].join("/");
+        this.#patternList = [p, ...prest];
+        this.#globList = [g, ...grest];
+        this.length = this.#patternList.length;
+      } else if (this.isDrive() || this.isAbsolute()) {
+        const [p1, ...prest] = this.#patternList;
+        const [g1, ...grest] = this.#globList;
+        if (prest[0] === "") {
+          prest.shift();
+          grest.shift();
+        }
+        const p = p1 + "/";
+        const g = g1 + "/";
+        this.#patternList = [p, ...prest];
+        this.#globList = [g, ...grest];
+        this.length = this.#patternList.length;
+      }
+    }
+  }
+  /**
+   * The first entry in the parsed list of patterns
+   */
+  pattern() {
+    return this.#patternList[this.#index];
+  }
+  /**
+   * true of if pattern() returns a string
+   */
+  isString() {
+    return typeof this.#patternList[this.#index] === "string";
+  }
+  /**
+   * true of if pattern() returns GLOBSTAR
+   */
+  isGlobstar() {
+    return this.#patternList[this.#index] === GLOBSTAR;
+  }
+  /**
+   * true if pattern() returns a regexp
+   */
+  isRegExp() {
+    return this.#patternList[this.#index] instanceof RegExp;
+  }
+  /**
+   * The /-joined set of glob parts that make up this pattern
+   */
+  globString() {
+    return this.#globString = this.#globString || (this.#index === 0 ? this.isAbsolute() ? this.#globList[0] + this.#globList.slice(1).join("/") : this.#globList.join("/") : this.#globList.slice(this.#index).join("/"));
+  }
+  /**
+   * true if there are more pattern parts after this one
+   */
+  hasMore() {
+    return this.length > this.#index + 1;
+  }
+  /**
+   * The rest of the pattern after this part, or null if this is the end
+   */
+  rest() {
+    if (this.#rest !== void 0)
+      return this.#rest;
+    if (!this.hasMore())
+      return this.#rest = null;
+    this.#rest = new _Pattern(this.#patternList, this.#globList, this.#index + 1, this.#platform);
+    this.#rest.#isAbsolute = this.#isAbsolute;
+    this.#rest.#isUNC = this.#isUNC;
+    this.#rest.#isDrive = this.#isDrive;
+    return this.#rest;
+  }
+  /**
+   * true if the pattern represents a //unc/path/ on windows
+   */
+  isUNC() {
+    const pl = this.#patternList;
+    return this.#isUNC !== void 0 ? this.#isUNC : this.#isUNC = this.#platform === "win32" && this.#index === 0 && pl[0] === "" && pl[1] === "" && typeof pl[2] === "string" && !!pl[2] && typeof pl[3] === "string" && !!pl[3];
+  }
+  // pattern like C:/...
+  // split = ['C:', ...]
+  // XXX: would be nice to handle patterns like `c:*` to test the cwd
+  // in c: for *, but I don't know of a way to even figure out what that
+  // cwd is without actually chdir'ing into it?
+  /**
+   * True if the pattern starts with a drive letter on Windows
+   */
+  isDrive() {
+    const pl = this.#patternList;
+    return this.#isDrive !== void 0 ? this.#isDrive : this.#isDrive = this.#platform === "win32" && this.#index === 0 && this.length > 1 && typeof pl[0] === "string" && /^[a-z]:$/i.test(pl[0]);
+  }
+  // pattern = '/' or '/...' or '/x/...'
+  // split = ['', ''] or ['', ...] or ['', 'x', ...]
+  // Drive and UNC both considered absolute on windows
+  /**
+   * True if the pattern is rooted on an absolute path
+   */
+  isAbsolute() {
+    const pl = this.#patternList;
+    return this.#isAbsolute !== void 0 ? this.#isAbsolute : this.#isAbsolute = pl[0] === "" && pl.length > 1 || this.isDrive() || this.isUNC();
+  }
+  /**
+   * consume the root of the pattern, and return it
+   */
+  root() {
+    const p = this.#patternList[0];
+    return typeof p === "string" && this.isAbsolute() && this.#index === 0 ? p : "";
+  }
+  /**
+   * Check to see if the current globstar pattern is allowed to follow
+   * a symbolic link.
+   */
+  checkFollowGlobstar() {
+    return !(this.#index === 0 || !this.isGlobstar() || !this.#followGlobstar);
+  }
+  /**
+   * Mark that the current globstar pattern is following a symbolic link
+   */
+  markFollowGlobstar() {
+    if (this.#index === 0 || !this.isGlobstar() || !this.#followGlobstar)
+      return false;
+    this.#followGlobstar = false;
+    return true;
+  }
+};
+
+// node_modules/glob/dist/esm/ignore.js
+var defaultPlatform2 = typeof process === "object" && process && typeof process.platform === "string" ? process.platform : "linux";
+var Ignore = class {
+  relative;
+  relativeChildren;
+  absolute;
+  absoluteChildren;
+  platform;
+  mmopts;
+  constructor(ignored, { nobrace, nocase, noext, noglobstar, platform = defaultPlatform2 }) {
+    this.relative = [];
+    this.absolute = [];
+    this.relativeChildren = [];
+    this.absoluteChildren = [];
+    this.platform = platform;
+    this.mmopts = {
+      dot: true,
+      nobrace,
+      nocase,
+      noext,
+      noglobstar,
+      optimizationLevel: 2,
+      platform,
+      nocomment: true,
+      nonegate: true
+    };
+    for (const ign of ignored)
+      this.add(ign);
+  }
+  add(ign) {
+    const mm = new Minimatch(ign, this.mmopts);
+    for (let i = 0; i < mm.set.length; i++) {
+      const parsed = mm.set[i];
+      const globParts = mm.globParts[i];
+      if (!parsed || !globParts) {
+        throw new Error("invalid pattern object");
+      }
+      while (parsed[0] === "." && globParts[0] === ".") {
+        parsed.shift();
+        globParts.shift();
+      }
+      const p = new Pattern(parsed, globParts, 0, this.platform);
+      const m = new Minimatch(p.globString(), this.mmopts);
+      const children = globParts[globParts.length - 1] === "**";
+      const absolute = p.isAbsolute();
+      if (absolute)
+        this.absolute.push(m);
+      else
+        this.relative.push(m);
+      if (children) {
+        if (absolute)
+          this.absoluteChildren.push(m);
+        else
+          this.relativeChildren.push(m);
+      }
+    }
+  }
+  ignored(p) {
+    const fullpath = p.fullpath();
+    const fullpaths = `${fullpath}/`;
+    const relative = p.relative() || ".";
+    const relatives = `${relative}/`;
+    for (const m of this.relative) {
+      if (m.match(relative) || m.match(relatives))
+        return true;
+    }
+    for (const m of this.absolute) {
+      if (m.match(fullpath) || m.match(fullpaths))
+        return true;
+    }
+    return false;
+  }
+  childrenIgnored(p) {
+    const fullpath = p.fullpath() + "/";
+    const relative = (p.relative() || ".") + "/";
+    for (const m of this.relativeChildren) {
+      if (m.match(relative))
+        return true;
+    }
+    for (const m of this.absoluteChildren) {
+      if (m.match(fullpath))
+        return true;
+    }
+    return false;
+  }
+};
+
+// node_modules/glob/dist/esm/processor.js
+var HasWalkedCache = class _HasWalkedCache {
+  store;
+  constructor(store = /* @__PURE__ */ new Map()) {
+    this.store = store;
+  }
+  copy() {
+    return new _HasWalkedCache(new Map(this.store));
+  }
+  hasWalked(target, pattern) {
+    return this.store.get(target.fullpath())?.has(pattern.globString());
+  }
+  storeWalked(target, pattern) {
+    const fullpath = target.fullpath();
+    const cached = this.store.get(fullpath);
+    if (cached)
+      cached.add(pattern.globString());
+    else
+      this.store.set(fullpath, /* @__PURE__ */ new Set([pattern.globString()]));
+  }
+};
+var MatchRecord = class {
+  store = /* @__PURE__ */ new Map();
+  add(target, absolute, ifDir) {
+    const n = (absolute ? 2 : 0) | (ifDir ? 1 : 0);
+    const current = this.store.get(target);
+    this.store.set(target, current === void 0 ? n : n & current);
+  }
+  // match, absolute, ifdir
+  entries() {
+    return [...this.store.entries()].map(([path2, n]) => [
+      path2,
+      !!(n & 2),
+      !!(n & 1)
+    ]);
+  }
+};
+var SubWalks = class {
+  store = /* @__PURE__ */ new Map();
+  add(target, pattern) {
+    if (!target.canReaddir()) {
+      return;
+    }
+    const subs = this.store.get(target);
+    if (subs) {
+      if (!subs.find((p) => p.globString() === pattern.globString())) {
+        subs.push(pattern);
+      }
+    } else
+      this.store.set(target, [pattern]);
+  }
+  get(target) {
+    const subs = this.store.get(target);
+    if (!subs) {
+      throw new Error("attempting to walk unknown path");
+    }
+    return subs;
+  }
+  entries() {
+    return this.keys().map((k) => [k, this.store.get(k)]);
+  }
+  keys() {
+    return [...this.store.keys()].filter((t) => t.canReaddir());
+  }
+};
+var Processor = class _Processor {
+  hasWalkedCache;
+  matches = new MatchRecord();
+  subwalks = new SubWalks();
+  patterns;
+  follow;
+  dot;
+  opts;
+  constructor(opts, hasWalkedCache) {
+    this.opts = opts;
+    this.follow = !!opts.follow;
+    this.dot = !!opts.dot;
+    this.hasWalkedCache = hasWalkedCache ? hasWalkedCache.copy() : new HasWalkedCache();
+  }
+  processPatterns(target, patterns) {
+    this.patterns = patterns;
+    const processingSet = patterns.map((p) => [target, p]);
+    for (let [t, pattern] of processingSet) {
+      this.hasWalkedCache.storeWalked(t, pattern);
+      const root = pattern.root();
+      const absolute = pattern.isAbsolute() && this.opts.absolute !== false;
+      if (root) {
+        t = t.resolve(root === "/" && this.opts.root !== void 0 ? this.opts.root : root);
+        const rest2 = pattern.rest();
+        if (!rest2) {
+          this.matches.add(t, true, false);
+          continue;
+        } else {
+          pattern = rest2;
+        }
+      }
+      if (t.isENOENT())
+        continue;
+      let p;
+      let rest;
+      let changed = false;
+      while (typeof (p = pattern.pattern()) === "string" && (rest = pattern.rest())) {
+        const c = t.resolve(p);
+        t = c;
+        pattern = rest;
+        changed = true;
+      }
+      p = pattern.pattern();
+      rest = pattern.rest();
+      if (changed) {
+        if (this.hasWalkedCache.hasWalked(t, pattern))
+          continue;
+        this.hasWalkedCache.storeWalked(t, pattern);
+      }
+      if (typeof p === "string") {
+        const ifDir = p === ".." || p === "" || p === ".";
+        this.matches.add(t.resolve(p), absolute, ifDir);
+        continue;
+      } else if (p === GLOBSTAR) {
+        if (!t.isSymbolicLink() || this.follow || pattern.checkFollowGlobstar()) {
+          this.subwalks.add(t, pattern);
+        }
+        const rp = rest?.pattern();
+        const rrest = rest?.rest();
+        if (!rest || (rp === "" || rp === ".") && !rrest) {
+          this.matches.add(t, absolute, rp === "" || rp === ".");
+        } else {
+          if (rp === "..") {
+            const tp = t.parent || t;
+            if (!rrest)
+              this.matches.add(tp, absolute, true);
+            else if (!this.hasWalkedCache.hasWalked(tp, rrest)) {
+              this.subwalks.add(tp, rrest);
+            }
+          }
+        }
+      } else if (p instanceof RegExp) {
+        this.subwalks.add(t, pattern);
+      }
+    }
+    return this;
+  }
+  subwalkTargets() {
+    return this.subwalks.keys();
+  }
+  child() {
+    return new _Processor(this.opts, this.hasWalkedCache);
+  }
+  // return a new Processor containing the subwalks for each
+  // child entry, and a set of matches, and
+  // a hasWalkedCache that's a copy of this one
+  // then we're going to call
+  filterEntries(parent, entries) {
+    const patterns = this.subwalks.get(parent);
+    const results = this.child();
+    for (const e of entries) {
+      for (const pattern of patterns) {
+        const absolute = pattern.isAbsolute();
+        const p = pattern.pattern();
+        const rest = pattern.rest();
+        if (p === GLOBSTAR) {
+          results.testGlobstar(e, pattern, rest, absolute);
+        } else if (p instanceof RegExp) {
+          results.testRegExp(e, p, rest, absolute);
+        } else {
+          results.testString(e, p, rest, absolute);
+        }
+      }
+    }
+    return results;
+  }
+  testGlobstar(e, pattern, rest, absolute) {
+    if (this.dot || !e.name.startsWith(".")) {
+      if (!pattern.hasMore()) {
+        this.matches.add(e, absolute, false);
+      }
+      if (e.canReaddir()) {
+        if (this.follow || !e.isSymbolicLink()) {
+          this.subwalks.add(e, pattern);
+        } else if (e.isSymbolicLink()) {
+          if (rest && pattern.checkFollowGlobstar()) {
+            this.subwalks.add(e, rest);
+          } else if (pattern.markFollowGlobstar()) {
+            this.subwalks.add(e, pattern);
+          }
+        }
+      }
+    }
+    if (rest) {
+      const rp = rest.pattern();
+      if (typeof rp === "string" && // dots and empty were handled already
+      rp !== ".." && rp !== "" && rp !== ".") {
+        this.testString(e, rp, rest.rest(), absolute);
+      } else if (rp === "..") {
+        const ep = e.parent || e;
+        this.subwalks.add(ep, rest);
+      } else if (rp instanceof RegExp) {
+        this.testRegExp(e, rp, rest.rest(), absolute);
+      }
+    }
+  }
+  testRegExp(e, p, rest, absolute) {
+    if (!p.test(e.name))
+      return;
+    if (!rest) {
+      this.matches.add(e, absolute, false);
+    } else {
+      this.subwalks.add(e, rest);
+    }
+  }
+  testString(e, p, rest, absolute) {
+    if (!e.isNamed(p))
+      return;
+    if (!rest) {
+      this.matches.add(e, absolute, false);
+    } else {
+      this.subwalks.add(e, rest);
+    }
+  }
+};
+
+// node_modules/glob/dist/esm/walker.js
+var makeIgnore = (ignore, opts) => typeof ignore === "string" ? new Ignore([ignore], opts) : Array.isArray(ignore) ? new Ignore(ignore, opts) : ignore;
+var GlobUtil = class {
+  path;
+  patterns;
+  opts;
+  seen = /* @__PURE__ */ new Set();
+  paused = false;
+  aborted = false;
+  #onResume = [];
+  #ignore;
+  #sep;
+  signal;
+  maxDepth;
+  includeChildMatches;
+  constructor(patterns, path2, opts) {
+    this.patterns = patterns;
+    this.path = path2;
+    this.opts = opts;
+    this.#sep = !opts.posix && opts.platform === "win32" ? "\\" : "/";
+    this.includeChildMatches = opts.includeChildMatches !== false;
+    if (opts.ignore || !this.includeChildMatches) {
+      this.#ignore = makeIgnore(opts.ignore ?? [], opts);
+      if (!this.includeChildMatches && typeof this.#ignore.add !== "function") {
+        const m = "cannot ignore child matches, ignore lacks add() method.";
+        throw new Error(m);
+      }
+    }
+    this.maxDepth = opts.maxDepth || Infinity;
+    if (opts.signal) {
+      this.signal = opts.signal;
+      this.signal.addEventListener("abort", () => {
+        this.#onResume.length = 0;
+      });
+    }
+  }
+  #ignored(path2) {
+    return this.seen.has(path2) || !!this.#ignore?.ignored?.(path2);
+  }
+  #childrenIgnored(path2) {
+    return !!this.#ignore?.childrenIgnored?.(path2);
+  }
+  // backpressure mechanism
+  pause() {
+    this.paused = true;
+  }
+  resume() {
+    if (this.signal?.aborted)
+      return;
+    this.paused = false;
+    let fn = void 0;
+    while (!this.paused && (fn = this.#onResume.shift())) {
+      fn();
+    }
+  }
+  onResume(fn) {
+    if (this.signal?.aborted)
+      return;
+    if (!this.paused) {
+      fn();
+    } else {
+      this.#onResume.push(fn);
+    }
+  }
+  // do the requisite realpath/stat checking, and return the path
+  // to add or undefined to filter it out.
+  async matchCheck(e, ifDir) {
+    if (ifDir && this.opts.nodir)
+      return void 0;
+    let rpc;
+    if (this.opts.realpath) {
+      rpc = e.realpathCached() || await e.realpath();
+      if (!rpc)
+        return void 0;
+      e = rpc;
+    }
+    const needStat = e.isUnknown() || this.opts.stat;
+    const s = needStat ? await e.lstat() : e;
+    if (this.opts.follow && this.opts.nodir && s?.isSymbolicLink()) {
+      const target = await s.realpath();
+      if (target && (target.isUnknown() || this.opts.stat)) {
+        await target.lstat();
+      }
+    }
+    return this.matchCheckTest(s, ifDir);
+  }
+  matchCheckTest(e, ifDir) {
+    return e && (this.maxDepth === Infinity || e.depth() <= this.maxDepth) && (!ifDir || e.canReaddir()) && (!this.opts.nodir || !e.isDirectory()) && (!this.opts.nodir || !this.opts.follow || !e.isSymbolicLink() || !e.realpathCached()?.isDirectory()) && !this.#ignored(e) ? e : void 0;
+  }
+  matchCheckSync(e, ifDir) {
+    if (ifDir && this.opts.nodir)
+      return void 0;
+    let rpc;
+    if (this.opts.realpath) {
+      rpc = e.realpathCached() || e.realpathSync();
+      if (!rpc)
+        return void 0;
+      e = rpc;
+    }
+    const needStat = e.isUnknown() || this.opts.stat;
+    const s = needStat ? e.lstatSync() : e;
+    if (this.opts.follow && this.opts.nodir && s?.isSymbolicLink()) {
+      const target = s.realpathSync();
+      if (target && (target?.isUnknown() || this.opts.stat)) {
+        target.lstatSync();
+      }
+    }
+    return this.matchCheckTest(s, ifDir);
+  }
+  matchFinish(e, absolute) {
+    if (this.#ignored(e))
+      return;
+    if (!this.includeChildMatches && this.#ignore?.add) {
+      const ign = `${e.relativePosix()}/**`;
+      this.#ignore.add(ign);
+    }
+    const abs = this.opts.absolute === void 0 ? absolute : this.opts.absolute;
+    this.seen.add(e);
+    const mark = this.opts.mark && e.isDirectory() ? this.#sep : "";
+    if (this.opts.withFileTypes) {
+      this.matchEmit(e);
+    } else if (abs) {
+      const abs2 = this.opts.posix ? e.fullpathPosix() : e.fullpath();
+      this.matchEmit(abs2 + mark);
+    } else {
+      const rel = this.opts.posix ? e.relativePosix() : e.relative();
+      const pre = this.opts.dotRelative && !rel.startsWith(".." + this.#sep) ? "." + this.#sep : "";
+      this.matchEmit(!rel ? "." + mark : pre + rel + mark);
+    }
+  }
+  async match(e, absolute, ifDir) {
+    const p = await this.matchCheck(e, ifDir);
+    if (p)
+      this.matchFinish(p, absolute);
+  }
+  matchSync(e, absolute, ifDir) {
+    const p = this.matchCheckSync(e, ifDir);
+    if (p)
+      this.matchFinish(p, absolute);
+  }
+  walkCB(target, patterns, cb) {
+    if (this.signal?.aborted)
+      cb();
+    this.walkCB2(target, patterns, new Processor(this.opts), cb);
+  }
+  walkCB2(target, patterns, processor, cb) {
+    if (this.#childrenIgnored(target))
+      return cb();
+    if (this.signal?.aborted)
+      cb();
+    if (this.paused) {
+      this.onResume(() => this.walkCB2(target, patterns, processor, cb));
+      return;
+    }
+    processor.processPatterns(target, patterns);
+    let tasks = 1;
+    const next = () => {
+      if (--tasks === 0)
+        cb();
+    };
+    for (const [m, absolute, ifDir] of processor.matches.entries()) {
+      if (this.#ignored(m))
+        continue;
+      tasks++;
+      this.match(m, absolute, ifDir).then(() => next());
+    }
+    for (const t of processor.subwalkTargets()) {
+      if (this.maxDepth !== Infinity && t.depth() >= this.maxDepth) {
+        continue;
+      }
+      tasks++;
+      const childrenCached = t.readdirCached();
+      if (t.calledReaddir())
+        this.walkCB3(t, childrenCached, processor, next);
+      else {
+        t.readdirCB((_, entries) => this.walkCB3(t, entries, processor, next), true);
+      }
+    }
+    next();
+  }
+  walkCB3(target, entries, processor, cb) {
+    processor = processor.filterEntries(target, entries);
+    let tasks = 1;
+    const next = () => {
+      if (--tasks === 0)
+        cb();
+    };
+    for (const [m, absolute, ifDir] of processor.matches.entries()) {
+      if (this.#ignored(m))
+        continue;
+      tasks++;
+      this.match(m, absolute, ifDir).then(() => next());
+    }
+    for (const [target2, patterns] of processor.subwalks.entries()) {
+      tasks++;
+      this.walkCB2(target2, patterns, processor.child(), next);
+    }
+    next();
+  }
+  walkCBSync(target, patterns, cb) {
+    if (this.signal?.aborted)
+      cb();
+    this.walkCB2Sync(target, patterns, new Processor(this.opts), cb);
+  }
+  walkCB2Sync(target, patterns, processor, cb) {
+    if (this.#childrenIgnored(target))
+      return cb();
+    if (this.signal?.aborted)
+      cb();
+    if (this.paused) {
+      this.onResume(() => this.walkCB2Sync(target, patterns, processor, cb));
+      return;
+    }
+    processor.processPatterns(target, patterns);
+    let tasks = 1;
+    const next = () => {
+      if (--tasks === 0)
+        cb();
+    };
+    for (const [m, absolute, ifDir] of processor.matches.entries()) {
+      if (this.#ignored(m))
+        continue;
+      this.matchSync(m, absolute, ifDir);
+    }
+    for (const t of processor.subwalkTargets()) {
+      if (this.maxDepth !== Infinity && t.depth() >= this.maxDepth) {
+        continue;
+      }
+      tasks++;
+      const children = t.readdirSync();
+      this.walkCB3Sync(t, children, processor, next);
+    }
+    next();
+  }
+  walkCB3Sync(target, entries, processor, cb) {
+    processor = processor.filterEntries(target, entries);
+    let tasks = 1;
+    const next = () => {
+      if (--tasks === 0)
+        cb();
+    };
+    for (const [m, absolute, ifDir] of processor.matches.entries()) {
+      if (this.#ignored(m))
+        continue;
+      this.matchSync(m, absolute, ifDir);
+    }
+    for (const [target2, patterns] of processor.subwalks.entries()) {
+      tasks++;
+      this.walkCB2Sync(target2, patterns, processor.child(), next);
+    }
+    next();
+  }
+};
+var GlobWalker = class extends GlobUtil {
+  matches = /* @__PURE__ */ new Set();
+  constructor(patterns, path2, opts) {
+    super(patterns, path2, opts);
+  }
+  matchEmit(e) {
+    this.matches.add(e);
+  }
+  async walk() {
+    if (this.signal?.aborted)
+      throw this.signal.reason;
+    if (this.path.isUnknown()) {
+      await this.path.lstat();
+    }
+    await new Promise((res, rej) => {
+      this.walkCB(this.path, this.patterns, () => {
+        if (this.signal?.aborted) {
+          rej(this.signal.reason);
+        } else {
+          res(this.matches);
+        }
+      });
+    });
+    return this.matches;
+  }
+  walkSync() {
+    if (this.signal?.aborted)
+      throw this.signal.reason;
+    if (this.path.isUnknown()) {
+      this.path.lstatSync();
+    }
+    this.walkCBSync(this.path, this.patterns, () => {
+      if (this.signal?.aborted)
+        throw this.signal.reason;
+    });
+    return this.matches;
+  }
+};
+var GlobStream = class extends GlobUtil {
+  results;
+  constructor(patterns, path2, opts) {
+    super(patterns, path2, opts);
+    this.results = new Minipass({
+      signal: this.signal,
+      objectMode: true
+    });
+    this.results.on("drain", () => this.resume());
+    this.results.on("resume", () => this.resume());
+  }
+  matchEmit(e) {
+    this.results.write(e);
+    if (!this.results.flowing)
+      this.pause();
+  }
+  stream() {
+    const target = this.path;
+    if (target.isUnknown()) {
+      target.lstat().then(() => {
+        this.walkCB(target, this.patterns, () => this.results.end());
+      });
+    } else {
+      this.walkCB(target, this.patterns, () => this.results.end());
+    }
+    return this.results;
+  }
+  streamSync() {
+    if (this.path.isUnknown()) {
+      this.path.lstatSync();
+    }
+    this.walkCBSync(this.path, this.patterns, () => this.results.end());
+    return this.results;
+  }
+};
+
+// node_modules/glob/dist/esm/glob.js
+var defaultPlatform3 = typeof process === "object" && process && typeof process.platform === "string" ? process.platform : "linux";
+var Glob = class {
+  absolute;
+  cwd;
+  root;
+  dot;
+  dotRelative;
+  follow;
+  ignore;
+  magicalBraces;
+  mark;
+  matchBase;
+  maxDepth;
+  nobrace;
+  nocase;
+  nodir;
+  noext;
+  noglobstar;
+  pattern;
+  platform;
+  realpath;
+  scurry;
+  stat;
+  signal;
+  windowsPathsNoEscape;
+  withFileTypes;
+  includeChildMatches;
+  /**
+   * The options provided to the constructor.
+   */
+  opts;
+  /**
+   * An array of parsed immutable {@link Pattern} objects.
+   */
+  patterns;
+  /**
+   * All options are stored as properties on the `Glob` object.
+   *
+   * See {@link GlobOptions} for full options descriptions.
+   *
+   * Note that a previous `Glob` object can be passed as the
+   * `GlobOptions` to another `Glob` instantiation to re-use settings
+   * and caches with a new pattern.
+   *
+   * Traversal functions can be called multiple times to run the walk
+   * again.
+   */
+  constructor(pattern, opts) {
+    if (!opts)
+      throw new TypeError("glob options required");
+    this.withFileTypes = !!opts.withFileTypes;
+    this.signal = opts.signal;
+    this.follow = !!opts.follow;
+    this.dot = !!opts.dot;
+    this.dotRelative = !!opts.dotRelative;
+    this.nodir = !!opts.nodir;
+    this.mark = !!opts.mark;
+    if (!opts.cwd) {
+      this.cwd = "";
+    } else if (opts.cwd instanceof URL || opts.cwd.startsWith("file://")) {
+      opts.cwd = fileURLToPath2(opts.cwd);
+    }
+    this.cwd = opts.cwd || "";
+    this.root = opts.root;
+    this.magicalBraces = !!opts.magicalBraces;
+    this.nobrace = !!opts.nobrace;
+    this.noext = !!opts.noext;
+    this.realpath = !!opts.realpath;
+    this.absolute = opts.absolute;
+    this.includeChildMatches = opts.includeChildMatches !== false;
+    this.noglobstar = !!opts.noglobstar;
+    this.matchBase = !!opts.matchBase;
+    this.maxDepth = typeof opts.maxDepth === "number" ? opts.maxDepth : Infinity;
+    this.stat = !!opts.stat;
+    this.ignore = opts.ignore;
+    if (this.withFileTypes && this.absolute !== void 0) {
+      throw new Error("cannot set absolute and withFileTypes:true");
+    }
+    if (typeof pattern === "string") {
+      pattern = [pattern];
+    }
+    this.windowsPathsNoEscape = !!opts.windowsPathsNoEscape || opts.allowWindowsEscape === false;
+    if (this.windowsPathsNoEscape) {
+      pattern = pattern.map((p) => p.replace(/\\/g, "/"));
+    }
+    if (this.matchBase) {
+      if (opts.noglobstar) {
+        throw new TypeError("base matching requires globstar");
+      }
+      pattern = pattern.map((p) => p.includes("/") ? p : `./**/${p}`);
+    }
+    this.pattern = pattern;
+    this.platform = opts.platform || defaultPlatform3;
+    this.opts = { ...opts, platform: this.platform };
+    if (opts.scurry) {
+      this.scurry = opts.scurry;
+      if (opts.nocase !== void 0 && opts.nocase !== opts.scurry.nocase) {
+        throw new Error("nocase option contradicts provided scurry option");
+      }
+    } else {
+      const Scurry = opts.platform === "win32" ? PathScurryWin32 : opts.platform === "darwin" ? PathScurryDarwin : opts.platform ? PathScurryPosix : PathScurry;
+      this.scurry = new Scurry(this.cwd, {
+        nocase: opts.nocase,
+        fs: opts.fs
+      });
+    }
+    this.nocase = this.scurry.nocase;
+    const nocaseMagicOnly = this.platform === "darwin" || this.platform === "win32";
+    const mmo = {
+      // default nocase based on platform
+      ...opts,
+      dot: this.dot,
+      matchBase: this.matchBase,
+      nobrace: this.nobrace,
+      nocase: this.nocase,
+      nocaseMagicOnly,
+      nocomment: true,
+      noext: this.noext,
+      nonegate: true,
+      optimizationLevel: 2,
+      platform: this.platform,
+      windowsPathsNoEscape: this.windowsPathsNoEscape,
+      debug: !!this.opts.debug
+    };
+    const mms = this.pattern.map((p) => new Minimatch(p, mmo));
+    const [matchSet, globParts] = mms.reduce((set, m) => {
+      set[0].push(...m.set);
+      set[1].push(...m.globParts);
+      return set;
+    }, [[], []]);
+    this.patterns = matchSet.map((set, i) => {
+      const g = globParts[i];
+      if (!g)
+        throw new Error("invalid pattern object");
+      return new Pattern(set, g, 0, this.platform);
+    });
+  }
+  async walk() {
+    return [
+      ...await new GlobWalker(this.patterns, this.scurry.cwd, {
+        ...this.opts,
+        maxDepth: this.maxDepth !== Infinity ? this.maxDepth + this.scurry.cwd.depth() : Infinity,
+        platform: this.platform,
+        nocase: this.nocase,
+        includeChildMatches: this.includeChildMatches
+      }).walk()
+    ];
+  }
+  walkSync() {
+    return [
+      ...new GlobWalker(this.patterns, this.scurry.cwd, {
+        ...this.opts,
+        maxDepth: this.maxDepth !== Infinity ? this.maxDepth + this.scurry.cwd.depth() : Infinity,
+        platform: this.platform,
+        nocase: this.nocase,
+        includeChildMatches: this.includeChildMatches
+      }).walkSync()
+    ];
+  }
+  stream() {
+    return new GlobStream(this.patterns, this.scurry.cwd, {
+      ...this.opts,
+      maxDepth: this.maxDepth !== Infinity ? this.maxDepth + this.scurry.cwd.depth() : Infinity,
+      platform: this.platform,
+      nocase: this.nocase,
+      includeChildMatches: this.includeChildMatches
+    }).stream();
+  }
+  streamSync() {
+    return new GlobStream(this.patterns, this.scurry.cwd, {
+      ...this.opts,
+      maxDepth: this.maxDepth !== Infinity ? this.maxDepth + this.scurry.cwd.depth() : Infinity,
+      platform: this.platform,
+      nocase: this.nocase,
+      includeChildMatches: this.includeChildMatches
+    }).streamSync();
+  }
+  /**
+   * Default sync iteration function. Returns a Generator that
+   * iterates over the results.
+   */
+  iterateSync() {
+    return this.streamSync()[Symbol.iterator]();
+  }
+  [Symbol.iterator]() {
+    return this.iterateSync();
+  }
+  /**
+   * Default async iteration function. Returns an AsyncGenerator that
+   * iterates over the results.
+   */
+  iterate() {
+    return this.stream()[Symbol.asyncIterator]();
+  }
+  [Symbol.asyncIterator]() {
+    return this.iterate();
+  }
+};
+
+// node_modules/glob/dist/esm/has-magic.js
+var hasMagic = (pattern, options = {}) => {
+  if (!Array.isArray(pattern)) {
+    pattern = [pattern];
+  }
+  for (const p of pattern) {
+    if (new Minimatch(p, options).hasMagic())
+      return true;
+  }
+  return false;
+};
+
+// node_modules/glob/dist/esm/index.js
+function globStreamSync(pattern, options = {}) {
+  return new Glob(pattern, options).streamSync();
+}
+function globStream(pattern, options = {}) {
+  return new Glob(pattern, options).stream();
+}
+function globSync(pattern, options = {}) {
+  return new Glob(pattern, options).walkSync();
+}
+async function glob_(pattern, options = {}) {
+  return new Glob(pattern, options).walk();
+}
+function globIterateSync(pattern, options = {}) {
+  return new Glob(pattern, options).iterateSync();
+}
+function globIterate(pattern, options = {}) {
+  return new Glob(pattern, options).iterate();
+}
+var streamSync = globStreamSync;
+var stream = Object.assign(globStream, { sync: globStreamSync });
+var iterateSync = globIterateSync;
+var iterate = Object.assign(globIterate, {
+  sync: globIterateSync
+});
+var sync = Object.assign(globSync, {
+  stream: globStreamSync,
+  iterate: globIterateSync
+});
+var glob = Object.assign(glob_, {
+  glob: glob_,
+  globSync,
+  sync,
+  globStream,
+  stream,
+  globStreamSync,
+  streamSync,
+  globIterate,
+  iterate,
+  globIterateSync,
+  iterateSync,
+  Glob,
+  hasMagic,
+  escape,
+  unescape
+});
+glob.glob = glob;
+
+// src/tools/doc-sources.ts
+function hashContent(content) {
+  return createHash("sha256").update(content).digest("hex");
+}
+function assertSafeSlug(slug) {
+  if (!slug || /[\\/]|\.\./.test(slug)) {
+    throw new Error(`unsafe slug: ${JSON.stringify(slug)}`);
+  }
+}
+
+// src/tools/raw-inbox.ts
+function rawDir(brainDir, slug) {
+  return join(brainDir, "projects", slug, "raw");
+}
+function slugify(text) {
+  const s = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
+  return s || "item";
+}
+function compactStamp(iso) {
+  return iso.replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
+}
+function isBinary(buf) {
+  const n = Math.min(buf.length, 8192);
+  for (let i = 0; i < n; i++) if (buf[i] === 0) return true;
+  return false;
+}
+function contentTypeForFile(path2, binary) {
+  const ext2 = extname(path2).toLowerCase();
+  if (binary) return ext2 === ".pdf" ? "application/pdf" : "application/octet-stream";
+  return ext2 === ".md" || ext2 === ".markdown" ? "text/markdown" : "text/plain";
+}
+function serialize(item) {
+  const fm = ["---"];
+  fm.push(`id: ${item.id}`);
+  fm.push(`source: ${item.source}`);
+  fm.push(`captured_at: ${item.captured_at}`);
+  fm.push(`captured_by: ${item.captured_by}`);
+  fm.push(`content_type: ${item.content_type}`);
+  fm.push(`status: ${item.status}`);
+  if (item.target_node) fm.push(`target_node: ${item.target_node}`);
+  if (item.blob) fm.push(`blob: ${item.blob}`);
+  fm.push(`hash: ${item.hash}`);
+  fm.push(`gist: ${item.gist.replace(/\n/g, " ")}`);
+  fm.push("---", "", item.body, "");
+  return fm.join("\n");
+}
+function parse(content, id) {
+  const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
+  const base = {
+    id,
+    source: "",
+    captured_at: "",
+    captured_by: "user",
+    content_type: "",
+    status: "unprocessed",
+    hash: "",
+    gist: "",
+    body: ""
+  };
+  if (!m) {
+    return { ...base, body: content, malformed: true };
+  }
+  const [, fmText, body] = m;
+  const get = (k) => {
+    const mm = fmText.match(new RegExp(`^${k}:[ \\t]*(.*)$`, "m"));
+    return mm ? mm[1].trim() : void 0;
+  };
+  const status = get("status") ?? "";
+  const validStatus = status === "unprocessed" || status === "processed" || status === "discarded";
+  const item = {
+    id,
+    source: get("source") ?? "",
+    captured_at: get("captured_at") ?? "",
+    captured_by: get("captured_by") ?? "user",
+    content_type: get("content_type") ?? "",
+    status: validStatus ? status : "unprocessed",
+    target_node: get("target_node") || void 0,
+    blob: get("blob") || void 0,
+    hash: get("hash") ?? "",
+    gist: get("gist") ?? "",
+    body: body.trim()
+  };
+  if (!item.source || !item.captured_at || !item.content_type || !validStatus) item.malformed = true;
+  return item;
+}
+async function readItems(brainDir, slug) {
+  const dir = rawDir(brainDir, slug);
+  let names = [];
+  try {
+    names = (await fs.readdir(dir)).filter((n) => n.endsWith(".md"));
+  } catch {
+    return [];
+  }
+  const items = [];
+  for (const name of names.sort()) {
+    try {
+      const content = await fs.readFile(join(dir, name), "utf-8");
+      items.push(parse(content, name.replace(/\.md$/, "")));
+    } catch {
+    }
+  }
+  return items;
+}
+async function listItems(brainDir, slug) {
+  assertSafeSlug(slug);
+  return readItems(brainDir, slug);
+}
+async function unprocessedCount(brainDir, slug) {
+  assertSafeSlug(slug);
+  const items = await readItems(brainDir, slug);
+  return items.filter((i) => i.status === "unprocessed" || i.malformed).length;
+}
+async function setStatus(brainDir, slug, id, status) {
+  assertSafeSlug(slug);
+  const file = join(rawDir(brainDir, slug), `${id}.md`);
+  let content;
+  try {
+    content = await fs.readFile(file, "utf-8");
+  } catch {
+    return false;
+  }
+  const next = /^status:[ \t]*.*$/m.test(content) ? content.replace(/^status:[ \t]*.*$/m, `status: ${status}`) : content.replace(/^---\r?\n/, `---
+status: ${status}
+`);
+  const tmp = `${file}.tmp`;
+  await fs.writeFile(tmp, next);
+  await fs.rename(tmp, file);
+  return true;
+}
+async function captureItem(input) {
+  assertSafeSlug(input.slug);
+  const dir = rawDir(input.brainDir, input.slug);
+  const now = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
+  const capturedBy = input.capturedBy ?? "user";
+  let body = "";
+  let blobBuf;
+  let blobExt = "";
+  let contentType = "";
+  let gistSeed = "";
+  let hashInput = "";
+  if (input.kind === "paste") {
+    body = input.content ?? "";
+    contentType = "text/markdown";
+    gistSeed = body;
+    hashInput = `paste:${body}`;
+  } else if (input.kind === "url") {
+    const url = input.content ?? input.source;
+    body = url;
+    contentType = "text/uri-list";
+    gistSeed = url;
+    hashInput = `url:${url}`;
+  } else {
+    const buf = await fs.readFile(input.source);
+    const binary = isBinary(buf);
+    contentType = contentTypeForFile(input.source, binary);
+    hashInput = `file:${hashContent(buf.toString("binary"))}`;
+    if (binary) {
+      blobBuf = buf;
+      blobExt = extname(input.source) || ".bin";
+      gistSeed = basename(input.source);
+      body = `(binary ${contentType}; original captured as the sibling blob) \u2014 ${basename(input.source)}`;
+    } else {
+      body = buf.toString("utf-8");
+      gistSeed = body;
+    }
+  }
+  const hash = hashContent(hashInput);
+  const existing = (await readItems(input.brainDir, input.slug)).find((i) => i.hash === hash && i.status === "unprocessed");
+  if (existing) {
+    return { id: existing.id, duplicate: true, unprocessed: await unprocessedCount(input.brainDir, input.slug) };
+  }
+  await fs.mkdir(dir, { recursive: true });
+  const sourceSlug = input.kind === "url" ? slugify(input.content ?? input.source) : input.kind === "file" ? slugify(basename(input.source)) : slugify(body);
+  const baseId = `${compactStamp(now)}-${sourceSlug}`;
+  let id = baseId;
+  for (let n = 2; ; n++) {
+    try {
+      await fs.access(join(dir, `${id}.md`));
+      id = `${baseId}-${n}`;
+    } catch {
+      break;
+    }
+  }
+  const blob = blobBuf ? `${id}${blobExt}` : void 0;
+  if (blobBuf && blob) {
+    const btmp = join(dir, `${blob}.tmp`);
+    await fs.writeFile(btmp, blobBuf);
+    await fs.rename(btmp, join(dir, blob));
+  }
+  const gist = gistSeed.replace(/^#\s*/, "").split("\n").map((l) => l.trim()).find(Boolean)?.slice(0, 120) ?? "";
+  const item = {
+    id,
+    source: input.source,
+    captured_at: now,
+    captured_by: capturedBy,
+    content_type: contentType,
+    status: "unprocessed",
+    target_node: input.targetNode,
+    blob,
+    hash,
+    gist,
+    body
+  };
+  const file = join(dir, `${id}.md`);
+  const tmp = `${file}.tmp`;
+  await fs.writeFile(tmp, serialize(item));
+  await fs.rename(tmp, file);
+  return { id, duplicate: false, unprocessed: await unprocessedCount(input.brainDir, input.slug) };
+}
+
+// src/tools/raw-capture-cli.ts
+function resolveSlug(brainDir) {
+  if (process.env.SB_ACTIVE_SLUG) return process.env.SB_ACTIVE_SLUG;
+  try {
+    const pin = readFileSync(join2(brainDir, ".active-session-slug"), "utf-8").trim();
+    if (pin && existsSync(join2(brainDir, "projects", pin, "PROJECT.md"))) return pin;
+  } catch {
+  }
+  const base = basename2(process.cwd());
+  return base && base !== "/" && base !== "." ? base : void 0;
+}
+function takeNode(args) {
+  const i = args.indexOf("--node");
+  if (i >= 0 && args[i + 1]) return { rest: [...args.slice(0, i), ...args.slice(i + 2)], node: args[i + 1] };
+  return { rest: args };
+}
+async function main() {
+  const brainDir = process.env.BRAIN_DIR || join2(homedir(), ".second-brain");
+  const slug = resolveSlug(brainDir);
+  if (!slug) {
+    console.log("capture: could not resolve the active project (no slug). cd into a project.");
+    return;
+  }
+  const action = process.argv[2];
+  const { rest, node } = takeNode(process.argv.slice(3));
+  try {
+    if (action === "list") {
+      const items = await listItems(brainDir, slug);
+      const open = items.filter((i) => i.status === "unprocessed" || i.malformed).length;
+      console.log(`Raw inbox for ${slug} \u2014 ${items.length} item(s), ${open} unprocessed:`);
+      for (const i of items) {
+        console.log(`  - ${i.id} [${i.malformed ? "malformed" : i.status}] ${i.gist || i.source}`);
+      }
+      if (items.length === 0) console.log("  (empty \u2014 capture something, e.g. /second-brain:capture ./notes.md)");
+    } else if (action === "discard") {
+      const id = rest[0];
+      if (!id) {
+        console.log("usage: capture --discard ");
+        return;
+      }
+      console.log(await setStatus(brainDir, slug, id, "discarded") ? `Discarded ${id}.` : `No raw item with id ${id}.`);
+    } else if (action === "paste") {
+      const content = readFileSync(0, "utf-8");
+      if (!content.trim()) {
+        console.log("capture: nothing on stdin.");
+        return;
+      }
+      const r = await captureItem({ brainDir, slug, kind: "paste", source: "paste", content, targetNode: node });
+      console.log(`${r.duplicate ? "Already captured" : "Captured"} ${r.id} \u2014 ${r.unprocessed} unprocessed.`);
+    } else if (action === "capture") {
+      const src = rest[0];
+      if (!src) {
+        console.log("usage: capture  [--node ]  |  capture paste");
+        return;
+      }
+      let kind;
+      let content;
+      if (/^https?:\/\//i.test(src)) {
+        kind = "url";
+        content = src;
+      } else if (existsSync(src) && statSync(src).isFile()) {
+        kind = "file";
+      } else {
+        kind = "paste";
+        content = src;
+      }
+      const r = await captureItem({ brainDir, slug, kind, source: src, content, targetNode: node });
+      console.log(`${r.duplicate ? "Already captured" : "Captured"} ${r.id} (${kind}) \u2014 ${r.unprocessed} unprocessed.`);
+    } else {
+      const n = await unprocessedCount(brainDir, slug);
+      console.log(`usage: capture  | capture paste | capture --list | capture --discard   (${n} unprocessed)`);
+    }
+  } catch (e) {
+    console.log(`capture error: ${e instanceof Error ? e.message : String(e)}`);
+  }
+}
+main();
diff --git a/mcp/dist/tools/raw-capture-cli.d.ts b/mcp/dist/tools/raw-capture-cli.d.ts
new file mode 100644
index 0000000..1cfcb95
--- /dev/null
+++ b/mcp/dist/tools/raw-capture-cli.d.ts
@@ -0,0 +1,2 @@
+export {};
+//# sourceMappingURL=raw-capture-cli.d.ts.map
\ No newline at end of file
diff --git a/mcp/dist/tools/raw-capture-cli.d.ts.map b/mcp/dist/tools/raw-capture-cli.d.ts.map
new file mode 100644
index 0000000..9c1c403
--- /dev/null
+++ b/mcp/dist/tools/raw-capture-cli.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"raw-capture-cli.d.ts","sourceRoot":"","sources":["../../src/tools/raw-capture-cli.ts"],"names":[],"mappings":""}
\ No newline at end of file
diff --git a/mcp/dist/tools/raw-capture-cli.js b/mcp/dist/tools/raw-capture-cli.js
new file mode 100644
index 0000000..9b39c18
--- /dev/null
+++ b/mcp/dist/tools/raw-capture-cli.js
@@ -0,0 +1,94 @@
+import { homedir } from 'os';
+import { join, basename } from 'path';
+import { existsSync, readFileSync, statSync } from 'fs';
+import { captureItem, listItems, setStatus, unprocessedCount } from './raw-inbox.js';
+function resolveSlug(brainDir) {
+    if (process.env.SB_ACTIVE_SLUG)
+        return process.env.SB_ACTIVE_SLUG;
+    try {
+        const pin = readFileSync(join(brainDir, '.active-session-slug'), 'utf-8').trim();
+        if (pin && existsSync(join(brainDir, 'projects', pin, 'PROJECT.md')))
+            return pin;
+    }
+    catch { /* no pin */ }
+    const base = basename(process.cwd());
+    return base && base !== '/' && base !== '.' ? base : undefined;
+}
+/** Pull `--node ` out of argv; return the rest + the node value. */
+function takeNode(args) {
+    const i = args.indexOf('--node');
+    if (i >= 0 && args[i + 1])
+        return { rest: [...args.slice(0, i), ...args.slice(i + 2)], node: args[i + 1] };
+    return { rest: args };
+}
+async function main() {
+    const brainDir = process.env.BRAIN_DIR || join(homedir(), '.second-brain');
+    const slug = resolveSlug(brainDir);
+    if (!slug) {
+        console.log('capture: could not resolve the active project (no slug). cd into a project.');
+        return;
+    }
+    const action = process.argv[2];
+    const { rest, node } = takeNode(process.argv.slice(3));
+    try {
+        if (action === 'list') {
+            const items = await listItems(brainDir, slug);
+            const open = items.filter(i => i.status === 'unprocessed' || i.malformed).length;
+            console.log(`Raw inbox for ${slug} — ${items.length} item(s), ${open} unprocessed:`);
+            for (const i of items) {
+                console.log(`  - ${i.id} [${i.malformed ? 'malformed' : i.status}] ${i.gist || i.source}`);
+            }
+            if (items.length === 0)
+                console.log('  (empty — capture something, e.g. /second-brain:capture ./notes.md)');
+        }
+        else if (action === 'discard') {
+            const id = rest[0];
+            if (!id) {
+                console.log('usage: capture --discard ');
+                return;
+            }
+            console.log(await setStatus(brainDir, slug, id, 'discarded')
+                ? `Discarded ${id}.` : `No raw item with id ${id}.`);
+        }
+        else if (action === 'paste') {
+            const content = readFileSync(0, 'utf-8'); // stdin
+            if (!content.trim()) {
+                console.log('capture: nothing on stdin.');
+                return;
+            }
+            const r = await captureItem({ brainDir, slug, kind: 'paste', source: 'paste', content, targetNode: node });
+            console.log(`${r.duplicate ? 'Already captured' : 'Captured'} ${r.id} — ${r.unprocessed} unprocessed.`);
+        }
+        else if (action === 'capture') {
+            const src = rest[0];
+            if (!src) {
+                console.log('usage: capture  [--node ]  |  capture paste');
+                return;
+            }
+            let kind;
+            let content;
+            if (/^https?:\/\//i.test(src)) {
+                kind = 'url';
+                content = src;
+            }
+            else if (existsSync(src) && statSync(src).isFile()) {
+                kind = 'file';
+            }
+            else {
+                kind = 'paste';
+                content = src;
+            } // inline text fallback
+            const r = await captureItem({ brainDir, slug, kind, source: src, content, targetNode: node });
+            console.log(`${r.duplicate ? 'Already captured' : 'Captured'} ${r.id} (${kind}) — ${r.unprocessed} unprocessed.`);
+        }
+        else {
+            const n = await unprocessedCount(brainDir, slug);
+            console.log(`usage: capture  | capture paste | capture --list | capture --discard   (${n} unprocessed)`);
+        }
+    }
+    catch (e) {
+        console.log(`capture error: ${e instanceof Error ? e.message : String(e)}`);
+    }
+}
+main();
+//# sourceMappingURL=raw-capture-cli.js.map
\ No newline at end of file
diff --git a/mcp/dist/tools/raw-capture-cli.js.map b/mcp/dist/tools/raw-capture-cli.js.map
new file mode 100644
index 0000000..758af63
--- /dev/null
+++ b/mcp/dist/tools/raw-capture-cli.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"raw-capture-cli.js","sourceRoot":"","sources":["../../src/tools/raw-capture-cli.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
diff --git a/mcp/dist/tools/raw-inbox.d.ts b/mcp/dist/tools/raw-inbox.d.ts
new file mode 100644
index 0000000..122587e
--- /dev/null
+++ b/mcp/dist/tools/raw-inbox.d.ts
@@ -0,0 +1,36 @@
+export type RawStatus = 'unprocessed' | 'processed' | 'discarded';
+export type CapturedBy = 'user' | 'setup-scan' | 'dream';
+export interface RawItem {
+    id: string;
+    source: string;
+    captured_at: string;
+    captured_by: CapturedBy;
+    content_type: string;
+    status: RawStatus;
+    target_node?: string;
+    blob?: string;
+    hash: string;
+    gist: string;
+    body: string;
+    malformed?: boolean;
+}
+export interface CaptureInput {
+    brainDir: string;
+    slug: string;
+    source: string;
+    kind: 'file' | 'url' | 'paste';
+    content?: string;
+    targetNode?: string;
+    capturedBy?: CapturedBy;
+    now?: string;
+}
+export declare function rawDir(brainDir: string, slug: string): string;
+export declare function listItems(brainDir: string, slug: string): Promise;
+export declare function unprocessedCount(brainDir: string, slug: string): Promise;
+export declare function setStatus(brainDir: string, slug: string, id: string, status: RawStatus): Promise;
+export declare function captureItem(input: CaptureInput): Promise<{
+    id: string;
+    duplicate: boolean;
+    unprocessed: number;
+}>;
+//# sourceMappingURL=raw-inbox.d.ts.map
\ No newline at end of file
diff --git a/mcp/dist/tools/raw-inbox.d.ts.map b/mcp/dist/tools/raw-inbox.d.ts.map
new file mode 100644
index 0000000..48e6c23
--- /dev/null
+++ b/mcp/dist/tools/raw-inbox.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"raw-inbox.d.ts","sourceRoot":"","sources":["../../src/tools/raw-inbox.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,SAAS,GAAG,aAAa,GAAG,WAAW,GAAG,WAAW,CAAC;AAClE,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,YAAY,GAAG,OAAO,CAAC;AAEzD,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,UAAU,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,SAAS,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,GAAG,KAAK,GAAG,OAAO,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7D;AAwFD,wBAAsB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAGlF;AAED,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAKtF;AAED,wBAAsB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,CAY/G;AAED,wBAAsB,WAAW,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC,CAgFvH"}
\ No newline at end of file
diff --git a/mcp/dist/tools/raw-inbox.js b/mcp/dist/tools/raw-inbox.js
new file mode 100644
index 0000000..c84dd16
--- /dev/null
+++ b/mcp/dist/tools/raw-inbox.js
@@ -0,0 +1,210 @@
+import { promises as fs } from 'fs';
+import { join, basename, extname } from 'path';
+import { hashContent, assertSafeSlug } from './doc-sources.js';
+export function rawDir(brainDir, slug) {
+    return join(brainDir, 'projects', slug, 'raw');
+}
+/** kebab slug from arbitrary text: lowercase, non-alnum→'-', collapse, trim, cap length. */
+function slugify(text) {
+    const s = text.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40);
+    return s || 'item';
+}
+/** ISO '2026-06-03T14:15:00Z' → compact sortable '20260603T141500Z'. */
+function compactStamp(iso) {
+    return iso.replace(/[-:]/g, '').replace(/\.\d+Z$/, 'Z');
+}
+function isBinary(buf) {
+    const n = Math.min(buf.length, 8192);
+    for (let i = 0; i < n; i++)
+        if (buf[i] === 0)
+            return true;
+    return false;
+}
+function contentTypeForFile(path, binary) {
+    const ext = extname(path).toLowerCase();
+    if (binary)
+        return ext === '.pdf' ? 'application/pdf' : 'application/octet-stream';
+    return ext === '.md' || ext === '.markdown' ? 'text/markdown' : 'text/plain';
+}
+function serialize(item) {
+    const fm = ['---'];
+    fm.push(`id: ${item.id}`);
+    fm.push(`source: ${item.source}`);
+    fm.push(`captured_at: ${item.captured_at}`);
+    fm.push(`captured_by: ${item.captured_by}`);
+    fm.push(`content_type: ${item.content_type}`);
+    fm.push(`status: ${item.status}`);
+    if (item.target_node)
+        fm.push(`target_node: ${item.target_node}`);
+    if (item.blob)
+        fm.push(`blob: ${item.blob}`);
+    fm.push(`hash: ${item.hash}`);
+    fm.push(`gist: ${item.gist.replace(/\n/g, ' ')}`);
+    fm.push('---', '', item.body, '');
+    return fm.join('\n');
+}
+/** Tolerant flat-frontmatter parse. id comes from the filename (authoritative). */
+function parse(content, id) {
+    const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
+    const base = {
+        id, source: '', captured_at: '', captured_by: 'user', content_type: '',
+        status: 'unprocessed', hash: '', gist: '', body: '',
+    };
+    if (!m) {
+        return { ...base, body: content, malformed: true };
+    }
+    const [, fmText, body] = m;
+    const get = (k) => {
+        const mm = fmText.match(new RegExp(`^${k}:[ \\t]*(.*)$`, 'm'));
+        return mm ? mm[1].trim() : undefined;
+    };
+    const status = (get('status') ?? '');
+    const validStatus = status === 'unprocessed' || status === 'processed' || status === 'discarded';
+    const item = {
+        id,
+        source: get('source') ?? '',
+        captured_at: get('captured_at') ?? '',
+        captured_by: get('captured_by') ?? 'user',
+        content_type: get('content_type') ?? '',
+        status: validStatus ? status : 'unprocessed',
+        target_node: get('target_node') || undefined,
+        blob: get('blob') || undefined,
+        hash: get('hash') ?? '',
+        gist: get('gist') ?? '',
+        body: body.trim(),
+    };
+    // Malformed = missing the fields a well-formed capture always writes, or a bad status.
+    if (!item.source || !item.captured_at || !item.content_type || !validStatus)
+        item.malformed = true;
+    return item;
+}
+async function readItems(brainDir, slug) {
+    const dir = rawDir(brainDir, slug);
+    let names = [];
+    try {
+        names = (await fs.readdir(dir)).filter(n => n.endsWith('.md'));
+    }
+    catch {
+        return [];
+    }
+    const items = [];
+    for (const name of names.sort()) {
+        try {
+            const content = await fs.readFile(join(dir, name), 'utf-8');
+            items.push(parse(content, name.replace(/\.md$/, '')));
+        }
+        catch { /* skip unreadable */ }
+    }
+    return items;
+}
+export async function listItems(brainDir, slug) {
+    assertSafeSlug(slug);
+    return readItems(brainDir, slug);
+}
+export async function unprocessedCount(brainDir, slug) {
+    assertSafeSlug(slug);
+    const items = await readItems(brainDir, slug);
+    // malformed items count as unprocessed so they stay visible in the backlog.
+    return items.filter(i => i.status === 'unprocessed' || i.malformed).length;
+}
+export async function setStatus(brainDir, slug, id, status) {
+    assertSafeSlug(slug);
+    const file = join(rawDir(brainDir, slug), `${id}.md`);
+    let content;
+    try {
+        content = await fs.readFile(file, 'utf-8');
+    }
+    catch {
+        return false;
+    }
+    const next = /^status:[ \t]*.*$/m.test(content)
+        ? content.replace(/^status:[ \t]*.*$/m, `status: ${status}`)
+        : content.replace(/^---\r?\n/, `---\nstatus: ${status}\n`);
+    const tmp = `${file}.tmp`;
+    await fs.writeFile(tmp, next);
+    await fs.rename(tmp, file); // atomic
+    return true;
+}
+export async function captureItem(input) {
+    assertSafeSlug(input.slug);
+    const dir = rawDir(input.brainDir, input.slug);
+    const now = input.now ?? new Date().toISOString();
+    const capturedBy = input.capturedBy ?? 'user';
+    // Resolve content + blob + content_type by kind.
+    let body = '';
+    let blobBuf;
+    let blobExt = '';
+    let contentType = '';
+    let gistSeed = '';
+    let hashInput = '';
+    if (input.kind === 'paste') {
+        body = input.content ?? '';
+        contentType = 'text/markdown';
+        gistSeed = body;
+        hashInput = `paste:${body}`;
+    }
+    else if (input.kind === 'url') {
+        const url = input.content ?? input.source;
+        body = url;
+        contentType = 'text/uri-list';
+        gistSeed = url;
+        hashInput = `url:${url}`;
+    }
+    else {
+        const buf = await fs.readFile(input.source);
+        const binary = isBinary(buf);
+        contentType = contentTypeForFile(input.source, binary);
+        hashInput = `file:${hashContent(buf.toString('binary'))}`;
+        if (binary) {
+            blobBuf = buf;
+            blobExt = extname(input.source) || '.bin';
+            gistSeed = basename(input.source);
+            body = `(binary ${contentType}; original captured as the sibling blob) — ${basename(input.source)}`;
+        }
+        else {
+            body = buf.toString('utf-8');
+            gistSeed = body;
+        }
+    }
+    const hash = hashContent(hashInput);
+    // Dedup: an existing UNPROCESSED item with the same hash → return it.
+    const existing = (await readItems(input.brainDir, input.slug))
+        .find(i => i.hash === hash && i.status === 'unprocessed');
+    if (existing) {
+        return { id: existing.id, duplicate: true, unprocessed: await unprocessedCount(input.brainDir, input.slug) };
+    }
+    // Unique id (collision suffix).
+    await fs.mkdir(dir, { recursive: true });
+    const sourceSlug = input.kind === 'url' ? slugify(input.content ?? input.source)
+        : input.kind === 'file' ? slugify(basename(input.source))
+            : slugify(body);
+    const baseId = `${compactStamp(now)}-${sourceSlug}`;
+    let id = baseId;
+    for (let n = 2;; n++) {
+        try {
+            await fs.access(join(dir, `${id}.md`));
+            id = `${baseId}-${n}`;
+        }
+        catch {
+            break;
+        }
+    }
+    const blob = blobBuf ? `${id}${blobExt}` : undefined;
+    if (blobBuf && blob) {
+        const btmp = join(dir, `${blob}.tmp`);
+        await fs.writeFile(btmp, blobBuf);
+        await fs.rename(btmp, join(dir, blob));
+    }
+    const gist = gistSeed.replace(/^#\s*/, '').split('\n').map(l => l.trim()).find(Boolean)?.slice(0, 120) ?? '';
+    const item = {
+        id, source: input.source, captured_at: now, captured_by: capturedBy,
+        content_type: contentType, status: 'unprocessed',
+        target_node: input.targetNode, blob, hash, gist, body,
+    };
+    const file = join(dir, `${id}.md`);
+    const tmp = `${file}.tmp`;
+    await fs.writeFile(tmp, serialize(item));
+    await fs.rename(tmp, file);
+    return { id, duplicate: false, unprocessed: await unprocessedCount(input.brainDir, input.slug) };
+}
+//# sourceMappingURL=raw-inbox.js.map
\ No newline at end of file
diff --git a/mcp/dist/tools/raw-inbox.js.map b/mcp/dist/tools/raw-inbox.js.map
new file mode 100644
index 0000000..0529379
--- /dev/null
+++ b/mcp/dist/tools/raw-inbox.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"raw-inbox.js","sourceRoot":"","sources":["../../src/tools/raw-inbox.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,IAAI,CAAC;AACpC,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AA+B/D,MAAM,UAAU,MAAM,CAAC,QAAgB,EAAE,IAAY;IACnD,OAAO,IAAI,CAAC,QAAQ,EAAE,UAAU,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;AACjD,CAAC;AAED,4FAA4F;AAC5F,SAAS,OAAO,CAAC,IAAY;IAC3B,MAAM,CAAC,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAC9F,OAAO,CAAC,IAAI,MAAM,CAAC;AACrB,CAAC;AAED,wEAAwE;AACxE,SAAS,YAAY,CAAC,GAAW;IAC/B,OAAO,GAAG,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;AAC1D,CAAC;AAED,SAAS,QAAQ,CAAC,GAAW;IAC3B,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IACrC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE;QAAE,IAAI,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;IAC1D,OAAO,KAAK,CAAC;AACf,CAAC;AAED,SAAS,kBAAkB,CAAC,IAAY,EAAE,MAAe;IACvD,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;IACxC,IAAI,MAAM;QAAE,OAAO,GAAG,KAAK,MAAM,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,0BAA0B,CAAC;IACnF,OAAO,GAAG,KAAK,KAAK,IAAI,GAAG,KAAK,WAAW,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,YAAY,CAAC;AAC/E,CAAC;AAED,SAAS,SAAS,CAAC,IAAa;IAC9B,MAAM,EAAE,GAAa,CAAC,KAAK,CAAC,CAAC;IAC7B,EAAE,CAAC,IAAI,CAAC,OAAO,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;IAC1B,EAAE,CAAC,IAAI,CAAC,WAAW,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IAClC,EAAE,CAAC,IAAI,CAAC,gBAAgB,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;IAC5C,EAAE,CAAC,IAAI,CAAC,gBAAgB,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;IAC5C,EAAE,CAAC,IAAI,CAAC,iBAAiB,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC;IAC9C,EAAE,CAAC,IAAI,CAAC,WAAW,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IAClC,IAAI,IAAI,CAAC,WAAW;QAAE,EAAE,CAAC,IAAI,CAAC,gBAAgB,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;IAClE,IAAI,IAAI,CAAC,IAAI;QAAE,EAAE,CAAC,IAAI,CAAC,SAAS,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IAC7C,EAAE,CAAC,IAAI,CAAC,SAAS,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IAC9B,EAAE,CAAC,IAAI,CAAC,SAAS,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;IAClD,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,EAAE,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IAClC,OAAO,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACvB,CAAC;AAED,mFAAmF;AACnF,SAAS,KAAK,CAAC,OAAe,EAAE,EAAU;IACxC,MAAM,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,6CAA6C,CAAC,CAAC;IACvE,MAAM,IAAI,GAAY;QACpB,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,WAAW,EAAE,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,EAAE;QACtE,MAAM,EAAE,aAAa,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE;KACpD,CAAC;IACF,IAAI,CAAC,CAAC,EAAE,CAAC;QAAC,OAAO,EAAE,GAAG,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC;IAAC,CAAC;IAC/D,MAAM,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;IAC3B,MAAM,GAAG,GAAG,CAAC,CAAS,EAAsB,EAAE;QAC5C,MAAM,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,eAAe,EAAE,GAAG,CAAC,CAAC,CAAC;QAC/D,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IACvC,CAAC,CAAC;IACF,MAAM,MAAM,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAc,CAAC;IAClD,MAAM,WAAW,GAAG,MAAM,KAAK,aAAa,IAAI,MAAM,KAAK,WAAW,IAAI,MAAM,KAAK,WAAW,CAAC;IACjG,MAAM,IAAI,GAAY;QACpB,EAAE;QACF,MAAM,EAAE,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE;QAC3B,WAAW,EAAE,GAAG,CAAC,aAAa,CAAC,IAAI,EAAE;QACrC,WAAW,EAAG,GAAG,CAAC,aAAa,CAAgB,IAAI,MAAM;QACzD,YAAY,EAAE,GAAG,CAAC,cAAc,CAAC,IAAI,EAAE;QACvC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,aAAa;QAC5C,WAAW,EAAE,GAAG,CAAC,aAAa,CAAC,IAAI,SAAS;QAC5C,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,SAAS;QAC9B,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE;QACvB,IAAI,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE;QACvB,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE;KAClB,CAAC;IACF,uFAAuF;IACvF,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,WAAW,IAAI,CAAC,IAAI,CAAC,YAAY,IAAI,CAAC,WAAW;QAAE,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;IACnG,OAAO,IAAI,CAAC;AACd,CAAC;AAED,KAAK,UAAU,SAAS,CAAC,QAAgB,EAAE,IAAY;IACrD,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IACnC,IAAI,KAAK,GAAa,EAAE,CAAC;IACzB,IAAI,CAAC;QAAC,KAAK,GAAG,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC;QAAC,OAAO,EAAE,CAAC;IAAC,CAAC;IAC5F,MAAM,KAAK,GAAc,EAAE,CAAC;IAC5B,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,IAAI,EAAE,EAAE,CAAC;QAChC,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,OAAO,CAAC,CAAC;YAC5D,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;QACxD,CAAC;QAAC,MAAM,CAAC,CAAC,qBAAqB,CAAC,CAAC;IACnC,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,QAAgB,EAAE,IAAY;IAC5D,cAAc,CAAC,IAAI,CAAC,CAAC;IACrB,OAAO,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;AACnC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,QAAgB,EAAE,IAAY;IACnE,cAAc,CAAC,IAAI,CAAC,CAAC;IACrB,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC9C,4EAA4E;IAC5E,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,aAAa,IAAI,CAAC,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC;AAC7E,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,QAAgB,EAAE,IAAY,EAAE,EAAU,EAAE,MAAiB;IAC3F,cAAc,CAAC,IAAI,CAAC,CAAC;IACrB,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;IACtD,IAAI,OAAe,CAAC;IACpB,IAAI,CAAC;QAAC,OAAO,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC;QAAC,OAAO,KAAK,CAAC;IAAC,CAAC;IAC3E,MAAM,IAAI,GAAG,oBAAoB,CAAC,IAAI,CAAC,OAAO,CAAC;QAC7C,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,oBAAoB,EAAE,WAAW,MAAM,EAAE,CAAC;QAC5D,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,WAAW,EAAE,gBAAgB,MAAM,IAAI,CAAC,CAAC;IAC7D,MAAM,GAAG,GAAG,GAAG,IAAI,MAAM,CAAC;IAC1B,MAAM,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAC9B,MAAM,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,SAAS;IACrC,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,KAAmB;IACnD,cAAc,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC3B,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;IAC/C,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,IAAI,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAClD,MAAM,UAAU,GAAG,KAAK,CAAC,UAAU,IAAI,MAAM,CAAC;IAE9C,iDAAiD;IACjD,IAAI,IAAI,GAAG,EAAE,CAAC;IACd,IAAI,OAA2B,CAAC;IAChC,IAAI,OAAO,GAAG,EAAE,CAAC;IACjB,IAAI,WAAW,GAAG,EAAE,CAAC;IACrB,IAAI,QAAQ,GAAG,EAAE,CAAC;IAClB,IAAI,SAAS,GAAG,EAAE,CAAC;IAEnB,IAAI,KAAK,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;QAC3B,IAAI,GAAG,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC;QAC3B,WAAW,GAAG,eAAe,CAAC;QAC9B,QAAQ,GAAG,IAAI,CAAC;QAChB,SAAS,GAAG,SAAS,IAAI,EAAE,CAAC;IAC9B,CAAC;SAAM,IAAI,KAAK,CAAC,IAAI,KAAK,KAAK,EAAE,CAAC;QAChC,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,MAAM,CAAC;QAC1C,IAAI,GAAG,GAAG,CAAC;QACX,WAAW,GAAG,eAAe,CAAC;QAC9B,QAAQ,GAAG,GAAG,CAAC;QACf,SAAS,GAAG,OAAO,GAAG,EAAE,CAAC;IAC3B,CAAC;SAAM,CAAC;QACN,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAC5C,MAAM,MAAM,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;QAC7B,WAAW,GAAG,kBAAkB,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACvD,SAAS,GAAG,QAAQ,WAAW,CAAC,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC;QAC1D,IAAI,MAAM,EAAE,CAAC;YACX,OAAO,GAAG,GAAG,CAAC;YACd,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC;YAC1C,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YAClC,IAAI,GAAG,WAAW,WAAW,8CAA8C,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;QACtG,CAAC;aAAM,CAAC;YACN,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;YAC7B,QAAQ,GAAG,IAAI,CAAC;QAClB,CAAC;IACH,CAAC;IAED,MAAM,IAAI,GAAG,WAAW,CAAC,SAAS,CAAC,CAAC;IAEpC,sEAAsE;IACtE,MAAM,QAAQ,GAAG,CAAC,MAAM,SAAS,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;SAC3D,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,IAAI,CAAC,CAAC,MAAM,KAAK,aAAa,CAAC,CAAC;IAC5D,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO,EAAE,EAAE,EAAE,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;IAC/G,CAAC;IAED,gCAAgC;IAChC,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACzC,MAAM,UAAU,GAAG,KAAK,CAAC,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,MAAM,CAAC;QAC9E,CAAC,CAAC,KAAK,CAAC,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YACzD,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAClB,MAAM,MAAM,GAAG,GAAG,YAAY,CAAC,GAAG,CAAC,IAAI,UAAU,EAAE,CAAC;IACpD,IAAI,EAAE,GAAG,MAAM,CAAC;IAChB,KAAK,IAAI,CAAC,GAAG,CAAC,GAAI,CAAC,EAAE,EAAE,CAAC;QACtB,IAAI,CAAC;YAAC,MAAM,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;YAAC,EAAE,GAAG,GAAG,MAAM,IAAI,CAAC,EAAE,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC;YAAC,MAAM;QAAC,CAAC;IACzF,CAAC;IAED,MAAM,IAAI,GAAG,OAAO,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,OAAO,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IACrD,IAAI,OAAO,IAAI,IAAI,EAAE,CAAC;QACpB,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,MAAM,CAAC,CAAC;QACtC,MAAM,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAClC,MAAM,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC;IACzC,CAAC;IAED,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC;IAC7G,MAAM,IAAI,GAAY;QACpB,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,WAAW,EAAE,GAAG,EAAE,WAAW,EAAE,UAAU;QACnE,YAAY,EAAE,WAAW,EAAE,MAAM,EAAE,aAAa;QAChD,WAAW,EAAE,KAAK,CAAC,UAAU,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI;KACtD,CAAC;IACF,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;IACnC,MAAM,GAAG,GAAG,GAAG,IAAI,MAAM,CAAC;IAC1B,MAAM,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC;IACzC,MAAM,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAE3B,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;AACnG,CAAC"}
\ No newline at end of file
diff --git a/mcp/dist/tools/raw-inbox.test.d.ts b/mcp/dist/tools/raw-inbox.test.d.ts
new file mode 100644
index 0000000..0cb953f
--- /dev/null
+++ b/mcp/dist/tools/raw-inbox.test.d.ts
@@ -0,0 +1,2 @@
+export {};
+//# sourceMappingURL=raw-inbox.test.d.ts.map
\ No newline at end of file
diff --git a/mcp/dist/tools/raw-inbox.test.d.ts.map b/mcp/dist/tools/raw-inbox.test.d.ts.map
new file mode 100644
index 0000000..6dffb45
--- /dev/null
+++ b/mcp/dist/tools/raw-inbox.test.d.ts.map
@@ -0,0 +1 @@
+{"version":3,"file":"raw-inbox.test.d.ts","sourceRoot":"","sources":["../../src/tools/raw-inbox.test.ts"],"names":[],"mappings":""}
\ No newline at end of file
diff --git a/mcp/dist/tools/raw-inbox.test.js b/mcp/dist/tools/raw-inbox.test.js
new file mode 100644
index 0000000..61b27dc
--- /dev/null
+++ b/mcp/dist/tools/raw-inbox.test.js
@@ -0,0 +1,86 @@
+import { describe, it, expect } from 'vitest';
+import { promises as fs } from 'fs';
+import { join } from 'path';
+import { tmpdir } from 'os';
+import { captureItem, listItems, setStatus, unprocessedCount, rawDir } from './raw-inbox.js';
+async function brain() {
+    const brainDir = await fs.mkdtemp(join(tmpdir(), 'raw-'));
+    const slug = 'alpha';
+    await fs.mkdir(join(brainDir, 'projects', slug), { recursive: true });
+    return { brainDir, slug };
+}
+const NOW = '2026-06-03T14:15:00Z';
+describe('raw-inbox', () => {
+    it('captures pasted text as a markdown item with provenance frontmatter', async () => {
+        const { brainDir, slug } = await brain();
+        const r = await captureItem({ brainDir, slug, kind: 'paste', source: 'paste',
+            content: 'a rate-limit note', now: NOW });
+        expect(r.duplicate).toBe(false);
+        expect(r.id).toMatch(/^20260603T141500Z-/);
+        const items = await listItems(brainDir, slug);
+        expect(items).toHaveLength(1);
+        expect(items[0].status).toBe('unprocessed');
+        expect(items[0].content_type).toBe('text/markdown');
+        expect(items[0].captured_by).toBe('user');
+        expect(items[0].body).toContain('a rate-limit note');
+    });
+    it('captures a URL as an offline pointer (no fetch), content_type text/uri-list', async () => {
+        const { brainDir, slug } = await brain();
+        const r = await captureItem({ brainDir, slug, kind: 'url',
+            source: 'https://example.com/spec', content: 'https://example.com/spec', now: NOW });
+        const items = await listItems(brainDir, slug);
+        expect(items[0].content_type).toBe('text/uri-list');
+        expect(items[0].source).toBe('https://example.com/spec');
+        expect(items[0].body).toContain('https://example.com/spec');
+        expect(r.id).toContain('example-com');
+    });
+    it('captures a text file into the body; a binary file into a blob sidecar', async () => {
+        const { brainDir, slug } = await brain();
+        const txt = join(brainDir, 'note.md');
+        await fs.writeFile(txt, '# Title\nbody text');
+        await captureItem({ brainDir, slug, kind: 'file', source: txt, now: NOW });
+        const bin = join(brainDir, 'pic.bin');
+        await fs.writeFile(bin, Buffer.from([0x00, 0x01, 0x02, 0x00]));
+        const rb = await captureItem({ brainDir, slug, kind: 'file', source: bin, now: '2026-06-03T14:16:00Z' });
+        const items = await listItems(brainDir, slug);
+        const text = items.find(i => i.source === txt);
+        expect(text.blob).toBeUndefined();
+        expect(text.body).toContain('body text');
+        const binItem = items.find(i => i.source === bin);
+        expect(binItem.blob).toBe(`${rb.id}.bin`);
+        await expect(fs.access(join(rawDir(brainDir, slug), `${rb.id}.bin`))).resolves.toBeUndefined();
+    });
+    it('is idempotent: re-capturing identical content returns the existing unprocessed item', async () => {
+        const { brainDir, slug } = await brain();
+        const a = await captureItem({ brainDir, slug, kind: 'paste', source: 'paste', content: 'same', now: NOW });
+        const b = await captureItem({ brainDir, slug, kind: 'paste', source: 'paste', content: 'same', now: '2026-06-03T15:00:00Z' });
+        expect(b.duplicate).toBe(true);
+        expect(b.id).toBe(a.id);
+        expect(await unprocessedCount(brainDir, slug)).toBe(1);
+    });
+    it('records an optional target_node and supports status transitions + count', async () => {
+        const { brainDir, slug } = await brain();
+        const r = await captureItem({ brainDir, slug, kind: 'paste', source: 'paste',
+            content: 'evidence', targetNode: 'auth-design', now: NOW });
+        expect((await listItems(brainDir, slug))[0].target_node).toBe('auth-design');
+        expect(await unprocessedCount(brainDir, slug)).toBe(1);
+        expect(await setStatus(brainDir, slug, r.id, 'discarded')).toBe(true);
+        expect(await unprocessedCount(brainDir, slug)).toBe(0);
+        expect((await listItems(brainDir, slug))[0].status).toBe('discarded');
+    });
+    it('rejects an unsafe slug', async () => {
+        const { brainDir } = await brain();
+        await expect(captureItem({ brainDir, slug: '../escape', kind: 'paste', source: 'paste',
+            content: 'x', now: NOW })).rejects.toThrow();
+    });
+    it('flags a malformed item (missing frontmatter) without throwing', async () => {
+        const { brainDir, slug } = await brain();
+        await fs.mkdir(rawDir(brainDir, slug), { recursive: true });
+        await fs.writeFile(join(rawDir(brainDir, slug), 'broken.md'), 'no frontmatter here');
+        const items = await listItems(brainDir, slug);
+        const broken = items.find(i => i.id === 'broken');
+        expect(broken.malformed).toBe(true);
+        expect(await unprocessedCount(brainDir, slug)).toBe(1);
+    });
+});
+//# sourceMappingURL=raw-inbox.test.js.map
\ No newline at end of file
diff --git a/mcp/dist/tools/raw-inbox.test.js.map b/mcp/dist/tools/raw-inbox.test.js.map
new file mode 100644
index 0000000..5a2dca9
--- /dev/null
+++ b/mcp/dist/tools/raw-inbox.test.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"raw-inbox.test.js","sourceRoot":"","sources":["../../src/tools/raw-inbox.test.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
diff --git a/mcp/package.json b/mcp/package.json
index 51eb977..0e181ff 100644
--- a/mcp/package.json
+++ b/mcp/package.json
@@ -6,7 +6,7 @@
   "main": "dist/server.js",
   "scripts": {
     "build": "tsc && npm run bundle",
-    "bundle": "esbuild src/server.ts --bundle --platform=node --target=node20 --format=esm --external:@huggingface/transformers --outfile=dist/server.bundle.js --banner:js=\"import { createRequire } from 'module'; const require = createRequire(import.meta.url);\" && esbuild src/tools/knowledge-reindex.ts --bundle --platform=node --target=node20 --format=esm --external:@huggingface/transformers --outfile=dist/tools/knowledge-reindex.bundle.js && esbuild src/tools/knowledge-validate.ts --bundle --platform=node --target=node20 --format=esm --external:@huggingface/transformers --outfile=dist/tools/knowledge-validate.bundle.js && esbuild src/tools/knowledge-search-cli.ts --bundle --platform=node --target=node20 --format=esm --external:@huggingface/transformers --outfile=dist/tools/knowledge-search-cli.bundle.js && esbuild src/tools/episodic-index-cli.ts --bundle --platform=node --target=node20 --format=esm --external:@huggingface/transformers --outfile=dist/tools/episodic-index-cli.bundle.js && esbuild src/tools/episodic-search-cli.ts --bundle --platform=node --target=node20 --format=esm --external:@huggingface/transformers --outfile=dist/tools/episodic-search-cli.bundle.js && esbuild src/cli/sb-entry.ts --bundle --platform=node --target=node20 --format=esm --external:@huggingface/transformers --outfile=dist/cli/sb-entry.bundle.js && esbuild src/cli/persona-think-cli.ts --bundle --platform=node --target=node20 --format=esm --external:@huggingface/transformers --outfile=dist/cli/persona-think-cli.bundle.js && esbuild src/tools/doc-sources-cli.ts --bundle --platform=node --target=node20 --format=esm --external:@huggingface/transformers --outfile=dist/tools/doc-sources-cli.bundle.js && esbuild src/tools/doc-sources-config-cli.ts --bundle --platform=node --target=node20 --format=esm --external:@huggingface/transformers --outfile=dist/tools/doc-sources-config-cli.bundle.js && esbuild src/tools/graph-neighbors-cli.ts --bundle --platform=node --target=node20 --format=esm --external:@huggingface/transformers --outfile=dist/tools/graph-neighbors-cli.bundle.js && esbuild src/tools/graph-cluster-cli.ts --bundle --platform=node --target=node20 --format=esm --external:@huggingface/transformers --outfile=dist/tools/graph-cluster-cli.bundle.js && esbuild src/tools/ai-block-render-cli.ts --bundle --platform=node --target=node20 --format=esm --external:@huggingface/transformers --outfile=dist/tools/ai-block-render-cli.bundle.js",
+    "bundle": "esbuild src/server.ts --bundle --platform=node --target=node20 --format=esm --external:@huggingface/transformers --outfile=dist/server.bundle.js --banner:js=\"import { createRequire } from 'module'; const require = createRequire(import.meta.url);\" && esbuild src/tools/knowledge-reindex.ts --bundle --platform=node --target=node20 --format=esm --external:@huggingface/transformers --outfile=dist/tools/knowledge-reindex.bundle.js && esbuild src/tools/knowledge-validate.ts --bundle --platform=node --target=node20 --format=esm --external:@huggingface/transformers --outfile=dist/tools/knowledge-validate.bundle.js && esbuild src/tools/knowledge-search-cli.ts --bundle --platform=node --target=node20 --format=esm --external:@huggingface/transformers --outfile=dist/tools/knowledge-search-cli.bundle.js && esbuild src/tools/episodic-index-cli.ts --bundle --platform=node --target=node20 --format=esm --external:@huggingface/transformers --outfile=dist/tools/episodic-index-cli.bundle.js && esbuild src/tools/episodic-search-cli.ts --bundle --platform=node --target=node20 --format=esm --external:@huggingface/transformers --outfile=dist/tools/episodic-search-cli.bundle.js && esbuild src/cli/sb-entry.ts --bundle --platform=node --target=node20 --format=esm --external:@huggingface/transformers --outfile=dist/cli/sb-entry.bundle.js && esbuild src/cli/persona-think-cli.ts --bundle --platform=node --target=node20 --format=esm --external:@huggingface/transformers --outfile=dist/cli/persona-think-cli.bundle.js && esbuild src/tools/doc-sources-cli.ts --bundle --platform=node --target=node20 --format=esm --external:@huggingface/transformers --outfile=dist/tools/doc-sources-cli.bundle.js && esbuild src/tools/doc-sources-config-cli.ts --bundle --platform=node --target=node20 --format=esm --external:@huggingface/transformers --outfile=dist/tools/doc-sources-config-cli.bundle.js && esbuild src/tools/graph-neighbors-cli.ts --bundle --platform=node --target=node20 --format=esm --external:@huggingface/transformers --outfile=dist/tools/graph-neighbors-cli.bundle.js && esbuild src/tools/graph-cluster-cli.ts --bundle --platform=node --target=node20 --format=esm --external:@huggingface/transformers --outfile=dist/tools/graph-cluster-cli.bundle.js && esbuild src/tools/ai-block-render-cli.ts --bundle --platform=node --target=node20 --format=esm --external:@huggingface/transformers --outfile=dist/tools/ai-block-render-cli.bundle.js && esbuild src/tools/raw-capture-cli.ts --bundle --platform=node --target=node20 --format=esm --external:@huggingface/transformers --outfile=dist/tools/raw-capture-cli.bundle.js",
     "start": "node dist/server.bundle.js",
     "dev": "tsc --watch",
     "test": "vitest run"
diff --git a/mcp/src/tools/raw-capture-cli.ts b/mcp/src/tools/raw-capture-cli.ts
new file mode 100644
index 0000000..21df642
--- /dev/null
+++ b/mcp/src/tools/raw-capture-cli.ts
@@ -0,0 +1,68 @@
+import { homedir } from 'os';
+import { join, basename } from 'path';
+import { existsSync, readFileSync, statSync } from 'fs';
+import { captureItem, listItems, setStatus, unprocessedCount } from './raw-inbox.js';
+
+function resolveSlug(brainDir: string): string | undefined {
+  if (process.env.SB_ACTIVE_SLUG) return process.env.SB_ACTIVE_SLUG;
+  try {
+    const pin = readFileSync(join(brainDir, '.active-session-slug'), 'utf-8').trim();
+    if (pin && existsSync(join(brainDir, 'projects', pin, 'PROJECT.md'))) return pin;
+  } catch { /* no pin */ }
+  const base = basename(process.cwd());
+  return base && base !== '/' && base !== '.' ? base : undefined;
+}
+
+/** Pull `--node ` out of argv; return the rest + the node value. */
+function takeNode(args: string[]): { rest: string[]; node?: string } {
+  const i = args.indexOf('--node');
+  if (i >= 0 && args[i + 1]) return { rest: [...args.slice(0, i), ...args.slice(i + 2)], node: args[i + 1] };
+  return { rest: args };
+}
+
+async function main(): Promise {
+  const brainDir = process.env.BRAIN_DIR || join(homedir(), '.second-brain');
+  const slug = resolveSlug(brainDir);
+  if (!slug) { console.log('capture: could not resolve the active project (no slug). cd into a project.'); return; }
+
+  const action = process.argv[2];
+  const { rest, node } = takeNode(process.argv.slice(3));
+
+  try {
+    if (action === 'list') {
+      const items = await listItems(brainDir, slug);
+      const open = items.filter(i => i.status === 'unprocessed' || i.malformed).length;
+      console.log(`Raw inbox for ${slug} — ${items.length} item(s), ${open} unprocessed:`);
+      for (const i of items) {
+        console.log(`  - ${i.id} [${i.malformed ? 'malformed' : i.status}] ${i.gist || i.source}`);
+      }
+      if (items.length === 0) console.log('  (empty — capture something, e.g. /second-brain:capture ./notes.md)');
+    } else if (action === 'discard') {
+      const id = rest[0];
+      if (!id) { console.log('usage: capture --discard '); return; }
+      console.log(await setStatus(brainDir, slug, id, 'discarded')
+        ? `Discarded ${id}.` : `No raw item with id ${id}.`);
+    } else if (action === 'paste') {
+      const content = readFileSync(0, 'utf-8');           // stdin
+      if (!content.trim()) { console.log('capture: nothing on stdin.'); return; }
+      const r = await captureItem({ brainDir, slug, kind: 'paste', source: 'paste', content, targetNode: node });
+      console.log(`${r.duplicate ? 'Already captured' : 'Captured'} ${r.id} — ${r.unprocessed} unprocessed.`);
+    } else if (action === 'capture') {
+      const src = rest[0];
+      if (!src) { console.log('usage: capture  [--node ]  |  capture paste'); return; }
+      let kind: 'file' | 'url' | 'paste'; let content: string | undefined;
+      if (/^https?:\/\//i.test(src)) { kind = 'url'; content = src; }
+      else if (existsSync(src) && statSync(src).isFile()) { kind = 'file'; }
+      else { kind = 'paste'; content = src; }              // inline text fallback
+      const r = await captureItem({ brainDir, slug, kind, source: src, content, targetNode: node });
+      console.log(`${r.duplicate ? 'Already captured' : 'Captured'} ${r.id} (${kind}) — ${r.unprocessed} unprocessed.`);
+    } else {
+      const n = await unprocessedCount(brainDir, slug);
+      console.log(`usage: capture  | capture paste | capture --list | capture --discard   (${n} unprocessed)`);
+    }
+  } catch (e) {
+    console.log(`capture error: ${e instanceof Error ? e.message : String(e)}`);
+  }
+}
+
+main();

From 649cab7fa073063acf14ec39b13f210e685cb68e Mon Sep 17 00:00:00 2001
From: Cain-Ish 
Date: Wed, 3 Jun 2026 19:14:39 +0200
Subject: [PATCH 6/9] feat(kb): /second-brain:capture producer skill (SP-2 Task
 4)

Co-Authored-By: Claude Opus 4.8 (1M context) 
---
 skills/capture/SKILL.md   | 32 ++++++++++++++++++++++++++++
 tests/test-raw-capture.sh | 45 +++++++++++++++++++++++++++++++++++++++
 2 files changed, 77 insertions(+)
 create mode 100644 skills/capture/SKILL.md
 create mode 100644 tests/test-raw-capture.sh

diff --git a/skills/capture/SKILL.md b/skills/capture/SKILL.md
new file mode 100644
index 0000000..e6fd55c
--- /dev/null
+++ b/skills/capture/SKILL.md
@@ -0,0 +1,32 @@
+---
+name: capture
+description: Drop unprocessed material (a file, a URL, or pasted text) into the current project's raw inbox for later refinement into wiki notes. Usage: /second-brain:capture  | "" | --list | --discard  | --node .
+user-invocable: true
+disable-model-invocation: true
+allowed-tools: Read Bash(node *) Bash(test *) Bash(cat *) Bash(basename *)
+---
+
+# /second-brain:capture — raw inbox producer
+
+Hold unprocessed material in the active project's raw inbox
+(`~/.second-brain/projects//raw/`) until the maintainer refines it into wiki
+nodes. Raw items are **not** searched — they are a staging area, surfaced as a
+backlog count at session start.
+
+Map the user's argument to a `raw-capture-cli` action and run it. The CLI resolves
+the active project, stamps provenance, copies blobs, and dedups by content hash.
+
+```bash
+CLI="${CLAUDE_PLUGIN_ROOT}/mcp/dist/tools/raw-capture-cli.bundle.js"
+```
+
+- `` or `` or inline `"text"` → `node "$CLI" capture  [--node ]`
+- pasted/piped text → `node "$CLI" paste [--node ]` (reads stdin)
+- `--list` → `node "$CLI" list`
+- `--discard ` → `node "$CLI" discard `
+
+`--node ` records that the item is evidence for an existing wiki page
+(provenance only — the link is projected later when the maintainer processes it).
+
+Relay the CLI's output. If it reports "could not resolve the active project", tell
+the user to `cd` into their project directory first.
diff --git a/tests/test-raw-capture.sh b/tests/test-raw-capture.sh
new file mode 100644
index 0000000..8ace06e
--- /dev/null
+++ b/tests/test-raw-capture.sh
@@ -0,0 +1,45 @@
+#!/bin/bash
+# End-to-end: the capture CLI (which the skill drives) ingests material into a project's
+# raw inbox, is idempotent, lists items (flagging malformed), and discards.
+set -u
+ROOT="$(cd "$(dirname "$0")"/.. && pwd)"
+CLI="$ROOT/mcp/dist/tools/raw-capture-cli.bundle.js"
+SKILL="$ROOT/skills/capture/SKILL.md"
+fail(){ echo "FAIL: $1"; exit 1; }; pass(){ echo "PASS: $1"; }
+
+[ -f "$SKILL" ] || fail "skills/capture/SKILL.md missing"
+grep -q 'raw-capture-cli.bundle.js' "$SKILL" || fail "capture skill does not invoke the raw-capture CLI"
+grep -q 'user-invocable: true' "$SKILL" || fail "capture skill not user-invocable"
+pass "capture skill present and wired"
+
+command -v node >/dev/null 2>&1 || { echo "SKIP: node"; echo; echo "ALL PASS"; exit 0; }
+[ -f "$CLI" ] || { echo "SKIP: CLI bundle not built"; echo; echo "ALL PASS"; exit 0; }
+
+T=$(mktemp -d); export BRAIN_DIR="$T" SB_ACTIVE_SLUG=demo
+mkdir -p "$T/projects/demo"; : > "$T/projects/demo/PROJECT.md"
+run(){ node "$CLI" "$@"; }
+
+out=$(run capture "rate limit design note")
+echo "$out" | grep -q 'Captured .* — 1 unprocessed' || fail "first capture not reported ($out)"
+pass "capture creates an unprocessed item"
+
+out=$(run capture "rate limit design note")
+echo "$out" | grep -q 'Already captured' || fail "re-capture not idempotent ($out)"
+pass "identical re-capture is idempotent"
+
+RAW="$T/projects/demo/raw"
+[ "$(ls "$RAW"/*.md | wc -l)" -eq 1 ] || fail "expected exactly 1 item after idempotent re-capture"
+grep -q '^status: unprocessed$' "$RAW"/*.md || fail "item missing status: unprocessed"
+grep -q '^content_type: text/markdown$' "$RAW"/*.md || fail "item missing content_type"
+
+printf 'not a real item\n' > "$RAW/broken.md"
+run list | grep -q 'broken .*malformed' || fail "--list did not flag the malformed item"
+pass "list flags malformed items"
+
+ID=$(ls "$RAW" | grep -v broken | sed 's/\.md$//' | head -1)
+run discard "$ID" | grep -q "Discarded $ID" || fail "discard did not report"
+grep -q '^status: discarded$' "$RAW/$ID.md" || fail "status not flipped to discarded"
+pass "discard flips status"
+
+rm -rf "$T"
+echo; echo "ALL PASS"

From cfb203c26502c28aca60387396068a56bdf67066 Mon Sep 17 00:00:00 2001
From: Cain-Ish 
Date: Wed, 3 Jun 2026 21:53:33 +0200
Subject: [PATCH 7/9] feat(kb): session-load raw-inbox backlog banner (SP-2
 Task 5)

Co-Authored-By: Claude Opus 4.8 (1M context) 
---
 scripts/session-load.sh        | 13 +++++++++++++
 tests/test-raw-inbox-banner.sh | 20 ++++++++++++++++++++
 2 files changed, 33 insertions(+)
 create mode 100644 tests/test-raw-inbox-banner.sh

diff --git a/scripts/session-load.sh b/scripts/session-load.sh
index 656f689..c822a28 100755
--- a/scripts/session-load.sh
+++ b/scripts/session-load.sh
@@ -412,6 +412,19 @@ if [ -f "$project_file" ] && [ -f "$SEARCH_CLI" ] && command -v node >/dev/null
   fi
 fi
 
+# 5-raw. SP-2: raw-inbox backlog — surface how many unprocessed items await processing
+# for this project, so the user knows there's material to refine. Kill switch SB_RAW_INBOX=off.
+if [ "${SB_RAW_INBOX:-on}" != "off" ]; then
+  RAW_DIR_PATH="$BRAIN_DIR/projects/$slug/raw"
+  if [ -d "$RAW_DIR_PATH" ]; then
+    RAW_N=$(grep -rl '^status: unprocessed$' "$RAW_DIR_PATH" 2>/dev/null | wc -l | tr -d ' ')
+    if [ "${RAW_N:-0}" -gt 0 ]; then
+      sb_append "$(printf '## ⓘ raw inbox — %s unprocessed item(s)\nRun `/second-brain:capture --list` to review; the maintainer refines them into notes.\n\n' "$RAW_N")" \
+        "raw-inbox-banner" 250
+    fi
+  fi
+fi
+
 # 5a. Graph neighbourhood — current typed dependencies of the project's key
 # entities (the Cross-references slugs). Surfaces "changing A affects/requires
 # B,C,D" in the hot tier so a fresh session recalls the dependency web without
diff --git a/tests/test-raw-inbox-banner.sh b/tests/test-raw-inbox-banner.sh
new file mode 100644
index 0000000..a8c921a
--- /dev/null
+++ b/tests/test-raw-inbox-banner.sh
@@ -0,0 +1,20 @@
+#!/bin/bash
+# session-load surfaces a raw-inbox backlog banner for the active project, gated by SB_RAW_INBOX.
+set -u
+ROOT="$(cd "$(dirname "$0")"/.. && pwd)"
+SL="$ROOT/scripts/session-load.sh"
+fail(){ echo "FAIL: $1"; exit 1; }; pass(){ echo "PASS: $1"; }
+
+grep -q 'SB_RAW_INBOX' "$SL" || fail "session-load.sh has no SB_RAW_INBOX gate"
+grep -q 'raw-inbox-banner' "$SL" || fail "session-load.sh has no raw-inbox banner block"
+pass "session-load references the raw-inbox banner + kill switch"
+
+# Prove the documented count expression counts unprocessed items.
+T=$(mktemp -d); RAW="$T/projects/demo/raw"; mkdir -p "$RAW"
+mk(){ printf -- '---\nstatus: %s\n---\nx\n' "$1" > "$RAW/$2.md"; }
+mk unprocessed a; mk unprocessed b; mk discarded c
+N=$(grep -rl '^status: unprocessed$' "$RAW" 2>/dev/null | wc -l | tr -d ' ')
+[ "$N" = "2" ] || fail "expected the banner count expression to yield 2 (got $N)"
+pass "the banner count expression counts unprocessed items (2)"
+rm -rf "$T"
+echo; echo "ALL PASS"

From dc4b66b1d5e06e44570ac9ec9cee12ff5fb5b314 Mon Sep 17 00:00:00 2001
From: Cain-Ish 
Date: Wed, 3 Jun 2026 23:13:13 +0200
Subject: [PATCH 8/9] fix(kb): harden raw inbox (deep-review findings)

- C1 (critical): setStatus now rejects a path-traversal id, so a malicious
  '--discard ../../wiki/page' can no longer rewrite an arbitrary .md outside raw/.
- W1/#1 (high): serialize sanitizes newlines in every frontmatter value (fmValue),
  so a newline in 'source' can no longer inject a fake 'status:' line that the
  first-match parser reads back and silently flips the item to discarded.
- I3: inline capture records canonical 'source: paste' (was the raw text).
- W2: resolveSlug rejects a '..' cwd basename.
- Banner: count open backlog as total - (processed|discarded), matching the
  module's unprocessedCount (malformed items are now visible at session start too).
New regression tests for the traversal + injection exploits.

Co-Authored-By: Claude Opus 4.8 (1M context) 
---
 mcp/dist/tools/raw-capture-cli.bundle.js | 33 +++++++++++++++---------
 mcp/dist/tools/raw-capture-cli.js        |  8 +++---
 mcp/dist/tools/raw-capture-cli.js.map    |  2 +-
 mcp/dist/tools/raw-inbox.d.ts.map        |  2 +-
 mcp/dist/tools/raw-inbox.js              | 29 ++++++++++++++-------
 mcp/dist/tools/raw-inbox.js.map          |  2 +-
 mcp/dist/tools/raw-inbox.test.js         | 19 ++++++++++++++
 mcp/dist/tools/raw-inbox.test.js.map     |  2 +-
 mcp/src/tools/raw-capture-cli.ts         |  8 +++---
 mcp/src/tools/raw-inbox.test.ts          | 21 +++++++++++++++
 mcp/src/tools/raw-inbox.ts               | 30 ++++++++++++++-------
 scripts/session-load.sh                  |  6 ++++-
 tests/test-raw-capture.sh                |  1 +
 tests/test-raw-inbox-banner.sh           | 14 ++++++----
 14 files changed, 128 insertions(+), 49 deletions(-)
 mode change 100644 => 100755 tests/test-raw-capture.sh
 mode change 100644 => 100755 tests/test-raw-inbox-banner.sh

diff --git a/mcp/dist/tools/raw-capture-cli.bundle.js b/mcp/dist/tools/raw-capture-cli.bundle.js
index 605fa71..b8ded08 100644
--- a/mcp/dist/tools/raw-capture-cli.bundle.js
+++ b/mcp/dist/tools/raw-capture-cli.bundle.js
@@ -6102,18 +6102,24 @@ function contentTypeForFile(path2, binary) {
   if (binary) return ext2 === ".pdf" ? "application/pdf" : "application/octet-stream";
   return ext2 === ".md" || ext2 === ".markdown" ? "text/markdown" : "text/plain";
 }
+function fmValue(s) {
+  return s.replace(/[\r\n]+/g, " ");
+}
+function isSafeId(id) {
+  return !!id && !/[\\/]|\.\./.test(id);
+}
 function serialize(item) {
   const fm = ["---"];
-  fm.push(`id: ${item.id}`);
-  fm.push(`source: ${item.source}`);
-  fm.push(`captured_at: ${item.captured_at}`);
-  fm.push(`captured_by: ${item.captured_by}`);
-  fm.push(`content_type: ${item.content_type}`);
-  fm.push(`status: ${item.status}`);
-  if (item.target_node) fm.push(`target_node: ${item.target_node}`);
-  if (item.blob) fm.push(`blob: ${item.blob}`);
-  fm.push(`hash: ${item.hash}`);
-  fm.push(`gist: ${item.gist.replace(/\n/g, " ")}`);
+  fm.push(`id: ${fmValue(item.id)}`);
+  fm.push(`source: ${fmValue(item.source)}`);
+  fm.push(`captured_at: ${fmValue(item.captured_at)}`);
+  fm.push(`captured_by: ${fmValue(item.captured_by)}`);
+  fm.push(`content_type: ${fmValue(item.content_type)}`);
+  fm.push(`status: ${fmValue(item.status)}`);
+  if (item.target_node) fm.push(`target_node: ${fmValue(item.target_node)}`);
+  if (item.blob) fm.push(`blob: ${fmValue(item.blob)}`);
+  fm.push(`hash: ${fmValue(item.hash)}`);
+  fm.push(`gist: ${fmValue(item.gist)}`);
   fm.push("---", "", item.body, "");
   return fm.join("\n");
 }
@@ -6185,6 +6191,7 @@ async function unprocessedCount(brainDir, slug) {
 }
 async function setStatus(brainDir, slug, id, status) {
   assertSafeSlug(slug);
+  if (!isSafeId(id)) return false;
   const file = join(rawDir(brainDir, slug), `${id}.md`);
   let content;
   try {
@@ -6290,7 +6297,7 @@ function resolveSlug(brainDir) {
   } catch {
   }
   const base = basename2(process.cwd());
-  return base && base !== "/" && base !== "." ? base : void 0;
+  return base && base !== "/" && base !== "." && base !== ".." ? base : void 0;
 }
 function takeNode(args) {
   const i = args.indexOf("--node");
@@ -6338,6 +6345,7 @@ async function main() {
       }
       let kind;
       let content;
+      let source = src;
       if (/^https?:\/\//i.test(src)) {
         kind = "url";
         content = src;
@@ -6346,8 +6354,9 @@ async function main() {
       } else {
         kind = "paste";
         content = src;
+        source = "paste";
       }
-      const r = await captureItem({ brainDir, slug, kind, source: src, content, targetNode: node });
+      const r = await captureItem({ brainDir, slug, kind, source, content, targetNode: node });
       console.log(`${r.duplicate ? "Already captured" : "Captured"} ${r.id} (${kind}) \u2014 ${r.unprocessed} unprocessed.`);
     } else {
       const n = await unprocessedCount(brainDir, slug);
diff --git a/mcp/dist/tools/raw-capture-cli.js b/mcp/dist/tools/raw-capture-cli.js
index 9b39c18..ff1d27f 100644
--- a/mcp/dist/tools/raw-capture-cli.js
+++ b/mcp/dist/tools/raw-capture-cli.js
@@ -12,7 +12,7 @@ function resolveSlug(brainDir) {
     }
     catch { /* no pin */ }
     const base = basename(process.cwd());
-    return base && base !== '/' && base !== '.' ? base : undefined;
+    return base && base !== '/' && base !== '.' && base !== '..' ? base : undefined;
 }
 /** Pull `--node ` out of argv; return the rest + the node value. */
 function takeNode(args) {
@@ -67,6 +67,7 @@ async function main() {
             }
             let kind;
             let content;
+            let source = src;
             if (/^https?:\/\//i.test(src)) {
                 kind = 'url';
                 content = src;
@@ -77,8 +78,9 @@ async function main() {
             else {
                 kind = 'paste';
                 content = src;
-            } // inline text fallback
-            const r = await captureItem({ brainDir, slug, kind, source: src, content, targetNode: node });
+                source = 'paste';
+            } // inline text → canonical paste source
+            const r = await captureItem({ brainDir, slug, kind, source, content, targetNode: node });
             console.log(`${r.duplicate ? 'Already captured' : 'Captured'} ${r.id} (${kind}) — ${r.unprocessed} unprocessed.`);
         }
         else {
diff --git a/mcp/dist/tools/raw-capture-cli.js.map b/mcp/dist/tools/raw-capture-cli.js.map
index 758af63..c1d92b4 100644
--- a/mcp/dist/tools/raw-capture-cli.js.map
+++ b/mcp/dist/tools/raw-capture-cli.js.map
@@ -1 +1 @@
-{"version":3,"file":"raw-capture-cli.js","sourceRoot":"","sources":["../../src/tools/raw-capture-cli.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"raw-capture-cli.js","sourceRoot":"","sources":["../../src/tools/raw-capture-cli.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
diff --git a/mcp/dist/tools/raw-inbox.d.ts.map b/mcp/dist/tools/raw-inbox.d.ts.map
index 48e6c23..b3c146e 100644
--- a/mcp/dist/tools/raw-inbox.d.ts.map
+++ b/mcp/dist/tools/raw-inbox.d.ts.map
@@ -1 +1 @@
-{"version":3,"file":"raw-inbox.d.ts","sourceRoot":"","sources":["../../src/tools/raw-inbox.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,SAAS,GAAG,aAAa,GAAG,WAAW,GAAG,WAAW,CAAC;AAClE,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,YAAY,GAAG,OAAO,CAAC;AAEzD,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,UAAU,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,SAAS,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,GAAG,KAAK,GAAG,OAAO,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7D;AAwFD,wBAAsB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAGlF;AAED,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAKtF;AAED,wBAAsB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,CAY/G;AAED,wBAAsB,WAAW,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC,CAgFvH"}
\ No newline at end of file
+{"version":3,"file":"raw-inbox.d.ts","sourceRoot":"","sources":["../../src/tools/raw-inbox.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,SAAS,GAAG,aAAa,GAAG,WAAW,GAAG,WAAW,CAAC;AAClE,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,YAAY,GAAG,OAAO,CAAC;AAEzD,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,UAAU,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,SAAS,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,GAAG,KAAK,GAAG,OAAO,CAAC;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,MAAM,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7D;AAiGD,wBAAsB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAGlF;AAED,wBAAsB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAKtF;AAED,wBAAsB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,CAa/G;AAED,wBAAsB,WAAW,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,CAAC,CAgFvH"}
\ No newline at end of file
diff --git a/mcp/dist/tools/raw-inbox.js b/mcp/dist/tools/raw-inbox.js
index c84dd16..9faaafe 100644
--- a/mcp/dist/tools/raw-inbox.js
+++ b/mcp/dist/tools/raw-inbox.js
@@ -26,20 +26,27 @@ function contentTypeForFile(path, binary) {
         return ext === '.pdf' ? 'application/pdf' : 'application/octet-stream';
     return ext === '.md' || ext === '.markdown' ? 'text/markdown' : 'text/plain';
 }
+/** Flatten a frontmatter value to a single line. The parser is unquoted-flat-YAML, so a value
+ *  containing a newline would otherwise inject a spurious field (e.g. a fake `status:` line that
+ *  the first-match parser reads back, silently flipping the item's status). Strip CR/LF on write. */
+function fmValue(s) { return s.replace(/[\r\n]+/g, ' '); }
+/** A raw item id is always `-` (internally generated). Reject anything that could
+ *  escape the raw/ dir when an id arrives from outside (e.g. `--discard ../../wiki/page`). */
+function isSafeId(id) { return !!id && !/[\\/]|\.\./.test(id); }
 function serialize(item) {
     const fm = ['---'];
-    fm.push(`id: ${item.id}`);
-    fm.push(`source: ${item.source}`);
-    fm.push(`captured_at: ${item.captured_at}`);
-    fm.push(`captured_by: ${item.captured_by}`);
-    fm.push(`content_type: ${item.content_type}`);
-    fm.push(`status: ${item.status}`);
+    fm.push(`id: ${fmValue(item.id)}`);
+    fm.push(`source: ${fmValue(item.source)}`);
+    fm.push(`captured_at: ${fmValue(item.captured_at)}`);
+    fm.push(`captured_by: ${fmValue(item.captured_by)}`);
+    fm.push(`content_type: ${fmValue(item.content_type)}`);
+    fm.push(`status: ${fmValue(item.status)}`);
     if (item.target_node)
-        fm.push(`target_node: ${item.target_node}`);
+        fm.push(`target_node: ${fmValue(item.target_node)}`);
     if (item.blob)
-        fm.push(`blob: ${item.blob}`);
-    fm.push(`hash: ${item.hash}`);
-    fm.push(`gist: ${item.gist.replace(/\n/g, ' ')}`);
+        fm.push(`blob: ${fmValue(item.blob)}`);
+    fm.push(`hash: ${fmValue(item.hash)}`);
+    fm.push(`gist: ${fmValue(item.gist)}`);
     fm.push('---', '', item.body, '');
     return fm.join('\n');
 }
@@ -109,6 +116,8 @@ export async function unprocessedCount(brainDir, slug) {
 }
 export async function setStatus(brainDir, slug, id, status) {
     assertSafeSlug(slug);
+    if (!isSafeId(id))
+        return false; // never let an external id (`--discard `) escape raw/
     const file = join(rawDir(brainDir, slug), `${id}.md`);
     let content;
     try {
diff --git a/mcp/dist/tools/raw-inbox.js.map b/mcp/dist/tools/raw-inbox.js.map
index 0529379..d80691f 100644
--- a/mcp/dist/tools/raw-inbox.js.map
+++ b/mcp/dist/tools/raw-inbox.js.map
@@ -1 +1 @@
-{"version":3,"file":"raw-inbox.js","sourceRoot":"","sources":["../../src/tools/raw-inbox.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"raw-inbox.js","sourceRoot":"","sources":["../../src/tools/raw-inbox.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
diff --git a/mcp/dist/tools/raw-inbox.test.js b/mcp/dist/tools/raw-inbox.test.js
index 61b27dc..34a2c90 100644
--- a/mcp/dist/tools/raw-inbox.test.js
+++ b/mcp/dist/tools/raw-inbox.test.js
@@ -73,6 +73,25 @@ describe('raw-inbox', () => {
         await expect(captureItem({ brainDir, slug: '../escape', kind: 'paste', source: 'paste',
             content: 'x', now: NOW })).rejects.toThrow();
     });
+    it('setStatus rejects a path-traversal id and never touches a file outside raw/', async () => {
+        const { brainDir, slug } = await brain();
+        await captureItem({ brainDir, slug, kind: 'paste', source: 'paste', content: 'real', now: NOW });
+        const victim = join(brainDir, 'projects', slug, 'victim.md'); // a sibling of raw/, outside it
+        await fs.writeFile(victim, '---\nstatus: keep\n---\n');
+        const ok = await setStatus(brainDir, slug, '../victim', 'discarded');
+        expect(ok).toBe(false);
+        expect(await fs.readFile(victim, 'utf-8')).toBe('---\nstatus: keep\n---\n');
+    });
+    it('sanitizes newlines in frontmatter values so a crafted source cannot inject/flip status', async () => {
+        const { brainDir, slug } = await brain();
+        await captureItem({ brainDir, slug, kind: 'url',
+            source: 'http://x/\nstatus: discarded', content: 'http://x/\nstatus: discarded', now: NOW });
+        const items = await listItems(brainDir, slug);
+        expect(items).toHaveLength(1);
+        expect(items[0].status).toBe('unprocessed'); // the injected "status: discarded" did NOT take effect
+        expect(items[0].source).not.toContain('\n');
+        expect(await unprocessedCount(brainDir, slug)).toBe(1);
+    });
     it('flags a malformed item (missing frontmatter) without throwing', async () => {
         const { brainDir, slug } = await brain();
         await fs.mkdir(rawDir(brainDir, slug), { recursive: true });
diff --git a/mcp/dist/tools/raw-inbox.test.js.map b/mcp/dist/tools/raw-inbox.test.js.map
index 5a2dca9..682bf8b 100644
--- a/mcp/dist/tools/raw-inbox.test.js.map
+++ b/mcp/dist/tools/raw-inbox.test.js.map
@@ -1 +1 @@
-{"version":3,"file":"raw-inbox.test.js","sourceRoot":"","sources":["../../src/tools/raw-inbox.test.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
+{"version":3,"file":"raw-inbox.test.js","sourceRoot":"","sources":["../../src/tools/raw-inbox.test.ts"],"names":[],"mappings":"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"}
\ No newline at end of file
diff --git a/mcp/src/tools/raw-capture-cli.ts b/mcp/src/tools/raw-capture-cli.ts
index 21df642..525449f 100644
--- a/mcp/src/tools/raw-capture-cli.ts
+++ b/mcp/src/tools/raw-capture-cli.ts
@@ -10,7 +10,7 @@ function resolveSlug(brainDir: string): string | undefined {
     if (pin && existsSync(join(brainDir, 'projects', pin, 'PROJECT.md'))) return pin;
   } catch { /* no pin */ }
   const base = basename(process.cwd());
-  return base && base !== '/' && base !== '.' ? base : undefined;
+  return base && base !== '/' && base !== '.' && base !== '..' ? base : undefined;
 }
 
 /** Pull `--node ` out of argv; return the rest + the node value. */
@@ -50,11 +50,11 @@ async function main(): Promise {
     } else if (action === 'capture') {
       const src = rest[0];
       if (!src) { console.log('usage: capture  [--node ]  |  capture paste'); return; }
-      let kind: 'file' | 'url' | 'paste'; let content: string | undefined;
+      let kind: 'file' | 'url' | 'paste'; let content: string | undefined; let source = src;
       if (/^https?:\/\//i.test(src)) { kind = 'url'; content = src; }
       else if (existsSync(src) && statSync(src).isFile()) { kind = 'file'; }
-      else { kind = 'paste'; content = src; }              // inline text fallback
-      const r = await captureItem({ brainDir, slug, kind, source: src, content, targetNode: node });
+      else { kind = 'paste'; content = src; source = 'paste'; } // inline text → canonical paste source
+      const r = await captureItem({ brainDir, slug, kind, source, content, targetNode: node });
       console.log(`${r.duplicate ? 'Already captured' : 'Captured'} ${r.id} (${kind}) — ${r.unprocessed} unprocessed.`);
     } else {
       const n = await unprocessedCount(brainDir, slug);
diff --git a/mcp/src/tools/raw-inbox.test.ts b/mcp/src/tools/raw-inbox.test.ts
index 37ed85e..c91f764 100644
--- a/mcp/src/tools/raw-inbox.test.ts
+++ b/mcp/src/tools/raw-inbox.test.ts
@@ -81,6 +81,27 @@ describe('raw-inbox', () => {
       content: 'x', now: NOW })).rejects.toThrow();
   });
 
+  it('setStatus rejects a path-traversal id and never touches a file outside raw/', async () => {
+    const { brainDir, slug } = await brain();
+    await captureItem({ brainDir, slug, kind: 'paste', source: 'paste', content: 'real', now: NOW });
+    const victim = join(brainDir, 'projects', slug, 'victim.md'); // a sibling of raw/, outside it
+    await fs.writeFile(victim, '---\nstatus: keep\n---\n');
+    const ok = await setStatus(brainDir, slug, '../victim', 'discarded');
+    expect(ok).toBe(false);
+    expect(await fs.readFile(victim, 'utf-8')).toBe('---\nstatus: keep\n---\n');
+  });
+
+  it('sanitizes newlines in frontmatter values so a crafted source cannot inject/flip status', async () => {
+    const { brainDir, slug } = await brain();
+    await captureItem({ brainDir, slug, kind: 'url',
+      source: 'http://x/\nstatus: discarded', content: 'http://x/\nstatus: discarded', now: NOW });
+    const items = await listItems(brainDir, slug);
+    expect(items).toHaveLength(1);
+    expect(items[0].status).toBe('unprocessed'); // the injected "status: discarded" did NOT take effect
+    expect(items[0].source).not.toContain('\n');
+    expect(await unprocessedCount(brainDir, slug)).toBe(1);
+  });
+
   it('flags a malformed item (missing frontmatter) without throwing', async () => {
     const { brainDir, slug } = await brain();
     await fs.mkdir(rawDir(brainDir, slug), { recursive: true });
diff --git a/mcp/src/tools/raw-inbox.ts b/mcp/src/tools/raw-inbox.ts
index ee30280..fbb04ba 100644
--- a/mcp/src/tools/raw-inbox.ts
+++ b/mcp/src/tools/raw-inbox.ts
@@ -58,18 +58,27 @@ function contentTypeForFile(path: string, binary: boolean): string {
   return ext === '.md' || ext === '.markdown' ? 'text/markdown' : 'text/plain';
 }
 
+/** Flatten a frontmatter value to a single line. The parser is unquoted-flat-YAML, so a value
+ *  containing a newline would otherwise inject a spurious field (e.g. a fake `status:` line that
+ *  the first-match parser reads back, silently flipping the item's status). Strip CR/LF on write. */
+function fmValue(s: string): string { return s.replace(/[\r\n]+/g, ' '); }
+
+/** A raw item id is always `-` (internally generated). Reject anything that could
+ *  escape the raw/ dir when an id arrives from outside (e.g. `--discard ../../wiki/page`). */
+function isSafeId(id: string): boolean { return !!id && !/[\\/]|\.\./.test(id); }
+
 function serialize(item: RawItem): string {
   const fm: string[] = ['---'];
-  fm.push(`id: ${item.id}`);
-  fm.push(`source: ${item.source}`);
-  fm.push(`captured_at: ${item.captured_at}`);
-  fm.push(`captured_by: ${item.captured_by}`);
-  fm.push(`content_type: ${item.content_type}`);
-  fm.push(`status: ${item.status}`);
-  if (item.target_node) fm.push(`target_node: ${item.target_node}`);
-  if (item.blob) fm.push(`blob: ${item.blob}`);
-  fm.push(`hash: ${item.hash}`);
-  fm.push(`gist: ${item.gist.replace(/\n/g, ' ')}`);
+  fm.push(`id: ${fmValue(item.id)}`);
+  fm.push(`source: ${fmValue(item.source)}`);
+  fm.push(`captured_at: ${fmValue(item.captured_at)}`);
+  fm.push(`captured_by: ${fmValue(item.captured_by)}`);
+  fm.push(`content_type: ${fmValue(item.content_type)}`);
+  fm.push(`status: ${fmValue(item.status)}`);
+  if (item.target_node) fm.push(`target_node: ${fmValue(item.target_node)}`);
+  if (item.blob) fm.push(`blob: ${fmValue(item.blob)}`);
+  fm.push(`hash: ${fmValue(item.hash)}`);
+  fm.push(`gist: ${fmValue(item.gist)}`);
   fm.push('---', '', item.body, '');
   return fm.join('\n');
 }
@@ -135,6 +144,7 @@ export async function unprocessedCount(brainDir: string, slug: string): Promise<
 
 export async function setStatus(brainDir: string, slug: string, id: string, status: RawStatus): Promise {
   assertSafeSlug(slug);
+  if (!isSafeId(id)) return false; // never let an external id (`--discard `) escape raw/
   const file = join(rawDir(brainDir, slug), `${id}.md`);
   let content: string;
   try { content = await fs.readFile(file, 'utf-8'); } catch { return false; }
diff --git a/scripts/session-load.sh b/scripts/session-load.sh
index c822a28..dd5978e 100755
--- a/scripts/session-load.sh
+++ b/scripts/session-load.sh
@@ -417,7 +417,11 @@ fi
 if [ "${SB_RAW_INBOX:-on}" != "off" ]; then
   RAW_DIR_PATH="$BRAIN_DIR/projects/$slug/raw"
   if [ -d "$RAW_DIR_PATH" ]; then
-    RAW_N=$(grep -rl '^status: unprocessed$' "$RAW_DIR_PATH" 2>/dev/null | wc -l | tr -d ' ')
+    # "open" = items not yet processed/discarded = unprocessed + malformed, matching the module's
+    # unprocessedCount (which counts malformed items as backlog). total - closed; mawk-free.
+    RAW_TOTAL=$(find "$RAW_DIR_PATH" -maxdepth 1 -name '*.md' 2>/dev/null | wc -l | tr -d ' ')
+    RAW_CLOSED=$(grep -rlE '^status: (processed|discarded)$' "$RAW_DIR_PATH" 2>/dev/null | wc -l | tr -d ' ')
+    RAW_N=$(( ${RAW_TOTAL:-0} - ${RAW_CLOSED:-0} ))
     if [ "${RAW_N:-0}" -gt 0 ]; then
       sb_append "$(printf '## ⓘ raw inbox — %s unprocessed item(s)\nRun `/second-brain:capture --list` to review; the maintainer refines them into notes.\n\n' "$RAW_N")" \
         "raw-inbox-banner" 250
diff --git a/tests/test-raw-capture.sh b/tests/test-raw-capture.sh
old mode 100644
new mode 100755
index 8ace06e..69ab34b
--- a/tests/test-raw-capture.sh
+++ b/tests/test-raw-capture.sh
@@ -31,6 +31,7 @@ RAW="$T/projects/demo/raw"
 [ "$(ls "$RAW"/*.md | wc -l)" -eq 1 ] || fail "expected exactly 1 item after idempotent re-capture"
 grep -q '^status: unprocessed$' "$RAW"/*.md || fail "item missing status: unprocessed"
 grep -q '^content_type: text/markdown$' "$RAW"/*.md || fail "item missing content_type"
+grep -q '^source: paste$' "$RAW"/*.md || fail "inline capture should record canonical source: paste"
 
 printf 'not a real item\n' > "$RAW/broken.md"
 run list | grep -q 'broken .*malformed' || fail "--list did not flag the malformed item"
diff --git a/tests/test-raw-inbox-banner.sh b/tests/test-raw-inbox-banner.sh
old mode 100644
new mode 100755
index a8c921a..343b2fb
--- a/tests/test-raw-inbox-banner.sh
+++ b/tests/test-raw-inbox-banner.sh
@@ -9,12 +9,16 @@ grep -q 'SB_RAW_INBOX' "$SL" || fail "session-load.sh has no SB_RAW_INBOX gate"
 grep -q 'raw-inbox-banner' "$SL" || fail "session-load.sh has no raw-inbox banner block"
 pass "session-load references the raw-inbox banner + kill switch"
 
-# Prove the documented count expression counts unprocessed items.
+# Prove the banner count expression = open backlog (unprocessed + malformed; excludes processed/discarded),
+# matching the module's unprocessedCount.
 T=$(mktemp -d); RAW="$T/projects/demo/raw"; mkdir -p "$RAW"
 mk(){ printf -- '---\nstatus: %s\n---\nx\n' "$1" > "$RAW/$2.md"; }
-mk unprocessed a; mk unprocessed b; mk discarded c
-N=$(grep -rl '^status: unprocessed$' "$RAW" 2>/dev/null | wc -l | tr -d ' ')
-[ "$N" = "2" ] || fail "expected the banner count expression to yield 2 (got $N)"
-pass "the banner count expression counts unprocessed items (2)"
+mk unprocessed a; mk unprocessed b; mk discarded c; mk processed d
+printf 'no frontmatter\n' > "$RAW/broken.md"   # malformed → counts as open
+RAW_TOTAL=$(find "$RAW" -maxdepth 1 -name '*.md' 2>/dev/null | wc -l | tr -d ' ')
+RAW_CLOSED=$(grep -rlE '^status: (processed|discarded)$' "$RAW" 2>/dev/null | wc -l | tr -d ' ')
+N=$(( RAW_TOTAL - RAW_CLOSED ))
+[ "$N" = "3" ] || fail "expected open=3 (a,b unprocessed + broken malformed; c,d closed), got $N"
+pass "the banner count = open backlog incl. malformed (3)"
 rm -rf "$T"
 echo; echo "ALL PASS"

From 885648f5d20018ee1234c49f1968199a2f2bdaa2 Mon Sep 17 00:00:00 2001
From: Cain-Ish 
Date: Wed, 3 Jun 2026 23:14:14 +0200
Subject: [PATCH 9/9] =?UTF-8?q?chore(release):=20raw=20inbox=20(SP-2)=20?=
 =?UTF-8?q?=E2=80=94=20bump=200.24.11=20+=20migration=20row?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-Authored-By: Claude Opus 4.8 (1M context) 
---
 .claude-plugin/marketplace.json | 2 +-
 .claude-plugin/plugin.json      | 2 +-
 skills/upgrade/SKILL.md         | 1 +
 3 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json
index e70afcc..0e2f127 100644
--- a/.claude-plugin/marketplace.json
+++ b/.claude-plugin/marketplace.json
@@ -9,7 +9,7 @@
       "name": "second-brain",
       "source": "./",
       "description": "Self-evolving AI second brain. Auto-learns from sessions, discovers tools, maintains a local knowledge base, and self-critiques code quality.",
-      "version": "0.24.10"
+      "version": "0.24.11"
     }
   ]
 }
diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json
index 0b26ab8..b7e9a7b 100644
--- a/.claude-plugin/plugin.json
+++ b/.claude-plugin/plugin.json
@@ -1,7 +1,7 @@
 {
   "name": "second-brain",
   "description": "Self-evolving AI second brain. Automatically learns from sessions, discovers tools, maintains a local knowledge base, and self-critiques code quality — getting smarter with every interaction.",
-  "version": "0.24.10",
+  "version": "0.24.11",
   "author": {
     "name": "second-brain"
   },
diff --git a/skills/upgrade/SKILL.md b/skills/upgrade/SKILL.md
index be79832..214c06e 100644
--- a/skills/upgrade/SKILL.md
+++ b/skills/upgrade/SKILL.md
@@ -35,6 +35,7 @@ Each migration is identified by its target version. Run only migrations whose ta
 | To version | Migration | Idempotent check |
 |---|---|---|
 | **vector-deps health** (re-runs every upgrade) | Smoke-import `@huggingface/transformers` from `$CLAUDE_PLUGIN_ROOT/mcp`. On failure, run `bash $CLAUDE_PLUGIN_ROOT/bin/install-vector-deps.sh`. **Why**: mcp bundles mark `@huggingface/transformers` as esbuild `--external` because its native binaries (`onnxruntime-node`, `sharp`) can't be statically packed. A plugin cache refresh ships `dist/` but does not touch `node_modules/`, so vector search silently degrades to text-only on every fresh install or cache wipe. Without this gate the user sees no error — just empty embeddings and degraded recall. Idempotent: the script is a no-op when the package and import smoke-check both succeed. | `cd "$CLAUDE_PLUGIN_ROOT/mcp" && node --input-type=module -e 'await import("@huggingface/transformers"); console.log("ok")' >/dev/null 2>&1` — if exit 0, skip. Otherwise run `bash "$CLAUDE_PLUGIN_ROOT/bin/install-vector-deps.sh"`; report the ~70MB network requirement before installing. |
+| **0.24.11** | KB **raw inbox** (SP-2 of the consolidation vision; design `docs/specs/2026-06-03-raw-inbox-design`, plan `docs/plans/2026-06-03-raw-inbox`). A per-project staging area `~/.second-brain/projects//raw/` for **unprocessed** material — dropped with provenance, held **out** of `knowledge_search`, surfaced as a backlog, and later refined into wiki nodes by the maintainer (SP-4, deferred). New user-invocable **`/second-brain:capture`**: `` copies a file (binary → blob sidecar); `` → an **offline pointer** (no fetch); inline text / `paste` → markdown; `--node ` pre-records provenance; `--list` / `--discard `; idempotent via content-hash. Each item is `raw/.md` with flat-YAML provenance frontmatter (`status: unprocessed|processed|discarded`); the work-list is **derived** by scanning status (no index file — drift-free). New `raw` group in `kb-schema.json` (dual-reader, `searchable:false`). `session-load.sh` surfaces a backlog banner (open = unprocessed + malformed; mawk-free `total − closed`), kill switch `SB_RAW_INBOX=off`. Security-hardened (deep-review): `setStatus` rejects path-traversal ids (a `--discard ../../wiki/page` can't rewrite an arbitrary `.md`), and every frontmatter value is newline-sanitized (no `status:` injection). **No MCP server tool change** — capture rides a standalone `raw-capture-cli` bundle (server stays 2.6.4). New `raw-inbox.ts` (9 vitest, incl. traversal + injection regressions) + `test-raw-capture.sh` + `test-raw-inbox-banner.sh` + raw-group schema guards. **Additive + back-compat:** `raw/` appears lazily on first capture; with no captures and `SB_RAW_INBOX` unset, behaviour is unchanged. SP-3 setup deep-scan + SP-4 maintainer drain are deferred. | No precondition — bumping the marker is sufficient. Capture material with `/second-brain:capture`; set `SB_RAW_INBOX=off` to suppress the backlog banner. |
 | **0.24.10** | Release-gate **typecheck guard**. `tests/run-all.sh` runs `vitest`, which transpiles per-file and does **not** typecheck the project — so a real `tsc` error (and the stale committed `dist/` left behind when `npm run build` = `tsc && bundle` fails) slipped through the "ALL GREEN" gate invisibly. That is exactly how the 0.24.7 `EDGE_TYPES` zod-enum cast broke the build yet shipped in **both** 0.24.7 and 0.24.8. New `tests/test-mcp-typecheck.sh` runs `tsc --noEmit` (no side effects; skips cleanly when `node` or the local `typescript` is absent) and is auto-included via the `test-*.sh` glob, so the gate now **fails on a type error** — closing the "validate the real capability" gap (the gate was testing a cheap proxy, not the real `npm run build`). Test-only — no state migration, no MCP change. | No precondition — bumping the marker is sufficient. |
 | **0.24.9** | KB **project-scoped serving** (SP-1 of the consolidation vision; design `docs/specs/2026-06-03-project-scoped-serving-design`, plan `docs/plans/2026-06-03-project-scoped-serving`). When a project is active, `knowledge_search` now serves *that project's* knowledge first instead of the whole wiki — killing the cross-project noise the per-prompt persona injection used to feed Claude. Each candidate is tiered: **T1** same `project:` facet **plus** the active project's own local-docs (registry pages are tier-1 by construction); **T2** the graph-neighbourhood of the project's anchor pages (`SB_SCOPE_HOPS`, default 2, 0–4); **T3** shared / no-facet; **T4** other project. Sorted **scoped-first** (tier asc, then score desc) and **auto-broadens** back to the full global pool when fewer than `SB_SCOPE_MIN_HITS` (default 3, 0–100) in-scope hits clear the score floor — so a legitimately cross-project answer is never starved. Wired through the single `knowledge_search` chokepoint so **both** injection surfaces inherit it: `knowledge-search-cli` forwards `SB_ACTIVE_SLUG`/`BRAIN_DIR`; `persona-context.sh` (per-prompt) and `session-load.sh` (session-start) export the active slug. The slug→project map + anchors are built from **wiki docs only** so a local-doc never overwrites a same-basename wiki page's project (which would leak that other-project page into scope). Kill switches: `SB_PROJECT_SCOPE=off`, `scope:"all"`, `SB_SCOPE_MIN_HITS`, `SB_SCOPE_HOPS`. Also unblocks the build: the 0.24.7 `EDGE_TYPES` source-of-truth migration left `server.ts` casting the zod edge enum to `[string, …]`, widening the `knowledge_relate`/`knowledge_neighbors` arg types back to `string` and breaking `tsc` (and thus `tsc && bundle`) since 0.24.7 — fixed to the `[EdgeType, …]` tuple. MCP server → 2.6.4. **Additive + back-compat:** with no active project (or `SB_PROJECT_SCOPE=off` / `scope:"all"`) the ranking is byte-for-byte the prior global search. New tests: 6 SP-1 vitest cases (incl. local-doc tier-1 + the same-basename leak guard) + `test-search-cli-scope.sh`. | No precondition — bumping the marker is sufficient. Scoping activates automatically when a project is active; set `SB_PROJECT_SCOPE=off` to restore the old global behaviour. |
 | **0.24.8** | Persona **behavioral protocol** — the Four Principles (SP-0 of the consolidation vision; distilled from Karpathy's Dec-2025 agent-failure-mode post). New `skills/using-second-brain/principles.md` (canonical source: **Think Before Coding · Simplicity First · Surgical Changes · Goal-Driven Execution**, full + a marked compact block) is referenced by `using-second-brain` as standing context and **re-surfaced once per session** by `persona-context.sh` on the first coding-intent prompt (memo-deduped; kill switch `SB_PRINCIPLES_INJECT=off`) — the just-in-time salience a static CLAUDE.md can't give. New advisory `scripts/simplicity-gate.sh` (PostToolUse on Write/Edit/MultiEdit): nudges toward a smaller, naive-correct version when a single change writes > `SB_SIMPLICITY_GATE_LINES` (default 150) lines — never blocks; kill switch `SB_SIMPLICITY_GATE=off`. *Goal-Driven* reuses the existing `stop-verify-gate`. Structural detectors for Surgical-Changes/assumptions are deferred (too fuzzy to gate without false-positive spam). The principles live in the **behavioral** layer only — never the user's identity `persona-card.md`. New guards: `test-persona-principles.sh`, `test-simplicity-gate.sh`. Prompt/script/hook/test-only — no state migration, no MCP change. | No precondition — bumping the marker is sufficient. |