Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

12 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

second-brain

Turns raw daily notes into a structured, cited wiki — on Fangorn, sealed, with a local model. Nothing leaves the machine except ciphertext.

raw namespace                          wiki namespace
(written from drive)                   (written only by this script)

readNamespace ──▶ unseal ──▶ llama3.2:3b ──▶ seal ──▶ commit + push
                                                      notes, days, concepts, index

Why it reads from the chain

The drive MCP would hand back the same note text, and with less ceremony. Two things it can't give you:

  • CIDs. A vertex CID is the only immutable, citable reference to the exact bytes a page was distilled from. read_note returns a string; read_published has a CID but routes through the public endpoint, so private notes come back as a placeholder. Provenance has no other source.
  • A snapshot. The relay's working tree changes under you mid-run. A published vertex is what it was when you signed for it.

Setup

You need ollama running locally and a Fangorn wallet.

pnpm install
ollama pull llama3.2:3b
cat > .env <<'EOF'
AGENT_PRIVATE_KEY=0x...   # this agent's own wallet — generate a fresh one
OWNER_ADDRESS=0x...       # you: the wallet that owns the raw namespace
READ_SECRET=0x...         # from drive's agent panel; opens sealed notes
EOF
pnpm start

The agent never gets your private key. Three credentials, and only the middle one is yours:

  • AGENT_PRIVATE_KEY — the agent's own wallet. It publishes the wiki and pays the gas. Generate a fresh one; fund it with a little Arbitrum Sepolia ETH. The script registers it as a publisher on first run (free).
  • OWNER_ADDRESS — your address, so the agent knows whose raw notes to read. Public information.
  • READ_SECRET — from drive's agent panel: tick "also hand over the decryption key" when minting a token, and it's shown once, next to the token. It is not a wallet key: it decrypts, and that is all — it cannot spend, publish, or sign. It is bound to one namespace, and it cannot be revoked — re-keying means re-sealing every note.

If you run the agent as yourself, omit READ_SECRET and the script derives it from AGENT_PRIVATE_KEY — the single-wallet setup, where the agent's wallet is your drive login wallet. (Privy embedded wallets never expose a private key, so that mode needs external-wallet login; the three-credential setup above does not.)

env default
AGENT_PRIVATE_KEY required; the agent's own wallet
OWNER_ADDRESS required; owner of the raw namespace
READ_SECRET derived from the agent key decrypt-only credential
RAW_NAMESPACE second brain where you author
WIKI_NAMESPACE second brain wiki written only by this script
OLLAMA_MODEL llama3.2:3b any local model that honours a JSON schema
OLLAMA_HOST http://127.0.0.1:11434
PINATA_GATEWAY optional read gateway

node index.js --selfcheck runs the pure parts — date extraction, slugs, the raw/derived split, concept aliasing, wikilink targets, seal/unseal binding. No network, no model.

node --env-file=.env index.js --dry runs the whole pipeline and writes the rendered pages to ./out instead of publishing. Prompt tuning is a dozen iterations of reading the pages; none of them should cost a tx.

What gets published

A walkable tree, not a pile of pages:

index.md            front page, laid out like Wikipedia's
├── notes.md        every note, grouped by kind
│   └── <note>.md   one page per source note
├── days.md         the timeline
│   └── day-*.md    a date, its notes, their key points
└── concepts.md     hubs, then the mentioned-once vocabulary
    └── concept-*.md  a concept, its gloss, and every note it appears in

Note pages are the only ones the model writes: title, kind, one-sentence summary, key points, concepts with a gloss each, open questions, and the source CID they were distilled from. Everything else is derived — no model, so identical mentions produce identical bytes.

index.md borrows Wikipedia's front page: a featured note (the one sharing the most concepts with the rest — its own words, not a summary of a summary), did you know (one fact per note, so it's a cross-section rather than the same note five times), open questions (the loose ends, which nothing else surfaces), a browse table, recently, and how it connects with the mermaid map.

Every pick is deterministic — most-connected first, ties broken by path. Wikipedia rotates its front page because it has editors and a clock; a random or dated pick would rewrite this page and its CID on every run while saying nothing new.

The three section pages exist as pages rather than as headings on one long index because they double as the folders in .tree.json: drive nests its sidebar under a real note, so a browsable tree and a readable table of contents are the same three files.

.tree.json is the one vertex published in the clear (tag meta, payload {path, content, updatedAt} — exactly what drive writes). The browser has to draw the sidebar before it holds any key, and structure was never the secret: filenames are already clear on every page. Without it a namespace is sixty loose pages in no order, all cross-linked and none of it walkable.

One page per note, not per day: a day bucket blends a trail run, a standup and a paper into one flat summary, while a note keeps its own voice, cites exactly one CID, and re-distills on its own when only it changed. Days and concepts fall out of the note pages for free.

Page vertices are tagged doc and .tree.json is tagged meta, matching drive exactly — the tag becomes the vertex's schemaId, and it is how drive tells a page from the structure that holds it.

Page payloads are { path, updatedAt, enc, data?, source? }. Clear: identity, ordering, provenance. Sealed: the prose, plus (on note pages) the structured data it was rendered from, so a cached note still feeds the derived pages with no model call. That's the same shape drive publishes, with the same resourceId = keccak256("<namespace>:<path>"), so the body is the rendered page — there is no separate output layer to keep in sync.

The wiki is sealed under its own namespace-bound key, so in the single-wallet setup drive derives it the same way the browser does. With a separate agent wallet you can't reproduce that derivation, so a handed-over READ_SECRET takes precedence: it's the one credential both sides hold.

Note that drive can't render the sealed wiki today — the browser only ever seals, it has no unseal-on-read path, and the namespace belongs to the agent rather than to you. The tree will draw; the bodies won't open. Reading them back is --dry's job for now.

Design notes

One writer per namespace. Drive owns raw; this script owns wiki. Two writers moving the same namespace head contend for the same compare-and-swap, and the loser's publish reverts.

The wiki belongs to the agent's address. commitStateRoot keys a namespace by msg_sender and the registry has no delegation primitive — no addWriter, no operator approval — so whoever publishes is the publisher. That's fine here: the wiki is derived, so losing the agent key costs you an address, not knowledge; re-run with a new agent and it rebuilds from raw. It also means the publisher address records which agent produced a page, which is the thing you want once more than one of them is writing.

Never distill your own output. Two ways that happens, both guarded. Same namespace name under the same wallet is the same timeline, so a replace: true publish would land the wiki on top of the notes it came from — the script refuses to start. And a raw namespace that already holds a previous agent's pages (under a wiki/ folder, or as orphans outside the tree) filters them out via .tree.json, which is the only thing that knows the folder a flat vertex path sits in.

Incremental by source CID and prompt. A note page records the content address of the note it came from, plus distilledBy (<model>/v<n>) — because editing the prompt doesn't move a source CID, and without that second key every prompt improvement would apply to new notes only, leaving the wiki a mix of two eras. Bump PROMPT_VERSION when SYSTEM or SCHEMA changes. Unchanged input → the prior payload is re-staged verbatim → identical bytes → identical CID → free in storage, invisible in the diff, and no model call. Derived pages compare their rendered markdown against the previous run and reuse the payload when it matches. Run time tracks what changed, not how long you've been journaling.

Failures skip, they don't stub. A 3B model gets JSON wrong sometimes. The schema is passed as a grammar constraint rather than parsed out of a fenced block, and when a note still fails it is left out so the next run retries it. A placeholder page is a lie that looks finished.

One concept, one page. A small model spells the same thing two ways across notes — SSE here, Server-Sent Events (SSE) there. A parenthetical is the model naming both forms, so it seeds an alias table every renderer resolves through: the note page and the concept page can't disagree about the slug, which is what a dead wikilink is made of.

Provenance is a payload field, not an edge. Edges may only reference vertices staged in the same commit, so they cannot point from wiki into raw. Cross-namespace citation lives in source: {path, cid}. Edges within the commit (note --mentions--> concept, day --on--> note) are staged alongside the wikilinks that mirror them.

Snapshot commits, incremental content. Each run stages the whole wiki with replace: true, so current contents are exactly what the last run produced — and a note deleted from raw disappears from the wiki, rather than lingering as an orphan. Staging everything is not the same as rewriting everything: an untouched page is re-staged as the byte-identical prior payload, which hashes to the same vertex CID and never enters the commit's diff. History is not lost — it's the commit chain, walkable with fangorn.log().

Page paths don't depend on their neighbours. A note's page is named from its own source path, and contested slugs (two notes slugging the same, or a note slugging onto index.md) are settled by a hash of that same source path. If arrival order decided it instead, adding a note that sorts earlier would rename a page that was already published and break every link into it.

Each run prints what it cost:

65 pages: 58 unchanged, 7 rewritten · 1 model call(s)

Selling it later

Bodies are sealed self-hkdf-v1 (your key alone). The paid path is worker-usdc-v1: sealed to the access worker, released after SettlementRegistry.isSettled(buyer, resourceId) passes. resourceId is already one-per-page, which is the unit of sale.

Deferred because it needs the access worker's URL, static X25519 pubkey and upload token, and nothing is priced yet. It costs nothing to defer: the plaintext was never public, so switching is a re-seal under the same resourceId — no leak to undo, no change to the graph.

Publishing a page in the clear is the one-way door. Anything public is content-addressed and permanent; it can never become paid. Sealing everything now and opening summaries later is the reversible direction.

Troubleshooting

Ollama refusing connections on Linux — try disabling ipv6:

sudo sysctl -w net.ipv6.conf.all.disable_ipv6=1
sudo sysctl -w net.ipv6.conf.default.disable_ipv6=1

If a run fails inside storage or the metagraph engine, some native deps were skipped at install: pnpm approve-builds.

index.py is the previous version — same idea, over the drive MCP, without provenance or incrementality. Kept for reference; delete it once this has run.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages