Maintained for context-window resilience. This file is the single source of truth for where the project is, what's been built, what's broken, and what's next. Read this first when picking up after a long break or in a fresh chat session.
Last updated: 2026-05-24 (post-v1.0 sprint, latest: 10-item V1.x sprint — quick wins + all six medium items). Forward-looking work-needed view: docs/14-roadmap.md.
Open-source local-first knowledge base implementing Andrej Karpathy's LLM Wiki pattern (gist: https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f).
- One folder, three layers: raw sources → LLM-maintained wiki → user's CLAUDE.md schema.
- Three operations: ingest, query, lint.
- Everything is a file: pages, chats, schema, log all
.mdin a folder the user owns. - Local-first, BYOK: no telemetry, no remote storage, OpenRouter API key in OS keychain.
Repo: https://github.com/ddsyasas/llm-wiki (public, MIT).
~70 commits on main, tagged v1.0.0. Build plan (docs/10) steps 0–14 complete plus full P0 + all 4 P1 items (graph view, diff view, approval gate, export-to-zip) + P2 multi-wiki UI. Post-V1 sprint added: multi-wiki switcher + Settings tab + header chip + Cmd+K integration, first-run welcome wizard, asymmetric chat layout, source-lineage UI, lint quick-fixes wave, /log + /graph routes, /about + /help + /developers doc pages, defensive JSON parse, GitHub SEO topics applied (18), consolidated roadmap doc, sources retry/delete actions, wiki page delete with soft-trash + Undo banner, 10-item V1.x sprint covering all roadmap quick wins + medium items (mobile chip, replay tour, footer hint, setup gate, Cmd+K theme/copy/cross-wiki, wiki templates, export zip, approval gate, per-page diff view).
For the what's-next view see docs/14-roadmap.md. This file is the execution history (what was done + why), not the forward plan.
HEAD Doc pages: About, Help, Developers + footer links + dev-log update
4a987ca Surface source lineage: Sources section on wiki pages + /sources/[id] detail
9a010f7 Teach tailwind-merge about our custom text-* font sizes
39d5861 Loading skeletons + click-state on clickable cards
5ce3780 Redesign /wiki landing — card grid grouped by type
f5e2b4e Add /log page — in-browser view of log.md
0927a61 Show the actual log.md path on the Lint page
48f64d1 Lint history: always-visible Recent runs panel on /lint
e34575a Lint history: append summary to log.md, show previous-run delta
f206195 Add docs/dev-setup.md — run, stop, recover from stuck ports
67530dc Widen split-pane editors + fill viewport height
b3a9261 Refresh index entry on every page edit + create
46f07a3 Fix Apply-suggested-fix routing + surface no-op when LLM punts
5e59b69 Lint quick-fixes: rebuild index, fix all broken links, create stub, apply suggested fix
c776f7c Defensive JSON parse: strip markdown fences + prose before JSON.parse
61306b0 Add dedicated chat model slot; fix hardcoded retired-slug fallback
5f43e51 Update model slugs to Claude 4.x; replace text-input with select dropdown
187ecb6 Karpathy-pattern audit: first-run gate, sources list, chat-as-source ingest
914316f Add docs/dev-log.md — project state for context-window resilience
… (steps 0–14 + design passes — see git log for the full history)
Test suite: ~125+ core + 17 llm + 11 ingestion = 153+ passing, 1 smoke skipped (no API key), 1 chokidar flake (not blocking).
llm-wiki/
├── apps/web/ Next.js 14 App Router app + CLI
│ ├── bin/llm-wiki.mjs Plain-JS CLI entry (no compile step)
│ ├── scripts/copy-standalone-assets.mjs
│ ├── next.config.mjs output: standalone, externals, transpile
│ └── src/
│ ├── app/ route handlers + page.tsx files
│ │ ├── (root layout, page.tsx home)
│ │ ├── wiki/ has nested layout (sidebar)
│ │ ├── chats/ has nested layout (sidebar)
│ │ ├── sources/ query/ lint/ schema/ settings/ (no sidebar)
│ │ └── api/ REST endpoints
│ ├── components/
│ │ ├── app-shell.tsx passthrough: header + main + footer
│ │ ├── app-header.tsx wordmark + nav + Cmd+K hint + theme toggle
│ │ ├── footer.tsx
│ │ ├── theme-provider.tsx + theme-toggle.tsx
│ │ ├── command-palette.tsx ⌘K modal
│ │ ├── cost-preview.tsx
│ │ ├── page-shell.tsx PageContainer / PageHeader / Card primitives
│ │ ├── wiki/ sidebar, markdown-view, page-editor, page-view
│ │ ├── chats/ sidebar, chat-view, promote-message
│ │ ├── settings/ general / models / api / costs / about tabs
│ │ └── ui/ shadcn (Button, Input, Textarea)
│ └── lib/ server-config, server-wiki, cost-estimate,
│ use-wiki-settings, utils
└── packages/
├── core/ ingest/query/lint primitives + file I/O
│ └── src/
│ ├── ingest.ts query.ts lint.ts chat.ts editor.ts
│ ├── wiki.ts initWikiFolder, readPage, writePage, etc.
│ ├── db.ts db-pages.ts db-sources.ts db-chats.ts db-usage.ts
│ │ db-sync.ts better-sqlite3 + 7 tables (FTS5 included)
│ ├── sync.ts chokidar live watch + syncWikiToDb
│ ├── config.ts secrets.ts global + per-wiki config; keytar w/
│ │ file fallback
│ ├── links.ts extractWikiLinks, findBacklinks
│ ├── schema.ts zod IngestResponse / Query / Lint
│ ├── prompts/ ingest / query / chat / lint builders
│ ├── templates.ts default CLAUDE.md / index.md / log.md
│ └── types.ts PageType, PageRow, ChatRow, etc.
├── ingestion/ source format parsers
│ └── src/
│ ├── detect.ts extension + magic-byte sniff
│ ├── plain.ts markdown.ts
│ ├── html.ts Readability + jsdom + turndown
│ ├── url.ts fetch + extractHtml
│ ├── docx.ts mammoth + turndown
│ ├── pptx.ts xlsx.ts officeparser
│ ├── pdf.ts image.ts vision pass-through (base64)
│ └── types.ts
└── llm/ OpenRouter client wrapper
└── src/
├── client.ts createClient, callLLM, chatComplete
├── models.ts DEFAULT_MODELS, SUGGESTED_MODELS, PRICING
└── errors.ts typed error hierarchy
User's wiki folder (default ~/llm-wiki-default):
<wiki>/
├── CLAUDE.md schema (editable; the LLM's contract)
├── index.md auto-maintained catalog
├── log.md append-only event log
├── .gitignore auto-generated, ignores .llm-wiki/
├── raw/ original source files (immutable)
├── wiki/ LLM-maintained .md pages with YAML frontmatter
├── chats/{inbox,pinned,archive}/ thread .md files
└── .llm-wiki/
├── meta.sqlite pages/sources/chats/usage + FTS5 index
├── settings.json per-wiki config
├── page-history/ edit backups
├── schema-history/ schema edit backups (last 10)
└── trash/chats/ deleted chats, 30-day TTL
Global config: ~/.llm-wiki/config.json (recentWikis, uiTheme; openrouterKey only when keychain unavailable).
OS keychain entry: service llm-wiki, account openrouter.
-
No standalone publishable tarball yet. Step 13 produced a CLI that works in dev. Step 15 (publish to npm) is deferred — packaging workspace deps + the standalone server 500 issue both need attention.
-
Plain JS CLI.
apps/web/bin/llm-wiki.mjsis pure ESM that inlines initWikiFolder + config helpers rather than importing from@llm-wiki/core(which uses TS-source extensionless imports that plain Node ESM can't resolve). Trade-off: small code duplication; benefit: no compile step. -
Sidebar layout uses body h-screen + main overflow-y-auto. Previous attempts with
min-h-screenandh-[calc(...)]had the sidebar collapsing to its content height. Locking body to viewport and using flex stretch defaults fixes this cleanly. Seeapps/web/src/app/layout.tsxandapp-shell.tsx. -
JSON schema example embedded in prompts. Haiku-grade models drift on JSON adherence when the schema is only described in prose. Each prompt builder now ends with an explicit
JSON_SHAPEblock showing the literal expected object. Seepackages/core/src/prompts/{ingest,query,lint}.ts. -
One wiki = one topic, by design. Per Karpathy. The
topicfield in Settings → General describes the wiki's scope; multi-topic = multiple wiki folders +LLM_WIKI_PATHswitching. -
Native deps (keytar, better-sqlite3, mammoth, jsdom, etc.) externalized in webpack.
next.config.mjsputs them inexperimental.serverComponentsExternalPackagesANDwebpack.externalsso Next's transpilePackages walker doesn't try to bundle.nodebinaries or massive DOM emulations. -
Theme via simple localStorage + script-tag preflight. No
next-themesdep. InlineTHEME_INIT_SCRIPTin<head>applies thedarkclass before hydration;ThemeProviderkeeps state in React. BothThemeToggle(header) and the General tab buttons usemountedflag to avoid SVG hydration mismatch. -
FTS5 query sanitization.
searchPagessplits the query on whitespace and quotes each token as a phrase. Hyphens like[[other-page]]would otherwise be interpreted as FTS5 column exclusion operators and throwno such column: page.
Design pass (b937b5d) — real fonts (Fraunces / Crimson Pro / Inter / JetBrains Mono via next/font/google), paper palette per docs/08, unified AppShell, real home page with stat tiles + action cards, PageContainer/PageHeader primitives.
Sidebar height fix (e6cc5b2) — body locked to h-screen, main is the single scroll region, sidebars stretch via flex default. Sidebar bg/border bumped from /50 and /70 opacities to solid for dark-mode visibility.
Kbd / theme hydration / sidebar (cf8fd0a) — kbd hints switched to Inter sans (mono mis-baselines ⌘ glyph at small sizes). Layouts decoupled from double min-h-screen chain. Theme toggle added to header.
API key view-mode (06d877c) — Settings → API tab now defaults to a "view" mode showing the masked key (sk-or-v1-••••••••••••••••teme) read-only with Test/Replace/Remove buttons. Clicking Replace switches to edit mode with input + Save/Cancel. Also fixed hydration error on ThemeToggle + GeneralTab theme buttons (mounted-flag pattern). Topic field gained help box explaining one-wiki-one-topic + how to switch folders.
Prompt JSON shape (b442594) — Haiku was stuffing the user's wiki topic into indexEntries[].category instead of using the enum. All three prompts (ingest, query, lint) now embed an explicit JSON shape example with strict field rules. Sources page error UI wraps schema-validation errors in a friendly banner with retry advice + collapsible technical detail.
- Default model slugs are outdated.
anthropic/claude-3-5-sonnetandanthropic/claude-3-5-haikuwere retired. OpenRouter returns "model not available." Need to update to current Claude 4.x family slugs (anthropic/claude-sonnet-4.6,anthropic/claude-haiku-4.5, etc.). - Model selection should be a dropdown, not free-text. Currently
apps/web/src/components/settings/models-tab.tsxuses<Input>with a<datalist>. User wants a proper<select>with curated options + an "Other (custom)" option. - Pricing table needs updating to match new model slugs.
- Standalone server 500 on requests — standalone bundle boots ("Ready in 58ms") but every request errors. Likely native deps + pnpm symlink issue. Dev-mode CLI start works fine. Real fix is Step 15 territory.
pnpm packproduces tarball butnpm install ./tarballfails because@llm-wiki/*deps are stillworkspace:*.pnpm publishrewrites these but we haven't actually published.
docs/INSTALL.md,docs/OPENROUTER_SETUP.md,docs/MODELS.md,docs/TROUBLESHOOTING.md- README screenshots
- npm publish workflow (resolves the two items above)
- Versioning + changelog
- Chokidar live-watch test in
sync.test.tsoccasionally flakes (poll window timing-sensitive). Re-run usually passes. Not blocking; flake is ~1 in 5 runs.
cd "/Users/ddsyasas/ProjectFiles/Dev Projects/Business dev/llm-wiki"
# Pick a wiki folder
export LLM_WIKI_PATH=~/llm-wiki-default
# Dev server
pnpm dev
# Or via the CLI
node apps/web/bin/llm-wiki.mjs start ~/llm-wiki-default
# Tests
pnpm --filter @llm-wiki/core test # 114 tests
pnpm --filter @llm-wiki/llm test # 14 + 1 skipped (smoke)
pnpm --filter @llm-wiki/ingestion test # 11 tests
pnpm -r typecheck # all 4 packages
# Smoke test the real OpenRouter call (requires key)
OPENROUTER_API_KEY=sk-or-v1-... pnpm --filter @llm-wiki/llm testpkill -f "next-server" # nuclear option, only matches Next.js servers
lsof -nP -iTCP:3000 -sTCP:LISTEN -t | xargs kill # specific portFour quantum-computing source texts live in chat history (Shor's algorithm, Grover's algorithm, Quantum Error Correction, Quantum Supremacy with a deliberate Grover-1994-vs-1996 contradiction). Use these to exercise ingest, query, chat, lint end-to-end. The contradiction should ideally surface in lint or in the ingest contradictions array.
- Create
apps/web/src/app/api/<name>/route.tsexportingGET/POST/etc. - Use
openWikiContext()from@/lib/server-wikito get a DB connection + settings. - Always close the db in a
finallyblock. - Mark
export const dynamic = "force-dynamic"to skip caching.
- Pure CRUD →
packages/core/src/db-*.ts. - Higher-level orchestration → its own module (
chat.ts,ingest.ts,editor.ts). - Always re-export from
packages/core/src/index.ts.
shadcn-ui CLI is now npx shadcn@latest add <component>. Or paste the component source manually (we did this for Button, Input, Textarea — keeps deps low).
| Need to | Look in |
|---|---|
| Add or change a wiki I/O primitive | packages/core/src/wiki.ts |
| Change an LLM prompt | packages/core/src/prompts/*.ts |
| Tweak the zod schema for an operation | packages/core/src/schema.ts |
| Add a new source format | packages/ingestion/src/<format>.ts + register in apps/web/src/app/api/ingest/route.ts runExtractor |
| Add a new top-level nav item | apps/web/src/components/app-header.tsx PRIMARY_NAV or UTIL_NAV |
| Add or change a model preset | packages/llm/src/models.ts DEFAULT_MODELS / SUGGESTED_MODELS / PRICING |
| Change CSS palette | apps/web/src/app/globals.css :root and .dark |
| Add a typography variant | apps/web/tailwind.config.ts fontSize extension |
| Fix something in the CLI | apps/web/bin/llm-wiki.mjs (plain JS, no compile) |
User asked to verify the app actually implements Karpathy's pattern as someone-without-heavy-tech-knowledge would experience it in the browser. Re-grounded against docs/01-vision, docs/04-features-v1, docs/06-ingest-pipeline, docs/07-chat-threads, docs/08-ui-design.
Verified working ✅
- Three layers are all reachable from the top nav: Sources (raw), Wiki (LLM-maintained pages with index + per-page view + backlinks + inline edit), Schema (split-pane
CLAUDE.mdeditor with auto-backup to.llm-wiki/schema-history/, last 10 kept). - Three operations each have a dedicated route with cost preview where applicable: Ingest (
/sources), Query (/query), Lint (/lintwith severity grouping, broken-link auto-fix, clickable follow-up questions). - Per-message "Save as wiki page" on every chat assistant turn and on query results when a
suggestedNewPageis returned. - Five model slots (ingest / query / chat / lint / vision), per-slot dropdowns + custom-slug escape hatch in Settings → Models.
Gaps found and closed
- No first-run gate — user without API key or topic landed on a dashboard with stat tiles, hit "Add a source", failed at first ingest. Docs/04 P0 #1 explicitly required a setup wizard. → Built
components/onboarding.tsx.apps/web/src/app/page.tsxserver-side checksgetApiKey()andsettings.topic; if either is missing, renders the wizard instead of the dashboard. Single-card design (topic + key fields + Test button + OpenRouter signup link) — docs/08's three-step modal was overkill for two fields. - No sources list on
/sources— only the ingest form existed, so after ingesting you had no way to see your sources or trace which pages came from which. Docs/08 §"Sources view" required a list with format / size / dates / page count. → AddedGET /api/sources(joinspage_sourcesfor a count) +components/sources/sources-list.tsx. The list sits above the ingest form and re-fetches automatically after a successful ingest via a nonce bump. - No "Ingest whole chat as a source" — only per-message promote existed. Docs/06 §"Special case" and docs/07 §"Ingest the whole chat" explicitly call this out as the pattern for promoting a useful thread into the permanent wiki layer. → Added a header button on
chat-view.tsxthat stringifies the messages with role markers and POSTs to/api/ingestwithtitle: "Chat: …". Result banner shows links to created and updated pages. - Lint not surfaced on home — three operations but only two were action cards (Add source, Ask question). → Home action grid is now four cards (Sources / Query / Wiki / Lint), wraps to 2×2 on tablet and 1×4 on desktop. Lint card is disabled-effectively (CTA "Add a source first") when
pageCount === 0.
What I deliberately did NOT add
- Per-source "Re-ingest" / "Delete" buttons. Docs/08 mention them in the source detail view, but the data round-trip + edge cases (unlinking the page_sources rows when a source is deleted; running ingest a second time updates rather than replaces) is bigger than a single-session change. Tracking as a follow-up.
- Per-source "What pages did this produce" drill-down. The list shows a
N pagescount today; the click-through view is a follow-up. - Sources detail view (extracted markdown shown alongside the source). Same reason — follow-up.
- Server-side gating of
/wiki,/query,/lintfor missing key. Direct-bookmark hits to those routes still fail loud at the API layer (OpenRouter API key not configured). The home gate covers the common path; full middleware redirect is a follow-up if direct-bookmark UX matters.
Files touched this audit
apps/web/src/app/page.tsx— added first-run check, expanded action grid to 4apps/web/src/components/onboarding.tsx— newapps/web/src/app/api/sources/route.ts— newapps/web/src/components/sources/sources-list.tsx— newapps/web/src/app/sources/page.tsx— added list section + refresh nonceapps/web/src/components/chats/chat-view.tsx— added Ingest → wiki button + result banner
After the Karpathy-pattern audit shipped, the next session covered four arcs in one stretch. Capturing here so future-me / future-devs can see the why behind each commit, not just the diff.
User hit "Run lint" → got LLM response was not valid JSON: Unexpected token ''. Root cause: Anthropic models don't natively support response_format: json_object. OpenRouter forwards the request but the model still wraps output in json … markdown fences. Our parser calledJSON.parse(raw)` and choked on the leading backtick.
Fix: new extractJsonBody() in packages/llm/src/client.ts that (1) strips a surrounding ``` fence with optional language tag, (2) slices to the first { through the last } so any preamble like "Here's the JSON you asked for:" gets ignored. Three new client.test.ts cases lock the regex (fence with tag / bare fence / prose prepend). Applies to every LLM operation — ingest, query, chat, lint.
The lint page reported 19 issues but only had a per-issue "Remove broken link" button. Reporting alone is homework. User: "can we add an option for users to fix those automatically or something because reporting is not enough."
Built four fix capabilities, all dispatched via POST /api/lint/fix with a type discriminator:
rebuild-index(local, free) — newrebuildIndexFromPages(wikiPath, db)inpackages/core/src/index-builder.ts. LiftedparseIndexEntries/renderIndex/CATEGORY_HEADINGSout of ingest.ts into this shared module so the rebuild can reuse what the LLM ingest already uses. Preserves hand-tuned summaries; auto-generates one-line summaries for newly-added entries; removes orphan entries whose pages were deleted manually.fix-all-broken-links(local, free) — bulk iteration over the existingremoveBrokenLink. Confirms before mass-rewriting.create-stub-page(LLM, ~$0.01) — newcreateStubPage()inpackages/core/src/lint-fixes.ts. Gathers backlinks for the missing slug, feeds them as context, asks the LLM for{title, type, content, tags}with a strict zod schema. Falls back torebuild-indexwhen the slug already has a page (i.e. "missing from index" issues).apply-suggested-fix(LLM, ~$0.01) — newapplyLintSuggestedFix()inlint-fixes.ts. Reads the affected page, sends to LLM with the suggested-fix instruction, writes the rewritten body viaapplyManualEdit(backed up to.llm-wiki/page-history/).
Routing bug fixed mid-stream (46f07a3): contradictions list multiple affectedPages (e.g. ["grovers-algorithm", "lov-grover"]) but the client was always sending affectedPages[0] to the LLM. The Grover contradiction's suggested fix said "update lov-grover" → we kept sending grovers-algorithm (already correct) → LLM returned no-op → 2 useless rewrites of the correct page, the wrong page never touched. Backups in .llm-wiki/page-history/ proved it. New targetPageForFix() extracts kebab-case slugs from the suggested-fix text, intersects with affectedPages, prefers the LAST match (LLMs phrase fixes as "X says Y but Z says W; update Z" — target appears later). Plus no-op detection on the server: if the LLM returns unchanged content, skip the write, return noop: true, UI shows amber "LLM made no change" instead of false success.
Index drift fixed (b3a9261): even after a successful Apply-suggested-fix on lov-grover, lint kept flagging the same "page says X but index says Y" contradiction on re-run. Root cause: applyManualEdit wrote the page file but never updated the matching summary line in index.md. The index entry for lov-grover ("…developed in 1994…") was generated from a snapshot of the page body at ingest time and went stale the moment the page was edited. New refreshIndexEntryForSlug() in index-builder.ts re-extracts a one-line summary via firstSentence() and updates just that entry. Called automatically from applyManualEdit and createPage — every page edit now keeps its index entry fresh. Bonus: firstSentence() regex updated to strip ANY bracketed content, not just kebab-case slugs, so [[Mathematician]] (the LLM had stuck non-slug text inside brackets) no longer leaks into rendered summaries.
User asked whether a lint history would be useful. Recommended a lightweight version, user said yes.
lintWiki()now appends tolog.mdwith the format docs/03 specified all along:## [stamp] lint | N issues — health+ bullet detail. Sits alongside ingest / edit / schema entries so log.md becomes a single chronological timeline.previousRunfield returned from lintWiki by reading the most-recent lint heading from log.md before appending the new one. UI shows "Previous run 2h ago: 19 issues · −15 fewer now" in green/amber.- Always-visible "Recent runs" panel at the top of /lint (later request — initial implementation only showed the delta after a fresh run, which the user correctly noted wasn't useful before the second run). New
getLintHistory(wikiPath, limit)helper +GET /api/lint/history?limit=10. Loaded on mount, refreshed after every successful lint. /logpage — server-rendered view of log.md that splits by## [headings, reverses (newest first), renders each entry as a card with clickable[[wikilinks]]jumping to wiki pages. Replaced the "show the on-disk path + clipboard copy" footer with aView full timeline →link to/log.
User: "I see small delay and stucking feeling while click on cards… not fast enough." Real issue, not overthinking — Next.js App Router waits for the new route's server work to finish before swapping pages. During that wait (100ms–2s in dev), the user sees the old page unchanged.
Fixed with two layers:
loading.tsxfiles at every heavy route (root, wiki, wiki/[slug], lint, log, sources, chats/[id]). Each renders a Suspense fallback the instant a Link is clicked. Four reusable skeleton variants incomponents/loading-skeleton.tsx(page-card-grid, article, list-of-cards, chat-with-messages) chosen per route to roughly mimic the actual layout — less jarring than a spinner.active:scale-[0.99]on every clickable card (wiki cards, home stat tiles, home action cards). Card snaps inward by 1% on click with a 100ms transition — immediate "click registered" feedback in the same frame as the click, even before the skeleton fires.
Bonus bug fix (9a010f7): the chats sidebar's + New chat button rendered with near-black text in light mode (should have been cream-on-deep-red). Diagnosed: our cn() helper uses tailwind-merge; twMerge ships knowing the default Tailwind text-{size} scale but not our custom sizes (text-display, text-h1, text-ui, text-caption, etc.). When it saw text-ui next to text-primary-foreground, it had no way to tell which was font-size and which was color — and the className override won, dropping the color class. The button inherited body's near-black text-foreground. Fixed in apps/web/src/lib/utils.ts via extendTailwindMerge({ extend: { classGroups: { "font-size": [{ text: [...] }] } } }). Prevents this class of bug anywhere else a custom-size class collides with a color class on the same element.
Schema editor and per-page edit view felt cramped — each column ~360–420px wide, fixed 480–560px tall. Looked unfinished. Added xl size to PageContainer (max-w-[1400px]); schema uses it directly, wiki PageView expands article from max-w-3xl (reading) to max-w-[1400px] when entering edit mode. Both panes now use min-h-[calc(100vh-…)] + matched border/padding/bg so the two sides look symmetric.
The /wiki landing was just rendering index.md as raw markdown — looked like a debug view. Replaced with a card grid grouped by type (Overviews → Concepts → Entities → Comparisons → Sources). Each card has: type label, relative-time stamp, title in display font, line-clamp-3 summary, tag chips. Page header summarizes counts ("10 pages · 6 concepts · 4 entities · last update 2h ago"). Width bumped from max-w-3xl to max-w-6xl so 3 cards fit per row. Sidebar unchanged.
Closes the user's "don't we lose data?" concern with UI. Every wiki page already had a sources: [uuid] frontmatter array + rows in the page_sources join table; we just never surfaced them. Two new surfaces:
/sources/[id]detail page — server-renders metadata strip (format, size, dates, original URL/filename), "Contributed to N wiki pages" chips linking back to the pages this source compiled into, and the raw content rendered through MarkdownView. Binary files (PDF/image bytes detected via UTF-8 replacement char) show "open in your editor" instead of dumping garbage.- "Sources" section on every
/wiki/[slug]above Backlinks. Source chips link to/sources/[id]. Bidirectional graph traversal: any wiki page → its sources → all OTHER pages that source produced → those pages' sources → …
Sources list rows on /sources were also made into Links to /sources/[id] (previously inert div rows).
User feedback: the existing onboarding (single card with topic + API key) was functional but flat. New users landed cold without context about what they were about to build. Replaced with a 4-step wizard that fires once ever:
Step 1: Welcome. Big serif wordmark + one-paragraph value-prop in Crimson Pro + a footnote naming the on-disk folder. Two paths out: [Get started →] and a quieter skip the tour link.
Step 2: Topic. Single Input with a placeholder showing the user's likely real example. Enter-to-advance, Next button gated on non-empty. Reminds the user they can change it later in Settings.
Step 3: API key. Password input + Test button + OpenRouter signup link inline. Test result shows green/red below the field. Clicking Next saves both topic and key in parallel before advancing — so the user can't get into a state where they typed two things and lost them by backing out.
Step 4: Tour. Five numbered cards explaining Sources → Wiki → Graph → Query/Chats → Lint. Each row uses a display-serif numeral + title + one-line description. CTA is [Take me to Sources →] which marks onboarding complete and routes the user to /sources for their first real action.
Skippable at every step. Skip = save what's been entered + mark onboarding complete + router.refresh() → falls back to the minimal single-card form if topic/key still missing, or the dashboard if everything got captured.
Mechanism:
- New
onboardingCompletedAt?: stringISO timestamp onGlobalConfig. Set on first wizard completion OR first skip. Idempotent — re-call preserves the original timestamp. - New
setOnboardingCompleted()helper inpackages/core/src/config.ts. 3 new tests. - New
POST /api/onboardingroute. Single-purpose endpoint so callers don't have to merge with apiKey writes. apps/web/src/app/page.tsxreadsglobalCfg.onboardingCompletedAtand passesisFirstRunprop. Wizard mode vs minimal mode is one boolean.- The onboarding component now wraps two implementations:
<FirstRunWizard>for first-run,<MinimalOnboarding>for everyone else. Same save plumbing, different chrome.
Subsequent flow:
- Create a new wiki via Settings → Wikis: topic is collected in the create form, so the user lands on the dashboard for the new wiki without seeing onboarding again
- API key gets deleted somehow: minimal form fires (already completed onboarding, just needs the key)
- Switch wikis where the destination has no topic: shouldn't happen since switch validates the path is initialized, but the minimal form catches it
Per docs/08 tone: calm, not loud. No animations, no progress bars sweeping, no confetti. Just a clean stepper with three dots, restrained typography, and Sun Tzu-level brevity in each step's copy.
Settings → Wikis is the management surface (create / remove / detail view), but the user pointed out switching needed a faster path for "I'm deep in a page, want to flip wikis without losing my place." Added two surfaces, both additive:
-
Active-wiki chip + dropdown in the header, next to the wordmark. Shows the active wiki's topic (truncated to 14rem), click → dropdown with: active wiki summary, "Switch to" list of other on-disk recents (each switches in-place via
router.refresh()so the user stays on whatever page they're on), and "+ Create new wiki" / "Manage wikis…" links to/settings?tab=wikis. Closes on outside-click + Escape. -
Command palette gains a "Wikis" group. Switching from Cmd+K: each non-active, on-disk recent shows as "Switch to " with the path as hint. Discriminated
Actiontype so the same activation handler dispatches navigate (push href) vs switch-wiki (POST + refresh). Plus a "Manage wikis…" link that deep-links to/settings?tab=wikis. -
Settings page gains
?tab=URL param support viauseSearchParams, so the deep-link from the header dropdown / Cmd+K lands on the right tab.
Decisions worth flagging:
- Cmd+K filters to on-disk wikis only (skips missing-folder entries) — switching to a missing folder would error.
- Header chip is hidden on small screens (
hidden sm:block); mobile uses the existing hamburger menu where the wikis link lives in Settings. - The header dropdown stays single-purpose (everyday switching) and links out to Settings for CRUD — keeps the dropdown small + the management story unified.
User flagged that the app could only hold one wiki at a time. Real limitation — a physicist might want "Quantum computing" but also a separate "Machine learning research" wiki, and the current per-folder workflow required restarting the dev server with a different env var. Discussed four approaches (env-var swap, CLI port juggling, in-app switcher, full URL namespacing); user picked the in-app switcher (option C). Full design in docs/13-multi-wiki.md.
Core change: resolveWikiPath() in apps/web/src/lib/server-wiki.ts now checks env → global config activeWiki → default, in that order. Sync read of ~/.llm-wiki/config.json because it runs in server-component render paths and the file is tiny + OS-cached. Every existing API route inherits the new behavior transparently — no per-route changes needed.
Config additions in packages/core/src/config.ts: new activeWiki?: string field on GlobalConfig, new setActiveWiki(path) + removeRecentWiki(path) helpers. The latter clears activeWiki when the active wiki itself is removed, so resolveWikiPath cleanly falls back to the default. 6 new config-test cases (12 → 18 passing).
API: new apps/web/src/app/api/wikis/route.ts — GET lists recents with topic + on-disk-exists check enriched server-side; POST is discriminated by type: "switch" | "create" | "remove". Tilde expansion + resolve() normalize user-pasted paths. Switch requires the path to exist (no auto-create on plain switch); create runs initWikiFolder + stamps the topic into per-wiki settings.
UI: new WikisTab slots into the Settings tabs strip between General and Models. Active wiki rendered at the top with a primary-color border + "active" chip; other recents listed below with Switch + Remove. Inline create form (collapsed by default) auto-fills the folder path from the topic via slugify (~/llm-wiki-<slug>). After every successful mutation, router.refresh() re-renders every server component so the new wiki shows up app-wide without a hard reload.
Decisions worth flagging:
- The env var (
LLM_WIKI_PATH) still wins over the global config when set — explicit override for CI, scripting, advanced CLI use. Most users will leave it unset and the picker becomes canonical. - Remove action is config-only — never touches the on-disk wiki folder. Data safety > convenience.
- One wiki active per app instance. To browse two truly side-by-side, run two dev servers on different ports (no V1 work needed for that path; documented in Help → Multiple wikis).
- True URL-namespaced multi-tenancy (
/w/<id>/...) is V2 — would let multiple wikis be visible in different browser tabs simultaneously. Additive on top of this switcher when it becomes worth doing.
About + Help pages updated with multi-wiki sections. Help TOC gets a "Multiple wikis — switching + creating" entry near the top.
After the graph view shipped, the project crossed the "this is genuinely a v1 product" threshold. Closed out the release prep:
- README.md completely rewritten. The prior 63-line draft referenced files that don't exist (CONTRIBUTING.md, docs/openrouter-setup.md) and an unpublished npm package (
@yasas/llm-wiki). New ~200-line README is a real product page: gap-analysis table, full v1.0 feature inventory (with graph view), on-disk layout, install-from-source instructions, links to in-app docs (/about /help /developers) AND repo/docs/*, honest status section listing what shipped vs what's deferred to V1.x/V2, full stack table. - Version bumped 0.1.0 → 1.0.0 in
apps/web/package.jsonandapps/web/src/components/footer.tsxAPP_VERSION. Workspace packages stay at 0.0.0 (internal-only, workspace:* deps don't care). docs/04-features-v1.mdP1 #10 marked ✅ (wiki graph view shipped) with a pointer todocs/12-graph-view.md.- About / Help / Developers pages woven with graph-view explanations during the previous session (commit
556cd31) so the in-app docs are consistent with the README. - Git tag
v1.0.0+ GitHub release with summary notes.
Open questions list (in §"Open questions for future sessions" below) is the authoritative work-needed list. The big rocks for V1.x are: diff view, approval gate, export-to-zip, production build, CLI npm publish.
User asked for an Obsidian-style 3D graph view ("knowledge as neural network") to make the wiki's compounding structure visible. Full design doc + decisions in docs/12-graph-view.md.
Shipped: new packages/core/src/graph.ts (buildGraph(wikiPath, db)) reusing the existing uniqueLinkedSlugs parser from links.ts; 7-test suite covers empty/single/linked/broken/self-link/dedupe/preview-strip cases. New /graph route with server component that calls buildGraph, dynamic-imports react-force-graph-3d (~600KB bundle hidden behind ssr: false so other routes don't pay). vault-graph.tsx client component handles the 3D scene + side panel + URL state (?node=<slug>) via window.history.replaceState so selection clicks don't trigger router re-renders. Theme reactivity via MutationObserver on <html> watching our ThemeProvider's class toggle — WebGL canvas can't read CSS vars directly. Five hardcoded type colors (overview=red, concept=cyan, entity=amber, comparison=violet, source=slate) — restrained enough to look right in both light and dark modes. Node size scales with degree; selected node turns near-white, neighbors keep their color, non-neighbors dim to barely-visible — same focus mechanic Obsidian uses. New deps in apps/web only: react-force-graph-3d + three. Added Graph to PRIMARY_NAV after Wiki.
Two things deferred: a CLI graph subcommand printing stats+orphans (nice-to-have), and a 5th home-page action card (existing 4 already crowded). 2D toggle, search/filter overlay, persistent camera state are all V2.
User asked for "proper about page", a "developer page / doc page" set, and a wiki-style reference accessible to local + future cloud users.
/about— Karpathy-pattern story, gap analysis (RAG vs. note-taking), who-it's-for grid (researchers, lawyers, doctors, journalists, educators, indie hackers), design principles, stack overview, credits to Yasas + Karpathy, MIT./help— user-facing how-to. Mental model (3 layers + 3 ops), first-run setup, sources, wiki browsing, query vs. chats, lint with all fix types explained, schema editing, settings, on-disk folder layout, troubleshooting. With TOC. Like Wikipedia'sHelp:namespace./developers— technical reference. Stack, monorepo layout (with directory tree), the three operations as code with file paths + entry points, JSON contracts, FTS5 + SQLite tables, how to add a source format, how to swap LLM providers, where prompts live, how lint quick-fixes dispatch, test suite locations, contributing pointers. Points back to/docs/on GitHub for the design contract.- Footer updated with About / Help / Developers / GitHub / Pattern by Karpathy. Reachable from every screen.
Four roadmap items shipped together in one session, in this order so the safe wins land before the unknown debug work. All four touched zero existing feature behavior — they extend coverage, add a new surface, or fix non-runtime-visible build issues.
V1.x carryover from sprint P. requireSetup() already gated /, /wiki, /wiki/[slug], /sources/[id], /chats, /chats/[id], /log, /graph. The four leaf "use client" pages — /sources, /query, /lint, /schema — bypassed it and fell loud at the API layer with a no-key error.
Pattern per route:
- Rename
page.tsx→<route>-view.tsx, convert default export to named export - New tiny
page.tsx:asyncserver component,await requireSetup(), render the view
Eight files changed, no behavior change for set-up users. Direct bookmarks from a fresh install (or after key revocation) now redirect cleanly to / instead of crashing in fetch.
New cross-wiki overview surface — answers the question the per-wiki home page can't: "across all my wikis, how many pages / sources / chats do I have, and how much have I actually spent?"
- Page:
apps/web/src/app/dashboard/page.tsx(server). Opens every recent wiki's DB read-only, aggregates pageCount + sourceCount + chatCount + costCents + folder mtime, sorts by recency. Roll-up tile row across all wikis + per-wiki card list. Wikis whose folder is gone get a "folder missing" pill; wikis that exist on disk but haven't been initialized get a "not initialized" pill (we deliberately skipopenDbon those — it has a side effect of creating.llm-wiki/). - API:
apps/web/src/app/api/wikis/health/route.tsreturns the same aggregation as JSON for future use (Cmd+K stats overlay, etc.). - Switch button:
apps/web/src/components/dashboard/switch-wiki-button.tsx— leaf client component on each card, reuses the existing/api/wikisPOST plumbing. - Wired into nav: Cmd+K → "Go to → Dashboard"; Settings → Wikis header gets a
↗ Health dashboardlink next to↓ Export active wiki.
The longest-standing blocker. next build previously failed on ESLint and the standalone bundle 500'd on every request when it did build. Three categories of fix:
-
Build itself — 50+ unescaped-apostrophe errors (rule is noise for copy-heavy components, turned off in
.eslintrc.json) + threeeslint-disable-next-line @typescript-eslint/no-explicit-anycomments that referenced a plugin we don't have loaded (removed; theanythey guarded was fine since no rule fires on it anyway). -
Standalone runtime 500s — Next's nft tracer marks
serverComponentsExternalPackagesas "do not bundle", but in this workspace (pnpm +transpilePackageswalking through@llm-wiki/core) it ALSO drops them from the standalone trace. Server booted but every request died withCannot find module 'better-sqlite3'. Postbuildcopy-standalone-assets.mjsnow resolves each external from the right workspace root (apps/webforarchiver;packages/corefor the rest), then recursively copies the package + every runtime dep into.next/standalone/node_modules/. 147 packages total (8 externals + 139 transitives).
The source apps/web/package.json is marked private: true and lists @llm-wiki/* via workspace:*. pnpm publish rewrites those but only if you ALSO publish the workspace packages — which we don't want for V1 (one public package, not four).
New script: apps/web/scripts/build-publish-tarball.mjs. Builds a self-contained apps/web/dist-publish/:
- Rewritten package.json: public name
@yasas/llm-wiki, exactly one runtime dep (openfor browser auto-launch), full npm metadata (description / keywords / repository / bugs / homepage). - Standalone bundle + CLI bin + static assets + README + LICENSE.
.pnpm/flatten step: promotes every package buried in.pnpm/<pkg>@<version>/node_modules/<pkg>/to a top-levelnode_modules/<pkg>/. Without this, the post-npm packtree only had.pnpm/paths, which Node's regular resolver can't see, and Next bombed onCannot find module 'styled-jsx/package.json'from inside its bundled server. Promoted 28 packages in this build.
Two new apps/web scripts: build:publish (full build + dist assembly) and pack:publish (build:publish + npm pack smoke test). dist-publish/ is gitignored.
Verified end-to-end:
- Tarball: 28.9MB packaged / 120MB unpacked / 9,426 files
npm install <tarball>in a clean temp dir succeeds with just 11 deps (theopentree)llm-wiki doctorreports clean installllm-wiki start --port 3940 --no-openboots the standalone server- Every route from
/through/api/usagereturns HTTP 200
All that's left for actual npm publish is the manual cd apps/web/dist-publish && npm publish --access public invocation under the user's @yasas scope credentials.
The 1.0.0 tarball built in sprint Q was darwin-arm64-only — better-sqlite3 and keytar ship platform-specific .node binaries, and my copy script grabbed whichever was built on the dev machine. Sprint R fixes that and ships the first cross-platform release.
apps/web/scripts/build-publish-tarball.mjs now strips the platform-locking packages from the standalone bundle AND lists them as runtime dependencies in the published package.json:
better-sqlite3— native .node binary, per-platformkeytar— native .node binary, per-platformchokidar— pure JS but with optional fsevents on macOSjsdom,mammoth,officeparser,@mozilla/readability— heavy pure JS, no point bundling
Result: npm install -g <tarball> runs each package's normal install on the user's machine, which fetches the right prebuilt binary for their OS+arch via prebuild-install. One tarball, every platform.
Versions are pinned to ^<currently-installed> discovered from the standalone tree — not free-floating, so users get patch/minor updates of the externals automatically but stay on the major we've tested against.
Strip step deletes both dist-publish/.next/standalone/node_modules/<name>/ AND dist-publish/.next/standalone/node_modules/.pnpm/<encoded-name>@*/. Without the .pnpm cleanup, Node could still load the bundled (wrong-arch) copy on a user's machine.
Semver minor — V1.x sprint added real features post-1.0.0 (mobile sidebar, diff view, approval gate, export-to-zip, wiki templates, cross-wiki search, setup gate completion, wiki health dashboard, replay-tour fix, LLM cost computation). Wiki on-disk format unchanged — users upgrading from 1.0.0 keep all their data.
Created with gh release create v1.1.0 ... --prerelease for cross-platform verification, then promoted to latest after all three platforms passed.
Install paths now:
# Direct from release URL
npm install -g https://github.com/ddsyasas/llm-wiki/releases/download/v1.1.0/yasas-llm-wiki-1.1.0.tgz
# Or download + install
curl -LO https://github.com/ddsyasas/llm-wiki/releases/download/v1.1.0/yasas-llm-wiki-1.1.0.tgz
npm install -g ./yasas-llm-wiki-1.1.0.tgz- macOS (darwin/arm64):
npm install -g, doctor passes,llm-wiki startboots standalone server, every route returns 200. - Windows:
npm install -g, doctor passes, server boots, UI works. - Ubuntu (WSL): install succeeded (162 packages, including native deps fresh-built for linux/x64), but
llm-wiki: command not found. Diagnosis: non-standard npm prefix → bin dir not on PATH. Fixed viaecho 'export PATH="$(npm prefix -g)/bin:$PATH"' >> ~/.bashrc. Added as a troubleshooting entry in/help+ README.
- README.md — Quick start now leads with
npm install -g <tarball>instead ofgit clone + pnpm dev. From-source path moved to second position labeled "for development or contributing." New "llm-wiki: command not foundafter install" troubleshooting block. v1.0 → v1.1.0 throughout. - docs/14-roadmap.md — status block updated (cross-platform install ✓, v1.1.0 released). Open items trimmed to actual npm publish + CI matrix + the three V1.x items still genuinely deferred.
- In-app
/help— new "Dashboard — stats across every wiki" section documenting the Q-sprint addition. Two new Trouble entries: "command not found after npm install" and "native module error after install on Linux." Trouble component widened to accept ReactNode (was string-only). - In-app
/developers— new "Build + publish pipeline" section covering the two-artifact split (standalone bundle vs. publishable tarball) and thepnpm build:publish/pnpm pack:publishscripts. - In-app
/about— Install footer line mentioning the GitHub Releases path.
- The actual
npm publish --access publicto the registry — deliberate manual step (irreversible). - A GH Actions matrix that installs the tarball + runs
llm-wiki doctoron macos-latest / ubuntu-latest / windows-latest. Right now cross-platform was verified manually on three user machines; CI would catch regressions automatically.
The capstone. After R verified the cross-platform tarball on macOS / Linux / Windows, S pushed the package to the public npm registry under the user's actual scope (syasas, not the placeholder yasas from earlier docs).
- Scope rename:
@yasas/llm-wiki→@syasas/llm-wikithroughout the codebase (build script, README, docs/02 + /09 + /10 + /11, in-app /developers + /about). The@yasasscope was already taken; user's actual npm handle issyasas. - First-run welcome banner (commit
3e99454): two complementary surfaces so users always see "you're installed, here's what to type next":bin/postinstall.mjs— runs duringnpm installif the user passed--foreground-scripts(npm 10+ captures lifecycle output by default; this only fires in verbose mode).bin/llm-wiki.mjsmain() — checks for~/.llm-wiki/config.jsonand prints the same banner if absent. Always fires on the actual first invocation of anyllm-wiki <cmd>, regardless of how the package was installed. Suppressed by--quietand skipped forversion/helpto keep their output clean for scripting.
- GitHub Release v1.1.0 asset replaced — old
yasas-llm-wiki-1.1.0.tgzdeleted, newsyasas-llm-wiki-1.1.0.tgzuploaded. Release notes updated to mention both install paths. npm publish --access public— actually ran, succeeded, package live at https://www.npmjs.com/package/@syasas/llm-wiki
Error: ENOENT, uv_cwd— the rebuild steprm -rf dist-publish && ...deleted the directory while the user's shell was sitting inside it.pwdlied (read from stale$PWD), but Node'sprocess.cwd()failed. Fix:cd /thencd <path>to force a realchdir(2).- 403 Forbidden — Two-factor authentication required — npm enforces 2FA for publishes by default. The user had set up 2FA but only saved the recovery codes (skipped the QR code → authenticator app step). Fix: passed
--otp=<recovery-code>to use one of the recovery codes. Long-term: reconfigure 2FA with an authenticator app (Apple Passwords / Authy / etc.) so future publishes are a 6-digit code, not a burned recovery code.
- README.md — "Quick start" now reads
npm install -g @syasas/llm-wiki. GitHub Releases retained as fallback. Status block updated. - docs/14-roadmap.md — header date + status block updated to "published to npm" ✓. "Publish to the npm registry" removed from the "still open" list.
- apps/web/src/app/about/page.tsx — footer install line points at npmjs.com instead of GitHub Releases.
- Long-term: reconfigure 2FA with a TOTP authenticator app instead of relying on recovery codes (one-time codes that burn per use).
- GH Actions matrix to install the tarball + run
llm-wiki doctoron macos-latest / ubuntu-latest / windows-latest after each release. Currently cross-platform was verified manually on 3 user machines. - Future patch releases (1.1.1, 1.2.0…) — same flow, just bump version +
pnpm build:publish+npm publish --otp=<code>.
First real-user bug post-publish. A friend on Windows ran llm-Wiki Doctor (capitalizing both — natural for Windows users since the filesystem is case-insensitive) and got unknown command: Doctor followed by the help text. Real bug, real user, ~5 min fix + republish.
-
Case-insensitive command dispatch (
bin/llm-wiki.mjs): lowercaseargs._[0]and the firstconfigsub-command before the switch.Doctor,DOCTOR,doctor,dOcToRall dispatch the same. Same forStart,Init, etc. -
Banner alignment fix (
bin/llm-wiki.mjs+bin/postinstall.mjs): the welcome box's right edge had drifted by 2 characters because the inside line padded by hand-counted spaces and the count was off. Switched to dynamic padding computed from the visible (non-ANSI) text width vs. a fixedinnerWidth = 49. Now box stays square regardless of version string length — future bumps tov1.10.0etc. won't break it.
Same as 1.1.0 but smoother because the muscle memory + scripts existed:
- Bump
apps/web/package.json+apps/web/src/components/footer.tsxAPP_VERSION → 1.1.1 pnpm install+pnpm --filter @llm-wiki/web build+node apps/web/scripts/build-publish-tarball.mjscd apps/web/dist-publish && npm pack→syasas-llm-wiki-1.1.1.tgz(27.8MB, 8738 files)- Local install verification: install tarball into
/tmp,llm-wiki version→ 1.1.1,llm-wiki Doctor(capital D) dispatched correctly - GitHub Release v1.1.1 created with new tarball as the only asset
npm publish --access publicfromdist-publish/— see "Publish gotcha: passkey 2FA" below
The sprint S notes assumed publishing would use --otp=<recovery-code> long-term. That turned out to be wrong: npm has migrated to passkey-based 2FA (WebAuthn). Recovery codes are NOT accepted as OTPs anymore — they're for account recovery only (resetting 2FA if you lose your Mac).
What actually happens during npm publish once passkey 2FA is set up:
npm publish --access public— no--otpflag- npm prints:
Visit https://www.npmjs.com/login/cli/<token> to authenticate - Open URL in browser → npm prompts "Authorize this publish?"
- macOS pops the passkey dialog → tap Touch ID
- Browser confirms → terminal proceeds →
+ @syasas/llm-wiki@X.Y.Z
Setup for this (one-time, done on 2026-05-24 mid-sprint-T): on the npm 2FA settings page, the only available method is now "Security key" (passkey). Selecting it triggers a macOS system dialog to save a passkey for npmjs.com — Touch ID confirms it. From then on, the Mac IS the 2FA device.
Common confusion to avoid next time:
- Don't paste a recovery code into the
--otpprompt — npm rejects it - The "long codes" npm gives you at setup are recovery codes, NOT TOTP codes
- There's no QR code anymore — TOTP authenticator apps don't apply
- If npm prompts "Enter OTP:" on the CLI, you're on an older 2FA flow. The newer flow prints a URL instead.
Long-term alternative for non-interactive publishes (CI/CD or just skipping Touch ID): create a granular access token at npmjs.com → settings → tokens with "Bypass 2FA Required to Publish" enabled, scoped to @syasas/llm-wiki. Then npm publish --token <token> or NPM_TOKEN=<token>. Not set up yet; the user does manual publishes via Touch ID.
- README.md: every "1.1.0" → "1.1.1" (header, status block, tarball URL fallback, doctor example output)
- docs/14-roadmap.md: header date + status block updated to v1.1.1
- Patch flow takes ~10 minutes end-to-end including verification — fast enough that small bugfixes don't pile up
- The build script's version pinning for native deps (reads
^<currently-installed>from the standalone tree) automatically picked up the same versions as 1.1.0, no drift - npm published cleanly (no scope-permission surprises since
@syasas/llm-wikiwas claimed during 1.1.0) - GitHub Release creation as the single fallback distribution path remains valuable — covers the rare "npm down" scenario or users behind corporate npm proxies
With v1.1.1 live on npm and cross-platform-verified, the work shifted from "make the product ship" to "make it look like a real open-source project worth contributing to and using." Three threads, in roughly the order they happened:
The repo at v1.1.0 was technically complete but visually mute and structurally uninviting — no screenshots, no CONTRIBUTING, no Code of Conduct, no SECURITY policy, no issue templates. Anyone landing on the GitHub page couldn't tell what the app looked like or how/where they could help. Three commits closed that gap:
-
9 screenshots added at
docs/screenshots/(home, wiki landing, graph, graph-node-panel, chat, schema editor, settings general, settings costs, wiki switcher) + a README "Screenshots" section that arranges them in a narrative flow (hero → wiki layer → operations → settings + multi-wiki). Filenames are kebab-case + numbered for sort order. Source files (inss/folder with macOS-format timestamps containing U+202F narrow-no-break-space) stay gitignored. -
Full contributor infrastructure in one commit: CONTRIBUTING.md (3 tiers of prioritized "what we need" — Quick wins / Medium / Big — 15 concrete items pulled from the roadmap, plus a "what we don't want" section addressing telemetry, framework swaps, AI-driveby PRs), CODE_OF_CONDUCT.md (Contributor Covenant 2.1 summarized in plain language with a link to the canonical text — avoids reproducing the explicit harassment examples that trip content filters), SECURITY.md (supported-versions table, private vuln reporting via GitHub Security Advisories, response SLAs, known security-relevant design choices),
.github/ISSUE_TEMPLATE/{bug,feature,config}.yml(structured forms that disable blank issues + route Discussions/security to the right surfaces), and.github/PULL_REQUEST_TEMPLATE.md(what/why/how-tested/screenshots/type/compat/checklist). -
Contributor walkthrough added at
docs/contributor-walkthrough.md(465 lines) — step-by-step "you've never sent a PR before" guide covering fork → clone → branch → commit → push → PR with exact commands at each step, plus a "common stumbles" section (committed to main by accident, tests fail on PR but pass locally, merge conflicts, wrong commit email). Linked from CONTRIBUTING.md's top callout, README's "Contributing" section, and.github/ISSUE_TEMPLATE/config.yml. -
Privacy cleanup pass: contributor docs had a personal Gmail (
yasasdreamz@gmail.com) as the contact channel. Swapped to business email (yasas@idersolutions.com) across CODE_OF_CONDUCT.md, CONTRIBUTING.md, SECURITY.md. Plus a separate ddsyasas@gmail.com leak in git author config of every commit was masked via GitHub's "Keep my email addresses private" + "Block command line pushes that expose my email" toggles + a local-then-global git config swap to the GitHub noreply (106056808+ddsyasas@users.noreply.github.com). End state: noreply for commits, business email for contact, personal Gmails invisible on any public surface.
The [[ LLM Wiki wordmark only existed as inline text+CSS in the running app. No SVG, no PNG, nothing usable for README hero / social cards / favicon variants. Three SVG variants shipped at apps/web/public/:
logo.svg— horizontal wordmark (280×64), matches the in-app header layoutlogo-hero.svg— stacked variant with tagline (800×400) for README hero + GitHub social preview + npm card. Includes@media (prefers-color-scheme: dark)so it auto-swaps colors based on viewer themefavicon.svg— just the[[mark, square 64×64. Next.js picks this up automatically
All three use Fraunces (display) + JetBrains Mono (mono) with Georgia/Menlo fallbacks for environments where Google Fonts can't load. Documented in docs/branding.md — design rationale, color tokens (light/dark), font sources, three options for SVG→PNG conversion (online, ImageMagick CLI, headless browser), how to set the GitHub social preview, in-app usage snippet.
README leads with the hero logo banner now (<img src="apps/web/public/logo-hero.svg" width="600"> centered above the H1) + a row of 4 status badges (npm version / MIT / latest release / PRs welcome). Standard polished-OSS-repo header style — React, Vue, Next.js itself all use this pattern.
Two days after the contributor infrastructure landed, the first external PR arrived: "feat: implement dynamic ollama provider and model slot structure" by Savindu Geethma. Picked from CONTRIBUTING.md's Medium item #7 (Ollama UI support) — confirming the prioritized work list actually drives contribution.
PR went through two rounds of review:
- Round 1: PR description claimed a
WikiSettingsrefactor inpackages/corebut the actual diff touched 21 files inapps/web/and zero files inpackages/. Result: 40+ TypeScript errors (Property 'provider' does not exist on type 'string') because the cloud-side refactor expectedpackages/coreto expose a newModelSlotConfigtype that didn't exist. Requested changes with a clear breakdown: missing core changes, no Ollama routing inpackages/llm/src/client.ts, no backward-compat migration, no tests, no docs update. - Round 2: Contributor pushed an updated commit (force-pushed amend, single commit overwriting the previous one). Good news:
packages/core+packages/llmchanges now present, typecheck errors dropped from 40+ to 3. Bad news: the force-push from an outdated base branch wiped out unrelated work frommain— specificallydocs/contributor-walkthrough.md(deleted entirely, -465 lines) plus pieces of README.md, CONTRIBUTING.md,.github/ISSUE_TEMPLATE/config.yml, andpackage.json. Plus apnpm-workspace.yamlchange (pnpm 10 migration) bundled in scope unrelated to the feature. Requested changes again: rebase on currentmainto restore the deleted files, fix the 3 remaining typecheck errors, pull the pnpm migration into a separate PR.
Currently awaiting Round 3. Worth flagging as a learning moment: the PR template asks contributors to confirm typecheck passes before submission, but trust-but-verify — pull the branch and run pnpm -r exec tsc --noEmit yourself before approving anything.
The OSS project now has a matching commercial / marketing presence. llmwiki.cc is a separate private repo (github.com/ddsyasas/llmwiki-cloud) with a Phase-2 marketing site (hero + 3 operations + gap analysis + waitlist form). The strategy is documented in detail in the cloud repo's docs/STRATEGY.md (which lives there, not here — it's business strategy).
The two repos are connected via npm packages, not git:
- This OSS repo publishes
@syasas/llm-wiki-{core,llm,ingestion}to npm (rename + publish step still pending — Phase 1 of the cloud plan) - The cloud repo will eventually import them like any other npm dependency
- OSS updates flow to cloud via Renovate auto-PRs — zero code copying, zero git syncing
- Marketing site shares brand (logos, fonts, colors) with the in-app design
Four touchpoints reference llmwiki.cc from this OSS repo:
- GitHub repo's Website field (set via
gh repo edit --homepage) - README badges row (new
site • llmwiki.ccchip alongside npm/license/release/PRs) - README install section restructured to "Three paths" with hosted at top
- In-app
/aboutpage footer line
Future npm publishes will set the package's homepage field to llmwiki.cc (currently still points at GitHub README on already-published 1.1.1). Done in the publish script — picks up next publish.
The character of the OSS project stays unchanged: local-first, BYOK, MIT, no telemetry. The hosted version is a complement, not a re-pitch. Deliberately did NOT touch docs/01-vision.md, docs/04-features-v1.md, in-app /help, in-app /developers — those serve audiences that came here specifically for the local-first promise.
| Surface | Status |
|---|---|
| Code | v1.1.1 on npm, cross-platform verified |
| Repo | Public, polished (hero + badges + screenshots + contributor infra) |
| Tests | 194 passing (158 core + 25 llm + 11 ingestion, 1 known chokidar flake) |
| First external contribution | PR #1 in review (Round 2 sent back, awaiting Round 3) |
| Marketing site | llmwiki.cc live (Phase 2 of cloud plan shipped) |
| Hosted product | Not built yet (Phase 4, gated by waitlist demand signal) |
| Privacy posture | Noreply commits, business email for contact, no personal Gmails public |
The Ollama PR (#2 from @savindugeethma) merged. Then a real user (the maintainer himself) hit the failure mode the PR didn't prevent: selecting Ollama in Settings without realizing Ollama itself has to be installed and running locally first, getting a generic "Connection error" with no in-app indication of what to do. Sprint V closes that gap and ships the feature as v1.2.0 — first minor version bump since the V1.x line started.
Round 2 of PR #2 passed all 4 verification checks from the prdocs playbook:
git diff main -- apps/web/src/app/about/page.tsxempty (branch caught up with main after rebase)git diff main -- packages/llm/src/client.ts | grep -E "loca.lt|Bypass-Tunnel"empty (LocalTunnel-specific code removed)pnpm -r exec tsc --noEmitexit 0- All test suites pass (159 + 25 + 11 = 195) with 1 new test for the legacy-string-shape migration
Bonus: the contributor also updated docs/05-llm-integration.md with an Ollama section, which was on the post-merge nice-to-have list. Squash-merged via gh pr merge 2 --squash --delete-branch. PR #1 closed pointing at #2.
What landed in main (28 files, +464/-172):
packages/core/src/config.ts— newModelProvider+ModelSlotConfigtypes;WikiSettings.defaultModels[slot]shape goes fromstringto{ provider, model };parseWikiSettingsincludes a backward-compat handler that wraps legacy plain-string slot values in{ provider: "openrouter", model: raw }so existing user configs auto-upgradepackages/llm/src/client.ts—createClient(apiKey, provider?)routes tohttp://localhost:11434/v1(orOLLAMA_BASE_URLenv var) when provider is"ollama";mapSdkErrornow takes baseURL and uses it to label errors as Ollama vs OpenRouter- Eight
apps/web/src/app/api/routes updated to readmodel.provider+model.modelfrom the new shape apps/web/src/components/settings/models-tab.tsx— Provider dropdown (OpenRouter / Ollama (Local)) per slot + 10 suggested Ollama models (llama3, mistral, phi3, llava, etc.)
Worth recording for future PR reviews: the 4-round process (PR#1 R1 → PR#1 R2 → PR#2 R1 → PR#2 R2) worked. Contributor accepted feedback directly across rounds, switched from force-pushed amends to separate commits when asked, dropped unrelated scope (pnpm-workspace.yaml migration) when called out, and removed LocalTunnel-specific code without arguing. Healthy signal.
Right after merge, opened the app and tried the new feature. Set the chat slot to Ollama, sent a message, got "Connection error" because Ollama wasn't installed. The failure mode was generic enough that a less-technical user would have no idea what to do — no in-app hint that Ollama needs separate installation, no link to setup docs, just a connection error from somewhere deep in the OpenAI SDK.
Three surfaces added in one commit (4 files, +633/-7):
-
Amber banner in Settings → Models (
models-tab.tsx) — renders when one or more slots currently use Ollama (reads fromoriginalsaved state, not draft). Lists which slots are affected ("ingest, chat") and links to the setup guide. Color is amber not red — it's a "heads-up" not "you broke something." -
New
/local-modelspage (apps/web/src/app/local-models/page.tsx) — ~500-line standalone setup guide. 8 TOC sections: when-to-use (local vs cloud trade-offs), install (macOS / Linux / Windows commands), pull a model, hardware requirements table covering 9 models with disk / RAM (min + recommended) / Apple-Silicon tokens-per-sec / CPU-only tokens-per-sec / "best for" columns, connect-to-LLM-Wiki walkthrough, custom OLLAMA_BASE_URL for tunneled/networked setups, troubleshooting (the 6 errors users actually hit including the "Connection error" that triggered this whole sprint), external resources. -
Cross-references so users find /local-models from the right places — TOC entry + Settings-Models bullet in
/helplinking inline, plus a brief mention in/developers"Swapping LLM providers" section clarifying that Ollama is first-class via the provider field.
The hardware table is the key piece for non-technical users: a quick picker tells someone with an old 8 GB laptop to start with phi3, someone with Apple Silicon 16 GB to use llama3, someone with a Mac Studio 64 GB unified to try llama3:70b. Numbers are rough order-of-magnitude (tokens/sec ±50%) but enough to set expectations before the user downloads a 40 GB model their machine can't actually run.
First minor version bump since 1.1.0 went out. Justified by the new feature surface (Ollama as a provider + the UX scaffolding around it). Following the established release workflow (see sprint S for the publish gotchas):
- Bump
apps/web/package.json1.1.1 → 1.2.0 - Bump
apps/web/src/components/footer.tsxAPP_VERSION to match pnpm installto pick up the version change in the lockfilepnpm --filter @llm-wiki/web buildto rebuild standalonenode apps/web/scripts/build-publish-tarball.mjsto assembleapps/web/dist-publish/npm packinsidedist-publish/to producesyasas-llm-wiki-1.2.0.tgz- Smoke test by installing the tarball into a clean
/tmpfolder + runningllm-wiki version gh release create v1.2.0 ...with the tarball as the only asset- User-triggered
npm publish --access public— passkey 2FA prompts for Touch ID
What users get on npm install -g @syasas/llm-wiki@latest:
- All of v1.1.1 (still works)
- Plus Ollama support via Settings → Models → Provider dropdown
- Plus the amber banner if any slot is configured for Ollama
- Plus
/local-modelssetup guide with full install + hardware info - Plus
/helpand/developerscross-references to the setup guide
Wiki on-disk format unchanged within v1.x, so existing v1.0/v1.1 users upgrade with zero migration friction. The defaultModels shape change is the only thing that touches stored data, and the parser handles old shape transparently.
| Surface | Status |
|---|---|
| OSS npm | v1.2.0 published |
| First external contribution | ✅ merged (PR #2 from @savindugeethma) |
| Ollama support | Live with full UX scaffolding around it |
| Marketing site llmwiki.cc | Live (Phase 2 of cloud plan) |
| Hosted product | Not built; pending demand validation (Phase 3 of cloud plan) |
| Tests | 195 passing |
Note: The work-needed list has been consolidated into
docs/14-roadmap.md. The questions below are design / architecture questions that don't translate cleanly into a roadmap entry — when in doubt, prefer the roadmap.
- Should we ship
next-themesinstead of the homegrown ThemeProvider? Tradeoff: ~5KB dep vs. zero deps + a few extra lines. - Should models.ts pull pricing from OpenRouter's
/modelsendpoint at runtime instead of hardcoding? More correct but adds latency + a cache layer. - Should the CLI use
tsxto import from@llm-wiki/coreinstead of inlining init logic? Eliminates duplication but adds a runtime dep. - Is there value in a "watch mode" for sources? (User drops files into
raw/from their file manager, app auto-ingests.) Mentioned in docs but deferred. - Per-source detail view: re-ingest, delete (with
page_sourcesunlink), and "what pages did this produce" drill-down. Audited-but-deferred from 2026-05-24. - Should non-home routes also redirect to onboarding when the key/topic are missing? Today they fail loud at the API layer. Middleware-level redirect would be friendlier for direct bookmarks.
- Per-page diff view when the LLM updates a page during ingest — docs/04 lists this as P1. Backups are written to
.llm-wiki/page-history/already; a "show me what changed" view is the missing UI. Probably 1-2 hours. - Approval gate for ingest — also docs/04 P1. Today ingest is auto-apply; a "preview changes first" mode would suit researchers who don't trust the LLM blindly.
- Wiki graph view (docs/04 P1) — force-directed visualization of [[wikilinks]] between pages. Nice-to-have, not critical. d3 or react-force-graph.
- Lint history view beyond the inline "Recent runs" panel — a dedicated
/lint/historyor sparkline trend chart on the Lint page. Only worth building if the inline panel turns out to feel insufficient. - Production build — V1 ships via
pnpm devtoday.next build+next startwould dramatically improve perceived perf (loading skeletons are a band-aid for dev-mode lazy compile). Standalone-bundle 500s noted in earlier sessions are still unresolved — needs Step 15 push. - CLI polish (Step 15) —
llm-wiki startworks butpnpm packinstall has unresolvedworkspace:*dep issues. Blocks npm publish. - Tauri / desktop installer (V2) — would solve the production-build problem AND give us native file-open helpers (currently the source-detail page can't open the raw file in the user's editor because browsers can't open arbitrary file:// paths from a web context).
- Sources detail enhancements (carryover from audit #5) — re-ingest, delete (with
page_sourcesunlink + page-history backup of affected pages), extracted-markdown sibling view for binary formats. - Loading skeleton coverage — added to root, wiki, wiki/[slug], lint, log, sources, chats/[id]. NOT added to: schema, query, settings, chats (index), home, about, help, developers — those are client-heavy or instant-render. Revisit if any of them start to feel laggy.
Read CLAUDE.md at the repo root + docs/01-vision.md through docs/11-attribution-license.md. They're the design contract; this dev log captures execution + drift from it.