{
  "markdown": "<img src=\"docs/logo.svg\" width=\"60\" alt=\"\">\n\n# todox\n\n**Working memory for developers and their agents.**\nNot a checklist — a log your next session can actually resume from.\n\n[![ci](https://github.com/beydemirfurkan/todox/actions/workflows/ci.yml/badge.svg)](https://github.com/beydemirfurkan/todox/actions/workflows/ci.yml)\n[![licence: MIT](https://img.shields.io/badge/licence-MIT-ffd84d)](LICENSE)\n[![live](https://img.shields.io/badge/live-todox.dev-6cb7f5)](https://www.todox.dev)\n\nAn issue tracker is written human-to-human. todox is written agent-to-agent,\nwith a human reading over its shoulder. Every task carries the decisions behind\nit, the approaches that failed, the questions still open, and the note the last\nsession left behind.\n\nA fresh agent calls `get_context`, reads that, and starts where the last one\nstopped — without walking into a wall somebody already hit. The briefing is\ncapped rather than unbounded, in rows *and* in bytes, and it reports what the\ncaps left out instead of trimming in silence. Nothing is ever cut mid-sentence:\nevery record comes back named and dated with its first line, and a `body` of\nnull means the budget was spent, not that the record is empty.\n\n## What goes in a log\n\n| kind | what it means |\n| --- | --- |\n| `decision` | what you chose, and why the alternatives lost |\n| `dead_end` | an approach that did **not** work — the highest-value entry, because it stops the repeat |\n| `question` | something only a human can answer |\n| `handoff` | end-of-session state, written for a stranger |\n| `note` | everything else |\n\nTwo things fall out of treating the log as the product:\n\n- **Stale context is flagged, and never faked.** Linked files are hashed by the\n  side that can see them — the agent — and the server stores the hashes and\n  compares. If the code moves on, `get_context` says the note may be lying.\n  Until an agent has actually looked, the note is marked as never checked\n  rather than claimed to be fresh: context that lies is worse than none, and\n  that includes lying about how sure we are.\n- **Reports come from the log, not from commits.** Every status change is an\n  event, so *what did I finish today, how long did it take, which model did it*\n  is a query rather than archaeology.\n- **A file can be asked what is known about it.** The same links that carry the\n  hashes are readable from the other end: `get_file_context` takes a path and\n  answers with the tasks that touched it, their dead ends, and any standing\n  note attached to it. Paths are folded to their repo-relative form, so a link\n  made on one machine is found from another.\n\n## Try it\n\n**[todox.dev](https://www.todox.dev)** — anyone can register. Small personal\ndeployment, no uptime promise. Self-host if the log matters to you.\n\n## Run your own\n\n```bash\npnpm install\ncp .env.example .env.local     # any Postgres 15+; see below for a container\npnpm db:migrate                # idempotent\npnpm seed                      # optional demo account: demo / todox-demo\npnpm dev\n```\n\n## Connect an agent\n\ntodox is a remote MCP server. There is nothing to install and no repository to\nclone: point any MCP client at the URL with an agent token.\n\nCreate a token on the Account page and it hands you text you can paste straight\ninto whichever agent you use, plus the config snippet for the four common ones.\nThe shape is always the same — one URL, one header:\n\n```bash\n# Claude Code. --scope user, because the default is this directory only.\nclaude mcp add --scope user --transport http todox https://www.todox.dev/api/mcp \\\n  --header \"Authorization: Bearer todox_…\"\n```\n\n```json\n// OpenCode v1 — ~/.config/opencode/opencode.json.\n// MCP key is `mcp` (server name is a direct key under it), NOT `mcpServers`.\n// `type` is `\"remote\"`, NOT `\"http\"` — the Claude/Cursor/VS Code value is\n// silently ignored on OpenCode.\n{\n  \"mcp\": {\n    \"todox\": {\n      \"type\": \"remote\",\n      \"url\": \"https://www.todox.dev/api/mcp\",\n      \"headers\": { \"Authorization\": \"Bearer todox_…\" }\n    }\n  }\n}\n```\n\n```jsonc\n// OpenCode v2 — same key, server now nested under `mcp.servers`.\n{\n  \"mcp\": {\n    \"servers\": {\n      \"todox\": {\n        \"type\": \"remote\",\n        \"url\": \"https://www.todox.dev/api/mcp\",\n        \"headers\": { \"Authorization\": \"Bearer todox_…\" }\n      }\n    }\n  }\n}\n```\n\n```toml\n# Codex — ~/.codex/config.toml\n[mcp_servers.todox]\nurl = \"https://www.todox.dev/api/mcp\"\nhttp_headers = { Authorization = \"Bearer todox_…\" }\n```\n\n```json\n// Cursor — ~/.cursor/mcp.json, the one in your home directory.\n{\n  \"mcpServers\": {\n    \"todox\": {\n      \"type\": \"http\",\n      \"url\": \"https://www.todox.dev/api/mcp\",\n      \"headers\": { \"Authorization\": \"Bearer todox_…\" }\n    }\n  }\n}\n```\n\n```json\n// VS Code — the user-level mcp.json (\"MCP: Open User Configuration\").\n// The root key is \"servers\", NOT \"mcpServers\". This is the one client\n// that differs, and getting it wrong is silent.\n{\n  \"servers\": {\n    \"todox\": {\n      \"type\": \"http\",\n      \"url\": \"https://www.todox.dev/api/mcp\",\n      \"headers\": { \"Authorization\": \"Bearer todox_…\" }\n    }\n  }\n}\n```\n\n> **The MCP config key and the `type` value differ per agent, and the\n> wrong combination is silently ignored — no error, no warning, the tool\n> just does not show up:**\n>\n> | agent | key | `type` |\n> | --- | --- | --- |\n> | Claude Code | `mcpServers.NAME` | `\"http\"` |\n> | OpenCode v1 | `mcp.NAME` | `\"remote\"` |\n> | OpenCode v2 | `mcp.servers.NAME` | `\"remote\"` |\n> | Cursor | `mcpServers.NAME` | `\"http\"` |\n> | VS Code (Copilot Chat) | `servers.NAME` | `\"http\"` |\n> | Codex | TOML `[mcp_servers.NAME]` | n/a |\n\nWhere those files live differs by platform, and VS Code is the one that is\nnot where a Linux habit puts it:\n\n| agent | macOS | Linux | Windows |\n| --- | --- | --- | --- |\n| Claude Code | `~/.claude.json` | same | same |\n| Cursor | `~/.cursor/mcp.json` | same | same |\n| Codex | `~/.codex/config.toml` | same | same |\n| OpenCode | `~/.config/opencode/opencode.json` | same | same |\n| VS Code | `~/Library/Application Support/Code/User/mcp.json` | `~/.config/Code/User/mcp.json` | `%APPDATA%\\Code\\User\\mcp.json` |\n\n**Install it globally, not per project.** Every one of these tools defaults to\nthe directory you are standing in — `claude mcp add` without a scope,\n`.cursor/mcp.json`, `.vscode/mcp.json` — and a memory that only exists in one\nrepository is the opposite of the point. It also fails quietly: the tools\nsimply are not there in the next project, so the agent never mentions them.\n\nSpell out `\"type\": \"http\"`. A client that finds a `url` without one tends to\nassume a local command and fails with something unhelpful.\n\n### Then tell your agent to use it\n\nConnecting is not the same as being used, and the gap is bigger than it looks.\nAn MCP server's `instructions` are background reading; a skill or a CLAUDE.md\nrule is an instruction. When they disagree, the server loses — measured, in a\nfresh project, with todox connected the whole time and never once called.\n\nSo put four lines in the memory file your agent actually obeys:\n\n```markdown\ntodox MCP is installed here — persistent memory across projects.\n\n- Call `get_context` before starting non-trivial work (cwd = your working\n  directory). It registers a new repo by itself.\n- `create_task` for anything that will not finish this session.\n- Before stopping, `log_entry(kind:'handoff')` on every task you touched,\n  and `dead_end` for approaches that failed.\n- Always pass your own model id.\n```\n\nOr let the installer do it:\n\n```bash\npnpm install:mcp claude-code --write-memory\n```\n\nIt is off unless asked, because that file is yours rather than ours, and it is\nidempotent — the block is fenced with an HTML comment, so a second run replaces\nit instead of leaving two sets of instructions where the older one wins. Add\n`--dry-run` to see the exact block first.\n\n**The user-level file, not the project one.** This is the same trap as the\nconfig above, one directory over:\n\n| Agent | The file that applies everywhere |\n| --- | --- |\n| Claude Code | `~/.claude/CLAUDE.md` |\n| Codex | `~/.codex/AGENTS.md` |\n| Cursor | `~/.cursor/rules/todox.md` |\n| VS Code | `~/.copilot/instructions/todox.md` |\n| OpenCode | `~/.config/opencode/AGENTS.md` |\n\nA repository's own `AGENTS.md`, and the per-project rules files the editors also\nread, apply inside that checkout only. A cross-project memory installed into one\nproject is the thing this whole section exists to avoid.\n\nThe token stays out of that file — it lives in your MCP config. This is the\nhabit, not the credential.\n\n### Optional: local mode\n\nThe hosted server has no filesystem — but your agent does, and that is enough:\nit sends the hash when it links a file and calls `report_file_hashes` with what\nit finds afterwards, so staleness works over HTTP like anywhere else.\n\nThe stdio server does that part itself rather than asking. Worth running if you\nwould rather not spend an agent's attention on it, or want the hashing to\nhappen even when the agent forgets. There is nothing to clone:\n\n```bash\nTODOX_TOKEN=todox_… TODOX_URL=https://www.todox.dev \\\n  npx https://github.com/beydemirfurkan/todox/releases/latest/download/todox-mcp.tgz\n```\n\nOr as an MCP config, which is the form an agent wants:\n\n```json\n{\n  \"mcpServers\": {\n    \"todox\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"-y\",\n        \"https://github.com/beydemirfurkan/todox/releases/latest/download/todox-mcp.tgz\"\n      ],\n      \"env\": { \"TODOX_TOKEN\": \"todox_…\", \"TODOX_URL\": \"https://www.todox.dev\" }\n    }\n  }\n}\n```\n\n**There is no npm package, and that is a decision rather than a to-do.** A\nGitHub Release needs no account and no token to publish or to install from, so\nthe tarball is the whole distribution and `npx` takes its URL directly. The URL\nabove always resolves to the newest release; every release also carries a\n`todox-mcp-<version>.tgz` if you would rather pin and choose when to move.\n\nIt carries only what the stdio server actually loads — no Next, no React, no\nPostgres driver, because it talks to the API over HTTP and never opens a\ndatabase. `pnpm pack:mcp` builds it, and fails the build if anything\nserver-side ever finds its way into the tool surface again.\n\nFrom a clone, `pnpm -C /path/to/todox mcp` still works and is what to use when\nyou are changing the tools themselves.\n\n### Tools\n\n| tool | what it does |\n| --- | --- |\n| `get_context` | **Call this first.** Standing rules, project decisions and gotchas, every open task with its decisions, dead ends, questions, files and last handoff — plus stale-file warnings. Resolves a project from a slug, a name, or any path inside it. Capped in rows and in bytes, never truncated: every record keeps its id, kind, date and first line, and a `body` of null means the budget was spent — `get_task` reads it. Pass `focus` — a sentence about what the session is for — and both budgets are spent on the records that answer it rather than the newest ones, which is what lets them be smaller. |\n| `create_task` | Capture work. Pass `cwd` or an explicit `project`; the tool schema requires one before the call runs. Registering a **new** one also needs `repo_root` or `repo_url`. The receipt returns the task path and body length without echoing the body. |\n| `update_task` | Status, title, body, priority. Moving to `doing`/`done` is where durations come from. |\n| `log_entry` | Append one of the five kinds. `answers_entry_id` closes a `question` — the only thing that does. |\n| `delete_entry` | For an entry that was wrong when it was written. One overtaken by later work is history, not an error — append instead. |\n| `activity_report` | Today / this week / any window: durations, models, importance, decisions, dead ends, open questions. `format:\"markdown\"` is written to be pasted into a status update. |\n| `link_files` | Attach paths with their hashes to a task or a context note. Safe to call again for the same file. |\n| `report_file_hashes` | Hosted only: what the linked files look like on disk now. The local process does this for itself. |\n| `accept_file_change` · `unlink_file` | Clear a stale warning once you have read the change, or drop a link that has stopped meaning anything. Nothing else can clear it — the server never sees the file. |\n| `add_context` | Knowledge that outlives a task; omit the project to make it account-wide. |\n| `get_context_note` | One **note** in full, for the ones whose body the briefing capped and for reading past a search snippet. An entry the budget did not reach is read with `get_task`. |\n| `get_file_context` | What is known about one file: the tasks that touched it with their dead ends, and the notes attached to it. Absolute or repo-relative; both find a link made on another machine. |\n| `update_context` · `delete_context` | Correct a note that turned out wrong. A log that can only be added to stops being worth reading. |\n| `search` | Across all your projects, ranked by relevance. Ask the question in words; quote a phrase to require it. Stems English and Turkish, and still matches the middle of an identifier.  Words that only one of the two languages treats as noise are dropped, so a question does not match every record containing the word \"a\". `kinds` narrows to dead ends or decisions; `project` stops it looking elsewhere.|\n| `get_task` | One task with its log and linked files. |\n| `list_tasks` · `list_projects` | The plain lists, when `get_context` is more than you need. Projects are newest-activity-first and carry `activity_at`. |\n| `create_project` · `update_project` | Rarely needed: `create_task` with a `cwd` registers one. A summary is worth adding. |\n| `delete_project` | The way back from a mistyped `cwd`. Takes the project and everything under it; `confirm` must be the slug. |\n| `merge_projects` | The way back from one repo registered twice. Moves tasks, notes and paths into the surviving project; `confirm` must be the slug of the one being merged away. |\n\nEvery write tool takes a `model`, and the server instructions tell the agent to\nalways pass it. That is what makes the per-model breakdown real rather than\nguessed.\n\n### Prompts\n\nThree, because there are three moments this is for. They show up in your\nclient's own menu, so you can see what the server does without reading\nanything:\n\n| prompt | when |\n| --- | --- |\n| `start_session` | before planning — read what earlier sessions established |\n| `wrap_up` | before finishing — leave a handoff, and the dead ends especially |\n| `standup` | when somebody asks what got done |\n\n## Deploying\n\nA container and a Postgres beside it. `docker-compose.yml` at the root is that,\nassembled — the database publishes no port at all and is reachable only over\nthe compose network:\n\n```bash\ncp .env.example .env       # set POSTGRES_PASSWORD and TODOX_PUBLIC_URL\ndocker compose up -d --build\ndocker compose exec app pnpm db:migrate\n```\n\nThe migration is a separate line on purpose; see the note at the end of this\nsection. todox.dev itself runs the same two containers on one host.\n\n| variable | why |\n| --- | --- |\n| `DATABASE_URL` | Postgres. When the database is a neighbour on the same network this is its service name, and no certificate or public port is involved. |\n| `DATABASE_POOL_MAX` | Optional, default 10. Connections this process may hold. Raise it only after checking the server's own `max_connections`, which every replica shares. |\n| `TODOX_PUBLIC_URL` | Verification links, reset links and the agent setup snippet are built from it — get it wrong and people, and their agents, land on the wrong host. |\n| `SMTP_HOST` · `SMTP_USER` · `SMTP_PASS` · `MAIL_FROM` (· `SMTP_PORT`) | Optional, but the first four together. Without them mail is printed to the server log rather than sent. Port defaults to 587 (STARTTLS). What `MAIL_FROM` may be depends on the provider: a mailbox provider usually wants the address that authenticated, while an API-key provider wants any address on a domain verified with it. If a sending limit is hit, messages are dropped and the failure shows up only in the log. |\n\nRun `pnpm db:migrate` when the schema changes. It deliberately does not run at\nstartup: DDL racing between instances of a rolling deploy is a bad way to\ndiscover lock contention, and the schema is idempotent precisely so the decision\ncan be made after a deploy rather than during one. From the host:\n\n```bash\ndocker exec <container> pnpm db:migrate\n```\n\nThat is also why the image keeps its dev dependencies instead of using Next's\n`standalone` output — pruning them removes `tsx` and everything under\n`scripts/`, and a database that is deliberately unreachable from the internet\ncan only be migrated from something already inside the network.\n\n### Knowing it actually deployed\n\nA merge is not a deploy, and the gap between them is silent: nothing errors,\nthe site stays up, and the only symptom is that a fix you watched go green is\nnot the one people are running. On 2026-09-05 this instance served code two\ndays and fifty-six commits old, and the thing that made it visible was looking\nrather than anything reporting it.\n\nThe image tag is the git sha and the container name changes on every deploy, so\none line answers it:\n\n```bash\ndocker inspect <container> --format '{{.Config.Image}}'\n```\n\nCompare that against `git rev-parse origin/main`. If they differ, the code you\nare reading is not the code that is running.\n\nAutomatic deploys are worth wiring up, and worth checking after you do. A\nplatform that pulls on a webhook can have the switch on and still never fire,\nbecause the switch and the webhook are two settings in two places: this\ninstance had auto-deploy enabled for weeks while the repository had no webhook\nat all, so nothing was ever told to look. After wiring one, confirm from the\nsending side — the delivery log — rather than from the switch, because a\nwebhook pointed at the wrong path answers cheerfully and does nothing.\n\n## Taking your data with you\n\nThe Account page has a **Download my data** button, and `/api/export` answers\nthe same file to a bearer token — so an agent can write the backup without the\nresult passing through a model. It carries every project you own with its\ntasks, log, context notes and file hashes, and nothing about anybody else: no\ncredential, no collaborator, no share token, and no projects that were shared\n*with* you, which belong to whoever made them.\n\nLoading one into an instance you run:\n\n```bash\npnpm db:import ./todox-export-2026-08-18.json your-username\n```\n\nAdditive, never destructive: nothing is deleted or overwritten, and a project\nwhose slug is taken arrives under the next free one. Task events come across\ntoo, so durations in a report on the restored copy say what they said on the\noriginal.\n\nComing from the old SQLite version? `pnpm db:import-sqlite [path]` copies a\n`~/.todox/todox.db` across.\n\n## Security\n\nPasswords are scrypt; sessions, agent tokens and email links are stored as\nhashes only. Ownership is enforced in one module, and a row belonging to someone\nelse answers 404 rather than 403 so ids cannot be probed. Rate limits live in\nthe database, so they hold across instances.\n\nDetails, and an honest list of what is **not** covered, in\n[SECURITY.md](SECURITY.md).\n\n## Known gaps\n\n- Search's full-text half is indexed; its substring half is not. The two are\n  asked separately and merged, which is what lets the first one use an index at\n  all — measured on 110k rows, a search went from 5.7s to 0.16s. What is left is\n  one sequential scan for the `ILIKE` arm that finds identifiers full-text\n  cannot, and indexing that needs `pg_trgm`, which needs a `CREATE EXTENSION`\n  this project cannot assume it is allowed to run.\n- Staleness is per-file hash; per-symbol would be the honest version. Hosted,\n  it depends on the agent actually sending hashes — the instructions ask, and\n  nothing can make it.\n- Coverage sits around 39%, and the shape matters more than the number: the\n  agent surface, the auth boundary and the repositories that answer \"is this\n  yours\" are covered, while much of the UI is not.\n- Observations only see what git can tell them, so they answer \"what changed\"\n  and never \"why\". The half that carries reasoning is a transcript, and the\n  only hook API that exposes one belongs to a single client.\n- The briefing's byte budget covers log bodies and note bodies. Two axes are\n  still bounded only by a row count: the *heads* of carried entries (fifty\n  tasks' worth), and task bodies. Both are far smaller than what the budget\n  fixed — one project went from 143 KB to about 55 KB — but neither is bounded\n  in bytes, and `pnpm bench:memory` prints both so the next reader does not\n  have to discover it.\n- **Observations are captured by the local process only.** Watching git means\n  running on the machine that holds the checkout, and the hosted endpoint has\n  none — so connected that way, the `observations` section of every briefing\n  stays empty and nothing anywhere fills it. The Try-it path in this README is\n  the hosted one, so that is most people. `npx todox-mcp` is the transport that\n  captures. Everything else works identically either way.\n- No 2FA, no per-session revocation, no audit log.\n- Share links are unlisted, not access-controlled.\n- No keyboard navigation beyond `/` for search.\n\n## Cutting a release\n\n```bash\ngit tag v0.1.1 && git push origin v0.1.1\n```\n\nThat is the procedure. The workflow checks the tag against `package.json`,\nruns the checks, builds the stdio package and attaches it to a GitHub Release —\nno account and no credential involved, so `npx <that tarball url>` works from\nthe first tag.\n\nTwo names go up: `todox-mcp-<version>.tgz`, and the same bytes as\n`todox-mcp.tgz` so that `/releases/latest/download/todox-mcp.tgz` is an address\nworth writing into a config once. Nothing is published to npm, on purpose — see\nthe local-mode section above.\n\n`server.json` pins the MCP registry entry to the same version and\n`server-json.test.ts` holds it there, so the tag, the package and the registry\nmove together or the release stops.\n\n## Contributing\n\nThe rules the codebase actually follows, and how to run the checks:\n[CONTRIBUTING.md](CONTRIBUTING.md).\n\nMIT — see [LICENSE](LICENSE).\n",
  "bytes": 22103,
  "sha": "8b39ced70ea905dcfaee6ef9db549c8ccc579d26af78a54c95480fd79f117b7f",
  "repo_slug": "beydemirfurkan/todox",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_beydemirfurkan_todox_96791285/readme"
}