Skip to main content
Glama
README.md
<h1 align="center">
  <img src="web/public/favicon.svg" width="72" height="72" alt=""><br>
  Pandan
</h1>

<p align="center">
  A small kanban board you run on your own machine — with an MCP server, so you
  can see what your coding agents are actually doing.
</p>

<p align="center">
  <img src="docs/demo.gif" width="820" alt="An agent adds a card to the board, it appears live, then moves itself to Review">
</p>

---

Agents do a lot of work you never see. Pandan gives them somewhere to write it
down, and gives you a board that updates while they work.

- **Runs on localhost.** One Node process, one SQLite file. No database to set
  up, no account, nothing leaves your machine.
- **Agents can read and write it.** A built-in MCP server with 19 tools, none of them destructive. An
  agent finds them itself — you do not have to explain your API to it.
- **The board updates live.** A card an agent adds appears in about a second.
  No refresh.
- **Small enough to read.** Around 2,300 lines. You can check what it does
  before you trust it with your work.

## The board

Keep as many boards as you like — one for work, one for home. The switcher is
in the top bar, and it remembers which one you had open. A project can be moved
between boards, and deleting a board takes its projects with it.

On a board, rows are projects. Columns are `To do`, `Next`, `Doing`, `Waiting`, `Review`
and `Done`, with `Next`, `Doing` and `Waiting` grouped under **In progress**.

**Waiting** is for work that has started but is held up by someone or something
else; say who or what in the card's notes.

**Review** is where an agent puts work it wants you to look at. It is a signal,
not a gate — an agent can still move a card straight to `Done` when the work
plainly needs no checking. The point is that it can tell you which is which.

Every card shows its ref: its project's key and the card's number in that
project, so a project with the key `CRC` counts `CRC-1`, `CRC-2`… from its first
card. That ref is what you say to an agent — "archive CRC-23", "move CRC-17 to
done" — and it is the `card_id` the API and the MCP tools take, so what you
read off the board is what you pass along. A project gets its key when it is
made, from its name unless you give one, and keeps it for good. A ref never
changes, even when the card moves to another project or board, and a number is
never handed out twice. Cards numbered per board before keep that ref too
(`MB-223`, shown when you hover over the ref), and `#300` finds the card with
id 300. Board keys are for display.

Cards carry a label colour, a due date, a flag, notes and a checklist. Late
cards go red. Right-click one to recolour it, and name a colour to turn it into
a real label such as "Blocked". The search box filters the whole board as you
type — words, or a card like `#46` or `SCH-46` — and rows can be dragged into
the order you want.

**Nothing is deleted.** Archiving takes a card off the board and keeps it, and
an agent key cannot delete anything at all — only you can, from the archive.
The archive has its own button in the top bar. A whole project can be archived
too, from its panel, and comes back from the same place.

Each project also holds notes, a repo link, links, contacts, and a dated update
log, so the context lives next to the work.

Rows fold away and every column scrolls on its own, so a project with fifty
cards takes the same room as one with five.

## The dashboard

The third view in the top bar, **Board | Notes | Dashboard**, has two halves:
one to act on your tasks and one to see how the board is doing. It shows the
open board only and updates live, like the board.

**Control** (left): work you can act on from the dashboard.

- **Quick add.** Pick a project and a column (To do, Next or Doing), type a
  title and press Enter. The field stays ready for the next card.
- **Working on.** The cards in Doing and Review.
- **Needs attention.** Cards that are waiting, flagged, overdue, or gone quiet
  (no change for 7 days or more in Doing, Waiting or Review). Each one is
  tagged with the reason.
- **Up next.** The first five cards in Next. **Start ▶** moves one to Doing.
- **Oldest open.** The five open cards made longest ago.
- **Rules for the lists:**
  - A card shows up in only one list.
  - A list longer than 8 cards shows the rest under "All N".
- **Row buttons.** Every row has these, and so does every card list a chart
  opens:
  - **→** moves the card one step on (To do → Next → Doing → Review → Done;
    Waiting goes back to Doing);
  - **✓** marks it done;
  - **⚑** flags or unflags it;
  - **⏸** moves it to Waiting;
  - **⋯** moves it to any column;
  - clicking the title opens the card.

**Insight** (right): every chart is upright and fills its panel.

- **Big numbers.** Open, Active (in Doing, Waiting or Review), Done (with its share of all work), Added
  in the last 4 weeks, Waiting and Flagged. Overdue and Gone quiet appear when
  there are any.
- **Board now.** The cards in each column.
- **Projects.** Each project's open work by column, with what is done stacked
  on top and its share done underneath.
- **Added and finished per week.**
- **Age of open work.** Open cards grouped by how long ago they were made.
- **Agents and time in column.** One line until there is data, then the cards
  agents touched against yours each week, and the median days a card spends in
  each column.

Click any number, bar or part of a column to list the cards behind it.

The numbers come from a move history that Pandan keeps from this version on.
On the first start it fills in the past from the dates your cards already
have:

- **Finish dates from before the upgrade are guesses.** Cards still in Done
  count in the week of their last change, drawn lighter and marked as
  estimates.
- **Cards already archived from Done count in Done only, not in a week.** Their
  last change is the day they were archived, not the day they were finished.
- **The medians use real moves only,** so they start empty.

An agent's work counts as an agent's only when it connects with its own
[agent key](#connect-an-agent). An agent that uses your password counts as you.

## The note board

Some things are not tasks: a phone number, a thought, a list for the shop.
Every board has a second view for those — **Board | Notes** in the top bar.

It is a cork board. **+ Note** puts a sticky note on it; drag it anywhere and
it stays there. A note has a title, a text, and one of the six card colours.
Click the text to write; click away, or press Escape, to see it formatted.

The text takes light marks: `**bold**`, `__underline__`, `~~struck~~`, and a
line starting with `- ` is a bullet. While you write, a small toolbar (B, U,
S, List) puts the marks around the selected words, and Ctrl+B / Ctrl+U do the
same. The marks stay plain text — never HTML — so a note can carry nothing
that runs.

Notes update live like cards, and they are on the API (`/boards/:id/notes`),
so an agent can leave you one. It cannot delete one: notes have no archive, so
deleting is for a person only. There are no MCP tools for notes yet.

## Run it

```bash
npx pandan-board
```

That is the whole install. It makes you a password on first run and opens on
<http://localhost:3000>.

Your board lives in `~/.pandan` — the database and an `.env` holding the
password. The password is never printed, so open that file to read it.

You need [Node 22.13 or newer](https://nodejs.org). Pandan uses Node's built-in
SQLite, so there is nothing to compile and no database to install.

### With Docker

```bash
docker run -d \
  -p 127.0.0.1:3000:3000 \
  -v pandan:/data \
  -e APP_PASSWORD=pick-something-long \
  ghcr.io/IdoGigi/pandan
```

Or with compose, which binds to localhost for you and builds from source:

```bash
npm run setup && docker compose up -d
```

### From the source

For hacking on it. A checkout keeps its board inside the checkout, so your
real one is never touched.

```bash
git clone https://github.com/IdoGigi/pandan.git
cd pandan
npm install
npm start
```

## Connect an agent

Each agent gets its own key. Revoking one stops that agent and nothing else —
your own password keeps working. A key can use the board, but it can never see,
make or revoke keys, so a leaked key cannot protect itself.

Make keys under **Agent keys** in the board, or let `npm run connect` do it.

**Claude Code** — one command:

```bash
npm run connect
```

With the board running, that makes an agent key, registers the MCP server with
it, and installs the `pandan` skill. Your board password is used once to make
the key and never handed to the agent. Pass an address if the board is not
local: `npm run connect https://board.example.com`.

By hand instead:

```bash
claude mcp add --transport http pandan http://localhost:3000/mcp \
  --header "Authorization: Bearer YOUR_PASSWORD"
```

**Anything that reads `.mcp.json`** — keep the password in an environment
variable, not in the file:

```json
{
  "mcpServers": {
    "pandan": {
      "type": "http",
      "url": "http://localhost:3000/mcp",
      "headers": { "Authorization": "Bearer ${PANDAN_KEY}" }
    }
  }
}
```

The agent then gets tools to read the board, add and edit cards, move them
between columns, manage projects and write update entries. Full list and a
plain REST API in [API.md](API.md).

## Make agents actually use it

An agent will not log its work unless you tell it to, and left alone it writes
far too much. [`examples/`](examples/) has the two pieces that fix that:

- `examples/CLAUDE.md-snippet.md` — a short rule to paste into your `CLAUDE.md`
- `examples/pandan-skill.md` — a Claude Code skill with the same rules in detail

Both are built around one idea: **the board is for glancing at.** Card titles
are capped at 8 words, log entries at one sentence, and most work is not worth
a card at all. A board with six clear cards beats one with forty true ones.

## Security — read this before exposing it

Pandan is built for **one person on one machine**. One password is your login.

- Agents get their own keys, which you can revoke one at a time. Only a hash of
  each key is stored, so a copy of the database does not hand them over.
- A key cannot list, make or revoke keys. That needs your password.
- Docker compose binds to `127.0.0.1` on purpose. It is not reachable from
  your network until you change that.
- There are still no user accounts. Anyone with your **password** has full
  control, including over the keys.
- If you put it on the internet, put it behind something that does real auth,
  and use a long random password.

Failed logins are rate limited, cookies are signed and HTTP-only, and stored
links only become clickable for `http`, `https`, `mailto` and `tel` — but none
of that makes it a multi-user app.

## Settings

| Name | Needed | What it does |
| --- | --- | --- |
| `APP_PASSWORD` | yes | The only password. The app refuses to start without it |
| `DB_PATH` | no | The database file. Defaults to `~/.pandan/pandan.db` |
| `PANDAN_HOME` | no | Where settings and the board live. Defaults to `~/.pandan` |
| `PORT` | no | Port to listen on. Defaults to 3000 |
| `NODE_ENV` | no | Set to `production` so the cookie gets the `Secure` flag |

## Development

```bash
npm run dev                  # server with reload
npm --prefix web run dev     # UI on :5173, proxies /api to :3000
npm run check                # syntax check the server
npm test                     # server tests: the real server on a throwaway database
npm run smoke                # headless UI test
```

`npm run smoke` mounts the real React app in jsdom against a fake API and clicks
through every flow with true pointer events. It fails if the app ever calls
`window.prompt`, `confirm` or `alert` — Pandan uses its own dialogs.

```
server/
  index.js    wiring, login, static files
  auth.js     password check, signed cookie, bearer token
  db.js       schema and migrations
  routes.js   the JSON API
  insights.js the dashboard numbers, from cards and their move history
  mcp.js      the MCP server
  events.js   live updates over Server-Sent Events
web/src/      the React board
```

Existing boards upgrade in place: new columns are added only when missing, and
new tables use `CREATE TABLE IF NOT EXISTS`.

## Licence

MIT. See [LICENSE](LICENSE).