diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json
index aa18f24..3debaac 100644
--- a/.claude-plugin/marketplace.json
+++ b/.claude-plugin/marketplace.json
@@ -10,7 +10,7 @@
"name": "docket",
"source": "./",
"description": "Field and tool reference for docket, the shared list every AI tool and project writes to.",
- "version": "2.0.0"
+ "version": "3.0.0"
}
]
}
diff --git a/.gitignore b/.gitignore
index 4161057..7b001ab 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@ dist/
.DS_Store
*.log
npm-debug.log*
+mcpb/*.mcpb
diff --git a/README.md b/README.md
index 3d7079d..ccdb6f6 100644
--- a/README.md
+++ b/README.md
@@ -9,6 +9,43 @@
Warp — across every project, before the work is worth a ticket. Local-first,
self-hostable, no SaaS account.**
+
+
+
+
+## Quick start
+
+**You need:** Node.js 18+ and at least one MCP host — Claude Code, Codex,
+Cursor, Windsurf, Claude Desktop, Zed or Warp.
+
+```sh
+npx -y @pasichdev/docket setup
+```
+
+That configures every host it finds on this machine and ends by telling you
+which ones. Then restart your agent and ask it *"add a todo: buy milk"* — it
+shows up at **http://localhost:8787**, a dashboard that started by itself the
+moment the agent connected.
+
+Only want Claude Code, and nothing written anywhere else?
+`claude mcp add docket -- npx -y @pasichdev/docket` does just that part.
+Other hosts: [Supported hosts](#supported-hosts).
+
+
+Optional: see what's open in a project when a session starts
+
+```sh
+npm install -g @pasichdev/docket # the hook runs a command, so it needs one on PATH
+docket hook install # then: docket hook doctor
+```
+
+`hook install` works without the global install too — it pins the command to
+this exact copy of docket and tells you it did — but the short form survives
+moving or reinstalling, and `npx` leaves nothing on `PATH`.
+
+
+## Why
+
A thought that shows up mid-session is worth capturing but not worth the
ceremony: a Notion template, a GitLab issue format, a ticket id you have to
invent. So today it evaporates. Docket is the layer underneath all of that —
@@ -53,34 +90,6 @@ order and the `.docket.json` override: **[`docs/workspaces.md`](docs/workspaces.
Regenerate these with node docs/assets/demo-seed.mjs — it builds the workspace they show, so they stay a picture of the real dashboard rather than a staged one.
-## Quick start
-
-**You need:** [Claude Code](https://claude.com/claude-code) (or another MCP host)
-and Node.js 18+ (`node --version`; get it from [nodejs.org](https://nodejs.org)).
-
-```sh
-npx -y @pasichdev/docket setup # one shared data dir, detected MCP hosts configured
-claude mcp add docket -- npx -y @pasichdev/docket
-```
-
-Restart Claude Code and ask it *"add a todo: buy milk"*. The web dashboard is
-at **http://localhost:8787** — it started itself the moment the first client
-connected.
-
-Optionally, to see what's open in a project when a session starts:
-
-```sh
-npm install -g @pasichdev/docket # the hook runs a command, so it needs one on PATH
-docket hook install # then: docket hook doctor
-```
-
-`hook install` works without the global install too — it pins the command to
-this exact copy of docket and tells you it did — but the short form survives
-moving or reinstalling, and `npx` leaves nothing on `PATH`.
-
-Using Claude Desktop, Cursor, Windsurf, Zed, or Warp instead? Same MCP config
-shape — see [Supported hosts](#supported-hosts).
-
## Upgrading from 2.x
**Read this before you upgrade if you have existing items.**
diff --git a/docs/assets/demo-setup.sh b/docs/assets/demo-setup.sh
index 34311b5..c15790f 100755
--- a/docs/assets/demo-setup.sh
+++ b/docs/assets/demo-setup.sh
@@ -59,9 +59,9 @@ seed() {
node "$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)/dist/web.js" > "$ROOT/web.log" 2>&1 &
sleep 2
-seed "acme/backend" "fix token refresh race" "drop the legacy auth path"
-seed "you/tracker" "ship the new nav"
-seed "you/notes" "write up the migration notes"
+seed "gitlab.com/acme/backend" "fix token refresh race" "drop the legacy auth path"
+seed "github.com/you/tracker" "ship the new nav"
+seed "github.com/you/notes" "write up the migration notes"
curl -sS -X POST "http://127.0.0.1:$DOCKET_WEB_PORT/api/todos" -H 'Content-Type: application/json' \
-d '{"title":"a thought with no project yet"}' > /dev/null
diff --git a/docs/assets/demo.gif b/docs/assets/demo.gif
new file mode 100644
index 0000000..6c3047a
Binary files /dev/null and b/docs/assets/demo.gif differ
diff --git a/docs/assets/logo.png b/docs/assets/logo.png
new file mode 100644
index 0000000..28df061
Binary files /dev/null and b/docs/assets/logo.png differ
diff --git a/docs/assets/logo.svg b/docs/assets/logo.svg
new file mode 100644
index 0000000..1f67b36
--- /dev/null
+++ b/docs/assets/logo.svg
@@ -0,0 +1 @@
+
diff --git a/glama.json b/glama.json
new file mode 100644
index 0000000..fe9fd89
--- /dev/null
+++ b/glama.json
@@ -0,0 +1,4 @@
+{
+ "$schema": "https://glama.ai/mcp/schemas/server.json",
+ "maintainers": ["pasichDev"]
+}
diff --git a/llms-install.md b/llms-install.md
new file mode 100644
index 0000000..9a247a9
--- /dev/null
+++ b/llms-install.md
@@ -0,0 +1,37 @@
+# Installing Docket (for AI agents)
+
+Docket is an MCP server over stdio. It needs Node.js 18 or newer and nothing else: no
+account, no API key, no network service.
+
+## Recommended: let Docket configure the host
+
+```sh
+npx -y @pasichdev/docket setup
+```
+
+It detects Claude Code, Codex, Cursor and Windsurf, adds itself to each, and prints which
+ones it configured. Non-interactive runs (no TTY) accept every default.
+
+## Manual: add it to one host's MCP config
+
+```json
+{
+ "mcpServers": {
+ "docket": {
+ "command": "npx",
+ "args": ["-y", "@pasichdev/docket"]
+ }
+ }
+}
+```
+
+Optional: `"env": { "DOCKET_DATA_DIR": "/absolute/path" }` to choose where the list is stored
+(default `~/.docket`). Every host that should share one list must use the same directory.
+
+## Verify
+
+Restart the host, then call `todo_add` with `{"title": "try docket"}` and `todo_list` with no
+arguments; the new item should be listed. A dashboard is served at http://localhost:8787 as
+soon as the first client connects.
+
+No other configuration is required.
diff --git a/mcpb/manifest.json b/mcpb/manifest.json
new file mode 100644
index 0000000..656a111
--- /dev/null
+++ b/mcpb/manifest.json
@@ -0,0 +1,49 @@
+{
+ "manifest_version": "0.4",
+ "name": "docket",
+ "display_name": "Docket",
+ "version": "3.0.0",
+ "description": "One list every AI tool you use can write to — scoped per project, local-first, self-hostable.",
+ "long_description": "Docket is a shared todo list and backlog that Claude, Codex, Cursor and any other MCP host can all write to. Items are filed under the project they were captured in, automatically, from the git remote of wherever the agent runs. A web dashboard starts on http://localhost:8787 the first time an agent connects. Everything stays on this machine unless you point it at your own self-hosted server.",
+ "author": { "name": "pasichDev", "url": "https://github.com/pasichDev" },
+ "repository": { "type": "git", "url": "https://github.com/pasichDev/docket" },
+ "homepage": "https://github.com/pasichDev/docket#readme",
+ "support": "https://github.com/pasichDev/docket/issues",
+ "icon": "icon.png",
+ "server": {
+ "type": "node",
+ "entry_point": "dist/launcher.js",
+ "mcp_config": {
+ "command": "node",
+ "args": ["${__dirname}/dist/launcher.js"],
+ "env": { "DOCKET_DATA_DIR": "${user_config.data_dir}" }
+ }
+ },
+ "tools": [
+ { "name": "todo_add", "description": "Capture work the moment it comes up, from any tool or project" },
+ { "name": "todo_list", "description": "What's open in this project (or every project)" },
+ { "name": "todo_edit", "description": "Edit an item's title, description, category, priority, due date, source link or list" },
+ { "name": "todo_claim", "description": "Mark an item as being worked on by the calling agent" },
+ { "name": "todo_release", "description": "Clear the in-progress claim without completing the item" },
+ { "name": "todo_complete", "description": "Mark an item as done" },
+ { "name": "todo_history", "description": "Who changed an item, how, and when" },
+ { "name": "todo_delete", "description": "Permanently remove an item" },
+ { "name": "todo_version", "description": "Data format version and start time of this docket process" },
+ { "name": "todo_check_update", "description": "Check npm for a newer docket" }
+ ],
+ "user_config": {
+ "data_dir": {
+ "type": "directory",
+ "title": "Data directory",
+ "description": "Where the list is stored. Use the same directory in every tool that should share it.",
+ "default": "${HOME}/.docket",
+ "required": true
+ }
+ },
+ "compatibility": {
+ "platforms": ["darwin", "win32", "linux"],
+ "runtimes": { "node": ">=18.0.0" }
+ },
+ "keywords": ["todo", "backlog", "tasks", "multi-agent", "local-first", "claude-code", "codex", "cursor"],
+ "license": "MIT"
+}
diff --git a/mcpb/pack.sh b/mcpb/pack.sh
new file mode 100755
index 0000000..eb09609
--- /dev/null
+++ b/mcpb/pack.sh
@@ -0,0 +1,20 @@
+#!/usr/bin/env bash
+# Builds docket-.mcpb — the one-click bundle for Claude Desktop and Smithery.
+#
+# It packs the PUBLISHED npm tarball, not this working tree, so the bundle is byte-for-byte
+# the release its version number claims to be. Run it after `npm publish`.
+#
+# ./mcpb/pack.sh # -> mcpb/docket-.mcpb
+set -euo pipefail
+here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+version="$(node -p "require('$here/manifest.json').version")"
+stage="$(mktemp -d)"
+trap 'rm -rf "$stage"' EXIT
+
+(cd "$stage" && npm pack --silent "@pasichdev/docket@$version" >/dev/null && tar -xzf ./*.tgz)
+cd "$stage/package"
+npm install --omit=dev --ignore-scripts --no-audit --no-fund --silent
+cp "$here/manifest.json" manifest.json
+cp "$here/../docs/assets/logo.png" icon.png
+npx -y @anthropic-ai/mcpb validate manifest.json
+npx -y @anthropic-ai/mcpb pack . "$here/docket-$version.mcpb"
diff --git a/src/setup.test.ts b/src/setup.test.ts
index 4e106e6..29fec62 100644
--- a/src/setup.test.ts
+++ b/src/setup.test.ts
@@ -1,6 +1,6 @@
import assert from "node:assert/strict";
import { test } from "node:test";
-import { automationDefault, parseDataDirectoryArg } from "./setup.js";
+import { automationDefault, hostInvocation, nextSteps, parseDataDirectoryArg } from "./setup.js";
test("parseDataDirectoryArg: reads an explicit data directory", () => {
assert.equal(parseDataDirectoryArg(["--data-dir", "/srv/docket"]), "/srv/docket");
@@ -18,3 +18,23 @@ test("automationDefault: --yes/-y force true even if stdin were a TTY (can't fli
assert.equal(automationDefault(["--yes"]), true);
assert.equal(automationDefault(["-y"]), true);
});
+
+const invocation = hostInvocation("@pasichdev/docket@3.0.0", { DOCKET_DATA_DIR: "/home/u/.docket" });
+
+test("nextSteps: names the hosts it configured and says what to try, instead of a start command that does not exist", () => {
+ const out = nextSteps({ configured: ["Codex", "Claude Code"], invocation, dashboardPort: 8787 });
+ assert.match(out, /ready in Claude Code, Codex/);
+ assert.match(out, /Restart Claude Code and ask it: "add a todo/);
+ assert.match(out, /http:\/\/localhost:8787/);
+ assert.doesNotMatch(out, /Start the server with/);
+});
+
+test("nextSteps: the manual snippet is a whole host entry — command, args and env — not an env block with nothing to attach it to", () => {
+ const out = nextSteps({ configured: [], invocation, dashboardPort: 9000 });
+ assert.match(out, /No MCP host was configured automatically/);
+ assert.match(out, /http:\/\/localhost:9000/);
+ const json = JSON.parse(out.slice(out.indexOf("{"))) as { mcpServers: { docket: { command: string; args: string[]; env: Record } } };
+ assert.deepEqual(json.mcpServers.docket, invocation);
+ // The pinned, --prefix form: bare `npx @pasichdev/docket` run inside a checkout of this repo resolves the local package and dies.
+ assert.ok(json.mcpServers.docket.args.includes("--prefix"));
+});
diff --git a/src/setup.ts b/src/setup.ts
index 2678c2c..e64f081 100644
--- a/src/setup.ts
+++ b/src/setup.ts
@@ -165,8 +165,10 @@ async function readHostConfig(target: string): Promise {
}
}
-async function configureHosts(env: Record): Promise {
+/** Returns the hosts it actually configured, so the closing message can name them rather than guess. */
+async function configureHosts(env: Record): Promise {
const serverArgs = hostInvocation(await packageSpec(), env).args;
+ const configured: string[] = [];
const envPairs = Object.entries(env).map(([key, value]) => `${key}=${value}`);
/**
@@ -183,7 +185,7 @@ async function configureHosts(env: Record): Promise {
capture: string[],
remove: string[],
add: string[],
- ): Promise => {
+ ): Promise => {
const previous = await execFileAsync(command, capture).then((r) => r.stdout, () => null);
await execFileAsync(command, remove).catch(() => undefined);
try {
@@ -191,6 +193,7 @@ async function configureHosts(env: Record): Promise {
if (result.stdout) process.stdout.write(result.stdout);
if (result.stderr) process.stderr.write(result.stderr);
console.log(`Configured ${label}.`);
+ return true;
} catch (error) {
console.warn(`Skipped ${label}: ${(error as ExecFileException).message ?? "command failed"}`);
if (previous?.includes("docket")) {
@@ -199,18 +202,20 @@ async function configureHosts(env: Record): Promise {
` ${command} ${add.join(" ")}`,
);
}
+ return false;
}
};
if (await commandExists("codex")) {
const codexEnvArgs = envPairs.flatMap((pair) => ["--env", pair]);
- await reconfigure(
+ const ok = await reconfigure(
"codex",
"Codex",
["mcp", "list"],
["mcp", "remove", "docket"],
["mcp", "add", "docket", ...codexEnvArgs, "--", "npx", ...serverArgs],
);
+ if (ok) configured.push("Codex");
}
if (await commandExists("claude")) {
// `claude mcp add` takes the name as a bare positional right after "add" — -e/--env
@@ -218,18 +223,58 @@ async function configureHosts(env: Record): Promise {
// it, so putting the name after -e makes it try to consume "docket" as a second
// (invalid) env var instead of the server name.
const envFlag = envPairs.length > 0 ? ["-e", ...envPairs] : [];
- await reconfigure(
+ const ok = await reconfigure(
"claude",
"Claude Code MCP",
["mcp", "list"],
["mcp", "remove", "--scope", "user", "docket"],
["mcp", "add", "docket", "--scope", "user", ...envFlag, "--", "npx", ...serverArgs],
);
+ if (ok) configured.push("Claude Code");
}
- for (const target of [`${homedir()}/.cursor/mcp.json`, `${homedir()}/.codeium/windsurf/mcp_config.json`]) {
- await configureJsonHost(target, serverArgs, env);
+ for (const [label, target] of [
+ ["Cursor", `${homedir()}/.cursor/mcp.json`],
+ ["Windsurf", `${homedir()}/.codeium/windsurf/mcp_config.json`],
+ ] as const) {
+ const outcome = await configureJsonHost(target, serverArgs, env);
+ if (outcome === "configured" || outcome === "created") configured.push(label);
}
+ return configured;
+}
+
+/**
+ * The last thing setup prints, and so the only part of its output most people read.
+ *
+ * It used to end on env snippets for hosts setup had just configured itself, followed by
+ * "Start the server with: npx -y @pasichdev/docket" — a step that does not exist (the first
+ * agent to connect starts everything) and, run inside a checkout of this repo, the one
+ * invocation that fails. What a new user needs from the last screen is what worked, the one
+ * thing to try, and where to look; the manual config is for hosts setup could not reach.
+ */
+export function nextSteps(opts: {
+ configured: string[];
+ invocation: { command: string; args: string[]; env: Record };
+ dashboardPort: number;
+}): string {
+ const { invocation, dashboardPort } = opts;
+ // Claude Code first when it is there: it is the host the README's quick start is written for.
+ const configured = [...opts.configured].sort((x, y) => Number(y === "Claude Code") - Number(x === "Claude Code"));
+ const lines: string[] = [""];
+ if (configured.length > 0) {
+ lines.push(`✓ docket is ready in ${configured.join(", ")}.`, "");
+ lines.push("Next:");
+ lines.push(` 1. Restart ${configured[0]} and ask it: "add a todo: try docket"`);
+ } else {
+ lines.push("No MCP host was configured automatically — add docket to yours (below), then:", "");
+ lines.push("Next:");
+ lines.push(` 1. Restart your agent and ask it: "add a todo: try docket"`);
+ }
+ lines.push(` 2. Watch it land on the dashboard: http://localhost:${dashboardPort}`);
+ lines.push(" (it starts by itself when the first agent connects)", "");
+ lines.push(`${configured.length > 0 ? "Any other MCP host" : "Your MCP host"} (Claude Desktop, Zed, …) takes:`);
+ lines.push(JSON.stringify({ mcpServers: { docket: invocation } }, null, 2));
+ return lines.join("\n");
}
export type JsonHostOutcome = "configured" | "created" | "skipped-unreadable" | "skipped-absent" | "failed";
@@ -322,17 +367,18 @@ async function runLocalSetup(reader: LineReader, args: string[]): Promise
// `docket backup` in one backed up an empty ~/.docket and reported success.
await writeDataDirectoryConfig(dataDirectory);
await writeDeploymentConfig({ mode: "local" });
- if (await shouldAutomate(reader, "Configure detected MCP agents automatically?", args)) await configureHosts({ DOCKET_DATA_DIR: dataDirectory });
+ const env = { DOCKET_DATA_DIR: dataDirectory };
+ const hosts = (await shouldAutomate(reader, "Configure detected MCP agents automatically?", args)) ? await configureHosts(env) : [];
if (await shouldAutomate(reader, "Install the docket skill for Claude Code?", args)) await installSkill();
if (await shouldAutomate(reader, "Install the docket skill (Codex and other AGENTS.md-ecosystem agents)?", args)) await installAgentsSkill();
if (await shouldAutomate(reader, "Install the todo_stats terminal helper and shell startup entry?", args)) await installStatsIntegration(dataDirectory);
- console.log("\nUse this same directory in every MCP host that should share the list:\n");
- console.log("Codex (config.toml):");
- console.log("[mcp_servers.docket.env]");
- console.log(`DOCKET_DATA_DIR = ${JSON.stringify(dataDirectory)}\n`);
- console.log("Claude Desktop / Cursor / Windsurf / Zed:");
- console.log(JSON.stringify({ env: { DOCKET_DATA_DIR: dataDirectory } }, null, 2));
- console.log("\nStart the server with: npx -y @pasichdev/docket");
+ console.log(
+ nextSteps({
+ configured: hosts,
+ invocation: hostInvocation(await packageSpec(), env),
+ dashboardPort: Number(process.env.DOCKET_WEB_PORT ?? 8787),
+ }),
+ );
}
/**