todo.open

A local-first task server with CLI, TUI, and web UI.
Your tasks are plain JSONL on disk — no database, no lock-in.

$ todoopen task create --title "Refactor auth module" --priority high
Created task_a1b2: Refactor auth module

$ todoopen task list
ID          TITLE                  STATUS      PRIORITY
task_a1b2   Refactor auth module   open        high

# open the web UI
$ todoopen web
Server listening on http://127.0.0.1:8080

# or the terminal UI
$ todoopen tui
Server listening · TUI connected

Install

Pick your preferred package manager.

$ npm install -g @justestif/todo-open
# install
$ mise use -g go:github.com/justEstif/todo-open/cmd/todoopen@latest
$ mise reshim

# update to latest (clear cache first)
$ mise cache clear && mise use -g go:github.com/justEstif/todo-open/cmd/todoopen@latest
$ mise reshim
$ git clone https://github.com/justEstif/todo-open.git
$ cd todo-open
$ go build ./cmd/todoopen ./cmd/todoopen-server

Then start the server:

$ todoopen web
Server listening on http://127.0.0.1:8080

Three interfaces, one API

CLI, web UI, and TUI all talk to the same local server. All stay in sync via SSE.

CLI

Create, list, update, and complete tasks from the command line. Scriptable, pipeable, fast.

Web UI

Browser-based interface with live SSE updates. Quick capture, search, filter, inline edit. No page reloads.

Terminal UI

A Bubble Tea TUI: browse, create, edit, navigate deps — all from the terminal. Starts the server automatically.

todo.open web UI screenshot showing task list, quick capture, and edit panel
# CLI
$ todoopen task create --title "Write tests" --priority high
$ todoopen task list
$ todoopen task done task_a1b2

# Web UI
$ todoopen web

# TUI
$ todoopen tui

# or point any of them at an existing server
$ todoopen tui --server http://127.0.0.1:8080

How it works

One server. Plain files. Everything else is a client.

  .todoopen/tasks.jsonl      ← plain text, yours forever
           │
     todoopen server         ← local HTTP :8080
     (REST + SSE)
           │
     ┌─────┼──────────────┬──────────┐
    CLI   web UI        TUI      adapters
         (live SSE)  (live SSE)  sync/view
Plain files

Tasks are stored as tasks.jsonl — one JSON object per line. No database, no daemon to manage. Open the file in any editor, grep it, version it.

Open API

Full REST + Server-Sent Events. Any tool that can speak HTTP can create, query, and watch tasks. curl is a valid client.

Live updates

Web UI and TUI subscribe to /v1/tasks/events via SSE — task changes appear instantly without polling.

Your data

No cloud dependency, no account required. Tasks stay on your filesystem. Export is just copying a file.

Adapters

Extend sync and view behavior with separate binaries. Write them in any language.

Adapter Kind What it does
todoopen-adapter-sync-git sync Push/pull tasks.jsonl to a git repo
todoopen-adapter-sync-s3 sync Sync workspace to an S3 bucket
todoopen-adapter-view-markdown view Render tasks as a TASKS.md file
build your own sync view Any language, any backend — just speak the protocol

The contracts are small — implement Name() and one or two methods:

// View adapter — render tasks however you want
Name() string
RenderTasks(ctx, []Task) ([]byte, error)

// Sync adapter — push/pull to any backend
Name() string
Push(ctx, []Task) error
Pull(ctx) ([]Task, error)

Configure in .todoopen/config.toml:

[adapters.git]
  bin = "todoopen-adapter-sync-git"

[adapters.git.config]
  remote = "origin"
  branch = "main"
[server]
  addr = "127.0.0.1:8080"

[adapters.git]
  bin = "todoopen-adapter-sync-git"

[adapters.git.config]
  remote = "${GIT_REMOTE}"   # expanded from env at runtime
  branch = "tasks"

[adapters.s3]
  bin = "todoopen-adapter-sync-s3"

[adapters.s3.config]
  bucket = "${S3_BUCKET}"
  region = "us-east-1"
Adapters communicate over stdin/stdout JSON and can be written in any language. Use ${VAR} in config values — expanded from the environment at runtime so secrets never live in the file. See adapters.md ↗ to build your own.

Agent support

Built-in primitives for AI agent coordination — leases, heartbeats, idempotency keys.

Agents can safely pick up and work on tasks without stepping on each other or on you. The workflow: fetch next task → claim with a lease → heartbeat while working → complete or release.

# fetch the next open task
$ curl -s localhost:8080/v1/tasks/next
{ "id": "task_a1b2", "title": "Refactor auth module" }

# claim it (idempotent, lease = 5 min)
$ curl -s -X POST localhost:8080/v1/tasks/task_a1b2/claim \
    -H 'X-Idempotency-Key: run-42'

# heartbeat to keep the lease alive
$ curl -s -X POST localhost:8080/v1/tasks/task_a1b2/heartbeat

# complete
$ curl -s -X POST localhost:8080/v1/tasks/task_a1b2/complete
Lease system

Claims expire automatically. If an agent crashes, the task becomes available again. No manual cleanup.

Idempotent ops

Pass X-Idempotency-Key on mutating requests. Safe to retry without duplicating work.

Human override

Humans can always release an agent's claim. The API and CLI never block human intervention.

Run todoopen --agent-info for a machine-readable description of all endpoints and the recommended workflow — no server needed. See agent-primitives.md ↗ for the full coordination contract.

Reference

Full documentation on GitHub.