olai GitHub

olai

A git-native outliner: memory for the AI era.

Outlines are .jsonl files, one line per node. Longer content is Markdown files referenced from the outline. Everything is tracked in git, and agents edit through the same operations layer as the keyboard.

nix run github:juspay/olai -- web path/to/outlines
kitchen.jsonl
Kitchen renovation 2/4
Demolition
Pick the counter stone
Order the new cabinets #blocking2026-08-07
Measure twice — the corner unit is the risky one.
▤ cabinets.md
Book the plumber
Paint samples on the north wall

The file format

Each outline is a .jsonl file: one JSON object per line, one node per line. Nodes have stable ids and parent pointers, so line-based git merges are safe and moving a subtree changes one field. See the format spec.

{"id":"kitchen","ord":"a","title":"Kitchen renovation"}
{"id":"demo","parent":"kitchen","ord":"a0",
 "title":"Demolition","done":"2026-08-09T14:02:11-04:00"}
{"id":"order","parent":"kitchen","ord":"a1",
 "title":"Order the new cabinets #blocking",
 "doing":true,"date":"2026-08-07","after":["demo"],
 "doc":"cabinets.md",
 "desc":"Measure twice — the corner unit is the risky one."}

The outline above, as stored on disk.

$ git diff
-{"id":"order","parent":"kitchen","ord":"a1", … }
+{"id":"order","parent":"now","ord":"a1", … }

Moving a subtree changes the parent field. Children reference order by id and are unaffected.

Compared to Workflowy and Dynalist

olai keeps the outlining model those tools established — indent, move, mirror, check off. The difference is where the data lives and what can reach it.

Workflowy, Dynalistolai
StorageA hosted databasePlain files in a directory you choose
SyncTheir cloudgit, or anything that moves files
HistoryIn-app version historygit log; every edit is a reviewable commit
Concurrent editsResolved by their serverLine-based git merges
Programmatic accessVendor API, where one existsAny agent over ACP or MCP; any text tool
SourceProprietaryOpen source (AGPL-3.0)

Markdown documents

Notes are Markdown. Longer content goes in .md files, referenced from the outline by the doc field. Each document gets its own page and appears in the sidebar's file tree.

# Cabinet specs

The corner unit is the **risky one** — see
[the measurements](measurements.md).

- 600 mm base × 4
- corner carousel × 1

cabinets.md — a plain Markdown file next to the outline, editable with any editor.

/doc/cabinets.md · attached to “Order the new cabinets”

Cabinet specs

The corner unit is the risky one — see the measurements.

  • 600 mm base × 4
  • corner carousel × 1

Daily notes

A Markdown file named for a date — 2026-08-12.md, anywhere in the directory — is that day’s note. No setting says so, and it can live wherever your vault already puts it. Open the day and the note is on top, with everything dated that day underneath it. Relative links between your Markdown files keep working from there.

Daily/
  2026/
    08/
      2026-08-12.md   ← the 12th
      2026-08-10-recap.md  ← not a daily note

Convention, not configuration: the basename has to be the date and nothing else.

/d/2026-08-12

2026-08-12

Fog on the water. Only the ferry has a deadline, so it went first.

kitchen.jsonl
Book the plumber2026-08-12
Pick the counter stonedone2026-08-12T16:20:03-04:00

Task status

A node can carry one of three marks: done, doing, or todo. A node with no mark is not a task and gets no checkbox.

completed taskdone
task in progressdoing
task not startedtodo
a noteno mark
read this book 1/3
chapter one
the streams chapter
exercises

1/3 is a display annotation. A parent's status is never computed from its children.

⌘⇧Enter, on the row

no mark todo doing no mark

done is not a stop on the ring.

Two keys write all three. +Enter finishes something, and takes that back: done is the mark with an instant on it, which is what puts the work on that day’s page. ++Enter walks the mark on, one step round the answers a person gives about work they have not finished. The last stop is an answer rather than a gap — a node carrying no mark is not an unfinished one, and walking to it is how you say a row was never a task.

Finishing something is a thing you mean, not a thing to do on the way past, so nothing stamps a completion while you are walking round — and the walk will not take finished work backwards. Press it on a finished row and the write is refused, in the same words an agent gets: nothing should decide on your behalf that finished work is not finished. ⌘Enter takes the done off, and the walk carries on from there.

Dates, and what is owed

A node can carry a date: the day it is scheduled for. The mark says whether it is work. Read together they answer a third question, and no field stores the answer — /agenda is that reading, taken of every outline at once.

/agenda · 2026-08-12

overdue

kitchen.jsonl

Kitchen renovation

Order the new cabinets #blocking2026-08-07

today

kitchen.jsonl

Kitchen renovation

Book the plumber2026-08-12

trip.jsonl

Ferry tickets go on sale2026-08-12

upcoming

2026-08-15

Template the countertop2026-08-15
overdue(n) ⇔ n carries todo or doing
             ∧ day(n.date) < today

Derived when the page is drawn, stored nowhere, and spelled once — the agenda's first section and the tone of every date badge are the same predicate. day() is the first ten characters and the comparison is plain string order: dates are text.

Overdue is a definition, not a flag. Nobody sets it and nothing stores it. done extinguishes it by construction — finished work is late at nothing — and a date with no mark is an occurrence: a delivery, a birthday, a ferry. It draws no checkbox, its pill never takes the attention tone, and when its day passes it simply leaves. A day going by is not a failure of a bullet.

Three sections, because they are three pieces of news. Overdue is every slipped task in the directory, oldest first, grouped by the outline it lives in — the one answer no day page can give, because a slipped task is on a day nobody visits. Today is what today's day page holds, minus what is finished. Upcoming is the next seven days that have anything on them, each heading a link to that day's own page; days with nothing do not appear.

A task nobody dated is absent entirely: it has no when to be late against, and inventing one is what this format refuses to do. An empty agenda says Nothing is due. and offers nothing to press — every row on it lives somewhere else, and that is where it is changed.

A date is set on the row. Press the date beside a title and the picker opens in place under it; a row with no date has no pill to press, so its way in is the ••• menu — Set date…, or Change date… on a row that has one. What is written is the ten characters you picked, never a timestamp this app invented on the way, because the control is the browser’s own <input type="date"> and a date is text.

kitchen.jsonl
Book the plumber2026-08-12
Scheduled for 2026-08-19 Set date Cancel

Empty the box and the button becomes Clear date — the ••• menu's own verb, and the same write.

Keyboard editing

Click a title to edit it in place. Each edit is one operation through the same layer the agent uses, and each lands as a git commit.

Entercommit, and open the next line
Tab / Shift+Tabindent, or out again
Alt+Shift+/move a row among its siblings
+Ztake back your last edit here
+Entertick it off, or take that back
++Enterwalk the mark on: to do, then doing, then none
Shift+Enterwrite the note under it
Escdrop what you were typing
++Zput that edit back

Undo is an inverse rather than a restore: it replays the edit that reverses yours, judged against the outline as it is now — so it never takes back what an agent or another tab did in the meantime, and one that no longer fits says why.

Not everything has a key. Hover a row and the ••• in the gutter carries the rest: the three marks and clearing one, setting, changing or clearing a date — the same picker the row’s own pill opens — retiring one placement of a mirrored node, archiving a subtree — it asks first, and says how many rows go with it — and copying the subtree as tab-indented plain text. Those go on the same undo stack, so ⌘Z takes a menu edit back too. There is no delete: archiving moves the subtree to an Archive.jsonl keeping its ids, so nothing that pointed at it breaks. Every one of those is the same operation an agent would perform, which is a rule olai holds itself to: both faces can do the same things to a directory.

Preferences, and fifteen palettes

One door in the app header, and two rows behind it: the theme, and what a page does with finished work. Both are this browser’s, which is the panel’s own first claim about itself: two machines reading the same outlines are entitled to look different, and the served directory neither knows nor cares.

preferences
Theme
leaf manuscript chalk pitch light dark vintage catppuccin chocolate hacker matcha moon neo one-dark robot

chalk is in force. Every colour on the page is this one table, so a pick repaints all of them at once.

Done
VisibleHidden

Pages open with finished work shown. Any page’s own Done switch still overrides it.

These are this browser’s. They are stored here, reach every tab you have olai open in, and are never sent to the server.

kitchen.jsonl · pitch
Kitchen renovation 2/4
Demolition
Order the new cabinets #blocking2026-08-07
Book the plumber
kitchen.jsonl · manuscript
Kitchen renovation 2/4
Demolition
Order the new cabinets #blocking2026-08-07
Book the plumber

The same rows, in two of the fifteen. A palette is eight colours with a name; picking one writes data-theme on <html>, and the stored pick lands there before the first paint, so a reload never flashes the wrong one.

Seven of the fifteen are dark grounds. There is no system chip and no auto: the operating system’s preference is not consulted at all, because two ways to be dark are two answers that can disagree — with each other, and with the reader who already said what they wanted. A page nobody has picked on reads in chalk, which is the default and the one palette that promises WCAG AA on every pair it paints; a unit test holds it to that, pair by pair.

Done: Visible or Hidden is a default, not a second switch. A reading belongs to a page — folds start fresh when you zoom — but “I do not want to look at finished work” is a claim about the reader, and pressing it on every page opened is what a preference exists to stop. So it moves the page you are on and leaves alone any page whose own Done switch you have already pressed. Hiding finished work is a row not drawn: never a node marked, never a file written.

Agents, over ACP

The chat panel connects to any agent that speaks ACP. Claude Code is bundled as the default; set OLAI_ACP_AGENT to use a different agent CLI. Olai hands an agent no filesystem — a fixed set of tools that can only name nodes — so every write it asks olai for is validated before it lands. What a coding assistant brings with it is its own: it edits files on the authority it already has in a terminal, and olai neither grants that nor pretends it away. It shows it.

Files go in by paste, by drag, or by the picker: drag a screenshot, a PDF or a text file onto the conversation — several at once if you like — and the panel lights up the area that will take it while you are still holding it. The agent is handed the path to read, not the bytes. They live in a temporary directory of that conversation's own, never under the directory being served, and go away with it. A picture shows itself; a document shows its name and size. Anything olai will not take is refused where you dropped it, by name.

A tool call is one folded line, and what the call changed is not folded away. There are two kinds of change and the panel draws them differently, because they are different things. A write through olai’s node tools shows what changed about the node, in the same words the commit panel uses for the same edit — marked done, note rewritten, moved — and the outline it landed in. A file the agent rewrote with its own tools shows a diff: the path, how many lines came and went, and the change itself, with the unchanged stretches collapsed so the first thing you read is what moved. It is trimmed to a few rows and expands where it stands. An outline never gets a text diff — one line per node means a text diff of one is a single enormous line with everything on it changing at once.

A tool server that does not arrive is named too. If this machine has an MCP server the conversation was meant to be handed — kolu’s terminals, when kolu is running here — and it would not answer, the panel says so under its header, with the reason the server itself gave and the file that was probed. A session quietly short of its tools looks exactly like a healthy one, and the difference is not something to go and read a log for.

chat · any ACP agent
plumber's booked — tick it off
set_done · book-plumber
Book the plumbermarked done
kitchen.jsonl
Done. Marked “Book the plumber” as done.
the base units are carcass-only — say so in the doc
Edit · cabinets.md

cabinets.md+3−2

1 # Cabinet specs
⋯ 3 unchanged
6-- 600 mm base × 4
6+- 600 mm base × 4, carcass only
7+- filler strip × 2
8 - corner carousel × 1

+4 more lines

Added the carcass note and the filler strips.
kitchen.jsonl · live
Kitchen renovation 3/4
Order the new cabinets #blocking
Book the plumber

The open page updates because the file on disk changed. The keyboard and the agent write through the same operations layer.

Git integration

Writes go to disk immediately and are committed when you choose — everything the repository has waiting, outlines and the files beside them, ticked one by one if you want a smaller commit, and pushed from the same panel. Commit messages are prefixed with olai:, so git log --grep '^olai' shows the audit trail.

┌─ Changes ─────────────────────────────────┐
│ olai: Outlines as a collection done       │
│   · chat agent · 12m ago · 1a2b3c4        │
│                                           │
│ OUTLINES ─────────────────────────────    │
│ ☑ roadmap.jsonl                           │
│   ✓  Outlines as a collection    done     │
│   ✎  Notes: one state, same line  note    │
│   +  Kolu integration: auto-…    created  │
│                                           │
│ OTHER FILES ──────────────────────────    │
│ ☑ README.md                    modified   │
│ ☐ notes/scratch.md            untracked   │
│                                           │
│ 2 commits not on origin/master   [ Push ] │
│         [ Commit 3 changes · 1 file ]     │
└───────────────────────────────────────────┘
$ claude mcp add olai -- olai mcp ~/outlines

The same tools are available over MCP for agents running in a terminal. There is no write CLI.