Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ Upstream baseline is [vitalio-sh/thunderbird-cli@`465613d`](https://github.com/v

Planned, **not on `main`**:

- Calendar task CRUD, notes and tasks, toward feature parity with [atbridge.ai](https://atbridge.ai). A read-only calendar-listing spike (`tb calendar list`) landed first, using a chrome-privileged Experiment API since Thunderbird's WebExtension model has no calendar access; event CRUD and cross-calendar clash detection (`tb calendar events`/`create`/`update`/`delete`/`clashes`) landed next, gated behind the `calendarWrite` access switch for writes — see [docs/decisions/calendar-backend.md](docs/decisions/calendar-backend.md) for the tradeoffs, including the ATN manual-review requirement this adds to every signed release that touches it.
- Calendar events CRUD, cross-calendar clash detection, task CRUD, and deterministic action-item extraction from email, toward feature parity with [atbridge.ai](https://atbridge.ai). A read-only calendar-listing spike (`tb calendars`) landed first, using a chrome-privileged Experiment API since Thunderbird's WebExtension model has no calendar access; event CRUD and clash detection (`tb calendar events`/`create`/`update`/`delete`/`clashes`, gated by `calendarWrite`) and task CRUD (`tb tasks`, gated by `tasksWrite`) plus action-item extraction (`tb action-items`) landed next — see [docs/decisions/calendar-backend.md](docs/decisions/calendar-backend.md) for the tradeoffs, including the ATN manual-review requirement this adds to every signed release that touches it.
- Extension stability pass: audit against the known reconnect/backoff and lifecycle fixes, with regression tests.

<a href="docs/diagrams/roadmap.html"><picture>
Expand Down
56 changes: 50 additions & 6 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
| **Extension** (background.js) | Inside Thunderbird | Pure WebExtension. Connects to bridge via WebSocket. Checks every request against the build-time access policy (`access-control.js`, see [docs/ACCESS-CONTROL.md](docs/ACCESS-CONTROL.md)), then translates it into `messenger.*` API calls |
| **Bridge** (bridge.js) | Host (daemon) | Stateless HTTP↔WebSocket proxy. Receives HTTP from CLI/MCP, forwards to extension, returns response. No business logic |
| **CLI** (tb) | Host or Docker | Thin HTTP client. Parses args, calls bridge, outputs JSON to stdout. 43 commands. Auto-starts bridge daemon if not running. Zero state |
| **MCP Server** (tb-mcp) | Host (alongside Claude Desktop) | Stdio-based MCP server. Exposes 16 curated tools to Claude Desktop and other MCP clients. Auto-starts bridge daemon if not running. Reuses CLI's HTTP client to call bridge |
| **MCP Server** (tb-mcp) | Host (alongside Claude Desktop) | Stdio-based MCP server. Exposes 26 curated tools to Claude Desktop and other MCP clients. Auto-starts bridge daemon if not running. Reuses CLI's HTTP client to call bridge |

### Request flow

Expand Down Expand Up @@ -596,8 +596,7 @@ Experimental — requires the `calendar` Experiment API vendored into the extens
the tech-decision doc, including the ATN manual-review requirement this adds to
signed releases). Calendar listing and event reads are ungated; event
create/update/delete require the `calendarWrite` access switch (default `false`
— see [docs/ACCESS-CONTROL.md](docs/ACCESS-CONTROL.md)). Task CRUD is not yet
implemented.
— see [docs/ACCESS-CONTROL.md](docs/ACCESS-CONTROL.md)).

```bash
# List calendars
Expand All @@ -616,6 +615,45 @@ tb calendar delete <eventId> --calendar <calendarId>
tb calendar clashes --start <date> --end <date>
```

### 20. Tasks

Calendar tasks (VTODO), through the vendored `calendar.tasks` Experiment API
(same `calendar` Experiment as Calendars above — see
[docs/decisions/calendar-backend.md](docs/decisions/calendar-backend.md)).
Writes are gated by the `tasksWrite` access switch (default off, see
[docs/ACCESS-CONTROL.md](docs/ACCESS-CONTROL.md)).

```bash
# List tasks, optionally filtered by calendar or completion state
tb tasks list [--calendar <calendarId>] [--completed] [--pending]
# Returns: [{id, calendarId, title, due, allDay, priority, completed, description, source}]

# Create a task (requires tasksWrite)
tb tasks create --calendar <calendarId> --title <title> [--due <date>] [--all-day] \
[--priority <n>] [--description <description>] [--source <messageId>]

# Update a task's fields (requires tasksWrite)
tb tasks update <taskId> --calendar <calendarId> [--title <title>] [--due <date>] [--all-day] \
[--priority <n>] [--description <description>] [--source <messageId>] [--completed] [--pending]
```

`priority` follows RFC 5545: 0 = undefined, 1-4 = high, 5 = normal, 6-9 = low.
`source` links a task back to the email it was created from, mirroring Notes'
`--source` convention.

### 21. Action Items

Deterministic (no LLM) extraction of candidate action items from a message
body — checklist/bullet syntax, imperative sentences ("Send the report..."),
request phrases ("please...", "can you...", "need to..."), and "by \<date\>"
due-date hints. Pure function (`extension/src/action-items.js`), unit tested
against fixtures independently of the bridge.

```bash
tb action-items <messageId>
# Returns: {items: [{text, checked, dueHint}], markdown}
```

---

## Implementation Details
Expand Down Expand Up @@ -945,7 +983,8 @@ Note: Extension development cannot happen in Docker. Edit `extension/src/backgro
- [x] Opt-in v2 output format, `--output-version 2` (#19)
- [x] Calendar read-only spike (`tb calendar list`, Experiment API) — see [docs/decisions/calendar-backend.md](docs/decisions/calendar-backend.md) (ODIAA-2327)
- [x] Calendar event CRUD + cross-calendar clash detection (`tb calendar events`/`create`/`update`/`delete`/`clashes`), gated by `calendarWrite` (ODIAA-2328)
- [ ] Calendar task CRUD, contacts write, notes, tasks — roadmap, see [docs/PLAN.md](docs/PLAN.md)
- [x] Contacts write (`contactsWrite`), Notes (`tb notes`)
- [x] Tasks CRUD (`tasksWrite`) + deterministic action-item extraction (`tb tasks`, `tb action-items`) (ODIAA-2329)

---

Expand Down Expand Up @@ -1208,15 +1247,16 @@ Claude Desktop ──stdio JSON-RPC──> tb-mcp ──HTTP──> Bridge ─
The MCP server:
- Has **no state** — every tool call is independent
- **Reuses** `cli/src/client.js` for HTTP calls (no code duplication)
- Exposes **16 high-level tools** rather than all 43 CLI commands
- Exposes **31 high-level tools** rather than all CLI commands
- Defaults to **safe behavior** (compose/reply/forward/edit → draft, not send)

### Tool Catalog

The 27 MCP tools are **curated** for AI agent use cases. Bulk admin operations (folder CRUD, identity management, bulk delete, etc.) are intentionally excluded — they belong in the CLI for explicit human control.
The 31 MCP tools are **curated** for AI agent use cases. Bulk admin operations (folder CRUD, identity management, bulk delete, etc.) are intentionally excluded — they belong in the CLI for explicit human control.

| MCP Tool | Maps to CLI commands |
|----------|---------------------|
| `calendar_list` | `tb calendars` |
| `email_stats` | `tb stats` |
| `email_search` | `tb search` |
| `email_list` | `tb list` |
Expand Down Expand Up @@ -1244,6 +1284,10 @@ The 27 MCP tools are **curated** for AI agent use cases. Bulk admin operations (
| `calendar_event_update` | `tb calendar update` (requires `calendarWrite`) |
| `calendar_event_delete` | `tb calendar delete` (requires `calendarWrite`) |
| `calendar_clashes` | `tb calendar clashes` |
| `task_list` | `tb tasks list` |
| `task_create` | `tb tasks create` (requires `tasksWrite`) |
| `task_update` | `tb tasks update` (requires `tasksWrite`) |
| `email_action_items` | `tb action-items` |

Notes are also exposed as MCP **resources** (`note://<name>`, `text/markdown`) so
clients that browse resources rather than call tools can list and read the
Expand Down
3 changes: 2 additions & 1 deletion access.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@
"folderRename": true,
"folderDelete": false,
"contactsWrite": false,
"calendarWrite": false
"calendarWrite": false,
"tasksWrite": false
}
86 changes: 86 additions & 0 deletions cli/src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -1429,6 +1429,92 @@ notes
output(data, getFormat(g), getOutputOpts(g));
}));

// ─── Tasks (ODIAA-2329, VTODO through the calendar_tasks Experiment API) ────

function taskPropertiesFromOpts(opts) {
const properties = {};
if (opts.title) properties.title = opts.title;
if (opts.due) properties.due = opts.due;
if (opts.allDay) properties.allDay = true;
if (opts.priority !== undefined) properties.priority = parseInt(opts.priority, 10);
if (opts.description) properties.description = opts.description;
if (opts.source) properties.source = opts.source;
return properties;
}

const tasks = program.command("tasks").description("Calendar tasks (VTODO)");

tasks
.command("list")
.description("List tasks, filtered by calendar/completion")
.option("--calendar <calendarId>", "limit to one calendar")
.option("--completed", "only completed tasks")
.option("--pending", "only pending (not completed) tasks")
.action(run(async (opts) => {
const g = program.opts();
const body = {};
if (opts.calendar) body.calendarId = opts.calendar;
if (opts.completed) body.completed = true;
else if (opts.pending) body.completed = false;
const data = await api("POST", "/tasks/list", body, getTimeout(g));
output(data, getFormat(g), getOutputOpts(g));
}));

tasks
.command("create")
.description("Create a task (requires tasksWrite access)")
.requiredOption("--calendar <calendarId>", "target calendar")
.requiredOption("--title <title>", "task title")
.option("--due <date>", "due date (ISO date/time, or YYYY-MM-DD with --all-day)")
.option("--all-day", "all-day due date")
.option("--priority <n>", "priority 0-9 (1-4 high, 5 normal, 6-9 low)")
.option("--description <description>", "description")
.option("--source <messageId>", "message id this task was created from")
.action(run(async (opts) => {
const g = program.opts();
const properties = taskPropertiesFromOpts(opts);
const data = await api("POST", "/tasks/create", { calendarId: opts.calendar, ...properties }, getTimeout(g));
output(data, getFormat(g), getOutputOpts(g));
}));

tasks
.command("update <taskId>")
.description("Update a task's fields (tasksWrite required)")
.requiredOption("--calendar <calendarId>", "calendar the task belongs to")
.option("--title <title>", "task title")
.option("--due <date>", "due date (ISO date/time, or YYYY-MM-DD with --all-day)")
.option("--all-day", "all-day due date")
.option("--priority <n>", "priority 0-9 (1-4 high, 5 normal, 6-9 low)")
.option("--description <description>", "description")
.option("--source <messageId>", "message id this task was created from")
.option("--completed", "mark completed")
.option("--pending", "mark not completed")
.action(run(async (taskId, opts) => {
const g = program.opts();
const fmt = getFormat(g);
const outOpts = getOutputOpts(g);
const properties = taskPropertiesFromOpts(opts);
if (opts.completed) properties.completed = true;
else if (opts.pending) properties.completed = false;
if (Object.keys(properties).length === 0) {
outputError({ message: "Provide at least one task property to update", code: "INVALID_ARGS" }, fmt, outOpts);
return;
}
const data = await api("POST", "/tasks/update", { calendarId: opts.calendar, id: taskId, ...properties }, getTimeout(g));
output(data, fmt, outOpts);
}));

// ─── Action items (ODIAA-2329, deterministic extraction from an email body) ─

program
.command("action-items <messageId>")
.description("Extract candidate action items from a message body as a Markdown checklist (deterministic, no LLM)")
.action(run(async (messageId) => {
const g = program.opts();
const data = await api("POST", `/messages/${messageId}/action-items`, {}, getTimeout(g));
output(data, getFormat(g), getOutputOpts(g));
}));

// ─── Parse & Run ──────────────────────────────────────────────────────

program.parseAsync(process.argv).catch(err => {
Expand Down
9 changes: 5 additions & 4 deletions docs/ACCESS-CONTROL.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ closed instead of shipping unrestricted.
| `folderDelete` | `false` | `tb folder-delete` (removes the folder and potentially its contents) |
| `contactsWrite` | `false` | `tb contacts create`/`tb contacts update`, MCP `contact_create`/`contact_update` |
| `calendarWrite` | `false` | `tb calendar create`/`update`/`delete`, MCP `calendar_event_create`/`calendar_event_update`/`calendar_event_delete` |
| `tasksWrite` | `false` | `tb tasks create`/`tb tasks update`, MCP `task_create`/`task_update` |

With a switch off, the route returns `FORBIDDEN: '<key>' is disabled by the add-on access
policy ...` before touching the mailbox. Routes with no switch at all (search, list, read,
Expand All @@ -37,11 +38,11 @@ operations that create, mutate, or send. Bridge-local endpoints (`/bridge/status
gated rather than removed from the extension entirely. Every write switch that predates
ODIAA-2306 defaults on, so an unmodified install behaves exactly as before this policy
existed; operators who want a more restrictive posture set the switches they want to disable
in their config file. `contactsWrite` and `calendarWrite` are the exception: like
`delete`/`folderDelete`, new *write* switches default off (opt-in), matching atbridge's
in their config file. `contactsWrite`, `calendarWrite`, and `tasksWrite` are the exception:
like `delete`/`folderDelete`, new *write* switches default off (opt-in), matching atbridge's
off-by-default write posture — enable them explicitly to let `tb contacts create`/`tb
contacts update` or `tb calendar create`/`update`/`delete` (or the equivalent MCP tools)
mutate an address book or calendar.
contacts update`, `tb calendar create`/`update`/`delete`, or `tb tasks create`/`tb tasks
update` (or the equivalent MCP tools) mutate an address book, a calendar, or a task.

## How a request is checked

Expand Down
24 changes: 23 additions & 1 deletion docs/COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ tb notes to-draft <name> --to <address> [--cc <a>] [--bcc <a>] [--subject <t>] [

Experimental (see `docs/decisions/calendar-backend.md`). Calendar listing and event reads are
ungated; `create`/`update`/`delete` require the `calendarWrite` access switch (default `false`,
see `docs/ACCESS-CONTROL.md`). Task CRUD is not yet implemented.
see `docs/ACCESS-CONTROL.md`).

```bash
tb calendar list # list calendars
Expand All @@ -297,6 +297,28 @@ tb calendar clashes --start <date> --end <date>
# find overlapping events across all calendars
```

## Tasks

Calendar tasks (VTODO), through the vendored `calendar.tasks` Experiment API (see
`docs/decisions/calendar-backend.md`).

```bash
tb tasks list [--calendar <calendarId>] [--completed] [--pending] # list tasks

# Requires tasksWrite access switch (default off, see docs/ACCESS-CONTROL.md)
tb tasks create --calendar <calendarId> --title <title> [--due <date>] [--all-day] \
[--priority <n>] [--description <description>] [--source <messageId>]
tb tasks update <taskId> --calendar <calendarId> [--title <title>] [--due <date>] [--all-day] \
[--priority <n>] [--description <description>] [--source <messageId>] [--completed] [--pending]
```

## Action Items

```bash
tb action-items <messageId> # deterministic (no LLM) extraction of candidate action items
# from a message body, rendered as a Markdown checklist
```

## Bulk Operations

```bash
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ this.calendar_calendars = class extends ExtensionAPI {
// Thunderbird's Experiments docs require a startup-cache invalidation on non-shutdown
// unload (disable/update/reload) — see
// https://developer.thunderbird.net/add-ons/mailextensions/experiments. This add-on has
// two experiment_apis entries (calendar_calendars, calendar_items) but only needs one
// three experiment_apis entries (calendar_calendars, calendar_items, calendar_tasks) but
// only needs one
// invalidation for the whole add-on; this one (the first one registered) owns it.
onShutdown(isAppShutdown) {
if (isAppShutdown) return;
Expand Down
Loading
Loading