Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions .opencode/plans/notes-ui-overhaul.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Notes UI Overhaul (scope: items 1–6 + 9 from ideas review)

State at pause: red test written — internal/api/media_test.go (5 cases,
build-fails on missing Server.mediaHandler). All research done.

## Batch 1 — server (TDD, red test exists)

1. internal/api/media.go — mediaHandler (test-ready):
- GET /v1/media?path=media/pic.jpg
- path.Clean("/"+rel) pin against s.vault.MediaPath(); prefix check -> 400
- extension allowlist -> Content-Type (jpg/jpeg/png/gif/webp/heic/pdf),
unsupported ext -> 400; missing -> 404 via http.ServeFile
- Cache-Control: private, max-age=3600
- register r.Get("/v1/media", s.mediaHandler) inside authed /v1 group

## Batch 2 — NoteView upgrades (UI + small server)

2. RelatedLink.types (internal/api/notes.go):
- noteHandler: SELECT result FROM jobs WHERE type='connections' AND
note_path=? ORDER BY created_at DESC LIMIT 1 (new queue method
GetConnectionsResultByPath or reuse); parse
{"connections":[{note_path,type}]} -> map target path -> []string types
- RelatedLink gains Types []string `json:"types,omitempty"`
3. React: image preview (api.ts + NoteView):
- client.mediaUrl(path): fetch('/v1/media?path=..', token header) -> blob
-> objectURL; NoteView renders <img> at top when note.type==='image'
&& note.source_file; revoke on unmount
4. Entity chips (NoteView + App + SearchView):
- entities from note response? NOTE: check NoteResponse lacks entities —
reader NoteContent.Entities exists; add Entities map to NoteResponse
(from note.Entities) with people/amounts/dates arrays
- NoteView chips: people/amounts/dates rows (gold people, neutral others)
- tap person -> App onSearchQuery flow: App gains searchQuery-driven
initialQuery passed to SearchView (new prop) which fires search on
mount; chip click = switch tab + prefill
5. Copy raw button: header icon (Copy) -> navigator.clipboard.writeText
(note.raw + frontmatter-ish markdown) -> toast "copied"
6. Source URL: show domain only (new URL(url).hostname), title attr full
7. Linked notes: move ABOVE content (right after linked chips = before
excerpt/full toggle); chip shows type badges from related_links[].types
(icons: contradiction GitCompare/Zap, revisit Repeat2, follow_up Clock,
person User, similar Sparkles); keep title + click behavior

## Batch 3 — typography pass (item 9)

8. SheetContent: sm:max-w-[560px] on md+ screens
9. note-section typography: heading scale (11px mono uppercase exists —
keep), body line-height 1.7, max-width for raw prose, Raw section
rendered via ReactMarkdown ALWAYS (fix query-time plain-text
inconsistency: wrap markdown render then apply highlighting via
HTML-escaped text-node walk — or simpler: render markdown, skip
highlighting in full view when markdown; keep highlight in excerpt
view) — DECIDE at impl: markdown always, highlight only ExcerptView
10. Footer: subtle divider + smaller mono

## Batch 4 — wrap

11. openapi: /v1/media endpoint + related_links.types; UI_SPEC note-view
section rewrite; docs sync
12. vitest: media blob preview mock, entity chips render, type badges;
Go: media handler tests green, notes.go types test
13. rebuild assets; live verify: image note preview, chips -> search,
linked notes above content with badges; full suites; commit(s)
47 changes: 47 additions & 0 deletions .opencode/plans/v1.2-connections-4-6.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# v1.2 Slice 1 — Connection Types 4–6

Decisions: all three types this round; priority = person 5 > contradiction 4 >
follow_up 3 ≈ amount 3 > similar 2 > revisit 1.

## Step 1 — Type 6: revisit (pure computation)
- internal/connections/revisit.go: `findRevisit(similar []Connection, cutoff) *Connection`
- Reuses semantic-similar matches already fetched in Find; if >=3 matches span
>6 months (oldest..newest), emit one connection:
label "you've returned to this idea N times since [date]", NotePath=oldest,
excerpt from newest (bookends)
- Table tests: span math, threshold boundary, <3 matches, no-span case

## Step 2 — Type 5: follow-up
- queue.Store += FindFollowupCandidates(ctx, person, keywords, before, limit)
-> FTS5 intent keywords ("follow up","todo","need to","will send","promise")
AND notes sharing the person entity, created before now-14d, excluding self
- followup.go detector: for each shared person of the new note ->
candidates -> check NO subsequent capture mentions that person after intent
date (existing GetNotesByEntity covers) -> emit
"you planned to follow up with [name] — no record of this happening"
- Tests incl. completed-intent exclusion

## Step 3 — Type 4: contradiction
- constants: ContradictionCheck system prompt -> strict JSON verdict
- connections.go += narrow interface ContradictionChecker
{GenerateWithSystemTemp}; Find signature gains optional checker (nil =
type skipped); worker passes adapter over w.llm at temp 0.2
- contradiction.go: top-5 similar above cfg.ContradictionThreshold (default
0.80); one LLM call per candidate; parse {"contradicts":bool} defensively;
fail-open per candidate; label "contradicts something you wrote [date]"
- Tests with fake checker (yes/no/garbage/error paths)

## Step 4 — Config + plumbing
- ConnectionsTypes += Contradiction/FollowUp/Revisit (*bool, nil=on)
- ConnectionsConfig += ContradictionThreshold float64 (default 0.80)
- priority map: person 5, contradiction 4, follow_up 3, amount 3, similar 2,
revisit 1
- Worker processConnections passes checker; config.example.yaml + SPEC
config section + UI_SPEC flare note updated

## Step 5 — Live verify + wrap
- Capture a note echoing an old Bob/Alice topic against testdata vault;
expect revisit/follow_up surfacing in result JSON + flares
- Full suite, lint; commits sequenced 1-4; docs commit last

Out of scope: voice notes, PDF (later v1.2 slices).
63 changes: 52 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@

> Your private treasury of thought. Local, secure, yours.

*New here? Try the [silly version](SILLY_README.md) of this readme — no jargon allowed.*

<!-- TODO: add asciinema recording or terminal screenshot here -->
<img src="charm-vhs-tape/khayal.gif" alt="Khayal demo" />

A local-first, privacy-focused second brain. Capture anything — text, images, URLs. Process locally with your own LLM. Search semantically and by keyword. Your data never leaves your machine.
A local-first, privacy-focused second brain. Capture anything — text, images, URLs, PDFs, voice. Process locally with your own LLM: tags, summaries, entities, and proactive connections that resurface what you've forgotten. Search semantically and by keyword, or ask AI questions over your own notes. Your data never leaves your machine.

## How It Works

Expand Down Expand Up @@ -42,14 +44,20 @@ Khayal and Obsidian are complementary. Khayal is a capture and retrieval layer

## Features

- **Capture** — Text, images, URLs, articles with zero friction
- **Process** — Tags, summaries, key ideas, entities via local LLM
- **Capture** — Text, images, URLs, articles with zero friction (voice notes and PDF ingestion on the roadmap)
- **Process** — Tags, summaries, key ideas, entities (people, amounts, dates, places, orgs, URLs) via local LLM
- **Proactive connections** — after every capture, khayal resurfaces related thoughts, shared people, matching amounts, **contradictions of things you wrote**, unfinished follow-ups, and ideas you keep revisiting
- **Capture intelligence** — relative dates resolved at capture; an LLM-maintained memory file keeps naming consistent across months
- **Search** — Keyword (FTS5) + semantic (chunk-level embeddings) hybrid, with passage-level excerpts
- **Store** — Plain markdown in your vault, yours forever
- **AI answers** — on-demand answers above search results, grounded in your own notes with `[n]` citations; explicit CTA, never automatic, never breaks search
- **Store** — Plain markdown in your vault, yours forever (Obsidian-friendly, with connection wikilinks written into frontmatter)
- **Vault care** — health report, broken-link repair, orphaned-media cleanup, duplicate detection, soft-delete with trash
- **Backup** — encrypted (age) vault/database/config backups with additive-merge restore
- **PWA** — Web interface, works offline, update notifications
- Live queue over WebSocket (job status streams in; polling fallback)
- AI answer with skeleton loading, connection flares on the queue, linked notes with reasons, entity chips, image previews
- Offline capture queue (syncs when server is back)
- Works as installable PWA on iOS and desktop
- Live pipeline visualization for queued notes
- Optional Face ID / Touch ID app lock (WebAuthn PRF) that encrypts the token at rest
- **CLI** — Full client (`kl`) + server admin (`khayal`)
- **Updates** — Built-in update checker via GitHub releases
Expand All @@ -74,6 +82,9 @@ curl -fsSL https://ollama.com/install.sh | sh
ollama pull nomic-embed-text
ollama pull qwen2.5:3b
ollama pull moondream

# optional: a larger model just for memory consolidation (recommended)
ollama pull qwen2.5:7b
```

### 2. Install Khayal
Expand Down Expand Up @@ -175,6 +186,20 @@ search:
chunk_min_words: 50
chunk_overlap_words: 35

connections:
enabled: true # proactive connections after every capture
similarity_threshold: 0.72
types: # toggle each detector independently
similar: true
person: true
amount: true
contradiction: true
follow_up: true
revisit: true

memory:
enabled: true # LLM context memory + memory.md consolidation

log:
level: info
file: logs/khayal.log
Expand All @@ -188,11 +213,14 @@ See [config.example.yaml](config.example.yaml) for all options.

| Location | Content |
|---|---|
| `~/Documents/brain/khayal/` | Your notes (plain markdown + media) |
| `~/Documents/brain/khayal/memory.md` | LLM-maintained memory (editable by hand) |
| `~/Documents/brain/khayal/.khayal-trash/` | Soft-deleted notes (recoverable) |
| `~/.config/khayal/khayal.db` | Search index + embeddings |
| `~/.config/khayal/config.yaml` | Server configuration |
| `~/.config/khayal/logs/` | Server logs |

All on your machine. Back up the vault directory — it's plain markdown.
All on your machine. Back up the vault directory — it's plain markdown (or use `khayal backup --encrypt`).

## Commands

Expand All @@ -207,6 +235,12 @@ All on your machine. Back up the vault directory — it's plain markdown.
| `khayal status` | Server status + update check |
| `khayal reindex` | Rebuild search index (FTS + chunk embeddings) |
| `khayal config` | View config (token redacted) |
| `khayal vault health` | Vault health report (notes, indexed %, orphans, broken links) |
| `khayal vault fix-links` | Remove broken wikilinks (dry-run by default) |
| `khayal vault clean-media` | Move orphaned media files to trash |
| `khayal vault show-duplicates` | Show potential duplicate notes |
| `khayal backup --dest <path>` | Backup vault, database, config (`--encrypt` for age encryption) |
| `khayal restore --from <path>` | Restore from backup (additive merge, refuses while running) |

### Client (`kl`)

Expand All @@ -215,7 +249,8 @@ All on your machine. Back up the vault directory — it's plain markdown.
| `kl "text"` | Capture text |
| `kl url "https://..."` | Capture URL |
| `kl image <path>` | Capture image |
| `kl search "query"` | Search vault |
| `kl search "query"` | Search vault (`--answer` adds a grounded AI answer) |
| `kl delete <path-or-id>` | Soft-delete a note (moved to `.khayal-trash/`) |
| `kl recent` | Recent captures |
| `kl stats` | Vault statistics |
| `kl status` | Server status + update check |
Expand All @@ -238,7 +273,10 @@ tail -f ~/.config/khayal/logs/khayal.log # view logs
Web interface at `http://127.0.0.1:1133`

- Capture text, URLs, images
- Search with excerpts
- Search with excerpts + on-demand **AI answers** with citations
- **Live queue** — job status streams over WebSocket, connection flares on finished captures
- **Note reader** — image previews, entity chips that jump to search, linked notes with reasons, copy-as-markdown
- **Delete** — two-step confirm, recoverable from trash
- Offline queue (IndexedDB)
- Update notification icon

Expand All @@ -257,12 +295,15 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for details.

## Roadmap

- **v1.0** — Core capture, search, CLI, PWA
- **v1.1** — Chunking, entity extraction, proactive connections
- **v1.0** ✅ — Core capture, search, CLI, PWA
- **v1.1** ✅ — Chunking, entity extraction, proactive connections, capture intelligence, AI answers, delete, vault commands, encrypted backups
- **v1.2** 🚧 — Contradiction / follow-up / revisit connections ✅ · voice notes · PDF ingestion
- **v1.3** — Graph connections, backlinks
- **v1.4** — YouTube / video ingestion
- **v1.5** — Browser extension
- **v2.0** — Setup wizard UI

See [SPEC.md](docs/SPEC.md) for full roadmap.
See [SPEC.md](docs/SPEC.md) for the full roadmap.

## License

Expand Down
68 changes: 68 additions & 0 deletions SILLY_README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Khayal — Explained Like You're Five

(This is the silly version. The serious version is in [README.md](README.md).)

## What is this thing?

Imagine you have a magic notebook.

Whenever you think something — "Tommy owes me a dollar", "I love the new treehouse plan", "need to call grandma" — you tell it to the notebook, and the notebook writes it down for you. Forever.

That's Khayal. It's a magic notebook that lives inside your computer.

## But wait, how does it WRITE for me?

There's a little robot helper living in your computer too. Its name is Ollama.

When you tell Khayal a thought, the robot:
1. Writes it down nicely (with a title and a summary)
2. Picks out the important words (like "Tommy" and "one dollar")
3. Puts it in a special list so you can find it later

The robot lives in YOUR computer. It never tells anyone else your secrets. Not even a little bit.

## The coolest part: the notebook REMEMBERS stuff you forgot

Say three weeks ago you wrote: "I want to build a treehouse."

Then today you write: "I was thinking about building a treehouse again!"

Khayal goes: "HEY! You thought about this before! Here's the old note!"

Sometimes it even says: "Um, last month you wrote the OPPOSITE of this. Did you change your mind?" That's called a contradiction, and catching them is the notebook's favorite game.

It also remembers things about people. If you wrote "need to call grandma" a whole month ago and you never called her, Khayal taps you on the shoulder: "You said you'd call grandma... and then you didn't."

(Sorry, grandma.)

## Finding old thoughts

You know how finding your favorite sock in a huge pile of laundry is hard?

Finding thoughts in Khayal is the opposite of that. You type what you remember — "treehouse", "grandma", "five dollars" — and BOOM, there it is. You can even ask questions like "what did I say about the treehouse?" and it answers using YOUR notes, with little numbers showing which note said what.

## Where do the thoughts live?

In your computer, in a folder, as regular story-files (called markdown). Not in a cloud far away. Not on someone else's computer. YOUR computer.

You can even open the folder and read the files yourself, or draw on them with crayons (that's called editing).

If you get scared you'll lose them, there's a treasure-chest command that copies everything and locks it up tight with a secret key.

## If you mess up

Deleted something by accident? It goes to the trash folder, not to the void. Like throwing paper in a wastebasket instead of a fire.

## The short version

- Khayal = magic notebook in your computer
- Ollama = the robot helper that does the writing
- Your thoughts = safe at home, never sent anywhere
- Old thoughts = easy to find, and the notebook reminds you when you forget
- Grandma = still waiting for that call

Now go capture a thought before you forget it. That's literally the whole point.

---

*Grown-ups: read the real [README](README.md) for how to install and run it.*
5 changes: 4 additions & 1 deletion config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,14 @@ connections:
min_age_days: 7 # never surface notes younger than this (explicit 0 = same-day allowed)
max_per_capture: 3 # strict quality gate
similarity_threshold: 0.72 # raw-cosine cutoff (unrelated ~0.49, related ~0.79)
contradiction_threshold: 0.80 # semantic floor for contradiction candidates (v1.2)
types:
similar: true # toggle each type independently
person: true
amount: true # only surfaces when corroborated by a shared person or high similarity

contradiction: true # LLM verdicts (v1.2)
follow_up: true # unfinished follow-up detection (v1.2)
revisit: true # recurring-idea detection (v1.2)
log:
level: info
worker_level: info
Expand Down
Loading
Loading