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 AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Four artifacts, one architecture. The three Node workspaces below are private an

| Artifact | What | Where |
|---|---|---|
| `thunderbird-cli` | `tb` CLI (43 commands) | `cli/` → bin `tb` in `@odience-network/thunderbird-cli-enhanced` |
| `thunderbird-cli` | `tb` CLI (47 commands) | `cli/` → bin `tb` in `@odience-network/thunderbird-cli-enhanced` |
| `thunderbird-cli-bridge` | Stateless HTTP↔WS proxy daemon | `bridge/` → bin `tb-bridge` in `@odience-network/thunderbird-cli-enhanced` |
| `thunderbird-cli-mcp` | MCP server (16 tools for Claude Desktop) | `mcp/` → bin `tb-mcp` in `@odience-network/thunderbird-cli-enhanced` |
| Thunderbird WebExtension | WS client inside Thunderbird | `extension/` → Mozilla-signed (unlisted) XPI in `dist/releases/` |
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ Without the skill, the MCP still works. With it, Claude automatically uses the s
|---|---|
| **Extension** (`extension/`) | Thunderbird WebExtension. Calls `messenger.*` APIs; every route is classified by the access policy. |
| **Bridge** (`bridge/`) | HTTP↔WebSocket proxy daemon on `127.0.0.1:7700`/`7701`. No business logic; buffers recent extension events for long-polling. |
| **CLI** (`cli/`) | `tb` command — 43 commands. Thin HTTP client. JSON output. |
| **CLI** (`cli/`) | `tb` command — 47 commands. Thin HTTP client. JSON output. |
| **MCP** (`mcp/`) | `tb-mcp` server — 16 curated tools for Claude Desktop. |

Thunderbird is the source of truth. The CLI never caches or stores email data.
Expand Down
43 changes: 37 additions & 6 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
│ ┌────────────────────┬──────────────────────┐ │
│ ↕ ↕ ↕ │
│ tb CLI (Node) tb-mcp Server Direct HTTP │
│ (43 commands) (27 MCP tools) (curl, scripts) │
│ (47 commands) (31 MCP tools) (curl, scripts) │
│ ↕ ↕ │
│ AI Agent Claude Desktop │
│ (Claude Code) (stdio MCP transport) │
Expand All @@ -40,8 +40,8 @@
| **Thunderbird** | Host | Source of truth. Stores all emails, syncs IMAP, renders UI for human oversight |
| **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 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 |
| **CLI** (tb) | Host or Docker | Thin HTTP client. Parses args, calls bridge, outputs JSON to stdout. 47 commands. Auto-starts bridge daemon if not running. Zero state |
| **MCP Server** (tb-mcp) | Host (alongside Claude Desktop) | Stdio-based MCP server. Exposes 41 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 @@ -694,6 +694,33 @@ right after the click, within the listener's timeout) to catch the event.

---

### 20. Deterministic Skills

Zero-LLM-reasoning shortcuts (ODIAA-2332): each composes existing read-only
endpoints (`/calendar/events/list`, `/calendar/clashes`, `/stats`,
`/messages/search`) and hands back pre-formatted, compact Markdown — no JSON
parsing or summarizing required by the caller. The same rendering functions
(`lib/skills.js`) back both the CLI and the matching `skill_*` MCP tools, so
output is byte-identical across surfaces. `--format json|compact|table`
opts back into the standard JSON envelope; otherwise Markdown is printed
regardless of `--output-version`.

```bash
# Today's events plus unread/flagged mail counts
tb today

# This week's events, grouped by day
tb week

# Overlapping events across all calendars in the next N days (default 7)
tb clashes [--days <n>]

# Recent mail from a sender address or domain, grouped into threads
tb from <address> [--limit <n>]
```

---

## Implementation Details

### Token Optimization Strategy
Expand Down Expand Up @@ -1285,12 +1312,12 @@ 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 **31 high-level tools** rather than all CLI commands
- Exposes **41 high-level tools** rather than all CLI commands
- Defaults to **safe behavior** (compose/reply/forward/edit → draft, not send)

### Tool Catalog

The 37 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 41 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 |
|----------|---------------------|
Expand Down Expand Up @@ -1322,6 +1349,10 @@ The 37 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` |
| `skill_today` | `tb today` |
| `skill_week` | `tb week` |
| `skill_clashes` | `tb clashes` |
| `skill_from` | `tb from` |
| `task_list` | `tb tasks list` |
| `task_create` | `tb tasks create` (requires `tasksWrite`) |
| `task_update` | `tb tasks update` (requires `tasksWrite`) |
Expand All @@ -1339,7 +1370,7 @@ local notes workspace directly.

### Why fewer MCP tools than CLI commands?

| | CLI (43 commands) | MCP (27 tools) |
| | CLI (47 commands) | MCP (31 tools) |
|---|---|---|
| Audience | Humans + scripts | AI agents |
| Discovery | `tb --help` | Tool descriptions in LLM context |
Expand Down
5 changes: 3 additions & 2 deletions cli/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# thunderbird-cli

> Low-level CLI to manage Mozilla Thunderbird email from the shell. 43 commands designed for AI agents.
> Low-level CLI to manage Mozilla Thunderbird email from the shell. 47 commands designed for AI agents.

[![tests](https://github.com/odience-network/thunderbird-cli-enhanced/actions/workflows/test.yml/badge.svg)](https://github.com/odience-network/thunderbird-cli-enhanced/actions/workflows/test.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
Expand All @@ -9,13 +9,14 @@ Part of the [Thunderbird CLI Enhanced](https://github.com/odience-network/thunde

## What it does

`tb` is a thin HTTP client that talks to a local Thunderbird WebExtension via a bridge daemon. It exposes 43 commands across all `messenger.*` APIs:
`tb` is a thin HTTP client that talks to a local Thunderbird WebExtension via a bridge daemon. It exposes 47 commands across all `messenger.*` APIs:

- **Search & read** — full-text search across accounts, batch reads, threads
- **Compose** — draft, open, or send (defaults to draft for safety)
- **Folders** — list, create, rename, delete, info, sync
- **Attachments** — list and download (base64 → file)
- **Bulk ops** — mark-read, move, delete, tag, fetch with filters
- **Deterministic skills** — `today`, `week`, `clashes`, `from` render ready-to-show Markdown, no model reasoning required
- **Token-optimized** — `--fields`, `--compact`, `--max-body` for AI use

All output is JSON wrapped in `{ok, data}` / `{ok, error, code}`.
Expand Down
94 changes: 94 additions & 0 deletions cli/src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ import {
searchNotes,
renderNoteHtml,
} from "./notes.js";
import {
startOfDay,
addDays,
renderToday,
renderWeek,
renderClashes,
groupMessagesIntoThreads,
renderFrom,
} from "../../lib/skills.js";

const MIME_TO_EXT = {
"application/pdf": ".pdf",
Expand Down Expand Up @@ -155,6 +164,23 @@ function parseIds(str) {
return str.split(",").map(id => parseInt(id.trim()));
}

// /calendar/events/list and /calendar/clashes return { error } (not a thrown error) when the
// calendar Experiment API hasn't loaded — see docs/decisions/calendar-backend.md.
function calendarErrorOf(result) {
return !Array.isArray(result) && result?.error ? result.error : null;
}

// Deterministic skill commands (ODIAA-2332) print compact Markdown by default — that's the
// point of a zero-parsing "skill" — but still honor an explicit --format for scripting, going
// through the normal v1/v2 JSON envelope machinery in that case.
function outputSkill(globalOpts, structuredData, markdown) {
if (globalOpts.format) {
output(structuredData, getFormat(globalOpts), getOutputOpts(globalOpts));
} else {
process.stdout.write(markdown);
}
}

// ─── Health ───────────────────────────────────────────────────────────

program
Expand Down Expand Up @@ -278,6 +304,74 @@ calendar
output(data, getFormat(g), getOutputOpts(g));
}));

// ─── Deterministic skills (ODIAA-2332) ─────────────────────────────────
// Zero-LLM-reasoning shortcuts: compose existing read-only endpoints, print compact Markdown.

program
.command("today")
.description("Today's calendar events plus unread/flagged mail counts (compact Markdown)")
.action(run(async () => {
const g = program.opts();
const date = startOfDay(new Date());
const end = addDays(date, 1);
const [eventsResult, stats, flaggedResult] = await Promise.all([
api("POST", "/calendar/events/list", { start: date.toISOString(), end: end.toISOString() }, getTimeout(g)),
api("GET", "/stats", null, getTimeout(g)),
api("POST", "/messages/search", { flagged: true, limit: 200 }, getTimeout(g)),
]);
const calendarError = calendarErrorOf(eventsResult);
const flagged = flaggedResult?.messages
? { count: flaggedResult.messages.length, hasMore: !!flaggedResult.hasMore }
: null;
const markdown = renderToday({
date, calendarError, flagged,
events: calendarError ? [] : eventsResult,
unreadTotal: stats?.totalUnread,
});
outputSkill(g, { date: date.toISOString(), events: eventsResult, unreadTotal: stats?.totalUnread, flagged }, markdown);
}));

program
.command("week")
.description("Calendar events for the next 7 days, grouped by day (compact Markdown)")
.action(run(async () => {
const g = program.opts();
const start = startOfDay(new Date());
const end = addDays(start, 7);
const eventsResult = await api("POST", "/calendar/events/list", { start: start.toISOString(), end: end.toISOString() }, getTimeout(g));
const calendarError = calendarErrorOf(eventsResult);
const markdown = renderWeek({ start, numDays: 7, calendarError, events: calendarError ? [] : eventsResult });
outputSkill(g, { start: start.toISOString(), end: end.toISOString(), events: eventsResult }, markdown);
}));

program
.command("clashes")
.description("Overlapping events across all calendars in the next N days (compact Markdown)")
.option("--days <n>", "look ahead this many days", "7")
.action(run(async (opts) => {
const g = program.opts();
const days = parseInt(opts.days) || 7;
const start = startOfDay(new Date());
const end = addDays(start, days);
const result = await api("POST", "/calendar/clashes", { start: start.toISOString(), end: end.toISOString() }, getTimeout(g));
const calendarError = calendarErrorOf(result);
const markdown = renderClashes({ start, end, calendarError, clashes: result?.clashes });
outputSkill(g, result, markdown);
}));

program
.command("from <address>")
.description("Recent mail from a sender address or domain, grouped into threads (compact Markdown)")
.option("-l, --limit <n>", "max messages to consider", "50")
.action(run(async (address, opts) => {
const g = program.opts();
const limit = parseInt(opts.limit) || 50;
const result = await api("POST", "/messages/search", { fromAddress: address, limit }, getTimeout(g));
const threads = groupMessagesIntoThreads(result?.messages);
const markdown = renderFrom({ address, threads, hasMore: !!result?.hasMore });
outputSkill(g, result, markdown);
}));

// ─── Bridge Status ────────────────────────────────────────────────────

program
Expand Down
15 changes: 14 additions & 1 deletion docs/COMMANDS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# CLI Command Reference

All 43 commands in the `tb` CLI. For the quick tour, see the [main README](../README.md). For AI-agent-focused usage, see [CLAUDE.md](CLAUDE.md).
All 47 commands in the `tb` CLI. For the quick tour, see the [main README](../README.md). For AI-agent-focused usage, see [CLAUDE.md](CLAUDE.md).

## Global Options

Expand Down Expand Up @@ -297,6 +297,19 @@ tb calendar clashes --start <date> --end <date>
# find overlapping events across all calendars
```

## Deterministic Skills

Zero-LLM-reasoning shortcuts (ODIAA-2332): each composes existing read-only endpoints above and
prints compact, pre-formatted Markdown by default — pass `--format json|compact|table` to get the
underlying structured data instead (e.g. for scripting). All read-only, ungated.

```bash
tb today # today's calendar events + unread/flagged mail counts
tb week # calendar events for the next 7 days, grouped by day
tb clashes [--days <n>] # overlapping events across all calendars (default: next 7 days)
tb from <address> [-l, --limit <n>] # recent mail from a sender/domain, grouped into threads
```

## Tasks

Calendar tasks (VTODO), through the vendored `calendar.tasks` Experiment API (see
Expand Down
Loading
Loading