granite-mem 0.1.12 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,23 +9,21 @@
9
9
  </p>
10
10
 
11
11
  > **The personal OS your agent runs on.**
12
- > Markdown files. One SQLite index. A typed contract your agent already knows how to operate.
12
+ > Give your agent a memory it can't hallucinate: plain markdown + SQLite full-text search + a typed contract it already knows how to operate. No LLM inside. Your agent brings the intelligence; Granite holds the ground truth.
13
13
 
14
14
  <p align="center">
15
15
  <img src="docs/screenshots/granite-graph.png" alt="Granite constellation graph" width="720">
16
16
  </p>
17
17
 
18
18
  <p align="center">
19
- <b>Install it with your agent.</b> Or run it standalone as a local markdown knowledge graph.
20
- </p>
21
-
22
- <p align="center">
23
- <a href="#your-vault-in-the-cloud"><img src="https://img.shields.io/badge/☁️_Deploy_your_Granite-one_command,_sleeps_when_idle-111111?style=for-the-badge" alt="Deploy your Granite"></a>
19
+ <code>npm install -g granite-mem</code>
24
20
  </p>
25
21
 
26
22
  ---
27
23
 
28
- ## The wow moment
24
+ **Jump to:** [The 60-second setup](#the-60-second-setup) · [Install it yourself](#or-install-it-yourself) · [How it works](#how-it-works) · [Why not Obsidian, Notion, or a vector store?](#why-not-obsidian-notion-or-a-vector-store) · [See it](#see-it) · [Types are contracts](#types-are-contracts-not-folders) · [The MCP server](#wired-for-agents-the-mcp-server) · [What Granite will never do](#what-granite-will-never-do) · [Beyond one machine](#beyond-one-machine) · [The full CLI](#the-full-cli) · [FAQ](#faq) · [Philosophy](#philosophy)
25
+
26
+ ## The 60-second setup
29
27
 
30
28
  Paste this into **Claude Code**, **Cursor**, or any MCP-capable agent:
31
29
 
@@ -41,9 +39,69 @@ Install Granite as my personal OS.
41
39
  drafts with --source agent.
42
40
  ````
43
41
 
44
- Sixty seconds later you have a live vault, a connected agent that **knows how to use it**, and three starter notes in `~/.granite/notes/`. No system prompt. No config. No cloud.
42
+ Sixty seconds later: a live vault, an agent that knows how to operate it, and its first three notes on disk in `~/.granite/notes/`. No system prompt. No config. No cloud.
43
+
44
+ That's the thesis of this project. No agent handy? Granite is a complete tool on its own:
45
+
46
+ ## Or install it yourself
47
+
48
+ ```bash
49
+ npm install -g granite-mem
50
+ granite init # note / source / synthesis / output
51
+ granite new "Ideas worth stealing"
52
+ granite search "steal"
53
+ granite serve # web UI + constellation graph
54
+ ```
55
+
56
+ Add `--template founder-os` to `init` when you want a full personal OS from the start: `person`, `organization`, `meeting`, and `learning` on top of the four defaults — eight types already wired with hooks, indexed fields, and lifecycles, in [150 lines of pure YAML](templates/founder-os.yml).
57
+
58
+ Everything Granite writes is a plain `.md` file with YAML frontmatter. Close the laptop, `git init` the vault, open the folder in any editor — it's just files.
59
+
60
+ ## How it works
45
61
 
46
- That's the thesis of this project.
62
+ Granite is a **deterministic substrate** for knowledge: markdown files as the source of truth, a SQLite index as derived state, and one fixed loop imposed on top. The intelligence is not in Granite — it's in whatever agent (or human) operates it.
63
+
64
+ ```
65
+ ┌──────────────────────────────────────────┐
66
+ │ your agent · the brains │
67
+ └────────────────────┬─────────────────────┘
68
+ │ MCP · CLI · web UI
69
+ ┌────────────────────▼─────────────────────┐
70
+ │ GRANITE │
71
+ │ │
72
+ │ capture ─▶ compile ─▶ query ─▶ output │
73
+ │ ▲ │ │
74
+ │ └──────────── lint ◀──────────┘ │
75
+ │ │
76
+ │ .md files SQLite FTS5 wikilinks │
77
+ │ (truth) (derived) (graph) │
78
+ └──────────────────────────────────────────┘
79
+ ```
80
+
81
+ - **Markdown is truth.** Every note is `<folder>/<slug>.md` with YAML frontmatter. Nothing you can't read in `cat`.
82
+ - **The index is disposable.** Full-text search, backlinks, and typed queries live in `.granite/index.db` — rebuilt from the files at any time.
83
+ - **Wikilinks are the graph.** `[[a-note]]` in any body resolves slug → title → alias, and the backlink graph falls out for free.
84
+
85
+ > Karpathy asked for *"an incredible new product instead of a hacky collection of scripts"* for [LLM knowledge bases](https://x.com/karpathy/status/2039805659525644595).
86
+ >
87
+ > This is our answer.
88
+
89
+ ## Why not Obsidian, Notion, or a vector store?
90
+
91
+ Granite doesn't compete with your note app. It competes with the pile of scripts you were about to write.
92
+
93
+ | | **Granite** | Obsidian | Notion | Vector memory | Plain files |
94
+ | ---------------------------------------------- | :---------: | :-------: | :-------: | :-----------: | :---------: |
95
+ | Plain markdown on disk | ✅ | ✅ | ❌ | ❌ | ✅ |
96
+ | Typed schemas with hooks & lifecycles | ✅ | plugins | databases | ❌ | ❌ |
97
+ | Agent-native MCP workflow | ✅ | community | limited | ✅ | ❌ |
98
+ | Deterministic retrieval (no embeddings) | ✅ | ✅ | ❌ | ❌ | `grep` |
99
+ | Structured queries over indexed fields | ✅ | plugins | ✅ | ❌ | ❌ |
100
+ | Provenance on every note | ✅ | ❌ | ❌ | partial | ❌ |
101
+ | Offline, no account, no telemetry | ✅ | ✅ | ❌ | ❌ | ✅ |
102
+ | Git-friendly | ✅ | ✅ | ❌ | ❌ | ✅ |
103
+
104
+ Obsidian is a great editor for humans. Vector memory is a great cache for agents. Granite is the shared substrate both can operate.
47
105
 
48
106
  ## See it
49
107
 
@@ -74,47 +132,11 @@ That's the thesis of this project.
74
132
  </tr>
75
133
  </table>
76
134
 
77
- ## What is Granite?
78
-
79
- A local-first markdown store with an opinionated flow. **No AI inside** — just plain files on disk, indexed by SQLite, queried deterministically.
80
-
81
- - **Imposed flow.** Capture, compile, query, output, lint. The shape is fixed; the content is yours.
82
- - **Four default note types** — `note`, `source`, `synthesis`, `output`. Add your own in `granite.yml` when your life grows a new shape.
83
- - **A specialized MCP** that teaches your agent how to use the vault. Drop any MCP-capable agent on it and it knows how to operate, no system prompt required.
135
+ ## Types are contracts, not folders
84
136
 
85
- Your agent brings the intelligence. Granite holds the ground truth.
86
-
87
- ## Try it standalone
88
-
89
- ```bash
90
- npm install -g granite-mem
91
- granite init
92
- granite serve
93
- ```
94
-
95
- That starts with the default knowledge model: `note`, `source`, `synthesis`, and `output`. Add `--template founder-os` when you want people, organizations, meetings, and learnings wired in from the start.
96
-
97
- ## Agent-native MCP
98
-
99
- > "A thin MCP server exposes capabilities. A strong MCP server shapes behavior."
100
-
101
- Granite's MCP surface is intention-first:
102
-
103
- - `granite_wakeup` to orient
104
- - `granite_research_topic` to inspect existing knowledge before writing
105
- - `granite_query` for structured filters over typed notes
106
- - `granite_compile_context` to assemble a graph-aware brief
107
- - `granite_plan_garden` to decide what to improve next
108
- - `granite_capture_knowledge` to write with protocol fields and type contracts
109
-
110
- The point is not to give an agent a file browser. The point is to give it a workflow it can follow.
111
-
112
- ## Types as active contracts
113
-
114
- This is what makes the agent feel native rather than bolted-on.
137
+ This is what makes an agent feel native rather than bolted-on. Every note type in `granite.yml` is an executable contract:
115
138
 
116
139
  ```yaml
117
- # granite.yml — every note type is an executable contract
118
140
  note_types:
119
141
  meeting:
120
142
  folder: notes/meetings
@@ -126,42 +148,14 @@ note_types:
126
148
  - { action: set_default, field: date, value: "${today}" }
127
149
  - { action: resolve_wikilinks, fields: [organization, attendees], auto_stub: true }
128
150
  indexed_fields: [date, organization]
129
- lifecycle:
130
- states: [active, archived]
131
- transitions:
132
- - { from: active, to: archived, trigger: stale_days, days: 180 }
133
151
  ```
134
152
 
135
153
  - `set_default` — fills `${today}` automatically
136
154
  - `resolve_wikilinks + auto_stub` — turns `organization: Acme Corp` into the slug `acme-corp`, creating the org note if missing (with a globally-unique slug so nothing gets silently overwritten)
137
- - `indexed_fields` — makes `granite_query { type: meeting, where: { date: { gte: "2026-01-01" } } }` O(1) and deterministic
138
- - `lifecycle` — `granite doctor` surfaces stale notes so gardening never drifts
139
-
140
- Add a type when your life has a new shape. The core stays small. For the formal protocol, see [docs/GRANITE_OBJECT_STANDARD.md](docs/GRANITE_OBJECT_STANDARD.md).
141
-
142
- ## Templates
143
-
144
- ```bash
145
- granite init # minimal: note / source / synthesis / output
146
- granite init --template founder-os # + person / organization / meeting / learning
147
- ```
148
-
149
- `founder-os` is the full personal-OS starter: people you talk to, orgs you work with, meetings you had, things you learned. Eight types, already wired with hooks, indexed fields, and lifecycles. Open `templates/founder-os.yml` — it's 150 lines of pure YAML.
150
-
151
- ## The hard boundary
152
-
153
- Granite will **never**:
155
+ - `indexed_fields` — makes `granite_query { type: meeting, where: { date: { gte: "2026-01-01" } } }` fast and deterministic
156
+ - `lifecycle` — declare states and stale-days transitions, and `granite doctor` surfaces drift before it rots
154
157
 
155
- - embed an LLM, run prompts, or hold an API key
156
- - compute embeddings or ship a vector store
157
- - run background agents or a scheduler
158
- - add overlapping CLI/MCP endpoints that blur the loop
159
-
160
- This is why your agent can be trusted with write access. The vault is a **deterministic substrate**. The intelligence is yours (or Claude's, or GPT's, or whoever you pay this quarter).
161
-
162
- ## Protocol fields
163
-
164
- Every note carries five shared fields so humans and agents share ground truth:
158
+ On top of its type, **every note carries five protocol fields**, so humans and agents share ground truth:
165
159
 
166
160
  | Field | Values | Purpose |
167
161
  |----------------|---------------------------------------|--------------------------------------|
@@ -171,144 +165,147 @@ Every note carries five shared fields so humans and agents share ground truth:
171
165
  | `durability` | `canonical` · `working` · `ephemeral` | keep / may drift / throwaway |
172
166
  | `derived_from` | `[slug, …]` | provenance for syntheses and outputs |
173
167
 
174
- Your agent reads these before writing and sets them as it works. You inherit a fully auditable trail.
168
+ Your agent reads these before writing and sets them as it works. You inherit a fully auditable trail. Add a type when your life grows a new shape — the core stays small. For the formal protocol, see [docs/GRANITE_OBJECT_STANDARD.md](docs/GRANITE_OBJECT_STANDARD.md).
175
169
 
176
- ## Local-first, by design
170
+ ## Wired for agents: the MCP server
177
171
 
178
- - Markdown files are the source of truth; the SQLite index in `~/.granite/index.db` is derived state and can be rebuilt at any time
179
- - no cloud, no telemetry, no account
180
- - `git init` your vault and you have versioning for free
181
- - `granite serve` gives you a local web UI — browse, search, explore local and cloud graphs
182
- - `granite daemon start` runs MCP + web UI in one background process
183
-
184
- ## Your vault in the cloud
172
+ > "A thin MCP server exposes capabilities. A strong MCP server shapes behavior."
185
173
 
186
- One command deploys a personal serverless Granite on [Fly.io Sprites](https://sprites.dev): a real persistent filesystem, an MCP endpoint that **wakes on request** (100–500 ms warm) and **sleeps when idle**. Idle cost ≈ storage only — a markdown vault is a few MB, so cents per month.
174
+ One line connects any MCP-capable agent to your vault:
187
175
 
188
176
  ```bash
189
- export SPRITES_TOKEN=… # from sprites.dev your account, your sprite, your data
190
- npx granite-mem deploy
177
+ claude mcp add granite -- granite mcp --vault ~/.granite
191
178
  ```
192
179
 
193
- Or store the Sprites API token once in a user-scoped Granite credentials file:
180
+ The surface is intention-first fourteen tools organized around the workflow, not around files:
194
181
 
195
- ```bash
196
- granite deploy login --token <sprites-token>
197
- ```
182
+ | Intent | Tools |
183
+ |------------|----------------------------------------------------------------------------------------------------------------|
184
+ | **Orient** | `granite_wakeup` · `granite_research_topic` · `granite_resolve` |
185
+ | **Read** | `granite_query` · `granite_compile_context` · `granite_understand_note` · `granite_extract_document` |
186
+ | **Write** | `granite_capture_knowledge` · `granite_import_document` · `granite_revise_note` · `granite_dispose_note` |
187
+ | **Garden** | `granite_plan_garden` · `granite_adjudicate_garden_opportunity` · `granite_list_garden_adjudications` |
188
+
189
+ Plus three prompts for the higher-level workflows (`granite_refine_note`, `granite_process_inbox`, `granite_compile_topic`), and resources for raw note and type-contract access. Start the server with `--role read` when an agent should inspect without mutating. An HTTP transport with bearer-token auth is available for remote setups — see [docs/DEPLOY.md](docs/DEPLOY.md).
198
190
 
199
- That writes `~/.granite/config/sprites.json` (mode `0600` where supported). It works on macOS, Linux, and Windows via the user's home directory. Commands resolve credentials in this order: `--token`, `SPRITES_TOKEN`, then the stored file. Remove it with `granite deploy logout`.
191
+ The point is not to give an agent a file browser. The point is to give it a workflow it can follow.
200
192
 
201
- That prints an MCP URL + bearer token ready to paste into Claude Code or Cursor:
193
+ ## What Granite will never do
194
+
195
+ Granite will **never**:
196
+
197
+ - embed an LLM, run prompts, or hold an API key
198
+ - compute embeddings or ship a vector store
199
+ - run background agents or a scheduler
200
+ - phone home — no telemetry, no account, no cloud dependency
201
+ - add overlapping CLI/MCP endpoints that blur the loop
202
+
203
+ This is why your agent can be trusted with write access. The vault is a deterministic substrate. The intelligence is yours (or Claude's, or GPT's, or whoever you pay this quarter).
204
+
205
+ ## Beyond one machine
206
+
207
+ **Cloud, if you want it.** One command deploys a personal serverless Granite on [Fly.io Sprites](https://sprites.dev): wakes on request in 100–500 ms, sleeps when idle, costs cents per month at rest. You own the sprite — there is no Granite cloud, no central admin, no relay.
202
208
 
203
209
  ```bash
210
+ granite deploy login --token <sprites-token> # or export SPRITES_TOKEN=…
211
+ granite deploy # prints an MCP URL + bearer token
212
+
204
213
  claude mcp add --transport http granite https://<your-sprite>.sprites.app/mcp \
205
214
  --header "Authorization: Bearer <token>"
206
215
  ```
207
216
 
208
- There is no central admin, no Granite cloud account, no relay: `granite deploy` provisions a sprite **you** own and pay for directly.
217
+ Multiple named instances, bulk upgrades, token rotation, and self-hosting the HTTP MCP server (a generic `Dockerfile` is included) are covered in [docs/DEPLOY.md](docs/DEPLOY.md).
209
218
 
210
- Manage instances from any machine after `deploy login`, or with `SPRITES_TOKEN`:
219
+ **Sync, without a relay.** Direct machine-to-machine over LAN, Tailscale, or a private DNS name — with per-device read/write tokens:
211
220
 
212
221
  ```bash
213
- granite deploy login --token <sprites-token>
214
- granite deploy work # a second instance (own vault, own token, own URL)
215
- granite deploy list # all instances: version, health, MCP URL
216
- granite deploy status --show-token
217
- granite deploy # re-run = upgrade that instance to your CLI version
218
- granite deploy --all # bulk remote update of every instance
219
- granite deploy destroy work # permanent — asks for confirmation
220
- ```
221
-
222
- Browse local and deployed vaults from the same local UI:
222
+ granite sync access grant ipad --role read # on the serving machine
223
+ granite sync serve --host 0.0.0.0 --port 8765
223
224
 
224
- ```bash
225
- SPRITES_TOKEN=… granite serve
225
+ granite sync remote add macbook http://100.x.y.z:8765 --token <read-token>
226
+ granite sync watch macbook --direction pull --interval 30
226
227
  ```
227
228
 
228
- `granite serve` discovers your managed Sprites using `SPRITES_TOKEN` or the stored `~/.granite/config/sprites.json` token. It keeps MCP bearer tokens on the local server and proxies read-only graph/search/note requests to the selected cloud instance. Use `granite serve --no-cloud` to browse only the local vault.
229
-
230
- Notes:
229
+ Conflict policies, push/pull details, and access management live in [docs/SYNC.md](docs/SYNC.md).
231
230
 
232
- - Document parsing (PDF/DOCX/XLSX/PPTX) is **disabled in cloud deployments** (`GRANITE_DISABLE_DOCUMENT_PARSING=1`). Extract and import documents on your local Granite.
233
- - Cloud instances expose MCP plus an authenticated read-only web API for the local UI switcher; note creation in the web UI remains local-only.
234
- - The Sprites API token is local user configuration at `~/.granite/config/sprites.json`; it is not written to the vault notes and is excluded from Granite sync manifests.
235
- - The vault lives at `/home/sprite/.granite` on the sprite, on durable object-storage-backed disk. There is no export/backup command in v1 — treat the sprite as the single copy for now.
231
+ ## The full CLI
236
232
 
237
- ### Advanced: run the HTTP MCP server yourself
233
+ <details>
234
+ <summary><b>Every command, one line each</b></summary>
238
235
 
239
- Granite can expose the MCP server over HTTP anywhere you can run Node. When binding outside localhost, a bearer token is required:
236
+ | Layer | Command | What it does |
237
+ |------------|----------------------------|------------------------------------------------------------------|
238
+ | setup | `granite init` | create a vault (optionally from a `--template`) |
239
+ | setup | `granite status` | vault health and what to do next |
240
+ | capture | `granite new <title>` | create a typed note |
241
+ | capture | `granite add [text]` | quick raw capture (arg or stdin) into the inbox |
242
+ | capture | `granite attach <file>` | attach an image/video/PDF and get markdown to embed |
243
+ | capture | `granite extract <file>` | raw text from PDF/DOCX/XLSX/PPTX without importing |
244
+ | capture | `granite import <file> --content <text>` | attach a document and create a linked source note |
245
+ | query | `granite list` | browse notes by type, status, source, date |
246
+ | query | `granite show <slug>` | read a full note |
247
+ | query | `granite search <query>` | full-text search across the vault |
248
+ | query | `granite open <slug>` | open a note in `$EDITOR` |
249
+ | query | `granite wakeup` | compact vault snapshot for loading agent context |
250
+ | compile | `granite edit <slug>` | update fields, body, tags, protocol state |
251
+ | compile | `granite backlinks <slug>` | inbound links to a note |
252
+ | compile | `granite suggest-links <slug>` | unlinked mentions worth linking |
253
+ | compile | `granite recommend <slug>` | what to link, tag, or write next |
254
+ | lint | `granite doctor` | broken links, missing fields, stale notes, line violations |
255
+ | lint | `granite types` | show note types and the flow between them |
256
+ | serve | `granite serve` | local web UI with the constellation graph (port 4321) |
257
+ | serve | `granite mcp` | MCP server (stdio or HTTP; `--role read\|write`) |
258
+ | serve | `granite daemon start` | MCP + web UI as one background process |
259
+ | cloud | `granite deploy …` | serverless instances on Fly.io Sprites ([docs](docs/DEPLOY.md)) |
260
+ | sync | `granite sync …` | direct multi-device sync ([docs](docs/SYNC.md)) |
240
261
 
241
- ```bash
242
- export GRANITE_MCP_TOKEN="$(openssl rand -hex 32)"
243
- granite mcp --vault ~/.granite \
244
- --transport http \
245
- --host 0.0.0.0 \
246
- --port 3321 \
247
- --web-api
248
- ```
262
+ Run `granite --help` for every flag.
249
263
 
250
- Clients must send the token on every MCP request:
264
+ </details>
251
265
 
252
- ```bash
253
- claude mcp add --transport http granite https://granite.example.com/mcp \
254
- --header "Authorization: Bearer $GRANITE_MCP_TOKEN"
255
- ```
266
+ ## FAQ
256
267
 
257
- A generic `Dockerfile` is included for self-hosting on your own infra (build it yourself; no public image is published). The `/health` endpoint is unauthenticated for platform checks. Do not expose `granite serve` or the web UI port `4321` on the public internet; the web UI is local-only and has no authentication. If `--web-api` is enabled on the HTTP MCP server, `/api/*` and `/assets/*` are guarded by the same bearer token as `/mcp`.
268
+ <details>
269
+ <summary><b>Is this an Obsidian replacement?</b></summary>
258
270
 
259
- ## Direct sync
271
+ No. Your vault is plain markdown with `[[wikilinks]]` — Obsidian opens it just fine. Granite adds the typed contracts, the deterministic index, and the MCP surface on top of files any editor can read.
260
272
 
261
- Granite sync is direct machine-to-machine. Run it over LAN, Tailscale, or a private DNS name; there is no relay, hosted worker, billing tier, or cloud authority.
273
+ </details>
262
274
 
263
- ```bash
264
- # Machine A
265
- granite sync access grant ipad --role read
266
- granite sync access grant desktop --role write
267
- granite sync serve --host 0.0.0.0 --port 8765
275
+ <details>
276
+ <summary><b>Where's the AI?</b></summary>
268
277
 
269
- # Read-only Machine B
270
- granite sync remote add macbook http://100.x.y.z:8765 --token <read-token>
271
- granite sync pull macbook
272
- granite sync watch macbook --direction pull --interval 30
278
+ In your agent. Granite is deliberately deterministic — that's precisely why an agent can be trusted with write access to it.
273
279
 
274
- # Write-capable Machine C
275
- granite sync remote add macbook http://100.x.y.z:8765 --token <write-token>
276
- granite sync run macbook
277
- granite sync watch macbook --interval 30
278
- ```
280
+ </details>
279
281
 
280
- Conflicts default to manual preservation with `.conflict.<device>.<timestamp>.md` files. For a personal multi-device setup, pick the device that wins conflicts:
282
+ <details>
283
+ <summary><b>No embeddings — how does search work?</b></summary>
281
284
 
282
- ```bash
283
- granite sync config --policy primary-wins --primary-this-device
284
- ```
285
+ SQLite FTS5 for full text, typed queries over indexed fields, and the wikilink graph for structure. Deterministic, explainable, and rebuildable from the files.
285
286
 
286
- MCP is scoped to one vault per server. Launch a read-only MCP server when an agent should inspect a synced vault without mutating it:
287
+ </details>
287
288
 
288
- ```bash
289
- granite mcp --vault ~/.granite --role read
290
- granite mcp --vault ~/.granite --role write
291
- ```
289
+ <details>
290
+ <summary><b>Can I use it without an agent?</b></summary>
292
291
 
293
- For the full CLI, run `granite --help`. For development, see [CLAUDE.md](CLAUDE.md).
292
+ Yes. The full CLI and the web UI work standalone. The MCP server is one door among three.
294
293
 
295
- ## Roadmap & status
294
+ </details>
296
295
 
297
- Granite is pre-1.0. The current release is **v0.1.9**, with the major agent-native loop pieces now in place: typed contracts, wakeup snapshots, deterministic garden planning, document import, daemon mode, and the constellation graph.
296
+ <details>
297
+ <summary><b>What happens to my data if Granite disappears?</b></summary>
298
298
 
299
- Read [CHANGELOG.md](CHANGELOG.md) for release history. The product boundary stays fixed: Granite stores and indexes local knowledge; agents bring the intelligence.
299
+ Nothing. It's a folder of markdown files. The index is derived and disposable; `git init` the vault and you have versioning and backup for free.
300
300
 
301
- ## Contributing
301
+ </details>
302
302
 
303
- Issues and focused PRs are welcome.
303
+ <details>
304
+ <summary><b>Does anything phone home?</b></summary>
304
305
 
305
- For local development, read [CLAUDE.md](CLAUDE.md). The key product rule is simple: no embedded LLM, no vector store, no autonomous scheduler inside Granite.
306
+ No. No telemetry, no account, no network calls unless you explicitly deploy to your own sprite, sync to your own machines, or run `granite serve` with cloud credentials configured (use `--no-cloud` to stay fully offline).
306
307
 
307
- > Karpathy wrote that there was room for "an incredible new product instead of a hacky collection of scripts" around LLM knowledge bases.
308
- >
309
- > Granite is the product that answers that call.
310
- >
311
- > — [@karpathy on LLM knowledge bases](https://x.com/karpathy/status/2039805659525644595)
308
+ </details>
312
309
 
313
310
  ## Philosophy
314
311
 
@@ -319,6 +316,12 @@ For local development, read [CLAUDE.md](CLAUDE.md). The key product rule is simp
319
316
  - protocol belongs in the core; **agent policy belongs outside it**
320
317
  - a personal OS is a thing you own — not a thing you rent
321
318
 
319
+ ## Status & contributing
320
+
321
+ Granite is pre-1.0 and moving fast — see [CHANGELOG.md](CHANGELOG.md) for release history. The product boundary stays fixed: Granite stores and indexes local knowledge; agents bring the intelligence.
322
+
323
+ Issues and focused PRs are welcome. For local development, read [CLAUDE.md](CLAUDE.md). The key product rule is simple: no embedded LLM, no vector store, no autonomous scheduler inside Granite.
324
+
322
325
  ---
323
326
 
324
327
  <p align="center">
@@ -1,5 +1,5 @@
1
1
  "use strict";(()=>{(function(){"use strict";const K=[{hex:"#ba6844",soft:"rgba(186,104,68,0.16)"},{hex:"#2e6b5b",soft:"rgba(46,107,91,0.18)"},{hex:"#735bb0",soft:"rgba(113,88,170,0.18)"},{hex:"#987b43",soft:"rgba(152,123,67,0.18)"}],qt={hex:"rgba(255,236,220,0.6)",soft:"rgba(255,236,220,0.12)"};let i=null,A=null,Mt=()=>{},T=[],B=[],R={},z={},Lt=[],I={},N="all",x=null,Et=null,X=[];const ut=new Map;let ft=null,ht=0,W=-1;const o={tx:0,ty:0,k:1},h=t=>document.getElementById(t),E=h("stage"),Vt=h("graph-canvas"),Pt=h("graph-labels"),pt=h("type-filters"),gt=h("dirA-sub"),Gt=h("stat-notes"),Kt=h("stat-edges"),Ct=h("stat-communities"),Ut=h("stat-density"),Y=h("zoom-val"),$t=h("empty-hint"),U=h("reader"),J=h("reader-body"),ot=h("reader-breadcrumb"),Jt=h("reader-kicker"),Zt=h("reader-title"),Qt=h("reader-deck"),te=h("reader-metaline"),rt=h("reader-article"),Tt=h("reader-links"),Rt=h("reader-progress"),ee=h("reader-resizer"),j=h("wikilink-preview"),q=h("command-bar-overlay"),V=h("command-input"),P=h("command-results");function b(t){return String(t??"").replace(/[&<>"']/g,e=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[e])}function ne(t){if(!t)return"";const e=new Date(t);return isNaN(e.getTime())?"":`${["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"][e.getMonth()]} ${e.getDate()}, ${e.getFullYear()}`}function mt(t){return((t||"").trim().match(/\S+/g)||[]).length}function St(t){return Math.max(1,Math.ceil(mt(t)/220))}function Dt(t){if(!t)return"";const e=t.split(`
2
- `);for(const s of e){const c=s.trim();if(!c||c.startsWith("#")||c.startsWith(">")||c.startsWith("```")||c.startsWith("|"))continue;const r=c.replace(/`([^`]+)`/g,"$1").replace(/\*\*([^*]+)\*\*/g,"$1").replace(/\[\[([^\]|]+)(\|([^\]]+))?\]\]/g,(a,n,l,u)=>u||n).replace(/\[([^\]]+)\]\([^)]+\)/g,"$1");if(!(r.length<12))return r.length>160?r.slice(0,157).trimEnd()+"\u2026":r}return""}function Z(t){return I[t]||K[0]}const vt="granite.instance";let O=localStorage.getItem(vt);function at(t){return fetch(t,{headers:O?{"X-Granite-Instance":O}:{}})}async function se(){const e=await(await fetch("/api/instances")).json(),s=e.instances||[],c=new Set(s.map(a=>a.id));(!O||!c.has(O))&&(O=e.default||"local",localStorage.removeItem(vt)),window.GRANITE_INSTANCE=O==="local"?null:O;const r=document.getElementById("instance-switcher");if(r&&s.length>1){r.innerHTML="";for(const a of s){const n=document.createElement("option");n.value=a.id,n.textContent=a.kind==="cloud"?`\u2601 ${a.label}${a.web_api?"":" (outdated)"}`:a.label,n.selected=a.id===O,r.appendChild(n)}r.hidden=!1,r.addEventListener("change",()=>{localStorage.setItem(vt,r.value),location.reload()})}}async function Ht(){try{try{await se()}catch(n){console.error("[granite] instance discovery failed",n)}const[t,e]=await Promise.all([at("/api/graph").then(n=>yt(n)),at("/api/types").then(n=>yt(n))]);T=t.nodes||[];const s=t.edges||[],c=e.types||[];Lt=Array.isArray(c)?c.map(n=>typeof n=="string"?n:n.name||n.type):Object.keys(c),I={},Lt.forEach((n,l)=>{I[n]=K[l%K.length]});for(const n of T)I[n.type]||(I[n.type]=K[Object.keys(I).length%K.length]);const r=new Set(T.map(n=>n.slug)),a=new Set;for(const n of s){if(!n.source||!n.target||n.source===n.target||!r.has(n.source)||!r.has(n.target))continue;const l=n.source<n.target?n.source+"|"+n.target:n.target+"|"+n.source;a.add(l)}B=[...a].map(n=>{const[l,u]=n.split("|");return{source:l,target:u}}),R=Object.fromEntries(T.map(n=>[n.slug,n])),z={};for(const n of T)z[n.slug]=new Set;for(const n of B)z[n.source].add(n.target),z[n.target].add(n.source);ie(),It(),ae(),ve(),T.length||(gt.textContent="Your vault is empty. Create a note with `granite new` to light up the sky.")}catch(t){console.error("[granite] boot failed",t),gt.textContent=t&&t.graniteMessage?t.graniteMessage:"Could not load the vault. Is the API running?"}}async function yt(t){const e=await t.json().catch(()=>({}));if(!t.ok){const s=new Error(e.error||"API error "+t.status);throw e.code&&(s.graniteMessage=e.error),s}return e}function ie(){const t=[{k:"all",label:"All",c:qt.hex}];for(const e of Object.keys(I))t.push({k:e,label:e,c:I[e].hex});pt.innerHTML="";for(const e of t){const s=document.createElement("button");s.type="button",s.className="dirA-filter"+(N===e.k?" active":""),s.style.setProperty("--tc",e.c),s.dataset.k=e.k,s.innerHTML=`<span class="dot" style="--tc:${e.c}"></span>${b(e.label)}`,s.addEventListener("click",()=>oe(e.k)),pt.appendChild(s)}}function oe(t){N=t;for(const e of pt.querySelectorAll(".dirA-filter"))e.classList.toggle("active",e.dataset.k===t);re(),kt="",Mt()}function re(){if(!i)return;if(N==="all"){for(let e=0;e<i.n;e++)i.alpha[e]=1;return}const t=x?i.idxOf[x]:-1;for(let e=0;e<i.n;e++){const s=i.slugOf[e],c=R[s];c&&(c.type===N||e===t?i.alpha[e]=1:i.alpha[e]=.12)}}function It(){Gt.textContent=T.length,Kt.textContent=B.length,Ut.textContent=T.length?(B.length/T.length).toFixed(1):"0.0",i&&(Ct.textContent=i.K),gt.textContent=`${T.length} notes \xB7 ${B.length} edges${i?" \xB7 "+i.K+" communities":""}, laid out in real time. Drag any star, hover to part the crowd, scroll to zoom deep.`}function ae(){if(!T.length)return;i=window.GraphEngine.createSimulation(T,B,2400,1800),A=window.GraphEngine.createRenderer(Vt,i),window.__granite={sim:i,renderer:A,view:o,nodes:T,links:B},Ct.textContent=i.K,It(),i.prebake(280),i.freeze(),new ResizeObserver(()=>{A.resize(),x||wt(!1)}).observe(E);function t(a=0){const n=E.getBoundingClientRect();if(n.width===0||n.height===0)return a>30?void 0:requestAnimationFrame(()=>t(a+1));try{A.resize(),wt(!0),ce()}catch(l){console.error("[granite] arrival boot failed",l)}}requestAnimationFrame(()=>t(0));let e=0;const s={k:NaN,tx:NaN,ty:NaN,active:null,hovered:-2,revealDone:!1};Mt=()=>{s.k=NaN,s.revealDone=!1};function c(){if(i.cooling>0||i.pinnedIdx>=0)return!0;if(!s.revealDone){let a=!0;for(let n=0;n<i.n;n++)if(i.alpha[n]<.999){a=!1;break}if(!a||i.edgeAlpha<.999)return!0;s.revealDone=!0}return o.k!==s.k||o.tx!==s.tx||o.ty!==s.ty||x!==s.active||W!==s.hovered}function r(){if(c()){const a=i.cooling>0||i.pinnedIdx>=0,n=!s.revealDone;(a||n)&&i.step(1/60),A.draw(e),e+=.016,de(),s.k=o.k,s.tx=o.tx,s.ty=o.ty,s.active=x,s.hovered=W}requestAnimationFrame(r)}requestAnimationFrame(r)}function ce(){if(!i||!A)return;for(let p=0;p<i.n;p++)i.alpha[p]=0;i.edgeAlpha=0;const t=performance.now(),e=1400,s=900,c=1800,r=o.k,a=Math.min(1.2,r*1.45),n=E.getBoundingClientRect();let l=1/0,u=-1/0,f=1/0,m=-1/0;for(let p=0;p<i.n;p++){const $=i.x[p],D=i.y[p];$<l&&(l=$),$>u&&(u=$),D<f&&(f=D),D>m&&(m=D)}const v=(l+u)/2,w=(f+m)/2;o.k=a,o.tx=n.width/(2*a)-v,o.ty=n.height/(2*a)-w,A.setView(o);const d=new Float32Array(i.n),g=new Map;for(let p=0;p<i.n;p++){const $=i.community[p];g.has($)||g.set($,[]),g.get($).push(p)}const M=[...g.keys()].sort((p,$)=>g.get($).length-g.get(p).length),y=e*.6;M.forEach((p,$)=>{const D=g.get(p).sort((_,it)=>i.degree[it]-i.degree[_]),bt=$/M.length*(y*.6);D.forEach((_,it)=>{const At=it/Math.max(1,D.length-1)*(y*.4);d[_]=bt+At})});const C=p=>1-Math.pow(1-p,3),L=p=>p<.5?2*p*p:1-Math.pow(-2*p+2,3)/2;function st(){const p=performance.now()-t,$=520;for(let H=0;H<i.n;H++){const jt=p-d[H];if(jt<=0){i.alpha[H]=0;continue}const ye=Math.min(1,jt/$);i.alpha[H]=C(ye)}const D=Math.min(1,Math.max(0,(p-(e-s))/s));i.edgeAlpha=C(D);const bt=Math.min(1,p/c),_=a+(r-a)*L(bt),it=n.width/(2*_)-v,At=n.height/(2*_)-w;if(o.k=_,o.tx=it,o.ty=At,A.setView(o),Y.textContent=Math.round(_*100)+"%",p<Math.max(e,c)+60)requestAnimationFrame(st);else{for(let H=0;H<i.n;H++)i.alpha[H]=1;i.edgeAlpha=1,o.k=r,o.tx=n.width/(2*r)-v,o.ty=n.height/(2*r)-w,A.setView(o),Y.textContent=Math.round(r*100)+"%",i.freeze()}}requestAnimationFrame(st)}function wt(t=!0){const e=E.getBoundingClientRect();if(e.width===0||e.height===0||!i)return;let s=1/0,c=-1/0,r=1/0,a=-1/0;for(let M=0;M<i.n;M++){const y=i.x[M],C=i.y[M],L=i.radius[M];y-L<s&&(s=y-L),y+L>c&&(c=y+L),C-L<r&&(r=C-L),C+L>a&&(a=C+L)}if(!isFinite(s))return;const n=80,l=Math.max(1,c-s),u=Math.max(1,a-r),f=Math.min((e.width-n*2)/l,(e.height-n*2)/u),m=Math.max(.22,Math.min(1.2,f)),v=(s+c)/2,w=(r+a)/2,d=e.width/(2*m)-v,g=e.height/(2*m)-w;o.tx=d,o.ty=g,o.k=m,A.setView(o),t&&(Y.textContent=Math.round(m*100)+"%")}let kt="",le=0;const Q=new Set,tt=new Map;function de(){if(!i)return;const t=le++%4===0,e=E.getBoundingClientRect(),s=o.k,c=i.n,r=et.hop1;let a;if(t){const l=new Set;if(x!=null){const d=i.idxOf[x];d!=null&&l.add(d);for(const g of r)i.idxOf[g]!=null&&l.add(i.idxOf[g])}W>=0&&l.add(W);const u=s<.3?30:s<.6?14:s<1?7:3;for(let d=0;d<c;d++)i.degree[d]>=u&&l.add(d);for(const d of Q)l.add(d);const f=[];for(const d of l){if(d==null||d<0)continue;const g=i.slugOf[d],M=R[g];if(!M||N!=="all"&&M.type!==N&&d!==i.idxOf[x])continue;const y=(i.x[d]+o.tx)*s,C=(i.y[d]+o.ty)*s;y<-60||y>e.width+60||C<-40||C>e.height+40||f.push({i:d,sx:y,sy:C,r:i.radius[d]*s,title:M.title||g})}const m=d=>{if(d.i===i.idxOf[x])return 1e4;if(d.i===W)return 9e3;if(r.has(i.slugOf[d.i]))return 5e3+i.degree[d.i];const g=Q.has(d.i)?2e3+Math.min(500,(tt.get(d.i)||0)*20):0;return i.degree[d.i]+g};f.sort((d,g)=>m(g)-m(d));const v=[],w=[];for(const d of f){const g=Math.min(140,6.5*d.title.length+10),y={x:d.sx-g/2,y:d.sy+d.r+4,w:g,h:16};let C=!1;for(const L of v)if(y.x<L.x+L.w&&y.x+y.w>L.x&&y.y<L.y+L.h&&y.y+y.h>L.y){C=!0;break}if(!C&&(v.push(y),w.push({...d,box:y}),w.length>80))break}a=w,Q.clear();for(const d of a)Q.add(d.i),tt.set(d.i,(tt.get(d.i)||0)+1);for(const[d]of tt)Q.has(d)||tt.delete(d);Nt=a}else{a=[];for(const l of Nt){const u=l.i,f=i.slugOf[u],m=R[f];if(!m)continue;const v=(i.x[u]+o.tx)*s,w=(i.y[u]+o.ty)*s;if(v<-60||v>e.width+60||w<-40||w>e.height+40)continue;const d=m.title||f,g=Math.min(140,6.5*d.length+10);a.push({i:u,sx:v,sy:w,r:i.radius[u]*s,title:d,box:{x:v-g/2,y:w+i.radius[u]*s+4,w:g,h:16}})}}let n="";for(const l of a){const u=i.slugOf[l.i],f=u===x,m=r.has(u);n+=`<div class="${f?"dirA-lbl active":m?"dirA-lbl hop1":"dirA-lbl"}" style="left:${l.box.x.toFixed(1)}px;top:${l.box.y.toFixed(1)}px;width:${l.box.w.toFixed(0)}px">${b(l.title)}</div>`}n!==kt&&(Pt.innerHTML=n,kt=n)}let Nt=[],et={hop1:new Set,hop2:new Set};function ue(t){const e=new Set(z[t]||[]),s=new Set;for(const c of e)for(const r of z[c]||[])r!==t&&!e.has(r)&&s.add(r);return{hop1:e,hop2:s}}function Ot(){!i||!A||(i.setActive(x),A.setHops(et))}let k=null;E.addEventListener("mousedown",t=>{if(!i)return;const e=E.getBoundingClientRect(),s=t.clientX-e.left,c=t.clientY-e.top,r=s/o.k-o.tx,a=c/o.k-o.ty,n=i.pickNode(r,a,14/o.k);n>=0?k={kind:"node",idx:n,moved:!1,x0:t.clientX,y0:t.clientY}:k={kind:"pan",x0:t.clientX,y0:t.clientY,tx:o.tx,ty:o.ty}}),window.addEventListener("mousemove",t=>{if(!i)return;const e=E.getBoundingClientRect(),s=t.clientX-e.left,c=t.clientY-e.top;if((s<0||c<0||s>e.width||c>e.height)&&!k)return;const r=s/o.k-o.tx,a=c/o.k-o.ty;if(i.clearHover(),!k){const n=i.pickNode(r,a,14/o.k);W=n,A.setHovered(n),E.style.cursor=n>=0?"pointer":"grab";return}if(k.kind==="pan")o.tx=k.tx+(t.clientX-k.x0)/o.k,o.ty=k.ty+(t.clientY-k.y0)/o.k,A.setView(o),E.classList.add("dragging");else if(k.kind==="node"){const n=t.clientX-k.x0,l=t.clientY-k.y0;!k.moved&&n*n+l*l>9&&(k.moved=!0),k.moved&&(i.pinnedIdx=k.idx,i.pinX=r,i.pinY=a,i.cooling=Math.max(i.cooling,.8),E.classList.add("dragging"))}}),window.addEventListener("mouseup",()=>{if(i){if(k&&k.kind==="node"&&!k.moved){const t=i.slugOf[k.idx];if(t){const e=R[t];(N==="all"||!e||e.type===N)&&F(t)}}i.pinnedIdx=-1,k=null,E.classList.remove("dragging")}}),E.addEventListener("mouseleave",()=>{i&&(i.clearHover(),W=-1,A.setHovered(-1))}),E.addEventListener("wheel",t=>{if(!i)return;t.preventDefault();const e=E.getBoundingClientRect(),s=t.clientX-e.left,c=t.clientY-e.top,r=-t.deltaY*.0015,a=Math.max(.15,Math.min(3,o.k*(1+r))),n=s/o.k-o.tx,l=c/o.k-o.ty;o.k=a,o.tx=s/a-n,o.ty=c/a-l,A.setView(o),Y.textContent=Math.round(a*100)+"%"},{passive:!1});function Ft(t){if(!i)return;const e=E.getBoundingClientRect(),s=e.width/2,c=e.height/2,r=Math.max(.15,Math.min(3,o.k*t)),a=s/o.k-o.tx,n=c/o.k-o.ty;o.k=r,o.tx=s/r-a,o.ty=c/r-n,A.setView(o),Y.textContent=Math.round(r*100)+"%"}h("zoom-in").addEventListener("click",()=>Ft(1.2)),h("zoom-out").addEventListener("click",()=>Ft(1/1.2)),h("zoom-reset").addEventListener("click",()=>wt(!0));async function F(t){if(!(!t||!R[t])){x=t,et=ue(t),Ot(),X=X[X.length-1]===t?X:[...X,t].slice(-6),i&&(i.cooling=Math.min(1,i.cooling+.15)),lt(),$t.hidden=!0,fe();try{const e=ut.get(t)||await _t(t);if(x!==t)return;Et=e,he(e)}catch(e){console.error("[granite] fetch note failed",t,e)}}}async function _t(t){const e=await at("/api/notes/"+encodeURIComponent(t));if(!e.ok)throw new Error("fetch "+t+": "+e.status);const s=await e.json();return ut.set(t,s),s}let nt=null;function fe(){if(!i||!x)return;const t=i.idxOf[x];if(t==null||t<0)return;const e=E.getBoundingClientRect();if(e.width===0)return;const s=ft??Math.min(900,window.innerWidth*.62),a=Math.max(280,e.width-s-24)/2,n=e.height/2,l=Math.max(o.k,.55),u=performance.now(),f=520,m=o.tx,v=o.ty,w=o.k;nt&&cancelAnimationFrame(nt);const d=M=>1-Math.pow(1-M,3);function g(){const M=Math.min(1,(performance.now()-u)/f),y=d(M),C=i.x[t],L=i.y[t],st=a/l-C,p=n/l-L;o.tx=m+(st-m)*y,o.ty=v+(p-v)*y,o.k=w+(l-w)*y,A.setView(o),Y.textContent=Math.round(o.k*100)+"%",M<1?nt=requestAnimationFrame(g):(o.tx=st,o.ty=p,o.k=l,A.setView(o),nt=null)}nt=requestAnimationFrame(g)}function Bt(){x=null,Et=null,et={hop1:new Set,hop2:new Set},Ot(),U.hidden=!0,$t.hidden=!1,lt()}function he(t){U.hidden=!1;const e=Z(t.type),s=mt(t.body),c=St(t.body),r=Dt(t.body),a=X.map(f=>R[f]).filter(Boolean);ot.innerHTML="";const n=document.createElementNS("http://www.w3.org/2000/svg","svg");n.setAttribute("width","12"),n.setAttribute("height","12"),n.setAttribute("viewBox","0 0 24 24"),n.setAttribute("fill","none"),n.setAttribute("stroke","currentColor"),n.setAttribute("stroke-width","1.5"),n.setAttribute("stroke-linecap","round"),n.setAttribute("stroke-linejoin","round"),n.innerHTML='<path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"/><path d="M14 3v5h5"/>',ot.appendChild(n),a.forEach((f,m)=>{if(m>0){const w=document.createElement("span");w.className="sep",w.textContent="\u203A",ot.appendChild(w)}const v=document.createElement("span");v.className="crumb"+(m===a.length-1?" current":""),v.title=f.title||f.slug,v.textContent=f.title||f.slug,m!==a.length-1&&v.addEventListener("click",()=>F(f.slug)),ot.appendChild(v)});const l=[`<span class="dirA-reader-type" style="background:${e.soft};color:${e.hex}">${b(t.type||"note")}</span>`];t.modified&&l.push(`<span>${b(ne(t.modified))}</span>`),l.push(`<span>\xB7 ${c} min read</span>`),l.push(`<span>\xB7 ${s} words</span>`),Jt.innerHTML=l.join(""),Zt.textContent=t.title||t.slug,Qt.textContent=r;const u=[`<span class="slug">${b(t.slug)}</span>`];for(const f of t.tags||[])u.push(`<span class="tag">#${b(f)}</span>`);te.innerHTML=u.join(""),rt.innerHTML=MarkdownRenderer.render(t.body||"",t.outgoing_links||[]),pe(t),J.scrollTop=0,ht=0,Rt.style.width="0%"}function pe(t){const e=[],s=(t.outgoing_links||[]).filter(n=>n.resolved&&n.resolved_slug),c=new Set,r=[];for(const n of s)n.resolved_slug!==t.slug&&(c.has(n.resolved_slug)||(c.add(n.resolved_slug),r.push(n.resolved_slug)));if(r.length){e.push(`<div><div class="dirA-links-section-label">Links out <span class="count">${r.length}</span></div><div class="dirA-links-list">`);for(const n of r){const l=R[n]||{title:n,slug:n,type:"note"},u=Z(l.type);e.push(`<button class="dirA-linkcard" data-nav="${b(n)}"><span class="dirA-linkcard-dot" style="background:${u.hex}"></span><div><div class="dirA-linkcard-title">${b(l.title||n)}</div><div class="dirA-linkcard-deck">${b(l.deck||"")}</div></div><span class="dirA-linkcard-arrow">\u2192</span></button>`)}e.push("</div></div>")}const a=t.backlinks||[];if(a.length){const n=new Set,l=[];for(const u of a){const f=u.source_slug||u.slug;if(!f||n.has(f))continue;n.add(f);const m=R[f],v=m?.title||u.source_title||f,w=m?.type||"note";l.push({slug:f,title:v,type:w,context:u.context||""})}e.push(`<div><div class="dirA-links-section-label">Backlinks <span class="count">${l.length}</span></div><div class="dirA-links-list">`);for(const u of l){const f=Z(u.type);e.push(`<button class="dirA-linkcard" data-nav="${b(u.slug)}"><span class="dirA-linkcard-dot" style="background:${f.hex}"></span><div><div class="dirA-linkcard-title">${b(u.title)}</div><div class="dirA-linkcard-deck">${b(u.context)}</div></div><span class="dirA-linkcard-arrow">\u2190</span></button>`)}e.push("</div></div>")}Tt.innerHTML=e.join("")}Tt.addEventListener("click",t=>{const e=t.target.closest("[data-nav]");e&&F(e.getAttribute("data-nav"))}),J.addEventListener("scroll",()=>{const t=J.scrollHeight-J.clientHeight;ht=t>0?Math.min(1,J.scrollTop/t):0,Rt.style.width=ht*100+"%"}),ee.addEventListener("mousedown",t=>{t.preventDefault(),U.classList.add("resizing");const e=t.clientX,s=ft??Math.min(900,window.innerWidth*.62);function c(a){const n=e-a.clientX,l=Math.max(480,Math.min(window.innerWidth-80,s+n));ft=l,U.style.setProperty("--reader-w",l+"px")}function r(){U.classList.remove("resizing"),window.removeEventListener("mousemove",c),window.removeEventListener("mouseup",r)}window.addEventListener("mousemove",c),window.addEventListener("mouseup",r)}),h("reader-close").addEventListener("click",Bt),h("reader-prev").addEventListener("click",()=>ct(-1)),h("reader-next").addEventListener("click",()=>ct(1));function ct(t){if(!x){if(!i)return;let r=0;for(let n=1;n<i.n;n++)i.degree[n]>i.degree[r]&&(r=n);const a=i.slugOf[r];a&&F(a);return}const e=[x,...et.hop1].filter(r=>R[r]);if(e.length<2)return;const s=e.indexOf(x),c=e[((s+t)%e.length+e.length)%e.length];c&&F(c)}rt.addEventListener("click",t=>{const e=t.target.closest("a.wikilink");if(!e)return;t.preventDefault();const s=e.getAttribute("data-slug");s&&F(s)});let xt=0;rt.addEventListener("mouseover",async t=>{const e=t.target.closest("a.wikilink");if(!e)return;const s=e.getAttribute("data-slug");if(!s)return;const c=e.getBoundingClientRect(),r=++xt;try{const a=ut.get(s)||await _t(s);if(r!==xt||!document.contains(e))return;ge(a,c)}catch{}}),rt.addEventListener("mouseout",t=>{t.target.closest("a.wikilink")&&(xt++,lt())});function ge(t,e){const s=Z(t.type),c=Dt(t.body),r=mt(t.body),a=St(t.body);j.innerHTML=`
2
+ `);for(const s of e){const c=s.trim();if(!c||c.startsWith("#")||c.startsWith(">")||c.startsWith("```")||c.startsWith("|"))continue;const r=c.replace(/`([^`]+)`/g,"$1").replace(/\*\*([^*]+)\*\*/g,"$1").replace(/\[\[([^\]|]+)(\|([^\]]+))?\]\]/g,(a,n,l,u)=>u||n).replace(/\[([^\]]+)\]\([^)]+\)/g,"$1");if(!(r.length<12))return r.length>160?r.slice(0,157).trimEnd()+"\u2026":r}return""}function Z(t){return I[t]||K[0]}const vt="granite.instance";let O=localStorage.getItem(vt);function at(t){return fetch(t,{headers:O?{"X-Granite-Instance":O}:{}})}async function se(){const e=await(await fetch("/api/instances")).json(),s=e.instances||[],c=new Set(s.map(a=>a.id));(!O||!c.has(O))&&(O=e.default||"local",localStorage.removeItem(vt)),window.GRANITE_INSTANCE=O==="local"?null:O;const r=document.getElementById("instance-switcher");if(r&&s.length>1){r.innerHTML="";for(const a of s){const n=document.createElement("option");n.value=a.id,n.textContent=a.kind==="cloud"?`\u2601 ${a.label}${a.web_api?"":" (outdated)"}`:a.label,n.selected=a.id===O,r.appendChild(n)}r.hidden=!1,r.addEventListener("change",()=>{localStorage.setItem(vt,r.value),location.reload()})}}async function Ht(){try{try{await se()}catch(n){console.error("[granite] instance discovery failed",n)}const[t,e]=await Promise.all([at("/api/graph").then(n=>yt(n)),at("/api/types").then(n=>yt(n))]);T=t.nodes||[];const s=t.edges||[],c=e.types||[];Lt=Array.isArray(c)?c.map(n=>typeof n=="string"?n:n.name||n.type):Object.keys(c),I={},Lt.forEach((n,l)=>{I[n]=K[l%K.length]});for(const n of T)I[n.type]||(I[n.type]=K[Object.keys(I).length%K.length]);const r=new Set(T.map(n=>n.slug)),a=new Set;for(const n of s){if(!n.source||!n.target||n.source===n.target||!r.has(n.source)||!r.has(n.target))continue;const l=n.source<n.target?n.source+"|"+n.target:n.target+"|"+n.source;a.add(l)}B=[...a].map(n=>{const[l,u]=n.split("|");return{source:l,target:u}}),R=Object.fromEntries(T.map(n=>[n.slug,n])),z={};for(const n of T)z[n.slug]=new Set;for(const n of B)z[n.source].add(n.target),z[n.target].add(n.source);ie(),It(),ae(),ve(),T.length||(gt.textContent="Your vault is empty. Create a note with `granite new` to light up the sky.")}catch(t){console.error("[granite] boot failed",t),gt.textContent=t&&t.graniteMessage?t.graniteMessage:"Could not load the vault. Is the API running?"}}async function yt(t){const e=await t.json().catch(()=>({}));if(!t.ok){const s=new Error(e.error||"API error "+t.status);throw e.code&&(s.graniteMessage=e.error),s}return e}function ie(){const t=[{k:"all",label:"All",c:qt.hex}];for(const e of Object.keys(I))t.push({k:e,label:e,c:I[e].hex});pt.innerHTML="";for(const e of t){const s=document.createElement("button");s.type="button",s.className="dirA-filter"+(N===e.k?" active":""),s.style.setProperty("--tc",e.c),s.dataset.k=e.k,s.innerHTML=`<span class="dot" style="--tc:${e.c}"></span>${b(e.label)}`,s.addEventListener("click",()=>oe(e.k)),pt.appendChild(s)}}function oe(t){N=t;for(const e of pt.querySelectorAll(".dirA-filter"))e.classList.toggle("active",e.dataset.k===t);re(),kt="",Mt()}function re(){if(!i)return;if(N==="all"){for(let e=0;e<i.n;e++)i.alpha[e]=1;return}const t=x?i.idxOf[x]:-1;for(let e=0;e<i.n;e++){const s=i.slugOf[e],c=R[s];c&&(c.type===N||e===t?i.alpha[e]=1:i.alpha[e]=.12)}}function It(){Gt.textContent=T.length,Kt.textContent=B.length,Ut.textContent=T.length?(B.length/T.length).toFixed(1):"0.0",i&&(Ct.textContent=i.K),gt.textContent=`${T.length} notes \xB7 ${B.length} edges${i?" \xB7 "+i.K+" communities":""}, laid out in real time. Drag any star, hover to part the crowd, scroll to zoom deep.`}function ae(){if(!T.length)return;i=window.GraphEngine.createSimulation(T,B,2400,1800),A=window.GraphEngine.createRenderer(Vt,i),window.__granite={sim:i,renderer:A,view:o,nodes:T,links:B},Ct.textContent=i.K,It(),i.prebake(280),i.freeze(),new ResizeObserver(()=>{A.resize(),x||wt(!1)}).observe(E);function t(a=0){const n=E.getBoundingClientRect();if(n.width===0||n.height===0)return a>30?void 0:requestAnimationFrame(()=>t(a+1));try{A.resize(),wt(!0),ce()}catch(l){console.error("[granite] arrival boot failed",l)}}requestAnimationFrame(()=>t(0));let e=0;const s={k:NaN,tx:NaN,ty:NaN,active:null,hovered:-2,revealDone:!1};Mt=()=>{s.k=NaN,s.revealDone=!1};function c(){if(i.cooling>0||i.pinnedIdx>=0)return!0;if(!s.revealDone){let a=!0;for(let n=0;n<i.n;n++)if(i.alpha[n]<.999){a=!1;break}if(!a||i.edgeAlpha<.999)return!0;s.revealDone=!0}return o.k!==s.k||o.tx!==s.tx||o.ty!==s.ty||x!==s.active||W!==s.hovered}function r(){if(c()){const a=i.cooling>0||i.pinnedIdx>=0,n=!s.revealDone;(a||n)&&i.step(1/60),A.draw(e),e+=.016,de(),s.k=o.k,s.tx=o.tx,s.ty=o.ty,s.active=x,s.hovered=W}requestAnimationFrame(r)}requestAnimationFrame(r)}function ce(){if(!i||!A)return;for(let p=0;p<i.n;p++)i.alpha[p]=0;i.edgeAlpha=0;const t=performance.now(),e=1400,s=900,c=1800,r=o.k,a=Math.min(1.2,r*1.45),n=E.getBoundingClientRect();let l=1/0,u=-1/0,f=1/0,m=-1/0;for(let p=0;p<i.n;p++){const $=i.x[p],D=i.y[p];$<l&&(l=$),$>u&&(u=$),D<f&&(f=D),D>m&&(m=D)}const v=(l+u)/2,w=(f+m)/2;o.k=a,o.tx=n.width/(2*a)-v,o.ty=n.height/(2*a)-w,A.setView(o);const d=new Float32Array(i.n),g=new Map;for(let p=0;p<i.n;p++){const $=i.community[p];g.has($)||g.set($,[]),g.get($).push(p)}const M=[...g.keys()].sort((p,$)=>g.get($).length-g.get(p).length),y=e*.6;M.forEach((p,$)=>{const D=g.get(p).sort((_,it)=>i.degree[it]-i.degree[_]),bt=$/M.length*(y*.6);D.forEach((_,it)=>{const At=it/Math.max(1,D.length-1)*(y*.4);d[_]=bt+At})});const C=p=>1-Math.pow(1-p,3),L=p=>p<.5?2*p*p:1-Math.pow(-2*p+2,3)/2;function st(){const p=performance.now()-t,$=520;for(let H=0;H<i.n;H++){const jt=p-d[H];if(jt<=0){i.alpha[H]=0;continue}const ye=Math.min(1,jt/$);i.alpha[H]=C(ye)}const D=Math.min(1,Math.max(0,(p-(e-s))/s));i.edgeAlpha=C(D);const bt=Math.min(1,p/c),_=a+(r-a)*L(bt),it=n.width/(2*_)-v,At=n.height/(2*_)-w;if(o.k=_,o.tx=it,o.ty=At,A.setView(o),Y.textContent=Math.round(_*100)+"%",p<Math.max(e,c)+60)requestAnimationFrame(st);else{for(let H=0;H<i.n;H++)i.alpha[H]=1;i.edgeAlpha=1,o.k=r,o.tx=n.width/(2*r)-v,o.ty=n.height/(2*r)-w,A.setView(o),Y.textContent=Math.round(r*100)+"%",i.freeze()}}requestAnimationFrame(st)}function wt(t=!0){const e=E.getBoundingClientRect();if(e.width===0||e.height===0||!i)return;let s=1/0,c=-1/0,r=1/0,a=-1/0;for(let M=0;M<i.n;M++){const y=i.x[M],C=i.y[M],L=i.radius[M];y-L<s&&(s=y-L),y+L>c&&(c=y+L),C-L<r&&(r=C-L),C+L>a&&(a=C+L)}if(!isFinite(s))return;const n=80,l=Math.max(1,c-s),u=Math.max(1,a-r),f=Math.min((e.width-n*2)/l,(e.height-n*2)/u),m=Math.max(.22,Math.min(1.2,f)),v=(s+c)/2,w=(r+a)/2,d=e.width/(2*m)-v,g=e.height/(2*m)-w;o.tx=d,o.ty=g,o.k=m,A.setView(o),t&&(Y.textContent=Math.round(m*100)+"%")}let kt="",le=0;const Q=new Set,tt=new Map;function de(){if(!i)return;const t=le++%4===0,e=E.getBoundingClientRect(),s=o.k,c=i.n,r=et.hop1;let a;if(t){const l=new Set;if(x!=null){const d=i.idxOf[x];d!=null&&l.add(d);for(const g of r)i.idxOf[g]!=null&&l.add(i.idxOf[g])}W>=0&&l.add(W);const u=s<.3?30:s<.6?14:s<1?7:3;for(let d=0;d<c;d++)i.degree[d]>=u&&l.add(d);for(const d of Q)l.add(d);const f=[];for(const d of l){if(d==null||d<0)continue;const g=i.slugOf[d],M=R[g];if(!M||N!=="all"&&M.type!==N&&d!==i.idxOf[x])continue;const y=(i.x[d]+o.tx)*s,C=(i.y[d]+o.ty)*s;y<-60||y>e.width+60||C<-40||C>e.height+40||f.push({i:d,sx:y,sy:C,r:i.radius[d]*s,title:M.title||g})}const m=d=>{if(d.i===i.idxOf[x])return 1e4;if(d.i===W)return 9e3;if(r.has(i.slugOf[d.i]))return 5e3+i.degree[d.i];const g=Q.has(d.i)?2e3+Math.min(500,(tt.get(d.i)||0)*20):0;return i.degree[d.i]+g};f.sort((d,g)=>m(g)-m(d));const v=[],w=[];for(const d of f){const g=Math.min(140,6.5*d.title.length+10),y={x:d.sx-g/2,y:d.sy+d.r+4,w:g,h:16};let C=!1;for(const L of v)if(y.x<L.x+L.w&&y.x+y.w>L.x&&y.y<L.y+L.h&&y.y+y.h>L.y){C=!0;break}if(!C&&(v.push(y),w.push({...d,box:y}),w.length>80))break}a=w,Q.clear();for(const d of a)Q.add(d.i),tt.set(d.i,(tt.get(d.i)||0)+1);for(const[d]of tt)Q.has(d)||tt.delete(d);Nt=a}else{a=[];for(const l of Nt){const u=l.i,f=i.slugOf[u],m=R[f];if(!m)continue;const v=(i.x[u]+o.tx)*s,w=(i.y[u]+o.ty)*s;if(v<-60||v>e.width+60||w<-40||w>e.height+40)continue;const d=m.title||f,g=Math.min(140,6.5*d.length+10);a.push({i:u,sx:v,sy:w,r:i.radius[u]*s,title:d,box:{x:v-g/2,y:w+i.radius[u]*s+4,w:g,h:16}})}}let n="";for(const l of a){const u=i.slugOf[l.i],f=u===x,m=r.has(u);n+=`<div class="${f?"dirA-lbl active":m?"dirA-lbl hop1":"dirA-lbl"}" style="left:${l.box.x.toFixed(1)}px;top:${l.box.y.toFixed(1)}px;width:${l.box.w.toFixed(0)}px">${b(l.title)}</div>`}n!==kt&&(Pt.innerHTML=n,kt=n)}let Nt=[],et={hop1:new Set,hop2:new Set};function ue(t){const e=new Set(z[t]||[]),s=new Set;for(const c of e)for(const r of z[c]||[])r!==t&&!e.has(r)&&s.add(r);return{hop1:e,hop2:s}}function Ot(){!i||!A||(i.setActive(x),A.setHops(et))}let k=null;E.addEventListener("mousedown",t=>{if(!i)return;const e=E.getBoundingClientRect(),s=t.clientX-e.left,c=t.clientY-e.top,r=s/o.k-o.tx,a=c/o.k-o.ty,n=i.pickNode(r,a,14/o.k);n>=0?k={kind:"node",idx:n,moved:!1,x0:t.clientX,y0:t.clientY}:k={kind:"pan",x0:t.clientX,y0:t.clientY,tx:o.tx,ty:o.ty}}),window.addEventListener("mousemove",t=>{if(!i)return;const e=E.getBoundingClientRect(),s=t.clientX-e.left,c=t.clientY-e.top;if((s<0||c<0||s>e.width||c>e.height)&&!k)return;const r=s/o.k-o.tx,a=c/o.k-o.ty;if(i.clearHover(),!k){const n=i.pickNode(r,a,14/o.k);W=n,A.setHovered(n),E.style.cursor=n>=0?"pointer":"grab";return}if(k.kind==="pan")o.tx=k.tx+(t.clientX-k.x0)/o.k,o.ty=k.ty+(t.clientY-k.y0)/o.k,A.setView(o),E.classList.add("dragging");else if(k.kind==="node"){const n=t.clientX-k.x0,l=t.clientY-k.y0;!k.moved&&n*n+l*l>9&&(k.moved=!0),k.moved&&(i.pinnedIdx=k.idx,i.pinX=r,i.pinY=a,i.cooling=Math.max(i.cooling,.8),E.classList.add("dragging"))}}),window.addEventListener("mouseup",()=>{if(i){if(k&&k.kind==="node"&&!k.moved){const t=i.slugOf[k.idx];if(t){const e=R[t];(N==="all"||!e||e.type===N)&&F(t)}}i.pinnedIdx=-1,k=null,E.classList.remove("dragging")}}),E.addEventListener("mouseleave",()=>{i&&(i.clearHover(),W=-1,A.setHovered(-1))}),E.addEventListener("wheel",t=>{if(!i)return;t.preventDefault();const e=E.getBoundingClientRect(),s=t.clientX-e.left,c=t.clientY-e.top,r=-t.deltaY*.0015,a=Math.max(.15,Math.min(3,o.k*(1+r))),n=s/o.k-o.tx,l=c/o.k-o.ty;o.k=a,o.tx=s/a-n,o.ty=c/a-l,A.setView(o),Y.textContent=Math.round(a*100)+"%"},{passive:!1});function Ft(t){if(!i)return;const e=E.getBoundingClientRect(),s=e.width/2,c=e.height/2,r=Math.max(.15,Math.min(3,o.k*t)),a=s/o.k-o.tx,n=c/o.k-o.ty;o.k=r,o.tx=s/r-a,o.ty=c/r-n,A.setView(o),Y.textContent=Math.round(r*100)+"%"}h("zoom-in").addEventListener("click",()=>Ft(1.2)),h("zoom-out").addEventListener("click",()=>Ft(1/1.2)),h("zoom-reset").addEventListener("click",()=>wt(!0));async function F(t){if(!(!t||!R[t])){x=t,et=ue(t),Ot(),X=X[X.length-1]===t?X:[...X,t].slice(-6),i&&(i.cooling=Math.min(1,i.cooling+.15)),lt(),$t.hidden=!0,fe();try{const e=ut.get(t)||await _t(t);if(x!==t)return;Et=e,he(e)}catch(e){console.error("[granite] fetch note failed",t,e)}}}async function _t(t){const e=await at("/api/notes/"+encodeURIComponent(t));if(!e.ok)throw new Error("fetch "+t+": "+e.status);const s=await e.json();return ut.set(t,s),s}let nt=null;function fe(){if(!i||!x)return;const t=i.idxOf[x];if(t==null||t<0)return;const e=E.getBoundingClientRect();if(e.width===0)return;const s=ft??Math.min(900,window.innerWidth*.62),a=Math.max(280,e.width-s-24)/2,n=e.height/2,l=Math.max(o.k,.55),u=performance.now(),f=520,m=o.tx,v=o.ty,w=o.k;nt&&cancelAnimationFrame(nt);const d=M=>1-Math.pow(1-M,3);function g(){const M=Math.min(1,(performance.now()-u)/f),y=d(M),C=i.x[t],L=i.y[t],st=a/l-C,p=n/l-L;o.tx=m+(st-m)*y,o.ty=v+(p-v)*y,o.k=w+(l-w)*y,A.setView(o),Y.textContent=Math.round(o.k*100)+"%",M<1?nt=requestAnimationFrame(g):(o.tx=st,o.ty=p,o.k=l,A.setView(o),nt=null)}nt=requestAnimationFrame(g)}function Bt(){x=null,Et=null,et={hop1:new Set,hop2:new Set},Ot(),U.hidden=!0,$t.hidden=!1,lt()}function he(t){U.hidden=!1;const e=Z(t.type),s=mt(t.body),c=St(t.body),r=Dt(t.body),a=X.map(f=>R[f]).filter(Boolean);ot.innerHTML="";const n=document.createElementNS("http://www.w3.org/2000/svg","svg");n.setAttribute("width","12"),n.setAttribute("height","12"),n.setAttribute("viewBox","0 0 24 24"),n.setAttribute("fill","none"),n.setAttribute("stroke","currentColor"),n.setAttribute("stroke-width","1.5"),n.setAttribute("stroke-linecap","round"),n.setAttribute("stroke-linejoin","round"),n.innerHTML='<path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z"/><path d="M14 3v5h5"/>',ot.appendChild(n),a.forEach((f,m)=>{if(m>0){const w=document.createElement("span");w.className="sep",w.textContent="\u203A",ot.appendChild(w)}const v=document.createElement("span");v.className="crumb"+(m===a.length-1?" current":""),v.title=f.title||f.slug,v.textContent=f.title||f.slug,m!==a.length-1&&v.addEventListener("click",()=>F(f.slug)),ot.appendChild(v)});const l=[`<span class="dirA-reader-type" style="background:${e.soft};color:${e.hex}">${b(t.type||"note")}</span>`];t.modified&&l.push(`<span>${b(ne(t.modified))}</span>`),l.push(`<span>\xB7 ${c} min read</span>`),l.push(`<span>\xB7 ${s} words</span>`),Jt.innerHTML=l.join(""),Zt.textContent=t.title||t.slug,Qt.textContent=r;const u=[`<span class="slug">${b(t.slug)}</span>`];for(const f of t.tags||[])u.push(`<span class="tag">#${b(f)}</span>`);te.innerHTML=u.join(""),rt.innerHTML=window.MarkdownRenderer.render(t.body||"",t.outgoing_links||[]),pe(t),J.scrollTop=0,ht=0,Rt.style.width="0%"}function pe(t){const e=[],s=(t.outgoing_links||[]).filter(n=>n.resolved&&n.resolved_slug),c=new Set,r=[];for(const n of s)n.resolved_slug!==t.slug&&(c.has(n.resolved_slug)||(c.add(n.resolved_slug),r.push(n.resolved_slug)));if(r.length){e.push(`<div><div class="dirA-links-section-label">Links out <span class="count">${r.length}</span></div><div class="dirA-links-list">`);for(const n of r){const l=R[n]||{title:n,slug:n,type:"note"},u=Z(l.type);e.push(`<button class="dirA-linkcard" data-nav="${b(n)}"><span class="dirA-linkcard-dot" style="background:${u.hex}"></span><div><div class="dirA-linkcard-title">${b(l.title||n)}</div><div class="dirA-linkcard-deck">${b(l.deck||"")}</div></div><span class="dirA-linkcard-arrow">\u2192</span></button>`)}e.push("</div></div>")}const a=t.backlinks||[];if(a.length){const n=new Set,l=[];for(const u of a){const f=u.source_slug||u.slug;if(!f||n.has(f))continue;n.add(f);const m=R[f],v=m?.title||u.source_title||f,w=m?.type||"note";l.push({slug:f,title:v,type:w,context:u.context||""})}e.push(`<div><div class="dirA-links-section-label">Backlinks <span class="count">${l.length}</span></div><div class="dirA-links-list">`);for(const u of l){const f=Z(u.type);e.push(`<button class="dirA-linkcard" data-nav="${b(u.slug)}"><span class="dirA-linkcard-dot" style="background:${f.hex}"></span><div><div class="dirA-linkcard-title">${b(u.title)}</div><div class="dirA-linkcard-deck">${b(u.context)}</div></div><span class="dirA-linkcard-arrow">\u2190</span></button>`)}e.push("</div></div>")}Tt.innerHTML=e.join("")}Tt.addEventListener("click",t=>{const e=t.target.closest("[data-nav]");e&&F(e.getAttribute("data-nav"))}),J.addEventListener("scroll",()=>{const t=J.scrollHeight-J.clientHeight;ht=t>0?Math.min(1,J.scrollTop/t):0,Rt.style.width=ht*100+"%"}),ee.addEventListener("mousedown",t=>{t.preventDefault(),U.classList.add("resizing");const e=t.clientX,s=ft??Math.min(900,window.innerWidth*.62);function c(a){const n=e-a.clientX,l=Math.max(480,Math.min(window.innerWidth-80,s+n));ft=l,U.style.setProperty("--reader-w",l+"px")}function r(){U.classList.remove("resizing"),window.removeEventListener("mousemove",c),window.removeEventListener("mouseup",r)}window.addEventListener("mousemove",c),window.addEventListener("mouseup",r)}),h("reader-close").addEventListener("click",Bt),h("reader-prev").addEventListener("click",()=>ct(-1)),h("reader-next").addEventListener("click",()=>ct(1));function ct(t){if(!x){if(!i)return;let r=0;for(let n=1;n<i.n;n++)i.degree[n]>i.degree[r]&&(r=n);const a=i.slugOf[r];a&&F(a);return}const e=[x,...et.hop1].filter(r=>R[r]);if(e.length<2)return;const s=e.indexOf(x),c=e[((s+t)%e.length+e.length)%e.length];c&&F(c)}rt.addEventListener("click",t=>{const e=t.target.closest("a.wikilink");if(!e)return;t.preventDefault();const s=e.getAttribute("data-slug");s&&F(s)});let xt=0;rt.addEventListener("mouseover",async t=>{const e=t.target.closest("a.wikilink");if(!e)return;const s=e.getAttribute("data-slug");if(!s)return;const c=e.getBoundingClientRect(),r=++xt;try{const a=ut.get(s)||await _t(s);if(r!==xt||!document.contains(e))return;ge(a,c)}catch{}}),rt.addEventListener("mouseout",t=>{t.target.closest("a.wikilink")&&(xt++,lt())});function ge(t,e){const s=Z(t.type),c=Dt(t.body),r=mt(t.body),a=St(t.body);j.innerHTML=`
3
3
  <div class="wl-preview-header">
4
4
  <span class="wl-preview-type" style="background:${s.soft};color:${s.hex}">${b(t.type||"note")}</span>
5
5
  <span class="wl-preview-meta">${a}min \xB7 ${r} words</span>
@@ -1 +1,5 @@
1
- "use strict";(()=>{})();
1
+ "use strict";(()=>{window.MarkdownRenderer=(()=>{function p(n,d){const h=/(\[\[([^\]]+)\]\])|(`([^`]+)`)|(\*\*(.+?)\*\*)|(\*(.+?)\*)|(\[([^\]]+)\]\(([^)]+)\))|(!\[([^\]]*)\]\(([^)]+)\))/g;let t="",i=0,e;for(;(e=h.exec(n))!==null;){if(e.index>i&&(t+=r(n.slice(i,e.index))),e[1]){const s=e[2].split("|"),c=s[0].trim(),m=s.length>1?s[1].trim():c,o=d?.find(g=>g.target===c),f=o?.resolved??!1,l=o?.resolved_slug||null;f&&l?t+=`<a class="wikilink" data-slug="${b(l)}" href="#" role="link">${r(m)}</a>`:t+=`<span class="wikilink-broken">${r(m)}</span>`}else if(e[3])t+=`<code class="inline-code">${r(e[4])}</code>`;else if(e[5])t+=`<strong>${r(e[6])}</strong>`;else if(e[7])t+=`<em>${r(e[8])}</em>`;else if(e[9])t+=`<a class="external-link" href="${b(e[11])}" target="_blank" rel="noopener">${r(e[10])}</a>`;else if(e[12]){const a=e[13],s=e[14];let c=s.startsWith("http")?s:`/assets/${s.replace(/^\.?\/?assets\//,"")}`;!s.startsWith("http")&&window.GRANITE_INSTANCE&&(c+=(c.includes("?")?"&":"?")+"instance="+encodeURIComponent(window.GRANITE_INSTANCE)),t+=`<img class="md-image" src="${b(c)}" alt="${b(a)}" loading="lazy">`}i=e.index+e[0].length}return i<n.length&&(t+=r(n.slice(i))),t}function v(n,d){const h=n.split(`
2
+ `),t=[];let i=!1,e=[],a="",s=!1,c=null,m=[];function o(){if(m.length>0){const f=c==="ol"?"ol":"ul";t.push(`<${f} class="md-list">${m.join("")}</${f}>`),m=[],s=!1,c=null}}for(let f=0;f<h.length;f++){const l=h[f];if(l.startsWith("```")){if(i){const u=a?`<span class="code-lang">${r(a)}</span>`:"";t.push(`<div class="code-block">${u}<pre><code>${r(e.join(`
3
+ `))}</code></pre></div>`),e=[],i=!1}else o(),i=!0,a=l.slice(3).trim();continue}if(i){e.push(l);continue}if(l.trim()===""){o();continue}if(l.trim().startsWith("|")&&l.trim().endsWith("|")){o();const u=[];let $=f;for(;$<h.length&&h[$].trim().startsWith("|")&&h[$].trim().endsWith("|");)u.push(h[$]),$++;if(u.length>=2){t.push(I(u,d)),f=$-1;continue}}const g=l.match(/^(#{1,3})\s+(.+)/);if(g){o();const u=g[1].length;t.push(`<h${u} class="md-h${u}">${p(g[2],d)}</h${u}>`);continue}if(/^(-{3,}|\*{3,}|_{3,})$/.test(l.trim())){o(),t.push('<hr class="md-hr">');continue}if(l.startsWith(">")){o();const u=l.replace(/^>\s?/,"");t.push(`<blockquote class="md-blockquote">${p(u,d)}</blockquote>`);continue}const w=l.match(/^(\s*)[*\-+]\s+(.+)/);if(w){c!=="ul"&&o(),s=!0,c="ul",m.push(`<li>${p(w[2],d)}</li>`);continue}const k=l.match(/^(\s*)\d+\.\s+(.+)/);if(k){c!=="ol"&&o(),s=!0,c="ol",m.push(`<li>${p(k[2],d)}</li>`);continue}o(),t.push(`<p class="md-body">${p(l,d)}</p>`)}return o(),i&&e.length>0&&t.push(`<div class="code-block"><pre><code>${r(e.join(`
4
+ `))}</code></pre></div>`),t.join(`
5
+ `)}function I(n,d){const h=a=>a.trim().replace(/^\||\|$/g,"").split("|").map(s=>s.trim()),t=h(n[0]),i=n.slice(2).map(h);let e='<div class="md-table-wrap"><table class="md-table"><thead><tr>';for(const a of t)e+=`<th>${p(a,d)}</th>`;e+="</tr></thead><tbody>";for(const a of i){e+="<tr>";for(const s of a)e+=`<td>${p(s,d)}</td>`;e+="</tr>"}return e+="</tbody></table></div>",e}function r(n){return n.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function b(n){return n.replace(/"/g,"&quot;").replace(/'/g,"&#39;")}return{render:v,parseInline:p,escapeHtml:r}})();})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "granite-mem",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "Local-first markdown memory system for humans and AI agents: MCP server, typed knowledge graph, deterministic garden planning.",
5
5
  "type": "module",
6
6
  "bin": {