@nynb/sandpaper 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Narayan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # Sandpaper
2
+
3
+ A **living project brain** for Claude Code projects. Sandpaper scaffolds a small static
4
+ site in `brain/` — a cover, three lenses (product / engineering / project), and three
5
+ books (log, decisions, learnings) — and teaches your agent to keep it current. After every
6
+ substantive turn, Claude *stamps* the brain: logs the work, refreshes the digest a fresh
7
+ session reads to rehydrate, flips plan tasks, records decisions and gotchas. The brain is
8
+ the **eyes** (state made visible); the terminal stays the **mouth** (steering). It never
9
+ copies your docs — every entry **links** to the canonical anchor in your spec or source.
10
+
11
+ No framework, no build step, no database. Plain HTML on disk, maintained by the agent,
12
+ readable by you.
13
+
14
+ ## Quick start
15
+
16
+ ```bash
17
+ # in your repo (npm package coming — GitHub install for now):
18
+ npx github:codevalley/sandpaper install-skill
19
+ ```
20
+
21
+ Then, inside Claude Code:
22
+
23
+ ```
24
+ /sandpaper:init # discovers your repo, asks a few questions, fills the brain
25
+ ```
26
+
27
+ And to view it:
28
+
29
+ ```bash
30
+ npx github:codevalley/sandpaper open # serves the repo + opens brain/index.html
31
+ ```
32
+
33
+ (An npm package is coming as `@nynb/sandpaper`, which will shorten these to `npx @nynb/sandpaper <cmd>`.)
34
+
35
+ ## What you get
36
+
37
+ - **`brain/`** — a multi-page static site: a **cover** (`index.html`) with a
38
+ machine-readable `#brain-state` digest, a one-line NOW, and derived progress;
39
+ **lenses** (`product/`, `engineering/`, `project/` — the plan board); **books**
40
+ (`log.html`, `decisions.html`, `learnings.html`). One `assets/theme.css` re-skins
41
+ everything.
42
+ - **The canvas** — a whiteboard on the cover. When Claude produces an explanation worth
43
+ keeping, it lands there as a readable board instead of scrolling past in the terminal
44
+ (last 5 kept, older ones fold away).
45
+ - **The refine toolbar** — serve the brain (or any HTML doc) locally and get an on-page
46
+ overlay: **Sand** (a scoped AI edit of the element you click), **Hands** (direct edit,
47
+ no AI), **Sling** (copies a terminal-ready instruction for bigger work). The file on
48
+ disk stays the single source of truth; the page live-reloads from it.
49
+
50
+ ## The CLI (plumbing — no AI)
51
+
52
+ | command | what it does |
53
+ |---|---|
54
+ | `sandpaper install-skill` | install the `/sandpaper:*` commands + hooks into this repo (also scaffolds `brain/`) |
55
+ | `sandpaper init` | scaffold `brain/` — assets, `.sandpaper/manifest.json`, a starter multi-page skeleton |
56
+ | `sandpaper upgrade` | bring an existing brain up to date (assets · hooks · commands · canvas), preserving your `theme.css` |
57
+ | `sandpaper rebuild` | full reset — back up the old brain to `brain.bak-<date>/`, lay down a fresh skeleton |
58
+ | `sandpaper doctor` | health-check a setup: assets, digest, links, source meta, manifest, hooks |
59
+ | `sandpaper open` | serve this repo's brain + open it in a browser |
60
+ | `sandpaper <doc.html \| dir>` | serve any doc or directory with the on-page refine toolbar |
61
+ | `sandpaper help` | usage |
62
+
63
+ Serves on `127.0.0.1:4848` by default (`$SANDPAPER_PORT` or the manifest's pinned `port`
64
+ override it; the server auto-bumps if the port is taken). `install-skill --no-hooks`
65
+ skips hook wiring and prints the settings snippet instead.
66
+
67
+ ## The commands (intelligence — inside Claude Code)
68
+
69
+ `/sandpaper:help` lists them all.
70
+
71
+ **Maintain the brain**
72
+
73
+ - `/sandpaper:stamp` — the full update after a substantive turn (log · NOW · digest · decisions · learnings · plan)
74
+ - `/sandpaper:log` — add one work-log row (the heartbeat)
75
+ - `/sandpaper:plan` — add or flip a task/initiative on the plan board
76
+ - `/sandpaper:decide` — record a decision, or open/resolve a question
77
+ - `/sandpaper:learn` — record a gotcha or verdict
78
+ - `/sandpaper:canvas` — elevate an explanation into a board on the cover's canvas
79
+ - `/sandpaper:sync` — reconcile the brain against the code; find and flag drift
80
+
81
+ **Set up & run**
82
+
83
+ - `/sandpaper:init` — discover the repo, run a short wizard, generate the brain
84
+ - `/sandpaper:open` — start the server and open the brain
85
+ - `/sandpaper:serve` — serve the brain (or any doc) with the refine toolbar
86
+ - `/sandpaper:theme` — re-skin from a brand colour or preset
87
+
88
+ ## The auto-updating brain
89
+
90
+ Two hooks keep the brain current without prodding. `install-skill` wires them into
91
+ `.claude/settings.json` (merged, deduped, your existing settings preserved); pass
92
+ `--no-hooks` to opt out, or remove the entries later to disable.
93
+
94
+ - **SessionStart** (`brain-inject.js`) — surfaces the brain's digest so a fresh `claude`
95
+ rehydrates from project state automatically.
96
+ - **Stop** (`brain-stamp-check.js`) — if a turn ends with uncommitted project changes but
97
+ nothing changed under `brain/`, it blocks once and asks the agent to stamp.
98
+ Self-limiting: it never loops, and the agent can decline by stopping again.
99
+
100
+ ## Publishing the brain
101
+
102
+ `brain/` is always publishable — point GitHub Pages, Vercel, Netlify, or Cloudflare at
103
+ the folder as-is; there is no build step. Links out of the brain (into your source and
104
+ specs) are written relative, and each page carries a `sandpaper:source` meta (set
105
+ automatically from your git origin); when the brain is served *away* from its repo, an
106
+ on-page resolver detects it and rewrites out-links to your source host at click time
107
+ instead of 404ing. See the deploy guide that ships inside every scaffolded brain:
108
+ [`brain/README.md`](brain/README.md).
109
+
110
+ ## Conventions & requirements
111
+
112
+ - **Node ≥ 18**, ESM, **zero runtime dependencies** — built-ins only.
113
+ - The document on disk is the single source of truth; the page reflects disk, never ahead of it.
114
+ - The brain **links, never copies** — roadmap, risks, and specs stay canonical in your docs.
115
+ - The server binds to `127.0.0.1` only.
116
+
117
+ ## License
118
+
119
+ MIT
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ // Sandpaper SessionStart hook — surface the brain digest so a fresh `claude` rehydrates
3
+ // from the brain FIRST, automatically (no "read the brain" prompt needed).
4
+ // Zero deps; prints the digest to stdout, which Claude Code adds to the session context.
5
+ import { readFileSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+
8
+ try {
9
+ const html = readFileSync(join(process.cwd(), 'brain', 'index.html'), 'utf8');
10
+ const m = html.match(/<script type="application\/json" id="brain-state">([\s\S]*?)<\/script>/);
11
+ if (!m) process.exit(0);
12
+ const d = JSON.parse(m[1]);
13
+ const out = [
14
+ `🪵 Sandpaper brain · ${d.project} · ${d.phase} · session ${d.session} · stamped ${d.updated}`,
15
+ `NOW — ${d.focus?.one || ''}${d.focus?.ref ? ` (${d.focus.ref})` : ''}`,
16
+ `Recent: ${(d.worklog || []).map((w) => w.one).join(' · ')}`,
17
+ (d.open && d.open.length) ? `Needs you: ${d.open.join(', ')}` : '',
18
+ `Canvas (board-first) — when your reply would be a substantial summary or explanation (a recap, an architecture, a comparison, a walkthrough, an analysis), the BOARD IS THE REPLY: write the elevated version as the current board in brain/index.html's <!-- BRAIN:CANVAS --> region (the .whiteboard; demote the previous to Earlier, cap 5) and leave just ONE line in the terminal — "📋 <gist> → on the canvas". Don't write it twice. Short answers and back-and-forth stay in the terminal. See the CANVAS discipline in the skill; /sandpaper:canvas forces one.`,
19
+ `Read brain/index.html to navigate. Stamp the brain after substantive turns (CLAUDE.md → "The project brain").`,
20
+ ].filter(Boolean).join('\n');
21
+ process.stdout.write(out + '\n');
22
+ } catch { /* no brain / unreadable — stay silent, never break the session */ }
23
+ process.exit(0);
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ // Sandpaper Stop hook — the brain's immune system. When a turn changed project files but DID NOT
3
+ // touch the brain, block once and tell the agent to stamp it. Automatic; no user prodding.
4
+ //
5
+ // Self-limiting (never loops):
6
+ // 1. `stop_hook_active` in the hook payload → we're already in a continuation, so allow the stop.
7
+ // 2. The check is idempotent — once the agent stamps (brain/ changes), it no longer fires.
8
+ // 3. The agent can override ("this turn needs no brain update") by simply stopping again.
9
+ import { readFileSync } from 'node:fs';
10
+ import { execFileSync } from 'node:child_process';
11
+
12
+ let input = {};
13
+ try { input = JSON.parse(readFileSync(0, 'utf8') || '{}'); } catch { /* no/!json stdin */ }
14
+ if (input.stop_hook_active) process.exit(0); // guard #1: don't re-block a continuation
15
+
16
+ let changed = [];
17
+ try {
18
+ changed = execFileSync('git', ['status', '--porcelain'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] })
19
+ .split('\n').filter(Boolean).map((l) => l.slice(3).trim());
20
+ } catch { process.exit(0); } // not a git repo / git missing — stay silent
21
+
22
+ const isProject = (f) => /\.(js|css|html|md|json)$/.test(f) && !f.startsWith('.sandpaper') && !f.includes('node_modules');
23
+ const proj = changed.filter(isProject);
24
+ const brainTouched = proj.some((f) => f.startsWith('brain/')); // guard #2: stamped → won't fire
25
+ const nonBrain = proj.filter((f) => !f.startsWith('brain/') && f !== 'CLAUDE.md');
26
+
27
+ if (nonBrain.length && !brainTouched) {
28
+ const reason =
29
+ `🪵 The brain isn't stamped. This turn changed ${nonBrain.length} project file(s) ` +
30
+ `(${nonBrain.slice(0, 6).join(', ')}${nonBrain.length > 6 ? ', …' : ''}) but nothing under brain/. ` +
31
+ `Before finishing, STAMP the brain (CLAUDE.md → "The project brain"): prepend one log row, refresh the ` +
32
+ `cover NOW + #brain-state digest, flip any plan-board tasks, add a decision/learning if one applies — then commit. ` +
33
+ `If this turn genuinely warrants no brain update, just stop again.`;
34
+ process.stdout.write(JSON.stringify({ decision: 'block', reason }) + '\n');
35
+ }
36
+ process.exit(0);
package/bin/cli.js ADDED
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ // Sandpaper CLI. Subcommands are the "plumbing" (no AI); a bare path falls through to `serve`.
3
+ // sandpaper install-skill | init | doctor | open | help | <doc.html|dir>
4
+ import { resolve, dirname, join } from 'node:path';
5
+ import { existsSync, statSync, readFileSync } from 'node:fs';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { execFile } from 'node:child_process';
8
+ import { startServer } from '../src/server.js';
9
+ import { installSkill, scaffold, doctor, upgrade, rebuild } from '../src/setup.js';
10
+
11
+ const PKG = join(dirname(fileURLToPath(import.meta.url)), '..');
12
+ const [cmd, ...rest] = process.argv.slice(2);
13
+
14
+ // Starting port: $SANDPAPER_PORT wins, else the repo's pinned .sandpaper/manifest.json "port",
15
+ // else 4848. The server auto-bumps from here if it's taken, so multiple repos never collide.
16
+ const startPort = () => {
17
+ if (process.env.SANDPAPER_PORT) return Number(process.env.SANDPAPER_PORT);
18
+ try { const m = JSON.parse(readFileSync(join(process.cwd(), '.sandpaper', 'manifest.json'), 'utf8')); if (m.port) return Number(m.port); } catch {}
19
+ return 4848;
20
+ };
21
+ const port = startPort();
22
+
23
+ const usage = () => console.log(`
24
+ 🪵 sandpaper — a living project brain
25
+
26
+ sandpaper install-skill install the /sandpaper commands + hooks into this repo
27
+ sandpaper init scaffold brain/ (assets + manifest + a starter cover)
28
+ sandpaper upgrade bring an existing brain up to date (assets · hooks · commands · canvas)
29
+ sandpaper rebuild full reset — back up the old brain + lay down a fresh skeleton
30
+ sandpaper doctor health-check a Sandpaper setup
31
+ sandpaper open serve this repo's brain + open it in a browser
32
+ sandpaper <doc.html | dir> serve with the on-page refine toolbar
33
+ sandpaper help this
34
+
35
+ Fresh repo? → sandpaper install-skill, then /sandpaper:init in Claude Code.
36
+ `);
37
+
38
+ const serve = async (target, openBrowser) => {
39
+ const isDir = statSync(target).isDirectory();
40
+ const url = await startServer(target, port, { brain: isDir });
41
+ console.log(`\n 🪵 Sandpaper\n ↳ ${isDir ? 'serving' : 'editing'} ${target}\n ↳ open ${url}\n`);
42
+ if (openBrowser) {
43
+ const u = isDir && existsSync(join(target, 'brain', 'index.html')) ? url + 'brain/index.html' : url;
44
+ const opener = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
45
+ execFile(opener, [u], () => {}); // best-effort; ignore failures
46
+ }
47
+ };
48
+
49
+ (async () => {
50
+ try {
51
+ if (!cmd || cmd === 'help' || cmd === '-h' || cmd === '--help') return usage();
52
+ if (cmd === 'install-skill') return installSkill(process.cwd(), PKG, { noHooks: rest.includes('--no-hooks') });
53
+ if (cmd === 'init') return scaffold(process.cwd(), PKG);
54
+ if (cmd === 'upgrade' || cmd === 'update') return upgrade(process.cwd(), PKG);
55
+ if (cmd === 'rebuild' || cmd === 'reset') return rebuild(process.cwd(), PKG);
56
+ if (cmd === 'doctor') return doctor(process.cwd());
57
+ if (cmd === 'open') return serve(process.cwd(), true);
58
+ const target = resolve(process.cwd(), cmd);
59
+ if (!existsSync(target)) { console.error(`\n sandpaper: unknown command or path: ${cmd}`); usage(); process.exit(1); }
60
+ return serve(target, false);
61
+ } catch (e) { console.error(' sandpaper:', e.message); process.exit(1); }
62
+ })();
@@ -0,0 +1,99 @@
1
+ # Deploying the brain
2
+
3
+ ## What this folder is
4
+
5
+ This folder is Sandpaper's own living brain: a small static site the terminal agent stamps
6
+ after each substantive turn (see `../CLAUDE.md` → "The project brain").
7
+
8
+ - `index.html` — the cover: NOW, the canvas (boards), digest, recent log, browse.
9
+ - `log.html` — append-only work log (the heartbeat).
10
+ - `decisions.html` — decisions + open questions (the why).
11
+ - `map.html` — components, architecture (linked), glossary.
12
+ - `learnings.html` — gotchas & verdicts.
13
+ - `product/` · `engineering/` · `project/` — the lenses; `wiki/` — the settled prose docs.
14
+
15
+ Styled by `assets/theme.css` + `assets/brain.css`, with a little vanilla JS in
16
+ `assets/brain.js`. No framework, no build step, no server-side anything. It is
17
+ **always publishable**: point any static host at this folder as-is and it works.
18
+
19
+ One design choice shapes everything below: the brain **links, never copies**. Canonical
20
+ truth lives in the parent repo — the spec docs (`../sandpaper.html`, `../engg-spec.html`),
21
+ source files, `package.json` — and the brain references them with relative paths (`../…`)
22
+ so they resolve on disk and whenever the whole repo is served.
23
+
24
+ ## Two deploy shapes
25
+
26
+ ### 1. Whole-repo deploy (recommended for public repos)
27
+
28
+ Serve the repo root and visit `/brain/`. Every out-of-brain link resolves: spec HTML docs
29
+ render with working `#anchors`, source files are viewable. GitHub Pages serving the repo
30
+ root does this perfectly.
31
+
32
+ ### 2. Brain-only deploy (site root = this folder)
33
+
34
+ The relative `../` refs can't resolve — there's nothing above the root. The built-in
35
+ resolver in `assets/brain.js` handles it. Each page's head carries:
36
+
37
+ ```html
38
+ <meta name="sandpaper:source" content="https://github.com/codevalley/sandpaper/blob/HEAD/" data-pkg="sandpaper" />
39
+ ```
40
+
41
+ On load, the page probes `../package.json` and checks its `name` against `data-pkg`.
42
+ If the probe fails (or the name doesn't match), the page knows it is detached, and
43
+ out-links open the source-host copy instead (rewritten at click time). Source and meta
44
+ files render fine on GitHub's blob view; spec **HTML** docs land on blob *source* view —
45
+ unrendered. Use the whole-repo shape if you want rendered specs. With no meta configured,
46
+ out-links dim with a tooltip instead of 404ing.
47
+
48
+ The meta is written automatically by `npx sandpaper init` / `upgrade` from the git origin
49
+ (or `package.json` → `"repository"`). `npx sandpaper doctor` verifies it is present and
50
+ consistent across pages.
51
+
52
+ ## Deployed brains are read-only
53
+
54
+ The refine toolbar (Sand / Hands / Sling) is injected only by the local `sandpaper`
55
+ server — a deployed brain has no toolbar and can't be edited from the page. By design:
56
+ the public copy is for reading.
57
+
58
+ ## Recipes
59
+
60
+ **GitHub Pages (simplest)** — Settings → Pages → Source: *Deploy from a branch*, branch
61
+ `main`, folder `/ (root)`. That's the whole-repo shape — visit
62
+ `https://<owner>.github.io/<repo>/brain/`. For the brain-only shape, use Source:
63
+ *GitHub Actions* with this workflow:
64
+
65
+ ```yaml
66
+ name: Deploy brain
67
+ on: { push: { branches: [main] } }
68
+ permissions: { contents: read, pages: write, id-token: write }
69
+ jobs:
70
+ deploy:
71
+ runs-on: ubuntu-latest
72
+ environment: { name: github-pages, url: "${{ steps.deployment.outputs.page_url }}" }
73
+ steps:
74
+ - uses: actions/checkout@v4
75
+ - uses: actions/upload-pages-artifact@v3
76
+ with: { path: brain } # 'path: .' switches to the whole-repo shape
77
+ - id: deployment
78
+ uses: actions/deploy-pages@v4
79
+ ```
80
+
81
+ **Vercel** — New Project → import the repo. Root Directory = repo root (or `brain/` for
82
+ brain-only), Framework Preset = *Other*, no build command, Output Directory = `./`.
83
+
84
+ **Netlify** — New site from Git. No build command. Publish directory: `brain` (or the
85
+ repo root).
86
+
87
+ **Cloudflare Pages** — Connect the repo. No build command. Build output directory:
88
+ `brain` (or `/`).
89
+
90
+ ## Privacy
91
+
92
+ Deploying the whole repo publishes **all** of its files, not just the brain. Brain-only
93
+ publishes just this folder — but its out-links point at the source host, which must be
94
+ public for them to work. Either way, assume everything the brain links to is visible.
95
+ Don't deploy a brain whose repo isn't ready to be read.
96
+
97
+ Remember what the brain *is*: distilled internal state. The canvas boards are derived
98
+ from working conversations, and the log/decisions/learnings record real reasoning —
99
+ read them with publishing eyes before pointing a host at this folder.