diff --git a/AGENTS.md b/AGENTS.md index 6a19512..f4dccc5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,9 +10,9 @@ Four artifacts, one architecture. The three Node workspaces below are private an | Artifact | What | Where | |---|---|---| -| `thunderbird-cli` | `tb` CLI (47 commands) | `cli/` → bin `tb` in `@odience-network/thunderbird-cli-enhanced` | +| `thunderbird-cli` | `tb` CLI (74 commands: mail, contacts, calendar, tasks, notes, skills, Fast Actions) | `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-cli-mcp` | MCP server (42 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/` | ``` @@ -27,8 +27,8 @@ Everything localhost-only. Thunderbird holds credentials; no creds pass through ```bash npm install # installs workspace deps for cli/, bridge/, mcp/ -npm test # 46 CLI/bridge integration tests -npm run test:mcp # 34 MCP server tests +npm test # CLI/bridge integration tests +npm run test:mcp # MCP server tests npm run bridge # start bridge (needed for live tests; not for unit tests) ``` @@ -42,7 +42,7 @@ Node 20+ required. All three packages are ES modules (`"type": "module"`). - **CLI framework:** `commander.js`. Command definitions in `cli/src/cli.js`; HTTP client + formatters in `cli/src/client.js`. - **Bridge:** vanilla Node `http` + `ws`. Zero business logic — it's a UUID-correlated proxy. Don't add state. - **MCP server:** `@modelcontextprotocol/sdk`. Stdio transport. Reuses `cli/src/client.js` — don't re-implement the HTTP client in `mcp/`. -- **Extension:** pure WebExtension (`manifest_version: 2`). No Experiment APIs. Compatible with Thunderbird 128+. +- **Extension:** WebExtension (`manifest_version: 2`) on `messenger.*`, plus three vendored Experiment APIs (`calendar_calendars`, `calendar_items`, `calendar_tasks`) for calendar and tasks only — don't add more without discussing, each one forces ATN manual review. Compatible with Thunderbird 128.0 – 156.* (`strict_max_version` is required with Experiment APIs). ### Output format @@ -53,7 +53,7 @@ By default every command outputs atomic JSON (`--output-version 2` / `TB_OUTPUT_ { "ok": false, "error": "message", "code": "ERROR_CODE" } ``` -Error codes: `BRIDGE_UNREACHABLE`, `EXTENSION_DISCONNECTED`, `AUTH_REQUIRED`, `FORBIDDEN`, `TIMEOUT`, `NOT_FOUND`, `INVALID_ARGS`, `THUNDERBIRD_ERROR`, `EVENT_TIMEOUT`, `RECONNECT_TIMEOUT`. Don't invent new ones — pick an existing one or extend the union intentionally. +Error codes: `BRIDGE_UNREACHABLE`, `EXTENSION_DISCONNECTED`, `AUTH_REQUIRED`, `FORBIDDEN`, `TIMEOUT`, `NOT_FOUND`, `INVALID_ARGS`, `THUNDERBIRD_ERROR`, `EVENT_TIMEOUT`, `RECONNECT_TIMEOUT`, plus `NO_ENGINE` / `ENGINE_FAILED` for `tb notes transcribe`. Don't invent new ones — pick an existing one or extend the union intentionally. ### Safety defaults (NEVER weaken) @@ -128,7 +128,7 @@ Tagging `vX.Y.Z` fires `.github/workflows/release.yml`: 3. Finds the signed XPI in `dist/releases/` (must be checked in) 4. Creates the GitHub Release with both XPIs attached -The signed XPI itself comes from `.github/workflows/sign-xpi.yml`: bumping `version` in `extension/manifest.json` on `main` runs `npm run verify`, signs through the addons.thunderbird.net API (`npm run sign:xpi`, secrets `MOZILLA_HUB_JWT_ISSUER` / `MOZILLA_HUB_JWT_SECRET`) and commits `dist/releases/--tb.xpi`. Tag after that commit lands. +The signed XPI itself comes from `.github/workflows/sign-xpi.yml`: bumping `version` in `extension/manifest.json` on `main` runs `npm run verify`, signs through the addons.thunderbird.net API (`npm run sign:xpi`, secrets `MOZILLA_HUB_JWT_ISSUER` / `MOZILLA_HUB_JWT_SECRET`) and commits `dist/releases/--tb.xpi`. Tag after that commit lands. `release.yml` attaches whichever signed XPI is newest in `dist/releases/`, so while ATN's manual review is pending (as for 2.4.0 at v1.3.0) a release ships the older signed build next to the unsigned current one — say so in the release notes. npm publish is manual (`cd cli && npm publish`) — intentionally, so a release tag without publish is a no-op you can recover from. @@ -139,7 +139,7 @@ npm publish is manual (`cd cli && npm publish`) — intentionally, so a release | `SPEC.md` | Full technical specification — source of truth for tool surface | | `SECURITY.md` | Threat model, CLI defenses, agent patterns | | `docs/SETUP.md` | User install guide | -| `docs/COMMANDS.md` | All 43 CLI commands reference | +| `docs/COMMANDS.md` | All 74 CLI commands reference | | `docs/CLAUDE.md` | Claude Code–focused CLI quick-ref (end-user oriented) | | `docs/distribution-log.md` | Launch venue submission tracker | | `skills/thunderbird-cli/SKILL.md` | End-user Claude skill (separate from this file) | @@ -152,7 +152,7 @@ Don't propose these without discussing first: - **Server-side filtering logic.** Thunderbird is source of truth; we don't reimplement IMAP filter semantics. - **Cloud deployment.** Bridge is localhost-only by design. No remote auth, no TLS, no tunnels. - **New credential flow.** We never touch user email credentials. If a feature needs OAuth, it doesn't belong here. -- **Breaking the CLI ↔ MCP symmetry.** CLI is the superset (38 cmds); MCP is the curated 12-tool subset. When adding a bulk op, CLI-only is the correct choice — don't expose it as an MCP tool unless it's individually-scoped. +- **Breaking the CLI ↔ MCP symmetry.** CLI is the superset (74 cmds); MCP is the curated 42-tool subset. When adding a bulk op, CLI-only is the correct choice — don't expose it as an MCP tool unless it's individually-scoped. ## Questions / weirdness diff --git a/CHANGELOG.md b/CHANGELOG.md index 384c795..ec8b3a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,9 +16,9 @@ Needs add-on 2.4.0 for the calendar, task and Fast Actions commands. Until ATN f ### Added - Calendars and events: `tb calendar list|events|create|update|delete|clashes` and MCP `calendar_list`/`calendar_events`/`calendar_event_create`/`calendar_event_update`/`calendar_event_delete`/`calendar_clashes`, backed by vendored `calendar_calendars` and events-only `calendar_items` Experiment APIs (see [docs/decisions/calendar-backend.md](docs/decisions/calendar-backend.md)). Clash detection across calendars is a pure function (`extension/src/calendar-clash.js`). Writes are gated by `calendarWrite`. (ODIAA-2328) -- Tasks: `tb tasks list|create|update` and MCP `task_list`/`task_create`/`task_update` through a vendored `calendar_tasks` Experiment API, with writes gated by `tasksWrite`. `tb action-items ` turns an email body into a Markdown checklist with a deterministic (no LLM) extractor. (ODIAA-2329) +- Tasks: `tb tasks list|create|update` and MCP `task_list`/`task_create`/`task_update` through a vendored `calendar_tasks` Experiment API, with writes gated by `tasksWrite`. `tb action-items ` (MCP `email_action_items`) turns an email body into a Markdown checklist with a deterministic (no LLM) extractor. (ODIAA-2329) - Deterministic skills: `tb today`/`week`/`clashes`/`from` and MCP `skill_today`/`skill_week`/`skill_clashes`/`skill_from` render compact Markdown from existing read-only endpoints; CLI and MCP output are byte-identical. (ODIAA-2332) -- Fast Actions: one-click Email → Note / Task / Event / Contact (`tb email-to-note`/`email-to-task`/`email-to-event`/`email-to-contact`, matching MCP tools, and context-menu items in Thunderbird's message list/display). `email-to-event` uses a new deterministic (no LLM) date/time/location parser (`extension/src/email-event-parse.js`) and falls back to a tentative draft event when it can't detect one. `email-to-contact` dedupes by sender email. Two new ungated routes (`GET /addressbooks`, `POST /messages/:id/event-draft`); no new access switch — writes are gated by the existing `tasksWrite`/`calendarWrite`/`contactsWrite` switches. (ODIAA-2333) +- Fast Actions: one-click Email → Note / Task / Event / Contact (`tb email-to-note`/`email-to-task`/`email-to-event`/`email-to-contact`, matching MCP tools, and context-menu items in Thunderbird's message list/display). "Save to Notes" hands the message to a waiting `tb notes listen` / MCP `notes_listen_once`, and `tb address-books` / MCP `address_book_list` lists the address books `email-to-contact` can write to. `email-to-event` uses a new deterministic (no LLM) date/time/location parser (`extension/src/email-event-parse.js`) and falls back to a tentative draft event when it can't detect one. `email-to-contact` dedupes by sender email. Two new ungated routes (`GET /addressbooks`, `POST /messages/:id/event-draft`); no new access switch — writes are gated by the existing `tasksWrite`/`calendarWrite`/`contactsWrite` switches. (ODIAA-2333) - **`tb notes transcribe `** / MCP `note_transcribe` — voice memo transcription into Notes. Local-first: at runtime it detects a locally-installed speech-to-text engine — [whisper.cpp](https://github.com/ggml-org/whisper.cpp) (`whisper-cli` on PATH, or @@ -33,7 +33,7 @@ Needs add-on 2.4.0 for the calendar, task and Fast Actions commands. Until ATN f stubbed engine (`test/fixtures/stub-*.mjs`). (ODIAA-2331) ### Changed -- Extension 2.4.0 declares `strict_max_version: "155.*"`. ATN requires a max version for any add-on with Experiment APIs, so each new Thunderbird major needs a manifest bump and re-signing. Signing also waits for ATN's manual review. +- Extension 2.4.0 declares `strict_max_version: "156.*"`. ATN requires a max version for any add-on with Experiment APIs, so each new Thunderbird major needs a manifest bump and re-signing. Signing also waits for ATN's manual review. - Release and sign-xpi workflows build on Node 22 (was Node 20, past end of life). The package still declares `engines.node >= 18`. ## [1.2.1] — 2026-09-27 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 44e8bf3..1979ec2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -18,10 +18,10 @@ thunderbird-cli-enhanced/ The four runtime components are intentionally separated: -- **extension** — runs inside Thunderbird, calls `messenger.*` APIs, talks to bridge over WebSocket +- **extension** — runs inside Thunderbird, calls `messenger.*` APIs (plus vendored calendar/task Experiment APIs), talks to bridge over WebSocket - **bridge** — stateless HTTP↔WS proxy, no business logic -- **cli** — thin HTTP client, parses args, formats JSON output -- **mcp** — MCP server, exposes 16 curated tools to Claude Desktop and other MCP clients +- **cli** — thin HTTP client, parses args, formats JSON output (74 commands; the notes workspace and local transcription run in `cli/` itself) +- **mcp** — MCP server, exposes 42 curated tools to Claude Desktop and other MCP clients ## Local development @@ -52,11 +52,13 @@ node cli/src/cli.js stats npm test # CLI/bridge integration (mock bridge) npm run test:bridge-auth # bridge TB_AUTH_TOKEN enforcement (real bridge process) npm run test:bridge-security # bridge browser-origin defenses (real bridge process) -npm run test:extension # background script, thread utils, access policy (mocked messenger API) +npm run test:extension # background script, thread utils, access policy, action items, calendar clashes (mocked messenger API) npm run test:sign # XPI signing script (mock ATN API) npm run test:docs # docs/COMMANDS.md and README match `tb --help`; diagrams are complete +npm run test:notes # notes workspace and transcription (stubbed STT engine) +npm run test:skills # deterministic skills (today/week/clashes/from) npm run test:mcp # MCP server integration (spawns server) -npm run test:all # all of the above + MCP concurrency +npm run test:all # all of the above + MCP concurrency + npm pack check npm run verify # everything CI runs: build XPI, lint, all test suites ``` diff --git a/README.md b/README.md index 21f987c..15d7034 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ [![Thunderbird](https://img.shields.io/badge/thunderbird-128%2B-blue)](https://www.thunderbird.net) [![MCP](https://img.shields.io/badge/MCP-compatible-purple)](https://modelcontextprotocol.io) -This is a maintained fork of [vitalio-sh/thunderbird-cli](https://github.com/vitalio-sh/thunderbird-cli). It integrates fixes and features from the community forks listed under [Why this fork](#why-this-fork) and adds an access policy, a signed-XPI release pipeline, and draft editing. +This is a maintained fork of [vitalio-sh/thunderbird-cli](https://github.com/vitalio-sh/thunderbird-cli). It integrates fixes and features from the community forks listed under [Why this fork](#why-this-fork) and adds an access policy, a signed-XPI release pipeline, draft editing, and (since 1.3.0) calendar, tasks, notes and one-click Fast Actions.

thunderbird-cli demo — Claude answering an email-overview question via MCP @@ -23,15 +23,18 @@ IMAP libraries force you to manage credentials, OAuth flows, and sync state — ## Features - 🔐 **Zero credential exposure** — all IMAP/SMTP stays in Thunderbird -- 🤖 **Claude Desktop ready** — 16 MCP tools, one-line config -- 📨 **43 CLI commands** — read, search, compose, reply, edit drafts, bulk ops, folder CRUD, attachments, contacts (read/write) -- 🛡️ **Access policy** — one policy baked into the add-on gates every write/send route for CLI, MCP and raw bridge calls; deletion is off by default and unknown routes fail closed ([docs/ACCESS-CONTROL.md](docs/ACCESS-CONTROL.md)) +- 🤖 **Claude Desktop ready** — 42 MCP tools, one-line config +- 📨 **74 CLI commands** — read, search, compose, reply, edit drafts, bulk ops, folder CRUD, attachments, contacts (read/write) +- 📅 **Calendar & tasks** — list calendars and events, create/update/delete events, cross-calendar clash detection, task CRUD, and deterministic action-item extraction from an email (needs add-on 2.4.0) +- ⚡ **Fast Actions & skills** — one-click Email → Note / Task / Event / Contact (CLI, MCP and Thunderbird's context menu); `tb today`/`week`/`clashes`/`from` render compact Markdown without spending LLM tokens +- 📝 **Notes workspace** — local Markdown notes ("Use as Context", "Save to Notes", render to a draft) and local voice-memo transcription (`tb notes transcribe`, whisper.cpp / faster-whisper, nothing leaves the machine) +- 🛡️ **Access policy** — one policy baked into the add-on gates every write/send route for CLI, MCP and raw bridge calls; deletion and contact/calendar/task writes (`contactsWrite`, `calendarWrite`, `tasksWrite`) are off by default and unknown routes fail closed ([docs/ACCESS-CONTROL.md](docs/ACCESS-CONTROL.md)) - ✉️ **Safe by default** — compose/reply/forward/edit save as drafts; permanent delete requires `--confirm` - 🚀 **Bridge auto-start** — the CLI and MCP server start the bridge daemon on first use - 🎯 **Token-optimized** — `--fields` selection, `--compact` mode, `--max-body` truncation, `-f json|compact|table`, opt-in leaner output with `--output-version 2` - ⚡ **Fast on large folders** — server-side full-text query, sort (TB 148+), indexed `--unread`/`--flagged` plus date/size/tag filtering, `--subject`/`--from` search - 🏠 **Localhost-only** — no cloud, no telemetry, nothing leaves your machine -- ✅ **Thunderbird 128+** — Mozilla-signed XPI built and signed by CI ([dist/releases/](dist/releases/)) +- ✅ **Thunderbird 128+** — Mozilla-signed XPI built and signed by CI ([dist/releases/](dist/releases/); add-on 2.4.0 is awaiting ATN review, see [Quick Start](#quick-start)) - 🧪 **Tested** — `npm run test:all` runs the CLI/bridge, bridge security, extension, access-control, signing and MCP suites ## Quick Start @@ -57,7 +60,7 @@ cd thunderbird-cli-enhanced Then: -1. Install the signed extension from [`dist/releases/`](dist/releases/): Thunderbird → Add-ons → ⚙ → **Install Add-on From File…** → the `*-tb.xpi` file. The signed 2.1.0 build predates the rename and still shows as "Thunderbird AI Bridge" in the Add-ons Manager. +1. Install the signed extension from [`dist/releases/`](dist/releases/): Thunderbird → Add-ons → ⚙ → **Install Add-on From File…** → the `*-tb.xpi` file. The only ATN-signed build is 2.1.0 (`thunderbird_ai_bridge-2.1.0-tb.xpi`): it predates the rename, shows as "Thunderbird AI Bridge" in the Add-ons Manager, and has no calendar, tasks or Fast Actions. Add-on 2.4.0 ("Thunderbird CLI Enhanced") is attached **unsigned** to the [v1.3.0 GitHub Release](https://github.com/odience-network/thunderbird-cli-enhanced/releases/tag/v1.3.0) while ATN's manual review is pending; load it via Tools → Developer Tools → Debug Add-ons → **Load Temporary Add-on…**, or on a Thunderbird build that allows unsigned add-ons. Details: [docs/SETUP.md](docs/SETUP.md#step-2-install-the-thunderbird-extension). 2. Try it — the bridge starts automatically on first use: ```bash @@ -91,6 +94,12 @@ tb attachment-download 11 1.2 --output invoice.pdf # Bulk archive old newsletters tb bulk move "account1://INBOX" "account1://Archive" \ --from "newsletter@" --older-than 30 + +# Today's events plus unread/flagged counts as compact Markdown (no LLM tokens) +tb today + +# Turn an email into a task (needs add-on 2.4.0 and tasksWrite) +tb email-to-task 89900 --calendar --due 2026-10-03 ``` Full command reference: **[docs/COMMANDS.md](docs/COMMANDS.md)** @@ -127,7 +136,7 @@ Full MCP guide: **[mcp/README.md](mcp/README.md)** ### Companion skill for Claude -A [Claude Skill](https://agentskills.io) ships alongside the MCP server. It teaches Claude *how to use* the 16 email tools well — token-efficient field selection, draft-by-default safety, checking trust signals before acting on links, recipes for common workflows. Install it from **[`skills/thunderbird-cli/`](skills/thunderbird-cli/)**: +A [Claude Skill](https://agentskills.io) ships alongside the MCP server. It teaches Claude *how to use* the 42 MCP tools well — token-efficient field selection, draft-by-default safety, checking trust signals before acting on links, recipes for common workflows. Install it from **[`skills/thunderbird-cli/`](skills/thunderbird-cli/)**: ```bash # Claude Code @@ -147,7 +156,7 @@ Without the skill, the MCP still works. With it, Claude automatically uses the s - Architecture — agents and shells call tb-mcp or the tb CLI, which talk HTTP to the localhost bridge; the bridge relays over WebSocket to the Thunderbird WebExtension, which checks the access policy and calls messenger.* APIs + Architecture — agents and shells call tb-mcp or the tb CLI, which talk HTTP to the localhost bridge; the bridge relays over WebSocket to the Thunderbird WebExtension, which checks the access policy and calls the messenger.* and calendar Experiment APIs; notes are local Markdown files the CLI and MCP server read directly
Click for the interactive version. More diagrams: docs/diagrams/ @@ -155,10 +164,10 @@ Without the skill, the MCP still works. With it, Claude automatically uses the s | Component | Role | |---|---| -| **Extension** (`extension/`) | Thunderbird WebExtension. Calls `messenger.*` APIs; every route is classified by the access policy. | +| **Extension** (`extension/`) | Thunderbird WebExtension. Calls `messenger.*` APIs, plus vendored calendar/task Experiment APIs; adds the Fast Actions context-menu items. 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 — 47 commands. Thin HTTP client. JSON output. | -| **MCP** (`mcp/`) | `tb-mcp` server — 16 curated tools for Claude Desktop. | +| **CLI** (`cli/`) | `tb` command — 74 commands (mail, contacts, calendar, tasks, notes, skills, Fast Actions). Thin HTTP client. JSON output. | +| **MCP** (`mcp/`) | `tb-mcp` server — 42 curated tools for Claude Desktop. | Thunderbird is the source of truth. The CLI never caches or stores email data. @@ -199,14 +208,23 @@ Upstream baseline is [vitalio-sh/thunderbird-cli@`465613d`](https://github.com/v ## Roadmap +Shipped in [1.3.0](CHANGELOG.md#130--2026-09-28), toward feature parity with [atbridge.ai](https://atbridge.ai): + +- Calendar read, event CRUD and cross-calendar clash detection (`tb calendar list`/`events`/`create`/`update`/`delete`/`clashes`, gated by `calendarWrite`), via chrome-privileged Experiment APIs since Thunderbird's WebExtension model has no calendar access — 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. +- Task CRUD (`tb tasks`, gated by `tasksWrite`) and deterministic action-item extraction (`tb action-items`). +- Contacts write (`tb contacts create`/`update`, gated by `contactsWrite`). +- Notes workspace and local voice-memo transcription (`tb notes`, `tb notes transcribe`). +- Deterministic skills (`tb today`/`week`/`clashes`/`from`) and one-click Fast Actions (`tb email-to-note`/`email-to-task`/`email-to-event`/`email-to-contact`, plus matching context-menu items in Thunderbird). + Planned, **not on `main`**: -- 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, followed by one-click Fast Actions (`tb email-to-note`/`email-to-task`/`email-to-event`/`email-to-contact`, plus matching context-menu items in Thunderbird) that compose those routes without a new access switch — 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. +- Remaining atbridge.ai parity items from [docs/PLAN.md §4](docs/PLAN.md#4-feature-parity-target-atbridgeai) not covered above. +- ATN signing of add-on 2.4.0 (manual review pending; until then 2.4.0 ships unsigned). - Roadmap lifecycle: upstream → fork merges → mail today → calendar and contacts write → notes and tasks → atbridge parity; an extension stability pass runs alongside + Roadmap lifecycle: upstream → fork merges → mail → calendar and contacts write → notes, tasks, skills and Fast Actions, all shipped in v1.3.0; still planned: an extension stability pass and the remaining atbridge parity gaps Details and status: [docs/PLAN.md](docs/PLAN.md). @@ -229,7 +247,7 @@ The niche: **you already trust Thunderbird with your credentials and account sta | Doc | What's inside | |---|---| | [docs/SETUP.md](docs/SETUP.md) | Installation, background service, Docker, troubleshooting | -| [docs/COMMANDS.md](docs/COMMANDS.md) | Full reference for all 43 CLI commands | +| [docs/COMMANDS.md](docs/COMMANDS.md) | Full reference for all 74 CLI commands | | [docs/ACCESS-CONTROL.md](docs/ACCESS-CONTROL.md) | The access policy: switches, defaults, how to change them | | [docs/diagrams/](docs/diagrams/) | Architecture, search sequence, access control, release and roadmap diagrams | | [docs/CLAUDE.md](docs/CLAUDE.md) | AI-agent-focused quick reference + security rules | diff --git a/SECURITY.md b/SECURITY.md index fb86c5c..9b7a643 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -473,7 +473,7 @@ adversarial text, never as instructions. | Junk exclusion | CLI defaults | Excludes spam from results | Spam-based injection | ✅ enforced | | Caller authentication | Bridge | Requires `Authorization: Bearer` (`TB_AUTH_TOKEN`) | Untrusted local process reaching the mailbox | ✅ enforced | | Origin / Host checks | Bridge | Rejects browser origins, rebinding hosts, web-page WebSockets | Hostile web page (CSRF, DNS rebinding, extension hijack) | ✅ enforced | -| Access policy | Extension | Every write/send route gated by a build-time switch, deny-by-default for unclassified routes; deletion off unless built with `delete` / `folderDelete` ([ACCESS-CONTROL.md](docs/ACCESS-CONTROL.md)) | Mass/permanent deletion, and any unreviewed new write route, by any caller | ✅ enforced | +| Access policy | Extension | Every write/send route gated by a build-time switch, deny-by-default for unclassified routes; deletion off unless built with `delete` / `folderDelete`, and contact, calendar and task writes off unless built with `contactsWrite` / `calendarWrite` / `tasksWrite` ([ACCESS-CONTROL.md](docs/ACCESS-CONTROL.md)) | Mass/permanent deletion, unwanted contact/calendar/task changes, and any unreviewed new write route, by any caller | ✅ enforced | | Read-only mode | Bridge | Disables all writes | Any write-based attack | ⏳ design | | Rate limiting | Bridge | Throttles write ops | Mass exfiltration/deletion | ⏳ design | | Audit log | Bridge | Logs all writes | Post-incident forensics | ⏳ design | diff --git a/SPEC.md b/SPEC.md index bb63bdb..8f6ab2c 100644 --- a/SPEC.md +++ b/SPEC.md @@ -21,7 +21,7 @@ │ ┌────────────────────┬──────────────────────┐ │ │ ↕ ↕ ↕ │ │ tb CLI (Node) tb-mcp Server Direct HTTP │ -│ (75 commands) (42 MCP tools) (curl, scripts) │ +│ (74 commands) (42 MCP tools) (curl, scripts) │ │ ↕ ↕ │ │ AI Agent Claude Desktop │ │ (Claude Code) (stdio MCP transport) │ @@ -40,7 +40,7 @@ | **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. 75 commands. Auto-starts bridge daemon if not running. Zero state | +| **CLI** (tb) | Host or Docker | Thin HTTP client. Parses args, calls bridge, outputs JSON to stdout. 74 commands. Auto-starts bridge daemon if not running. Zero state | | **MCP Server** (tb-mcp) | Host (alongside Claude Desktop) | Stdio-based MCP server. Exposes 42 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 @@ -71,14 +71,15 @@ More diagrams (architecture, access control, release, roadmap): [docs/diagrams/] | 120 – 127 | ⚠️ May work but untested | | **128 ESR** | ✅ Primary target (current LTS) | | 129 – 148 | ✅ Supported | -| **149+ (Nebula)** | ✅ Tested and working | +| **149 – 156 (Nebula)** | ✅ Tested and working | +| > 156 | ⛔ Blocked by `strict_max_version` until re-tested | Key API dependencies by version: - TB 120: `messenger.folders.get()`, `folders.query()` - TB 121: `messenger.messages.createTag()`, auto-pagination - TB 128: All APIs stable in ESR -Our manifest specifies `"strict_min_version": "128.0"`. +Our manifest specifies `"strict_min_version": "128.0"` and `"strict_max_version": "156.*"` (add-on ID `thunderbird-cli-enhanced@odience.net`, version 2.4.0 as of v1.3.0). Manifest format: `manifest_version: 2` (MV2). MV3 migration planned for future. --- @@ -879,7 +880,7 @@ The bridge is spawned as a detached process (`child.unref()`) so it outlives the ### Extension Implementation Notes -1. **Pure WebExtension** — manifest_version 2, no experiment_apis +1. **WebExtension + calendar Experiments** — manifest_version 2; mail uses stock `messenger.*` APIs, calendar/tasks use the vendored `calendar_calendars`, `calendar_items` and `calendar_tasks` Experiment APIs 2. **messenger.* API** — uses Thunderbird's native WebExtension APIs 3. **Auto-reconnect** — reconnects to bridge WebSocket with backoff (3s, 6s, 12s, then every 15s) if disconnected; retries immediately when the user returns from idle 4. **No state** — extension is stateless; all state lives in Thunderbird's mail store @@ -1051,8 +1052,8 @@ Note: Extension development cannot happen in Docker. Edit `extension/src/backgro - [x] Config file support (~/.config/thunderbird-cli/config.json) - [x] Environment variable overrides - [x] `--timeout` flag -- [ ] npm publish -- [ ] GitHub release with setup instructions +- [x] npm publish (`@odience-network/thunderbird-cli-enhanced`, with provenance) +- [x] GitHub release with setup instructions (v1.3.0) - [x] CLAUDE.md for agent integration ### Phase 6: Fork integration (Thunderbird CLI Enhanced) @@ -1066,8 +1067,11 @@ 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) -- [x] Contacts write (`contactsWrite`), Notes (`tb notes`) +- [x] Contacts write (`contactsWrite`) (#21), Notes (`tb notes`) (#22) - [x] Tasks CRUD (`tasksWrite`) + deterministic action-item extraction (`tb tasks`, `tb action-items`) (ODIAA-2329) +- [x] Local notes transcription (`tb notes transcribe`, whisper.cpp / faster-whisper) (ODIAA-2331) +- [x] Deterministic skills (`tb today`/`week`/`clashes`/`from`, `skill_*` MCP tools) (ODIAA-2332) +- [x] Fast Actions (`tb email-to-note`/`-task`/`-event`/`-contact`, context-menu items) (ODIAA-2333) --- @@ -1330,7 +1334,7 @@ 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 **41 high-level tools** rather than all CLI commands +- Exposes **42 high-level tools** rather than all CLI commands - Defaults to **safe behavior** (compose/reply/forward/edit → draft, not send) ### Tool Catalog @@ -1339,7 +1343,6 @@ The 42 MCP tools are **curated** for AI agent use cases. Bulk admin operations ( | MCP Tool | Maps to CLI commands | |----------|---------------------| -| `calendar_list` | `tb calendars` | | `email_stats` | `tb stats` | | `email_search` | `tb search` | | `email_list` | `tb list` | @@ -1389,7 +1392,7 @@ local notes workspace directly. ### Why fewer MCP tools than CLI commands? -| | CLI (75 commands) | MCP (42 tools) | +| | CLI (74 commands) | MCP (42 tools) | |---|---|---| | Audience | Humans + scripts | AI agents | | Discovery | `tb --help` | Tool descriptions in LLM context | @@ -1402,14 +1405,14 @@ The MCP catalog is intentionally tight to keep the LLM's tool list focused and p ### Distribution -The MCP server is published to npm as `thunderbird-cli-mcp` with a `tb-mcp` binary. Users add it to their Claude Desktop config: +The MCP server ships in the npm package `@odience-network/thunderbird-cli-enhanced` as the `tb-mcp` binary (alongside `tb` and `tb-bridge`). Users add it to their Claude Desktop config: ```json { "mcpServers": { "thunderbird": { "command": "npx", - "args": ["-y", "thunderbird-cli-mcp"] + "args": ["-y", "-p", "@odience-network/thunderbird-cli-enhanced", "tb-mcp"] } } } diff --git a/assets/architecture.svg b/assets/architecture.svg index d31e4c2..378dcb1 100644 --- a/assets/architecture.svg +++ b/assets/architecture.svg @@ -10,7 +10,7 @@ - thunderbird-cli — architecture + Thunderbird CLI Enhanced — architecture localhost-only · agents → bridge → Thunderbird · no cloud, no telemetry, no stored credentials @@ -37,21 +37,21 @@ tb - CLI · 38 commands - npm: thunderbird-cli + CLI · 74 commands + bin: tb tb-mcp - MCP server · 12 tools - npm: thunderbird-cli-mcp + MCP server · 42 tools + bin: tb-mcp Bridge daemon - npm: thunderbird-cli-bridge + bin: tb-bridge HTTP server @@ -70,21 +70,21 @@ WebExtension - 43 route handlers · WS client - signed on addons.thunderbird.net + route handlers · access policy + add-on 2.4.0 · WS client calls messenger.* API - WebExtension APIs + + calendar_* Experiment APIs IMAP / SMTP / EWS - Your email accounts + Mail, calendars, contacts credentials stay in Thunderbird tested: 22 accounts · 249K messages @@ -109,6 +109,6 @@ Design principles Thunderbird is source of truth · CLI is a dumb pipe · atomic JSON output · idempotent ops · --confirm for destructive actions - MIT · v1.0.1 + MIT · v1.3.0 diff --git a/assets/social-preview.png b/assets/social-preview.png index a118436..baf1e13 100644 Binary files a/assets/social-preview.png and b/assets/social-preview.png differ diff --git a/assets/social-preview.svg b/assets/social-preview.svg index 7e3b2cb..77d5235 100644 --- a/assets/social-preview.svg +++ b/assets/social-preview.svg @@ -17,16 +17,16 @@ - thunderbird-cli + thunderbird-cli-enhanced - Give Claude full access to your email - through Mozilla Thunderbird. + Give Claude your email, calendar, + tasks and notes via Thunderbird. - 38 CLI commands · 12 MCP tools for Claude Desktop + 74 CLI commands · 42 MCP tools for Claude Desktop @@ -67,14 +67,12 @@ MIT · - 80 tests + v1.3.0 · Thunderbird 128+ · - npm: thunderbird-cli - · - MCP compatible + bins: tb · tb-mcp - github.com/vitalio-sh/thunderbird-cli + github.com/odience-network/thunderbird-cli-enhanced diff --git a/bridge/README.md b/bridge/README.md index b70707c..5db1895 100644 --- a/bridge/README.md +++ b/bridge/README.md @@ -23,7 +23,13 @@ HTTP client (CLI / MCP / curl) ──HTTP→ tb-bridge ──WS→ Thunderbird E ## Install -From a clone of this repository (the published npm package comes from upstream and lags this fork): +From npm — `tb-bridge` ships in the same package as `tb` and `tb-mcp` (the unscoped `thunderbird-cli-bridge` package is upstream's): + +```bash +npm i -g @odience-network/thunderbird-cli-enhanced +``` + +Or from a clone of this repository: ```bash git clone https://github.com/odience-network/thunderbird-cli-enhanced @@ -132,7 +138,7 @@ All other paths get forwarded to the Thunderbird extension via WebSocket and the The bridge alone does nothing useful — you need: -1. **Mozilla Thunderbird 128+** with the signed extension from [`dist/releases/`](https://github.com/odience-network/thunderbird-cli-enhanced/tree/main/dist/releases) installed +1. **Mozilla Thunderbird 128+** with the Thunderbird CLI Enhanced add-on installed: the signed build from [`dist/releases/`](https://github.com/odience-network/thunderbird-cli-enhanced/tree/main/dist/releases) (2.1.0, email/contacts/notes), or the unsigned 2.4.0 build attached to the [v1.3.0 GitHub Release](https://github.com/odience-network/thunderbird-cli-enhanced/releases/tag/v1.3.0) for calendar, tasks and Fast Actions while ATN review is pending 2. **Optional:** the `tb` CLI (`cli/`) or the `tb-mcp` server (`mcp/`) as the HTTP client See the [main repo setup guide](https://github.com/odience-network/thunderbird-cli-enhanced/blob/main/docs/SETUP.md). diff --git a/cli/README.md b/cli/README.md index b455e97..12d851c 100644 --- a/cli/README.md +++ b/cli/README.md @@ -1,6 +1,6 @@ # thunderbird-cli -> Low-level CLI to manage Mozilla Thunderbird email from the shell. 47 commands designed for AI agents. +> Low-level CLI to manage Mozilla Thunderbird email, contacts, calendar, tasks and notes from the shell. 74 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) @@ -9,13 +9,17 @@ 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 47 commands across all `messenger.*` APIs: +`tb` is a thin HTTP client that talks to a local Thunderbird WebExtension via a bridge daemon. It exposes 74 commands across the `messenger.*` APIs and the add-on's calendar/task Experiment 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 +- **Contacts** — search, read, create/update (gated by `contactsWrite`), list address books +- **Calendar & tasks** — calendars, events CRUD, clash detection, task CRUD, action items from an email (add-on 2.4.0; writes gated by `calendarWrite` / `tasksWrite`) +- **Notes** — local Markdown workspace, render to a draft, local voice-memo transcription (`notes transcribe`) +- **Fast Actions** — `email-to-note`, `email-to-task`, `email-to-event`, `email-to-contact` - **Deterministic skills** — `today`, `week`, `clashes`, `from` render ready-to-show Markdown, no model reasoning required - **Token-optimized** — `--fields`, `--compact`, `--max-body` for AI use @@ -26,14 +30,20 @@ All output is JSON wrapped in `{ok, data}` / `{ok, error, code}`. This package alone is **not enough**. You need: 1. **Mozilla Thunderbird 128+** with email accounts configured -2. **`thunderbird-cli-bridge`** daemon running on `127.0.0.1:7700` -3. **The signed Thunderbird WebExtension** loaded in Thunderbird +2. **`tb-bridge`** daemon running on `127.0.0.1:7700` (the CLI auto-starts it) +3. **The Thunderbird CLI Enhanced add-on** loaded in Thunderbird — the ATN-signed 2.1.0 build covers email, contacts and notes; calendar, tasks and Fast Actions need 2.4.0, attached unsigned to the v1.3.0 GitHub Release while ATN review is pending See the [main repo setup guide](https://github.com/odience-network/thunderbird-cli-enhanced/blob/main/docs/SETUP.md) for the full installation. ## Install -From a clone of this repository (the published npm package comes from upstream and lags this fork): +From npm (one package ships `tb`, `tb-bridge` and `tb-mcp`; the unscoped `thunderbird-cli` package is upstream's): + +```bash +npm i -g @odience-network/thunderbird-cli-enhanced +``` + +Or from a clone of this repository: ```bash git clone https://github.com/odience-network/thunderbird-cli-enhanced @@ -63,6 +73,7 @@ Full reference: [docs/COMMANDS.md](https://github.com/odience-network/thunderbir | `TB_BRIDGE_HOST` | `127.0.0.1` | | `TB_BRIDGE_PORT` | `7700` | | `TB_AUTH_TOKEN` | (none) | +| `TB_NOTES_DIR` | `~/.config/thunderbird-cli/notes` | Config file: `~/.config/thunderbird-cli/config.json` diff --git a/cli/package.json b/cli/package.json index c2ddb11..42cf575 100644 --- a/cli/package.json +++ b/cli/package.json @@ -2,7 +2,7 @@ "name": "thunderbird-cli", "version": "1.3.0", "private": true, - "description": "Low-level CLI to manage Mozilla Thunderbird email from the shell. 41 commands for search, read, compose, reply, edit drafts, attachments, folders, bulk ops. Designed for AI agents.", + "description": "Low-level CLI to manage Mozilla Thunderbird email, contacts, calendar, tasks and notes from the shell. 74 commands for search, read, compose, reply, edit drafts, attachments, folders, bulk ops, calendar events, tasks, notes, Fast Actions and deterministic skills. Designed for AI agents.", "type": "module", "bin": { "tb": "./src/cli.js" diff --git a/dist/releases/README.md b/dist/releases/README.md index 02cffd7..5b7c6bb 100644 --- a/dist/releases/README.md +++ b/dist/releases/README.md @@ -7,6 +7,13 @@ This directory contains **ATN-signed** Thunderbird extension builds. These are t | File | Version | Signed by | Date | |---|---|---|---| | `thunderbird_ai_bridge-2.1.0-tb.xpi` | 2.1.0 (`thunderbird-cli-enhanced@odience.net`) | addons.thunderbird.net, unlisted | 2026-09-27 | +| `thunderbird_cli_enhanced-2.4.0-tb.xpi` (not yet here) | 2.4.0 (`thunderbird-cli-enhanced@odience.net`) | pending ATN manual review | — | + +2.1.0 predates the rename, so Thunderbird's Add-ons Manager shows it as "Thunderbird AI Bridge", +and it has no calendar, tasks or Fast Actions. 2.4.0 ("Thunderbird CLI Enhanced", the v1.3.0 +add-on) passed ATN automated validation and awaits manual review because of its calendar +Experiment APIs; until it lands here, the unsigned 2.4.0 XPI is attached to the v1.3.0 GitHub +Release. ## Why these are in git diff --git a/docs/ACCESS-CONTROL.md b/docs/ACCESS-CONTROL.md index 340cfc4..8c2a613 100644 --- a/docs/ACCESS-CONTROL.md +++ b/docs/ACCESS-CONTROL.md @@ -12,7 +12,7 @@ closed instead of shipping unrestricted. | Key | Default | What it allows | |---|---|---| | `downloadAttachments` | `true` | Downloading attachment content (`tb attachment-download`, MCP `email_attachments operation=download`) | -| `compose` | `true` | Creating a draft via `tb compose`/`tb reply`/`tb forward`, or editing one via `tb edit` | +| `compose` | `true` | Creating a draft via `tb compose`/`tb reply`/`tb forward`/`tb notes to-draft`, or editing one via `tb edit` | | `send` | `true` | Sending immediately with `--send` (requires `compose: true`) | | `move` | `true` | `tb move`, `tb bulk move` | | `copy` | `true` | `tb copy` | @@ -79,7 +79,8 @@ build. Without `--access-config`, the source defaults above are used. explicit user approval for consequential actions (see `skills/thunderbird-cli/SKILL.md`). - This is add-on-level policy, not authentication or transport security — it does not replace bridge authentication (`TB_AUTH_TOKEN`) or OS-level protections. -- Previously installed or signed XPIs predate this policy and still allow everything; install a +- Previously installed or signed XPIs (including the ATN-signed 2.1.0 in `dist/releases/`) + predate this policy and still allow everything; install a new build to get it. - The Fast Actions (`tb email-to-*`, the equivalent MCP tools, and the "Save to Notes"/"Create Task"/"Create Event"/"Add Sender to Contacts" context-menu items — ODIAA-2333) are composites diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md index 5817cea..a9eae3c 100644 --- a/docs/CLAUDE.md +++ b/docs/CLAUDE.md @@ -76,6 +76,32 @@ tb download-status # check: full | headers_only # Contacts tb contacts-search "john" tb contact +tb address-books # books you can write to +tb contacts create --book --email "a@b.com" --display-name "A B" # needs contactsWrite +tb contacts update --phone "+1 555 0100" # needs contactsWrite + +# Calendar (needs add-on 2.4.0; writes need calendarWrite) +tb calendar list +tb calendar events --start 2026-10-01 --end 2026-10-08 +tb calendar create --calendar --title "Review" --start 2026-10-02T10:00 --end 2026-10-02T11:00 +tb calendar clashes --start 2026-10-01 --end 2026-10-08 + +# Tasks (needs add-on 2.4.0; writes need tasksWrite) +tb tasks list --pending +tb tasks create --calendar --title "Send invoice" --due 2026-10-03 +tb action-items # Markdown checklist, no LLM + +# Deterministic skills (compact Markdown, no LLM tokens) +tb today +tb week +tb clashes --days 7 +tb from "boss@co.com" + +# Fast Actions (email → note / task / event / contact) +tb email-to-note +tb email-to-task --calendar +tb email-to-event --calendar +tb email-to-contact --book # Identities (for --from flag in compose) tb identities @@ -86,6 +112,7 @@ tb notes read # "Use as Context" tb notes save --body "text" [--title ] # "Save to Notes" tb notes append --body "more text" tb notes to-draft --to "a@b.com" # render to HTML draft, never sends +tb notes transcribe memo.m4a --engine faster-whisper # local speech-to-text, nothing leaves the machine ``` ## Output Format diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index bc0c2a3..fac4b71 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -1,6 +1,6 @@ # CLI Command Reference -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). +All 74 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 diff --git a/docs/PLAN.md b/docs/PLAN.md index b98dc3d..06c962e 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -2,6 +2,12 @@ Status: draft, baseline established. Tracks issue ODIAA-2303 ("Initialize project"). +> **2026-09-28 — v1.3.0 released.** Phase 2 (calendar, contacts write, notes, tasks) and the +> section 4 atbridge.ai feature set are shipped: calendar read + event CRUD + clashes +> (ODIAA-2328), tasks + action items (ODIAA-2329), contacts write (#21), notes (#22) + +> transcription (ODIAA-2331), deterministic skills (ODIAA-2332) and Fast Actions (ODIAA-2333). +> Add-on 2.4.0 is awaiting ATN manual review. The sections below keep the original narrative. + ## 1. Baseline `main` now starts from `vitalio-sh/thunderbird-cli` upstream `main` (commit `465613d`, @@ -235,16 +241,19 @@ reviewable batches instead of one mega-merge. Surveyed https://atbridge.ai. Feature areas to reach parity with, beyond current mailbox-only CLI/extension/bridge: -- **Calendar**: create/update/delete events, cross-calendar clash detection. -- **Contacts**: search/create/update across all Thunderbird address books. +- **Calendar**: create/update/delete events, cross-calendar clash detection. *(Shipped v1.3.0, ODIAA-2328.)* +- **Contacts**: search/create/update across all Thunderbird address books. *(Shipped v1.3.0, #21.)* - **Notes**: local Markdown workspace; "Use as Context" (note → AI chat) and "Save to Notes" (AI reply → note); voice memo transcription; render note as HTML email draft. -- **Tasks**: extract action items from an email into a checklist; create Thunderbird tasks. -- **Fast Actions**: Email→Note, Email→Task, Email→Event, Email→Contact one-click conversions. + *(Shipped v1.3.0, #22; transcription ODIAA-2331.)* +- **Tasks**: extract action items from an email into a checklist; create Thunderbird tasks. *(Shipped v1.3.0, ODIAA-2329.)* +- **Fast Actions**: Email→Note, Email→Task, Email→Event, Email→Contact one-click conversions. *(Shipped v1.3.0, ODIAA-2333.)* - **Skills**: deterministic slash commands (`/today`, `/week`, `/clashes`, `/from`-style) that don't spend LLM tokens — maps well onto our existing MCP tool model. + *(Shipped v1.3.0 as `tb today`/`week`/`clashes`/`from` and `skill_*` MCP tools, ODIAA-2332.)* - **Security posture to match/exceed**: local-first bridge on 127.0.0.1, opt-in (off by - default) write access for AI agents, draft-only mode. + default) write access for AI agents, draft-only mode. *(Met: `contactsWrite`/`calendarWrite`/ + `tasksWrite` all default off; compose defaults to draft.)* Not in scope to copy: their pricing/subscription mechanics — this project is open tooling, not a metered SaaS. @@ -253,7 +262,7 @@ not a metered SaaS. - Roadmap lifecycle: upstream → fork merges → mail today → calendar and contacts write → notes and tasks → atbridge parity; an extension stability pass runs alongside + Roadmap lifecycle: upstream → fork merges → mail → calendar and contacts write → notes, tasks, skills and Fast Actions, all shipped in v1.3.0; still planned: an extension stability pass and the remaining atbridge parity gaps 1. **Fork integration** (child issues per fork, section 3) — bring `main` up to the best @@ -263,14 +272,18 @@ not a metered SaaS. 2. **Calendar, Contacts, Notes, Tasks** — extend `extension`, `bridge`, `cli`, and `mcp` surfaces to cover the atbridge.ai-equivalent feature list in section 4, reusing the existing mailbox architecture and access-policy model from the `reinhardullrich` fork. - **Status:** not started (ODIAA-2306); contacts are read-only today. + **Status:** shipped in v1.3.0 (2026-09-28) — calendar read + event CRUD + clashes + (ODIAA-2328), tasks + action items (ODIAA-2329), contacts write (#21), notes (#22) + + transcription (ODIAA-2331), plus deterministic skills (ODIAA-2332) and Fast Actions + (ODIAA-2333). Originally tracked as ODIAA-2306. 3. **Extension stability pass** — audit `extension/` against the hardening commits already identified (reconnect/backoff, IPC queue, lifecycle fixes) and add regression coverage. **Status:** in progress — folder-info cache and MCP concurrency tests shipped in #8. 4. **Signed extension build pipeline** — GitHub Actions workflow (and a local pre-push equivalent) that builds, lints, tests, and produces a signed Thunderbird XPI, committed to `dist/releases/` per the existing `.gitignore` carve-out. **Status:** shipped - (`sign-xpi.yml`, #5 and #7; ODIAA-2308). + (`sign-xpi.yml`, #5 and #7; ODIAA-2308). Only 2.1.0 is ATN-signed so far; 2.4.0 is + pending ATN manual review (Experiment APIs). ## 6. Open questions for the CEO / board diff --git a/docs/RELEASING.md b/docs/RELEASING.md index c4cc7e2..897b55f 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -15,7 +15,7 @@ The `cli/`, `bridge/` and `mcp/` workspaces are `private: true` and keep upstrea 1. Bump the version in **all** of these (`npm run test:pack` fails if they disagree): `package.json`, `cli/package.json`, `bridge/package.json`, `mcp/package.json`, and both `version` fields in `server.json`. Then run `npm install` so `package-lock.json` follows. 2. Move the `[Unreleased]` section of `CHANGELOG.md` under the new version. -3. Make sure a signed `*-tb.xpi` for the current add-on version is in `dist/releases/` (see the sign-xpi workflow); the release job fails without one. +3. Make sure a signed `*-tb.xpi` for the current add-on version is in `dist/releases/` (see the sign-xpi workflow); the release job fails without one. It attaches the highest-versioned signed XPI it finds (`sort -V`), which can be older than the manifest while ATN review is pending — v1.3.0 shipped signed 2.1.0 alongside the unsigned 2.4.0. 4. Run `npm run test:all` and `npm run test:pack-smoke` locally, open a PR, merge. 5. Tag the merge commit and push the tag: diff --git a/docs/SETUP.md b/docs/SETUP.md index 5e305e2..4bdad1c 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -14,12 +14,14 @@ Docker/Devcontainer: ↕ http://host.docker.internal:7700 ``` -Almost entirely a plain WebExtension. Requires Thunderbird 128+. The one -exception is calendar support (`tb calendar list`/`events`/`create`/`update`/`delete`/`clashes`, -experimental — see [docs/decisions/calendar-backend.md](decisions/calendar-backend.md)), which -needs a chrome-privileged Experiment API since Thunderbird's WebExtension -permission model has no calendar access. Signed releases that include it need -a human ATN reviewer, which can take days — see the decision doc for status. +Almost entirely a plain WebExtension. Requires Thunderbird 128+ (add-on 2.4.0 +supports 128.0 – 156.*). The one exception is calendar and task support (`tb calendar +list`/`events`/`create`/`update`/`delete`/`clashes`, `tb tasks`, and the Fast Actions +that create events or tasks — see [docs/decisions/calendar-backend.md](decisions/calendar-backend.md)), +which needs chrome-privileged Experiment APIs (`calendar_calendars`, `calendar_items`, +`calendar_tasks`) since Thunderbird's WebExtension permission model has no calendar +access. Signed releases that include them need a human ATN reviewer, which can take +days — see [Step 2](#step-2-install-the-thunderbird-extension) for which build to install. ## Quick path: npm @@ -89,7 +91,9 @@ nohup node bridge/bridge.js > ~/.tb-bridge.log 2>&1 & The extension is signed by Mozilla through addons.thunderbird.net for self-distribution. It installs permanently and survives Thunderbird restarts. -1. Download the latest signed XPI (`--tb.xpi`; the current 2.1.0 build is `thunderbird_ai_bridge-2.1.0-tb.xpi` because it predates the rename to Thunderbird CLI Enhanced) from one of these locations: +> **Which build?** The only ATN-signed build today is **2.1.0** (`thunderbird_ai_bridge-2.1.0-tb.xpi`). It predates the rename, so it shows as "Thunderbird AI Bridge" in the Add-ons Manager, and it has no calendar, tasks or Fast Actions — email, contacts and notes commands work with it. **2.4.0** ("Thunderbird CLI Enhanced", needed for calendar, tasks and Fast Actions) is attached **unsigned** to the [v1.3.0 GitHub Release](https://github.com/odience-network/thunderbird-cli-enhanced/releases/tag/v1.3.0) while ATN's manual review (required for Experiment APIs) is pending. Load it via **Tools → Developer Tools → Debug Add-ons → Load Temporary Add-on…** (removed on restart), or install it permanently on a Thunderbird build that allows unsigned add-ons. Once 2.4.0 is signed it lands in `dist/releases/` as `thunderbird_cli_enhanced-2.4.0-tb.xpi`. + +1. Download the latest signed XPI (`--tb.xpi`, currently `thunderbird_ai_bridge-2.1.0-tb.xpi`) from one of these locations: - **Directly from `main`:** [`dist/releases/`](../dist/releases/) - **GitHub Releases:** https://github.com/odience-network/thunderbird-cli-enhanced/releases, attached by `release.yml` when a `v*` tag is pushed 2. Open Thunderbird → **Add-ons and Themes** @@ -98,7 +102,7 @@ The extension is signed by Mozilla through addons.thunderbird.net for self-distr 5. Confirm when Thunderbird asks to install 6. Check the bridge terminal — you should see: `[bridge] Extension connected` -> The signed XPI is byte-identical to the source in `extension/`, but Mozilla's trust registry marks it as verified. You **must** use the XPI downloaded from ATN (or our GitHub Releases) — a locally-built XPI won't install permanently. +> The signed XPI is built from the source in `extension/` at the version it was signed for, but Mozilla's trust registry marks it as verified. You **must** use the XPI downloaded from ATN (or our GitHub Releases) — a locally-built XPI won't install permanently. ### Option B: Temporary add-on (for developers making changes) @@ -115,7 +119,7 @@ When you reload after editing, click **Reload** next to the add-on in `about:deb ### Access policy -The add-on carries one access policy, fixed at build time, that every caller goes through. Deleting messages and deleting folders are off by default; a disabled operation returns `FORBIDDEN`. Check the active policy with `tb access`. To change it, copy `access.example.json` to `access.local.json`, edit it, and install a build from `npm run build:xpi -- --access-config access.local.json`. See [ACCESS-CONTROL.md](ACCESS-CONTROL.md). +The add-on carries one access policy, fixed at build time, that every caller goes through. Deleting messages and deleting folders are off by default, as are contact, calendar and task writes (`contactsWrite`, `calendarWrite`, `tasksWrite`); a disabled operation returns `FORBIDDEN`. Check the active policy with `tb access`. To change it, copy `access.example.json` to `access.local.json`, edit it, and install a build from `npm run build:xpi -- --access-config access.local.json`. See [ACCESS-CONTROL.md](ACCESS-CONTROL.md). ## Step 3: Install the CLI @@ -198,6 +202,8 @@ Environment variables override config file values: - `TB_BRIDGE_HOST` — bridge hostname - `TB_BRIDGE_PORT` — bridge HTTP port - `TB_AUTH_TOKEN` — auth token +- `TB_NOTES_DIR` — notes workspace (default `~/.config/thunderbird-cli/notes`, or `notesDir` in the config file) +- `TB_WHISPER_CPP_BIN` / `TB_WHISPER_CPP_MODEL` / `TB_FASTER_WHISPER_BIN` — local speech-to-text engine for `tb notes transcribe` (optional; install [whisper.cpp](https://github.com/ggml-org/whisper.cpp) or [faster-whisper](https://github.com/SYSTRAN/faster-whisper)'s `whisper-ctranslate2`; audio never leaves the machine) ## Troubleshooting diff --git a/docs/decisions/calendar-backend.md b/docs/decisions/calendar-backend.md index 6048a1d..3cb358f 100644 --- a/docs/decisions/calendar-backend.md +++ b/docs/decisions/calendar-backend.md @@ -78,11 +78,12 @@ Unchanged mechanism, same as every other route: `extension/src/access-control.js - `extension/experiments/calendar/schema/calendar-calendars.json` + `extension/experiments/calendar/parent/ext-calendar-calendars.js` — vendored/trimmed `calendar.calendars.query` only, from upstream `main` at `b7f7cb3e76807903a785a03784d6e7df7b213f21`, plus the required `onShutdown` cache invalidation. - `extension/manifest.json` — `experiment_apis.calendar_calendars` entry. -- `GET /calendars` — new ungated route: `extension/src/background.js` (dispatch) → `extension/src/access-control.js` (`UNGATED_GET`) → `cli/src/cli.js` (`tb calendars`) → `mcp/src/tools.js` (`calendar_list` tool). Returns `{ error: "calendar experiment not loaded" }` instead of throwing if the experiment failed to load. +- `GET /calendars` — new ungated route: `extension/src/background.js` (dispatch) → `extension/src/access-control.js` (`UNGATED_GET`) → `cli/src/cli.js` (`tb calendars`; renamed to `tb calendar list` when calendar CRUD shipped in v1.3.0) → `mcp/src/tools.js` (`calendar_list` tool). Returns `{ error: "calendar experiment not loaded" }` instead of throwing if the experiment failed to load. - `scripts/lint.mjs` / `scripts/sign-xpi.mjs` / `.github/workflows/sign-xpi.yml` — the signing-pipeline fix described above. - Tests: `test/extension.test.mjs`, `test/access-control.test.mjs`, `test/sign-xpi.test.mjs` — all passing (mocked `messenger.calendar.calendars.query` for the WebExtension-facing surface; the signing-path fix has real subprocess/HTTP-mock coverage). - Verified: `npm run build:xpi` succeeds; `npm run lint` passes (tolerating only the one expected, scoped error). - **Verified live**, end-to-end, in an isolated Thunderbird 155.0.1 instance (throwaway profile, `--no-remote`, bridge on ports 17700/17701 to avoid this environment's other running Thunderbird+bridge, which belongs to an unrelated project and was left untouched): + (Output recorded at spike time; the command is now `tb calendar list`.) ``` $ tb calendars {"ok":true,"data":[{"id":"d5836c5a-9458-4844-91fe-7e80d872d752","type":"storage", diff --git a/docs/diagrams/README.md b/docs/diagrams/README.md index c0b57ba..1581c65 100644 --- a/docs/diagrams/README.md +++ b/docs/diagrams/README.md @@ -6,11 +6,11 @@ The HTML files are standalone pages: GitHub shows their source, so download one | Diagram | Type | Shows | Embedded in | |---|---|---|---| -| [architecture](architecture.html) ([IR](src/architecture.json)) | architecture | Agents, `tb` / `tb-mcp`, bridge, extension, access policy, accounts | [README](../../README.md#how-it-works) | +| [architecture](architecture.html) ([IR](src/architecture.json)) | architecture | Agents, `tb` / `tb-mcp`, bridge, extension, access policy, mail + PIM data, local notes workspace | [README](../../README.md#how-it-works) | | [search-sequence](search-sequence.html) ([IR](src/search-sequence.json)) | sequence | One search end to end: auto-start (#13), server-side query (#14), liveness | [README](../../README.md#why-this-fork), [SPEC](../../SPEC.md#request-flow) | | [access-control](access-control.html) ([IR](src/access-control.json)) | dataflow | Policy from config to build to installed add-on; route classification and `FORBIDDEN` | [ACCESS-CONTROL](../ACCESS-CONTROL.md#how-a-request-is-checked) | -| [release-workflow](release-workflow.html) ([IR](src/release-workflow.json)) | workflow | Version bump → `sign-xpi.yml` → ATN → signed XPI → tag → `release.yml` | [AGENTS](../../AGENTS.md#release) | -| [roadmap](roadmap.html) ([IR](src/roadmap.json)) | lifecycle | Upstream → fork merges → mail today → calendar/contacts/notes/tasks → atbridge parity | [README](../../README.md#roadmap), [PLAN](../PLAN.md#5-roadmap-phases) | +| [release-workflow](release-workflow.html) ([IR](src/release-workflow.json)) | workflow | Version bump → `sign-xpi.yml` → ATN → signed XPI → tag → `release.yml` (GitHub Release + npm) | [AGENTS](../../AGENTS.md#release) | +| [roadmap](roadmap.html) ([IR](src/roadmap.json)) | lifecycle | Upstream → fork merges → mail → calendar/contacts → notes/tasks/skills/Fast Actions (all shipped in v1.3.0) → stability pass and atbridge parity (planned) | [README](../../README.md#roadmap), [PLAN](../PLAN.md#5-roadmap-phases) |

Architecture diff --git a/docs/diagrams/access-control-dark.png b/docs/diagrams/access-control-dark.png index a892ebd..077630a 100644 Binary files a/docs/diagrams/access-control-dark.png and b/docs/diagrams/access-control-dark.png differ diff --git a/docs/diagrams/access-control.html b/docs/diagrams/access-control.html index 96033a4..cfb94a4 100644 --- a/docs/diagrams/access-control.html +++ b/docs/diagrams/access-control.html @@ -5258,20 +5258,20 @@

Access policy: build time to request time

fail closed - - Handler · messenger.* call · 05 / Outcome · allowed + + Handler · mail / PIM call · 05 / Outcome · allowed Handler - messenger.* call + mail / PIM call allowed - - FORBIDDEN · off / unclassified · 05 / Outcome · no mail access + + FORBIDDEN · off / unclassified · 05 / Outcome · no data access FORBIDDEN off / unclassified - no mail access + no data access @@ -5532,7 +5532,7 @@

Access policy: build time to request time

Always available

    -
  • • Read routes (search, list, read, stats, sync) have no switch
  • +
  • • Read routes (search, list, read, calendar/task/contact lists) have no switch
  • • tb access shows the loaded policy
@@ -5543,8 +5543,8 @@

Always available

Switches

    -
  • • compose, send, move, copy, archive, mark, tag, folder ops default on
  • -
  • • delete and folderDelete default off (ODIAA-2304)
  • +
  • • 16 switches; mail writes (compose, send, move, tag, folder ops, ...) default on
  • +
  • • delete, folderDelete, contactsWrite, calendarWrite, tasksWrite default off
@@ -5845,190 +5845,6 @@

Fail closed

return getComputedStyle(document.body).backgroundColor || '#ffffff'; } - function cleanExportClone(clone) { - // View transforms and neighborhood focus are HTML exploration state, - // never part of a downloaded full-diagram artifact. - clone.style.removeProperty('transform'); - clone.style.removeProperty('clip-path'); - clone.removeAttribute('data-view-scale'); - clone.removeAttribute('data-focus-active'); - clone.removeAttribute('data-reach-active'); - clone.removeAttribute('data-lens-active'); - clone.removeAttribute('data-lens-flow-count'); - clone.removeAttribute('data-lens-flow-density'); - clone.removeAttribute('data-legend-preview-active'); - clone.removeAttribute('data-relationship-preview-active'); - clone.removeAttribute('data-relationship-direct-active'); - clone.removeAttribute('data-relationship-pin-active'); - clone.removeAttribute('data-intent-trace-active'); - clone.removeAttribute('data-route-picking'); - clone.removeAttribute('data-route-active'); - clone.removeAttribute('data-route-journey'); - clone.removeAttribute('data-share-route'); - clone.removeAttribute('data-share-reach'); - clone.removeAttribute('data-story-active'); - clone.removeAttribute('data-story-playing'); - clone.removeAttribute('data-story-beat'); - clone.removeAttribute('data-story-next'); - clone.removeAttribute('data-story-follow'); - clone.removeAttribute('data-chapter-handoff'); - clone.removeAttribute('data-chapter-anchor'); - clone.removeAttribute('data-chapter-preview'); - Array.prototype.forEach.call(clone.querySelectorAll('[data-chapter-handoff-overlay]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-chapter-role]'), function (el) { - el.removeAttribute('data-chapter-role'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-chapter-preview-role]'), function (el) { - el.removeAttribute('data-chapter-preview-role'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-story-overlay], [data-story-carrier-overlay]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-intent-trace-overlay]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-route-probe-overlay]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-route-journey-overlay]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-semantic-lens-overlay]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-legend-bridge-runtime]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-legend-kind]'), function (el) { - el.removeAttribute('data-legend-kind'); - el.removeAttribute('data-legend-label'); - el.removeAttribute('data-legend-count'); - el.removeAttribute('data-legend-zero'); - el.removeAttribute('data-legend-selected'); - el.removeAttribute('role'); - el.removeAttribute('tabindex'); - el.removeAttribute('aria-label'); - el.removeAttribute('aria-pressed'); - el.removeAttribute('aria-haspopup'); - el.removeAttribute('aria-controls'); - el.removeAttribute('aria-expanded'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-legend-bridge]'), function (el) { - el.removeAttribute('data-legend-bridge'); - el.removeAttribute('role'); - el.removeAttribute('aria-label'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-relationship-pulse-overlay]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-relationship-hit-overlay]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-source-evidence-beacon]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-source-evidence-count]'), function (el) { - var originalLabel = el.getAttribute('data-source-evidence-original-label'); - if (originalLabel == null || originalLabel === '') el.removeAttribute('aria-label'); - else el.setAttribute('aria-label', originalLabel); - el.removeAttribute('data-source-evidence-count'); - el.removeAttribute('data-source-evidence-original-label'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-story-step], [data-story-beat-state], [data-story-beat-step]'), function (el) { - el.removeAttribute('data-story-step'); - el.removeAttribute('data-story-beat-state'); - el.removeAttribute('data-story-beat-step'); - el.style.removeProperty('--story-step'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-focus-match], [data-focus-selected]'), function (el) { - el.removeAttribute('data-focus-match'); - el.removeAttribute('data-focus-selected'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-reach-match], [data-reach-origin], [data-reach-depth]'), function (el) { - el.removeAttribute('data-reach-match'); - el.removeAttribute('data-reach-origin'); - el.removeAttribute('data-reach-depth'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-lens-match], [data-lens-selected], [data-lens-peer]'), function (el) { - el.removeAttribute('data-lens-match'); - el.removeAttribute('data-lens-selected'); - el.removeAttribute('data-lens-peer'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-legend-preview-match], [data-legend-preview-selected], [data-legend-preview-peer]'), function (el) { - el.removeAttribute('data-legend-preview-match'); - el.removeAttribute('data-legend-preview-selected'); - el.removeAttribute('data-legend-preview-peer'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-relationship-preview], [data-relationship-preview-node], [data-relationship-preview-source], [data-relationship-preview-target]'), function (el) { - el.removeAttribute('data-relationship-preview'); - el.removeAttribute('data-relationship-preview-node'); - el.removeAttribute('data-relationship-preview-source'); - el.removeAttribute('data-relationship-preview-target'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-intent-trace-match], [data-intent-trace-selected]'), function (el) { - el.removeAttribute('data-intent-trace-match'); - el.removeAttribute('data-intent-trace-selected'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-route-match], [data-route-start], [data-route-end], [data-route-step], [data-route-candidate], [data-route-journey-state], [data-route-journey-current]'), function (el) { - el.removeAttribute('data-route-match'); - el.removeAttribute('data-route-start'); - el.removeAttribute('data-route-end'); - el.removeAttribute('data-route-step'); - el.removeAttribute('data-route-candidate'); - el.removeAttribute('data-route-journey-state'); - el.removeAttribute('data-route-journey-current'); - el.style.removeProperty('--route-step'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-share-route-match], [data-share-route-step], [data-share-route-start], [data-share-route-end], [data-share-route-middle]'), function (el) { - el.removeAttribute('data-share-route-match'); - el.removeAttribute('data-share-route-step'); - el.removeAttribute('data-share-route-start'); - el.removeAttribute('data-share-route-end'); - el.removeAttribute('data-share-route-middle'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-share-reach-match], [data-share-reach-origin], [data-share-reach-depth]'), function (el) { - el.removeAttribute('data-share-reach-match'); - el.removeAttribute('data-share-reach-origin'); - el.removeAttribute('data-share-reach-depth'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-detail], [data-detail-anchor]'), function (el) { - el.removeAttribute('data-detail'); - el.removeAttribute('data-detail-anchor'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-node-id][aria-pressed]'), function (el) { - el.setAttribute('aria-pressed', 'false'); - }); - return !clone.hasAttribute('data-view-scale') && - !clone.hasAttribute('data-focus-active') && - !clone.hasAttribute('data-reach-active') && - !clone.hasAttribute('data-lens-active') && - !clone.hasAttribute('data-lens-flow-count') && - !clone.hasAttribute('data-lens-flow-density') && - !clone.hasAttribute('data-legend-preview-active') && - !clone.hasAttribute('data-relationship-preview-active') && - !clone.hasAttribute('data-relationship-direct-active') && - !clone.hasAttribute('data-intent-trace-active') && - !clone.hasAttribute('data-route-picking') && - !clone.hasAttribute('data-route-active') && - !clone.hasAttribute('data-route-journey') && - !clone.hasAttribute('data-share-route') && - !clone.hasAttribute('data-share-reach') && - !clone.hasAttribute('data-story-active') && - !clone.hasAttribute('data-story-playing') && - !clone.hasAttribute('data-story-beat') && - !clone.hasAttribute('data-story-next') && - !clone.hasAttribute('data-story-follow') && - !clone.hasAttribute('data-chapter-handoff') && - !clone.hasAttribute('data-chapter-anchor') && - !clone.hasAttribute('data-chapter-preview') && - !clone.style.getPropertyValue('transform') && - !clone.style.getPropertyValue('clip-path') && - clone.querySelectorAll('[data-story-overlay], [data-story-carrier-overlay], [data-story-carrier-token], [data-story-step], [data-story-beat-state], [data-story-beat-step], [data-chapter-handoff-overlay], [data-chapter-role], [data-chapter-preview-role], [data-focus-match], [data-focus-selected], [data-reach-match], [data-reach-origin], [data-reach-depth], [data-semantic-lens-overlay], [data-lens-match], [data-lens-selected], [data-lens-peer], [data-legend-bridge], [data-legend-kind], [data-legend-bridge-runtime], [data-legend-count], [data-legend-zero], [data-legend-selected], [data-legend-preview-match], [data-legend-preview-selected], [data-legend-preview-peer], [data-relationship-hit-overlay], [data-relationship-pulse-overlay], [data-relationship-preview], [data-relationship-preview-node], [data-relationship-preview-source], [data-relationship-preview-target], [data-intent-trace-overlay], [data-intent-trace-match], [data-intent-trace-selected], [data-route-probe-overlay], [data-route-journey-overlay], [data-route-match], [data-route-start], [data-route-end], [data-route-step], [data-route-candidate], [data-route-journey-state], [data-route-journey-current], [data-share-route-match], [data-share-route-step], [data-share-route-start], [data-share-route-end], [data-share-route-middle], [data-share-reach-match], [data-share-reach-origin], [data-share-reach-depth], [data-source-evidence-beacon], [data-source-evidence-count], [data-source-evidence-original-label], [data-detail], [data-detail-anchor]').length === 0; - } - - /** * Serialize the main SVG into a standalone document. * @@ -6214,7 +6030,186 @@

Fail closed

var svg = document.querySelector('.diagram-container svg'); var clone = svg.cloneNode(true); - var canonicalStateClean = cleanExportClone(clone); + // View transforms and neighborhood focus are HTML exploration state, + // never part of a downloaded full-diagram artifact. + clone.style.removeProperty('transform'); + clone.style.removeProperty('clip-path'); + clone.removeAttribute('data-view-scale'); + clone.removeAttribute('data-focus-active'); + clone.removeAttribute('data-reach-active'); + clone.removeAttribute('data-lens-active'); + clone.removeAttribute('data-lens-flow-count'); + clone.removeAttribute('data-lens-flow-density'); + clone.removeAttribute('data-legend-preview-active'); + clone.removeAttribute('data-relationship-preview-active'); + clone.removeAttribute('data-relationship-direct-active'); + clone.removeAttribute('data-relationship-pin-active'); + clone.removeAttribute('data-intent-trace-active'); + clone.removeAttribute('data-route-picking'); + clone.removeAttribute('data-route-active'); + clone.removeAttribute('data-route-journey'); + clone.removeAttribute('data-share-route'); + clone.removeAttribute('data-share-reach'); + clone.removeAttribute('data-story-active'); + clone.removeAttribute('data-story-playing'); + clone.removeAttribute('data-story-beat'); + clone.removeAttribute('data-story-next'); + clone.removeAttribute('data-story-follow'); + clone.removeAttribute('data-chapter-handoff'); + clone.removeAttribute('data-chapter-anchor'); + clone.removeAttribute('data-chapter-preview'); + Array.prototype.forEach.call(clone.querySelectorAll('[data-chapter-handoff-overlay]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-chapter-role]'), function (el) { + el.removeAttribute('data-chapter-role'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-chapter-preview-role]'), function (el) { + el.removeAttribute('data-chapter-preview-role'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-story-overlay], [data-story-carrier-overlay]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-intent-trace-overlay]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-route-probe-overlay]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-route-journey-overlay]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-semantic-lens-overlay]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-legend-bridge-runtime]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-legend-kind]'), function (el) { + el.removeAttribute('data-legend-kind'); + el.removeAttribute('data-legend-label'); + el.removeAttribute('data-legend-count'); + el.removeAttribute('data-legend-zero'); + el.removeAttribute('data-legend-selected'); + el.removeAttribute('role'); + el.removeAttribute('tabindex'); + el.removeAttribute('aria-label'); + el.removeAttribute('aria-pressed'); + el.removeAttribute('aria-haspopup'); + el.removeAttribute('aria-controls'); + el.removeAttribute('aria-expanded'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-legend-bridge]'), function (el) { + el.removeAttribute('data-legend-bridge'); + el.removeAttribute('role'); + el.removeAttribute('aria-label'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-relationship-pulse-overlay]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-relationship-hit-overlay]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-source-evidence-beacon]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-source-evidence-count]'), function (el) { + var originalLabel = el.getAttribute('data-source-evidence-original-label'); + if (originalLabel == null || originalLabel === '') el.removeAttribute('aria-label'); + else el.setAttribute('aria-label', originalLabel); + el.removeAttribute('data-source-evidence-count'); + el.removeAttribute('data-source-evidence-original-label'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-story-step], [data-story-beat-state], [data-story-beat-step]'), function (el) { + el.removeAttribute('data-story-step'); + el.removeAttribute('data-story-beat-state'); + el.removeAttribute('data-story-beat-step'); + el.style.removeProperty('--story-step'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-focus-match], [data-focus-selected]'), function (el) { + el.removeAttribute('data-focus-match'); + el.removeAttribute('data-focus-selected'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-reach-match], [data-reach-origin], [data-reach-depth]'), function (el) { + el.removeAttribute('data-reach-match'); + el.removeAttribute('data-reach-origin'); + el.removeAttribute('data-reach-depth'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-lens-match], [data-lens-selected], [data-lens-peer]'), function (el) { + el.removeAttribute('data-lens-match'); + el.removeAttribute('data-lens-selected'); + el.removeAttribute('data-lens-peer'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-legend-preview-match], [data-legend-preview-selected], [data-legend-preview-peer]'), function (el) { + el.removeAttribute('data-legend-preview-match'); + el.removeAttribute('data-legend-preview-selected'); + el.removeAttribute('data-legend-preview-peer'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-relationship-preview], [data-relationship-preview-node], [data-relationship-preview-source], [data-relationship-preview-target]'), function (el) { + el.removeAttribute('data-relationship-preview'); + el.removeAttribute('data-relationship-preview-node'); + el.removeAttribute('data-relationship-preview-source'); + el.removeAttribute('data-relationship-preview-target'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-intent-trace-match], [data-intent-trace-selected]'), function (el) { + el.removeAttribute('data-intent-trace-match'); + el.removeAttribute('data-intent-trace-selected'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-route-match], [data-route-start], [data-route-end], [data-route-step], [data-route-candidate], [data-route-journey-state], [data-route-journey-current]'), function (el) { + el.removeAttribute('data-route-match'); + el.removeAttribute('data-route-start'); + el.removeAttribute('data-route-end'); + el.removeAttribute('data-route-step'); + el.removeAttribute('data-route-candidate'); + el.removeAttribute('data-route-journey-state'); + el.removeAttribute('data-route-journey-current'); + el.style.removeProperty('--route-step'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-share-route-match], [data-share-route-step], [data-share-route-start], [data-share-route-end], [data-share-route-middle]'), function (el) { + el.removeAttribute('data-share-route-match'); + el.removeAttribute('data-share-route-step'); + el.removeAttribute('data-share-route-start'); + el.removeAttribute('data-share-route-end'); + el.removeAttribute('data-share-route-middle'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-share-reach-match], [data-share-reach-origin], [data-share-reach-depth]'), function (el) { + el.removeAttribute('data-share-reach-match'); + el.removeAttribute('data-share-reach-origin'); + el.removeAttribute('data-share-reach-depth'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-detail], [data-detail-anchor]'), function (el) { + el.removeAttribute('data-detail'); + el.removeAttribute('data-detail-anchor'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-node-id][aria-pressed]'), function (el) { + el.setAttribute('aria-pressed', 'false'); + }); + var canonicalStateClean = !clone.hasAttribute('data-view-scale') && + !clone.hasAttribute('data-focus-active') && + !clone.hasAttribute('data-reach-active') && + !clone.hasAttribute('data-lens-active') && + !clone.hasAttribute('data-lens-flow-count') && + !clone.hasAttribute('data-lens-flow-density') && + !clone.hasAttribute('data-legend-preview-active') && + !clone.hasAttribute('data-relationship-preview-active') && + !clone.hasAttribute('data-relationship-direct-active') && + !clone.hasAttribute('data-intent-trace-active') && + !clone.hasAttribute('data-route-picking') && + !clone.hasAttribute('data-route-active') && + !clone.hasAttribute('data-route-journey') && + !clone.hasAttribute('data-share-route') && + !clone.hasAttribute('data-share-reach') && + !clone.hasAttribute('data-story-active') && + !clone.hasAttribute('data-story-playing') && + !clone.hasAttribute('data-story-beat') && + !clone.hasAttribute('data-story-next') && + !clone.hasAttribute('data-story-follow') && + !clone.hasAttribute('data-chapter-handoff') && + !clone.hasAttribute('data-chapter-anchor') && + !clone.hasAttribute('data-chapter-preview') && + !clone.style.getPropertyValue('transform') && + !clone.style.getPropertyValue('clip-path') && + clone.querySelectorAll('[data-story-overlay], [data-story-carrier-overlay], [data-story-carrier-token], [data-story-step], [data-story-beat-state], [data-story-beat-step], [data-chapter-handoff-overlay], [data-chapter-role], [data-chapter-preview-role], [data-focus-match], [data-focus-selected], [data-reach-match], [data-reach-origin], [data-reach-depth], [data-semantic-lens-overlay], [data-lens-match], [data-lens-selected], [data-lens-peer], [data-legend-bridge], [data-legend-kind], [data-legend-bridge-runtime], [data-legend-count], [data-legend-zero], [data-legend-selected], [data-legend-preview-match], [data-legend-preview-selected], [data-legend-preview-peer], [data-relationship-hit-overlay], [data-relationship-pulse-overlay], [data-relationship-preview], [data-relationship-preview-node], [data-relationship-preview-source], [data-relationship-preview-target], [data-intent-trace-overlay], [data-intent-trace-match], [data-intent-trace-selected], [data-route-probe-overlay], [data-route-journey-overlay], [data-route-match], [data-route-start], [data-route-end], [data-route-step], [data-route-candidate], [data-route-journey-state], [data-route-journey-current], [data-share-route-match], [data-share-route-step], [data-share-route-start], [data-share-route-end], [data-share-route-middle], [data-share-reach-match], [data-share-reach-origin], [data-share-reach-depth], [data-source-evidence-beacon], [data-source-evidence-count], [data-source-evidence-original-label], [data-detail], [data-detail-anchor]').length === 0; var vb = svg.viewBox.baseVal; var finiteSvgDimensions = Number.isFinite(vb.x) && Number.isFinite(vb.y) && @@ -6360,11 +6355,8 @@

Fail closed

clone.insertBefore(style, clone.firstChild); clone.insertBefore(bgRect, style.nextSibling); - // The XML declaration pins UTF-8: without it, consumers that guess an - // encoding instead of defaulting to UTF-8 mangle non-ASCII text. return { - svgString: '\n' + - new XMLSerializer().serializeToString(clone), + svgString: new XMLSerializer().serializeToString(clone), width: vb.width * scale, height: vb.height * scale, canonicalStateClean: canonicalStateClean, @@ -8014,8 +8006,8 @@

Fail closed

} function relationshipHitRecords() { var recordsByKey = {}; - var recordsById = Object.create(null); - var byId = Object.create(null); + var recordsById = {}; + var byId = {}; nodes().forEach(function (node) { byId[node.getAttribute('data-node-id')] = node; }); var records = []; edges().forEach(function (edge) { @@ -8765,7 +8757,7 @@

Fail closed

Archify.routeProbe.clear({ updateUrl: false, restoreFocus: false }); } var nodeList = nodes(); - var byId = Object.create(null); + var byId = {}; nodeList.forEach(function (node) { byId[node.getAttribute('data-node-id')] = node; }); var normalized = []; (ids || []).forEach(function (id) { @@ -8779,8 +8771,8 @@

Fail closed

clear({ updateUrl: false, preserveView: true }); activeIds = normalized; - var selected = Object.create(null); - var related = Object.create(null); + var selected = {}; + var related = {}; var seenEdges = {}; var matchedEdges = 0; normalized.forEach(function (id) { selected[id] = true; related[id] = true; }); @@ -9131,7 +9123,7 @@

Fail closed

clear({ announce: false }); var nodeList = nodes(); var edgeList = edges(); - var byId = Object.create(null); + var byId = {}; nodeList.forEach(function (node) { byId[node.getAttribute('data-node-id')] = node; }); var selected = byId[id]; if (!selected) return false; @@ -9140,7 +9132,7 @@

Fail closed

overlay.setAttribute('class', 'intent-trace-overlay'); overlay.setAttribute('data-intent-trace-overlay', ''); overlay.setAttribute('aria-hidden', 'true'); - var related = Object.create(null); + var related = {}; var seen = {}; var counts = { out: 0, in: 0, loop: 0 }; related[id] = true; @@ -9311,12 +9303,12 @@

Fail closed

try { views = JSON.parse(data.textContent || '[]'); } catch (_) { views = []; } if (!views.length) return { count: 0, active: function () { return null; } }; - var canonicalNodeIds = Object.create(null); + var canonicalNodeIds = {}; Array.prototype.forEach.call(svg.querySelectorAll('[data-node-id]'), function (node) { canonicalNodeIds[node.getAttribute('data-node-id')] = true; }); views.forEach(function (view) { - var seen = Object.create(null); + var seen = {}; view.focus = (view.focus || []).filter(function (id) { if (!canonicalNodeIds[id] || seen[id]) return false; seen[id] = true; @@ -9345,8 +9337,8 @@

Fail closed

function chapterDelta(previous, destination) { var previousFocus = previous ? previous.focus : []; var destinationFocus = destination ? destination.focus : []; - var previousIds = Object.create(null); - var destinationIds = Object.create(null); + var previousIds = {}; + var destinationIds = {}; previousFocus.forEach(function (id) { previousIds[id] = true; }); destinationFocus.forEach(function (id) { destinationIds[id] = true; }); return { @@ -9358,7 +9350,7 @@

Fail closed

function chapterAnchor(previous, destination, outgoingBeatIndex, delta) { if (!previous || !destination) return ''; - var stayIds = Object.create(null); + var stayIds = {}; delta.stay.forEach(function (id) { stayIds[id] = true; }); var activeBeat = outgoingBeatIndex >= 0 ? previous.focus[outgoingBeatIndex] : ''; if (activeBeat && stayIds[activeBeat]) return activeBeat; @@ -9376,7 +9368,7 @@

Fail closed

} function classifyHandoff(delta, anchor) { - var roles = Object.create(null); + var roles = {}; delta.stay.forEach(function (id) { roles[id] = 'stay'; }); delta.enter.forEach(function (id) { roles[id] = 'enter'; }); delta.leave.forEach(function (id) { roles[id] = 'leave'; }); @@ -9693,7 +9685,7 @@

Fail closed

if (activePreviewIndex === index && svg.getAttribute('data-chapter-preview') === views[index].id) return true; clearChapterPreviewPresentation(); var delta = chapterDelta(activeIndex >= 0 ? views[activeIndex] : null, views[index]); - var roles = Object.create(null); + var roles = {}; delta.stay.forEach(function (id) { roles[id] = 'stay'; }); delta.enter.forEach(function (id) { roles[id] = 'enter'; }); delta.leave.forEach(function (id) { roles[id] = 'leave'; }); @@ -10157,9 +10149,7 @@

Fail closed

function centerStoryStop(stop) { if (!stop || !trail) return false; - var bounds = stop.getBoundingClientRect(); - var viewportLeft = trail.getBoundingClientRect().left + trail.clientLeft; - var target = Math.max(0, trail.scrollLeft + bounds.left - viewportLeft - (trail.clientWidth - bounds.width) / 2); + var target = Math.max(0, stop.offsetLeft - (trail.clientWidth - stop.offsetWidth) / 2); trail.scrollLeft = target; return true; } @@ -10347,7 +10337,7 @@

Fail closed

var nodeList = Array.prototype.slice.call(svg.querySelectorAll('[data-node-id]')); var edgeList = Array.prototype.slice.call(svg.querySelectorAll('[data-edge-from][data-edge-to]')); - var byId = Object.create(null); + var byId = {}; nodeList.forEach(function (node) { byId[node.getAttribute('data-node-id')] = node; }); storySteps = view.focus.map(function (_, index) { return storyStep(view, index, edgeList, byId); }); var labels = []; @@ -10909,22 +10899,6 @@

Fail closed

}); storyMotionObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-motion'] }); } - if (typeof ResizeObserver === 'function') { - var chapterViewportWidth = chapterList.clientWidth; - var trailViewportWidth = trail.clientWidth; - var storyScrollObserver = new ResizeObserver(function () { - var chapterWidth = chapterList.clientWidth; - var trailWidth = trail.clientWidth; - if (chapterWidth !== chapterViewportWidth && activeIndex >= 0) centerChapterButton(chapterButtons[activeIndex]); - if (trailWidth !== trailViewportWidth && storyBeatIndex >= 0) { - centerStoryStop(trail.querySelectorAll('[data-story-node]')[storyBeatIndex]); - } - chapterViewportWidth = chapterWidth; - trailViewportWidth = trailWidth; - }); - storyScrollObserver.observe(chapterList); - storyScrollObserver.observe(trail); - } window.addEventListener('beforeprint', function () { clearChapterPreview({ clearIntents: true }); if (playing) pausePlayback(); @@ -11798,8 +11772,8 @@

Fail closed

return true; } function semanticIds(ids, includeNeighbors) { - var seeds = Object.create(null); - var wanted = Object.create(null); + var seeds = {}; + var wanted = {}; (ids || []).forEach(function (id) { seeds[id] = true; wanted[id] = true; }); if (includeNeighbors) { Array.prototype.forEach.call(svg.querySelectorAll('[data-edge-from][data-edge-to]'), function (edge) { @@ -13267,7 +13241,7 @@

Fail closed

} function outgoingByNode() { var byId = nodesById(); - var outgoing = Object.create(null); + var outgoing = {}; edges().forEach(function (edge) { var from = edge.getAttribute('data-edge-from'); var to = edge.getAttribute('data-edge-to'); @@ -13279,7 +13253,7 @@

Fail closed

} function reachableFrom(source) { var outgoing = outgoingByNode(); - var reached = Object.create(null); + var reached = {}; var queue = [source]; reached[source] = true; for (var cursor = 0; cursor < queue.length; cursor += 1) { @@ -13294,7 +13268,7 @@

Fail closed

} function hopDistancesFrom(source) { var outgoing = outgoingByNode(); - var distances = Object.create(null); + var distances = {}; var queue = [source]; distances[source] = 0; for (var cursor = 0; cursor < queue.length; cursor += 1) { @@ -13310,7 +13284,7 @@

Fail closed

function shortestDirectedPath(source, target) { if (source === target) return null; var outgoing = outgoingByNode(); - var previous = Object.create(null); + var previous = {}; var queue = [source]; previous[source] = null; for (var cursor = 0; cursor < queue.length && !Object.prototype.hasOwnProperty.call(previous, target); cursor += 1) { @@ -13840,7 +13814,7 @@

Fail closed

var sourceIds = nodes().map(function (node) { return node.getAttribute('data-node-id'); }).filter(function (id) { return outgoing[id] && outgoing[id].length; }); - var sourceBadges = Object.create(null); + var sourceBadges = {}; sourceIds.forEach(function (id) { sourceBadges[id] = viewerText('viewer.route.finder.source.badge'); }); return { kind: 'route-source', @@ -13856,7 +13830,7 @@

Fail closed

if (mode === 'target' && startId && byId[startId]) { var distances = hopDistancesFrom(startId); var targetIds = Object.keys(distances).filter(function (id) { return id !== startId && byId[id]; }); - var targetBadges = Object.create(null); + var targetBadges = {}; targetIds.forEach(function (id) { targetBadges[id] = viewerCount('viewer.route.hop', distances[id]); }); @@ -14065,7 +14039,7 @@

Fail closed

var MAX_LENS_FLOW_EDGES = 24; function nodesById() { - var byId = Object.create(null); + var byId = {}; Array.prototype.forEach.call(svg.querySelectorAll('[data-node-id][data-node-kind]'), function (node) { var id = node.getAttribute('data-node-id'); if (id && !byId[id]) byId[id] = node; @@ -14232,7 +14206,7 @@

Fail closed

return (byId[id].getAttribute('data-node-kind') || 'neutral') === kind; }); if (!matches.length) return false; - var chosen = Object.create(null); + var chosen = {}; matches.forEach(function (id) { chosen[id] = true; byId[id].setAttribute('data-legend-preview-match', ''); @@ -14408,7 +14382,7 @@

Fail closed

options = options || {}; cleanSvgState(); var byId = nodesById(); - var nodeKind = Object.create(null); + var nodeKind = {}; Object.keys(byId).forEach(function (id) { nodeKind[id] = byId[id].getAttribute('data-node-kind') || 'neutral'; }); if (!selectedKinds.length) { status.textContent = viewerText('viewer.lens.choose'); diff --git a/docs/diagrams/access-control.png b/docs/diagrams/access-control.png index ab82963..65ece7b 100644 Binary files a/docs/diagrams/access-control.png and b/docs/diagrams/access-control.png differ diff --git a/docs/diagrams/architecture-dark.png b/docs/diagrams/architecture-dark.png index e240bf6..35bea11 100644 Binary files a/docs/diagrams/architecture-dark.png and b/docs/diagrams/architecture-dark.png differ diff --git a/docs/diagrams/architecture.html b/docs/diagrams/architecture.html index dbe7eca..aa5089f 100644 --- a/docs/diagrams/architecture.html +++ b/docs/diagrams/architecture.html @@ -4,7 +4,7 @@ - thunderbird-cli-enhanced — System Architecture Diagram + Thunderbird CLI Enhanced — System Architecture Diagram +
    -
  • • Owns all credentials and sync state
  • +
  • • Owns all credentials, mail, calendars, tasks and contacts
  • +
  • • Calendar and tasks via Experiment APIs (calendar_*)
  • • Unclassified routes are refused
  • -
  • • delete and folderDelete are off by default
  • +
  • • delete, folderDelete and the contacts/calendar/tasks write switches are off by default
@@ -5844,190 +5864,6 @@

Thunderbird

return getComputedStyle(document.body).backgroundColor || '#ffffff'; } - function cleanExportClone(clone) { - // View transforms and neighborhood focus are HTML exploration state, - // never part of a downloaded full-diagram artifact. - clone.style.removeProperty('transform'); - clone.style.removeProperty('clip-path'); - clone.removeAttribute('data-view-scale'); - clone.removeAttribute('data-focus-active'); - clone.removeAttribute('data-reach-active'); - clone.removeAttribute('data-lens-active'); - clone.removeAttribute('data-lens-flow-count'); - clone.removeAttribute('data-lens-flow-density'); - clone.removeAttribute('data-legend-preview-active'); - clone.removeAttribute('data-relationship-preview-active'); - clone.removeAttribute('data-relationship-direct-active'); - clone.removeAttribute('data-relationship-pin-active'); - clone.removeAttribute('data-intent-trace-active'); - clone.removeAttribute('data-route-picking'); - clone.removeAttribute('data-route-active'); - clone.removeAttribute('data-route-journey'); - clone.removeAttribute('data-share-route'); - clone.removeAttribute('data-share-reach'); - clone.removeAttribute('data-story-active'); - clone.removeAttribute('data-story-playing'); - clone.removeAttribute('data-story-beat'); - clone.removeAttribute('data-story-next'); - clone.removeAttribute('data-story-follow'); - clone.removeAttribute('data-chapter-handoff'); - clone.removeAttribute('data-chapter-anchor'); - clone.removeAttribute('data-chapter-preview'); - Array.prototype.forEach.call(clone.querySelectorAll('[data-chapter-handoff-overlay]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-chapter-role]'), function (el) { - el.removeAttribute('data-chapter-role'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-chapter-preview-role]'), function (el) { - el.removeAttribute('data-chapter-preview-role'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-story-overlay], [data-story-carrier-overlay]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-intent-trace-overlay]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-route-probe-overlay]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-route-journey-overlay]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-semantic-lens-overlay]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-legend-bridge-runtime]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-legend-kind]'), function (el) { - el.removeAttribute('data-legend-kind'); - el.removeAttribute('data-legend-label'); - el.removeAttribute('data-legend-count'); - el.removeAttribute('data-legend-zero'); - el.removeAttribute('data-legend-selected'); - el.removeAttribute('role'); - el.removeAttribute('tabindex'); - el.removeAttribute('aria-label'); - el.removeAttribute('aria-pressed'); - el.removeAttribute('aria-haspopup'); - el.removeAttribute('aria-controls'); - el.removeAttribute('aria-expanded'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-legend-bridge]'), function (el) { - el.removeAttribute('data-legend-bridge'); - el.removeAttribute('role'); - el.removeAttribute('aria-label'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-relationship-pulse-overlay]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-relationship-hit-overlay]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-source-evidence-beacon]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-source-evidence-count]'), function (el) { - var originalLabel = el.getAttribute('data-source-evidence-original-label'); - if (originalLabel == null || originalLabel === '') el.removeAttribute('aria-label'); - else el.setAttribute('aria-label', originalLabel); - el.removeAttribute('data-source-evidence-count'); - el.removeAttribute('data-source-evidence-original-label'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-story-step], [data-story-beat-state], [data-story-beat-step]'), function (el) { - el.removeAttribute('data-story-step'); - el.removeAttribute('data-story-beat-state'); - el.removeAttribute('data-story-beat-step'); - el.style.removeProperty('--story-step'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-focus-match], [data-focus-selected]'), function (el) { - el.removeAttribute('data-focus-match'); - el.removeAttribute('data-focus-selected'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-reach-match], [data-reach-origin], [data-reach-depth]'), function (el) { - el.removeAttribute('data-reach-match'); - el.removeAttribute('data-reach-origin'); - el.removeAttribute('data-reach-depth'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-lens-match], [data-lens-selected], [data-lens-peer]'), function (el) { - el.removeAttribute('data-lens-match'); - el.removeAttribute('data-lens-selected'); - el.removeAttribute('data-lens-peer'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-legend-preview-match], [data-legend-preview-selected], [data-legend-preview-peer]'), function (el) { - el.removeAttribute('data-legend-preview-match'); - el.removeAttribute('data-legend-preview-selected'); - el.removeAttribute('data-legend-preview-peer'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-relationship-preview], [data-relationship-preview-node], [data-relationship-preview-source], [data-relationship-preview-target]'), function (el) { - el.removeAttribute('data-relationship-preview'); - el.removeAttribute('data-relationship-preview-node'); - el.removeAttribute('data-relationship-preview-source'); - el.removeAttribute('data-relationship-preview-target'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-intent-trace-match], [data-intent-trace-selected]'), function (el) { - el.removeAttribute('data-intent-trace-match'); - el.removeAttribute('data-intent-trace-selected'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-route-match], [data-route-start], [data-route-end], [data-route-step], [data-route-candidate], [data-route-journey-state], [data-route-journey-current]'), function (el) { - el.removeAttribute('data-route-match'); - el.removeAttribute('data-route-start'); - el.removeAttribute('data-route-end'); - el.removeAttribute('data-route-step'); - el.removeAttribute('data-route-candidate'); - el.removeAttribute('data-route-journey-state'); - el.removeAttribute('data-route-journey-current'); - el.style.removeProperty('--route-step'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-share-route-match], [data-share-route-step], [data-share-route-start], [data-share-route-end], [data-share-route-middle]'), function (el) { - el.removeAttribute('data-share-route-match'); - el.removeAttribute('data-share-route-step'); - el.removeAttribute('data-share-route-start'); - el.removeAttribute('data-share-route-end'); - el.removeAttribute('data-share-route-middle'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-share-reach-match], [data-share-reach-origin], [data-share-reach-depth]'), function (el) { - el.removeAttribute('data-share-reach-match'); - el.removeAttribute('data-share-reach-origin'); - el.removeAttribute('data-share-reach-depth'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-detail], [data-detail-anchor]'), function (el) { - el.removeAttribute('data-detail'); - el.removeAttribute('data-detail-anchor'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-node-id][aria-pressed]'), function (el) { - el.setAttribute('aria-pressed', 'false'); - }); - return !clone.hasAttribute('data-view-scale') && - !clone.hasAttribute('data-focus-active') && - !clone.hasAttribute('data-reach-active') && - !clone.hasAttribute('data-lens-active') && - !clone.hasAttribute('data-lens-flow-count') && - !clone.hasAttribute('data-lens-flow-density') && - !clone.hasAttribute('data-legend-preview-active') && - !clone.hasAttribute('data-relationship-preview-active') && - !clone.hasAttribute('data-relationship-direct-active') && - !clone.hasAttribute('data-intent-trace-active') && - !clone.hasAttribute('data-route-picking') && - !clone.hasAttribute('data-route-active') && - !clone.hasAttribute('data-route-journey') && - !clone.hasAttribute('data-share-route') && - !clone.hasAttribute('data-share-reach') && - !clone.hasAttribute('data-story-active') && - !clone.hasAttribute('data-story-playing') && - !clone.hasAttribute('data-story-beat') && - !clone.hasAttribute('data-story-next') && - !clone.hasAttribute('data-story-follow') && - !clone.hasAttribute('data-chapter-handoff') && - !clone.hasAttribute('data-chapter-anchor') && - !clone.hasAttribute('data-chapter-preview') && - !clone.style.getPropertyValue('transform') && - !clone.style.getPropertyValue('clip-path') && - clone.querySelectorAll('[data-story-overlay], [data-story-carrier-overlay], [data-story-carrier-token], [data-story-step], [data-story-beat-state], [data-story-beat-step], [data-chapter-handoff-overlay], [data-chapter-role], [data-chapter-preview-role], [data-focus-match], [data-focus-selected], [data-reach-match], [data-reach-origin], [data-reach-depth], [data-semantic-lens-overlay], [data-lens-match], [data-lens-selected], [data-lens-peer], [data-legend-bridge], [data-legend-kind], [data-legend-bridge-runtime], [data-legend-count], [data-legend-zero], [data-legend-selected], [data-legend-preview-match], [data-legend-preview-selected], [data-legend-preview-peer], [data-relationship-hit-overlay], [data-relationship-pulse-overlay], [data-relationship-preview], [data-relationship-preview-node], [data-relationship-preview-source], [data-relationship-preview-target], [data-intent-trace-overlay], [data-intent-trace-match], [data-intent-trace-selected], [data-route-probe-overlay], [data-route-journey-overlay], [data-route-match], [data-route-start], [data-route-end], [data-route-step], [data-route-candidate], [data-route-journey-state], [data-route-journey-current], [data-share-route-match], [data-share-route-step], [data-share-route-start], [data-share-route-end], [data-share-route-middle], [data-share-reach-match], [data-share-reach-origin], [data-share-reach-depth], [data-source-evidence-beacon], [data-source-evidence-count], [data-source-evidence-original-label], [data-detail], [data-detail-anchor]').length === 0; - } - - /** * Serialize the main SVG into a standalone document. * @@ -6213,7 +6049,186 @@

Thunderbird

var svg = document.querySelector('.diagram-container svg'); var clone = svg.cloneNode(true); - var canonicalStateClean = cleanExportClone(clone); + // View transforms and neighborhood focus are HTML exploration state, + // never part of a downloaded full-diagram artifact. + clone.style.removeProperty('transform'); + clone.style.removeProperty('clip-path'); + clone.removeAttribute('data-view-scale'); + clone.removeAttribute('data-focus-active'); + clone.removeAttribute('data-reach-active'); + clone.removeAttribute('data-lens-active'); + clone.removeAttribute('data-lens-flow-count'); + clone.removeAttribute('data-lens-flow-density'); + clone.removeAttribute('data-legend-preview-active'); + clone.removeAttribute('data-relationship-preview-active'); + clone.removeAttribute('data-relationship-direct-active'); + clone.removeAttribute('data-relationship-pin-active'); + clone.removeAttribute('data-intent-trace-active'); + clone.removeAttribute('data-route-picking'); + clone.removeAttribute('data-route-active'); + clone.removeAttribute('data-route-journey'); + clone.removeAttribute('data-share-route'); + clone.removeAttribute('data-share-reach'); + clone.removeAttribute('data-story-active'); + clone.removeAttribute('data-story-playing'); + clone.removeAttribute('data-story-beat'); + clone.removeAttribute('data-story-next'); + clone.removeAttribute('data-story-follow'); + clone.removeAttribute('data-chapter-handoff'); + clone.removeAttribute('data-chapter-anchor'); + clone.removeAttribute('data-chapter-preview'); + Array.prototype.forEach.call(clone.querySelectorAll('[data-chapter-handoff-overlay]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-chapter-role]'), function (el) { + el.removeAttribute('data-chapter-role'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-chapter-preview-role]'), function (el) { + el.removeAttribute('data-chapter-preview-role'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-story-overlay], [data-story-carrier-overlay]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-intent-trace-overlay]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-route-probe-overlay]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-route-journey-overlay]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-semantic-lens-overlay]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-legend-bridge-runtime]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-legend-kind]'), function (el) { + el.removeAttribute('data-legend-kind'); + el.removeAttribute('data-legend-label'); + el.removeAttribute('data-legend-count'); + el.removeAttribute('data-legend-zero'); + el.removeAttribute('data-legend-selected'); + el.removeAttribute('role'); + el.removeAttribute('tabindex'); + el.removeAttribute('aria-label'); + el.removeAttribute('aria-pressed'); + el.removeAttribute('aria-haspopup'); + el.removeAttribute('aria-controls'); + el.removeAttribute('aria-expanded'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-legend-bridge]'), function (el) { + el.removeAttribute('data-legend-bridge'); + el.removeAttribute('role'); + el.removeAttribute('aria-label'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-relationship-pulse-overlay]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-relationship-hit-overlay]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-source-evidence-beacon]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-source-evidence-count]'), function (el) { + var originalLabel = el.getAttribute('data-source-evidence-original-label'); + if (originalLabel == null || originalLabel === '') el.removeAttribute('aria-label'); + else el.setAttribute('aria-label', originalLabel); + el.removeAttribute('data-source-evidence-count'); + el.removeAttribute('data-source-evidence-original-label'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-story-step], [data-story-beat-state], [data-story-beat-step]'), function (el) { + el.removeAttribute('data-story-step'); + el.removeAttribute('data-story-beat-state'); + el.removeAttribute('data-story-beat-step'); + el.style.removeProperty('--story-step'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-focus-match], [data-focus-selected]'), function (el) { + el.removeAttribute('data-focus-match'); + el.removeAttribute('data-focus-selected'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-reach-match], [data-reach-origin], [data-reach-depth]'), function (el) { + el.removeAttribute('data-reach-match'); + el.removeAttribute('data-reach-origin'); + el.removeAttribute('data-reach-depth'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-lens-match], [data-lens-selected], [data-lens-peer]'), function (el) { + el.removeAttribute('data-lens-match'); + el.removeAttribute('data-lens-selected'); + el.removeAttribute('data-lens-peer'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-legend-preview-match], [data-legend-preview-selected], [data-legend-preview-peer]'), function (el) { + el.removeAttribute('data-legend-preview-match'); + el.removeAttribute('data-legend-preview-selected'); + el.removeAttribute('data-legend-preview-peer'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-relationship-preview], [data-relationship-preview-node], [data-relationship-preview-source], [data-relationship-preview-target]'), function (el) { + el.removeAttribute('data-relationship-preview'); + el.removeAttribute('data-relationship-preview-node'); + el.removeAttribute('data-relationship-preview-source'); + el.removeAttribute('data-relationship-preview-target'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-intent-trace-match], [data-intent-trace-selected]'), function (el) { + el.removeAttribute('data-intent-trace-match'); + el.removeAttribute('data-intent-trace-selected'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-route-match], [data-route-start], [data-route-end], [data-route-step], [data-route-candidate], [data-route-journey-state], [data-route-journey-current]'), function (el) { + el.removeAttribute('data-route-match'); + el.removeAttribute('data-route-start'); + el.removeAttribute('data-route-end'); + el.removeAttribute('data-route-step'); + el.removeAttribute('data-route-candidate'); + el.removeAttribute('data-route-journey-state'); + el.removeAttribute('data-route-journey-current'); + el.style.removeProperty('--route-step'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-share-route-match], [data-share-route-step], [data-share-route-start], [data-share-route-end], [data-share-route-middle]'), function (el) { + el.removeAttribute('data-share-route-match'); + el.removeAttribute('data-share-route-step'); + el.removeAttribute('data-share-route-start'); + el.removeAttribute('data-share-route-end'); + el.removeAttribute('data-share-route-middle'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-share-reach-match], [data-share-reach-origin], [data-share-reach-depth]'), function (el) { + el.removeAttribute('data-share-reach-match'); + el.removeAttribute('data-share-reach-origin'); + el.removeAttribute('data-share-reach-depth'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-detail], [data-detail-anchor]'), function (el) { + el.removeAttribute('data-detail'); + el.removeAttribute('data-detail-anchor'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-node-id][aria-pressed]'), function (el) { + el.setAttribute('aria-pressed', 'false'); + }); + var canonicalStateClean = !clone.hasAttribute('data-view-scale') && + !clone.hasAttribute('data-focus-active') && + !clone.hasAttribute('data-reach-active') && + !clone.hasAttribute('data-lens-active') && + !clone.hasAttribute('data-lens-flow-count') && + !clone.hasAttribute('data-lens-flow-density') && + !clone.hasAttribute('data-legend-preview-active') && + !clone.hasAttribute('data-relationship-preview-active') && + !clone.hasAttribute('data-relationship-direct-active') && + !clone.hasAttribute('data-intent-trace-active') && + !clone.hasAttribute('data-route-picking') && + !clone.hasAttribute('data-route-active') && + !clone.hasAttribute('data-route-journey') && + !clone.hasAttribute('data-share-route') && + !clone.hasAttribute('data-share-reach') && + !clone.hasAttribute('data-story-active') && + !clone.hasAttribute('data-story-playing') && + !clone.hasAttribute('data-story-beat') && + !clone.hasAttribute('data-story-next') && + !clone.hasAttribute('data-story-follow') && + !clone.hasAttribute('data-chapter-handoff') && + !clone.hasAttribute('data-chapter-anchor') && + !clone.hasAttribute('data-chapter-preview') && + !clone.style.getPropertyValue('transform') && + !clone.style.getPropertyValue('clip-path') && + clone.querySelectorAll('[data-story-overlay], [data-story-carrier-overlay], [data-story-carrier-token], [data-story-step], [data-story-beat-state], [data-story-beat-step], [data-chapter-handoff-overlay], [data-chapter-role], [data-chapter-preview-role], [data-focus-match], [data-focus-selected], [data-reach-match], [data-reach-origin], [data-reach-depth], [data-semantic-lens-overlay], [data-lens-match], [data-lens-selected], [data-lens-peer], [data-legend-bridge], [data-legend-kind], [data-legend-bridge-runtime], [data-legend-count], [data-legend-zero], [data-legend-selected], [data-legend-preview-match], [data-legend-preview-selected], [data-legend-preview-peer], [data-relationship-hit-overlay], [data-relationship-pulse-overlay], [data-relationship-preview], [data-relationship-preview-node], [data-relationship-preview-source], [data-relationship-preview-target], [data-intent-trace-overlay], [data-intent-trace-match], [data-intent-trace-selected], [data-route-probe-overlay], [data-route-journey-overlay], [data-route-match], [data-route-start], [data-route-end], [data-route-step], [data-route-candidate], [data-route-journey-state], [data-route-journey-current], [data-share-route-match], [data-share-route-step], [data-share-route-start], [data-share-route-end], [data-share-route-middle], [data-share-reach-match], [data-share-reach-origin], [data-share-reach-depth], [data-source-evidence-beacon], [data-source-evidence-count], [data-source-evidence-original-label], [data-detail], [data-detail-anchor]').length === 0; var vb = svg.viewBox.baseVal; var finiteSvgDimensions = Number.isFinite(vb.x) && Number.isFinite(vb.y) && @@ -6359,11 +6374,8 @@

Thunderbird

clone.insertBefore(style, clone.firstChild); clone.insertBefore(bgRect, style.nextSibling); - // The XML declaration pins UTF-8: without it, consumers that guess an - // encoding instead of defaulting to UTF-8 mangle non-ASCII text. return { - svgString: '\n' + - new XMLSerializer().serializeToString(clone), + svgString: new XMLSerializer().serializeToString(clone), width: vb.width * scale, height: vb.height * scale, canonicalStateClean: canonicalStateClean, @@ -8013,8 +8025,8 @@

Thunderbird

} function relationshipHitRecords() { var recordsByKey = {}; - var recordsById = Object.create(null); - var byId = Object.create(null); + var recordsById = {}; + var byId = {}; nodes().forEach(function (node) { byId[node.getAttribute('data-node-id')] = node; }); var records = []; edges().forEach(function (edge) { @@ -8764,7 +8776,7 @@

Thunderbird

Archify.routeProbe.clear({ updateUrl: false, restoreFocus: false }); } var nodeList = nodes(); - var byId = Object.create(null); + var byId = {}; nodeList.forEach(function (node) { byId[node.getAttribute('data-node-id')] = node; }); var normalized = []; (ids || []).forEach(function (id) { @@ -8778,8 +8790,8 @@

Thunderbird

clear({ updateUrl: false, preserveView: true }); activeIds = normalized; - var selected = Object.create(null); - var related = Object.create(null); + var selected = {}; + var related = {}; var seenEdges = {}; var matchedEdges = 0; normalized.forEach(function (id) { selected[id] = true; related[id] = true; }); @@ -9130,7 +9142,7 @@

Thunderbird

clear({ announce: false }); var nodeList = nodes(); var edgeList = edges(); - var byId = Object.create(null); + var byId = {}; nodeList.forEach(function (node) { byId[node.getAttribute('data-node-id')] = node; }); var selected = byId[id]; if (!selected) return false; @@ -9139,7 +9151,7 @@

Thunderbird

overlay.setAttribute('class', 'intent-trace-overlay'); overlay.setAttribute('data-intent-trace-overlay', ''); overlay.setAttribute('aria-hidden', 'true'); - var related = Object.create(null); + var related = {}; var seen = {}; var counts = { out: 0, in: 0, loop: 0 }; related[id] = true; @@ -9310,12 +9322,12 @@

Thunderbird

try { views = JSON.parse(data.textContent || '[]'); } catch (_) { views = []; } if (!views.length) return { count: 0, active: function () { return null; } }; - var canonicalNodeIds = Object.create(null); + var canonicalNodeIds = {}; Array.prototype.forEach.call(svg.querySelectorAll('[data-node-id]'), function (node) { canonicalNodeIds[node.getAttribute('data-node-id')] = true; }); views.forEach(function (view) { - var seen = Object.create(null); + var seen = {}; view.focus = (view.focus || []).filter(function (id) { if (!canonicalNodeIds[id] || seen[id]) return false; seen[id] = true; @@ -9344,8 +9356,8 @@

Thunderbird

function chapterDelta(previous, destination) { var previousFocus = previous ? previous.focus : []; var destinationFocus = destination ? destination.focus : []; - var previousIds = Object.create(null); - var destinationIds = Object.create(null); + var previousIds = {}; + var destinationIds = {}; previousFocus.forEach(function (id) { previousIds[id] = true; }); destinationFocus.forEach(function (id) { destinationIds[id] = true; }); return { @@ -9357,7 +9369,7 @@

Thunderbird

function chapterAnchor(previous, destination, outgoingBeatIndex, delta) { if (!previous || !destination) return ''; - var stayIds = Object.create(null); + var stayIds = {}; delta.stay.forEach(function (id) { stayIds[id] = true; }); var activeBeat = outgoingBeatIndex >= 0 ? previous.focus[outgoingBeatIndex] : ''; if (activeBeat && stayIds[activeBeat]) return activeBeat; @@ -9375,7 +9387,7 @@

Thunderbird

} function classifyHandoff(delta, anchor) { - var roles = Object.create(null); + var roles = {}; delta.stay.forEach(function (id) { roles[id] = 'stay'; }); delta.enter.forEach(function (id) { roles[id] = 'enter'; }); delta.leave.forEach(function (id) { roles[id] = 'leave'; }); @@ -9692,7 +9704,7 @@

Thunderbird

if (activePreviewIndex === index && svg.getAttribute('data-chapter-preview') === views[index].id) return true; clearChapterPreviewPresentation(); var delta = chapterDelta(activeIndex >= 0 ? views[activeIndex] : null, views[index]); - var roles = Object.create(null); + var roles = {}; delta.stay.forEach(function (id) { roles[id] = 'stay'; }); delta.enter.forEach(function (id) { roles[id] = 'enter'; }); delta.leave.forEach(function (id) { roles[id] = 'leave'; }); @@ -10156,9 +10168,7 @@

Thunderbird

function centerStoryStop(stop) { if (!stop || !trail) return false; - var bounds = stop.getBoundingClientRect(); - var viewportLeft = trail.getBoundingClientRect().left + trail.clientLeft; - var target = Math.max(0, trail.scrollLeft + bounds.left - viewportLeft - (trail.clientWidth - bounds.width) / 2); + var target = Math.max(0, stop.offsetLeft - (trail.clientWidth - stop.offsetWidth) / 2); trail.scrollLeft = target; return true; } @@ -10346,7 +10356,7 @@

Thunderbird

var nodeList = Array.prototype.slice.call(svg.querySelectorAll('[data-node-id]')); var edgeList = Array.prototype.slice.call(svg.querySelectorAll('[data-edge-from][data-edge-to]')); - var byId = Object.create(null); + var byId = {}; nodeList.forEach(function (node) { byId[node.getAttribute('data-node-id')] = node; }); storySteps = view.focus.map(function (_, index) { return storyStep(view, index, edgeList, byId); }); var labels = []; @@ -10908,22 +10918,6 @@

Thunderbird

}); storyMotionObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-motion'] }); } - if (typeof ResizeObserver === 'function') { - var chapterViewportWidth = chapterList.clientWidth; - var trailViewportWidth = trail.clientWidth; - var storyScrollObserver = new ResizeObserver(function () { - var chapterWidth = chapterList.clientWidth; - var trailWidth = trail.clientWidth; - if (chapterWidth !== chapterViewportWidth && activeIndex >= 0) centerChapterButton(chapterButtons[activeIndex]); - if (trailWidth !== trailViewportWidth && storyBeatIndex >= 0) { - centerStoryStop(trail.querySelectorAll('[data-story-node]')[storyBeatIndex]); - } - chapterViewportWidth = chapterWidth; - trailViewportWidth = trailWidth; - }); - storyScrollObserver.observe(chapterList); - storyScrollObserver.observe(trail); - } window.addEventListener('beforeprint', function () { clearChapterPreview({ clearIntents: true }); if (playing) pausePlayback(); @@ -11797,8 +11791,8 @@

Thunderbird

return true; } function semanticIds(ids, includeNeighbors) { - var seeds = Object.create(null); - var wanted = Object.create(null); + var seeds = {}; + var wanted = {}; (ids || []).forEach(function (id) { seeds[id] = true; wanted[id] = true; }); if (includeNeighbors) { Array.prototype.forEach.call(svg.querySelectorAll('[data-edge-from][data-edge-to]'), function (edge) { @@ -13266,7 +13260,7 @@

Thunderbird

} function outgoingByNode() { var byId = nodesById(); - var outgoing = Object.create(null); + var outgoing = {}; edges().forEach(function (edge) { var from = edge.getAttribute('data-edge-from'); var to = edge.getAttribute('data-edge-to'); @@ -13278,7 +13272,7 @@

Thunderbird

} function reachableFrom(source) { var outgoing = outgoingByNode(); - var reached = Object.create(null); + var reached = {}; var queue = [source]; reached[source] = true; for (var cursor = 0; cursor < queue.length; cursor += 1) { @@ -13293,7 +13287,7 @@

Thunderbird

} function hopDistancesFrom(source) { var outgoing = outgoingByNode(); - var distances = Object.create(null); + var distances = {}; var queue = [source]; distances[source] = 0; for (var cursor = 0; cursor < queue.length; cursor += 1) { @@ -13309,7 +13303,7 @@

Thunderbird

function shortestDirectedPath(source, target) { if (source === target) return null; var outgoing = outgoingByNode(); - var previous = Object.create(null); + var previous = {}; var queue = [source]; previous[source] = null; for (var cursor = 0; cursor < queue.length && !Object.prototype.hasOwnProperty.call(previous, target); cursor += 1) { @@ -13839,7 +13833,7 @@

Thunderbird

var sourceIds = nodes().map(function (node) { return node.getAttribute('data-node-id'); }).filter(function (id) { return outgoing[id] && outgoing[id].length; }); - var sourceBadges = Object.create(null); + var sourceBadges = {}; sourceIds.forEach(function (id) { sourceBadges[id] = viewerText('viewer.route.finder.source.badge'); }); return { kind: 'route-source', @@ -13855,7 +13849,7 @@

Thunderbird

if (mode === 'target' && startId && byId[startId]) { var distances = hopDistancesFrom(startId); var targetIds = Object.keys(distances).filter(function (id) { return id !== startId && byId[id]; }); - var targetBadges = Object.create(null); + var targetBadges = {}; targetIds.forEach(function (id) { targetBadges[id] = viewerCount('viewer.route.hop', distances[id]); }); @@ -14064,7 +14058,7 @@

Thunderbird

var MAX_LENS_FLOW_EDGES = 24; function nodesById() { - var byId = Object.create(null); + var byId = {}; Array.prototype.forEach.call(svg.querySelectorAll('[data-node-id][data-node-kind]'), function (node) { var id = node.getAttribute('data-node-id'); if (id && !byId[id]) byId[id] = node; @@ -14231,7 +14225,7 @@

Thunderbird

return (byId[id].getAttribute('data-node-kind') || 'neutral') === kind; }); if (!matches.length) return false; - var chosen = Object.create(null); + var chosen = {}; matches.forEach(function (id) { chosen[id] = true; byId[id].setAttribute('data-legend-preview-match', ''); @@ -14407,7 +14401,7 @@

Thunderbird

options = options || {}; cleanSvgState(); var byId = nodesById(); - var nodeKind = Object.create(null); + var nodeKind = {}; Object.keys(byId).forEach(function (id) { nodeKind[id] = byId[id].getAttribute('data-node-kind') || 'neutral'; }); if (!selectedKinds.length) { status.textContent = viewerText('viewer.lens.choose'); diff --git a/docs/diagrams/architecture.png b/docs/diagrams/architecture.png index 23c0bd3..5045169 100644 Binary files a/docs/diagrams/architecture.png and b/docs/diagrams/architecture.png differ diff --git a/docs/diagrams/release-workflow-dark.png b/docs/diagrams/release-workflow-dark.png index 0275ff7..4791ec9 100644 Binary files a/docs/diagrams/release-workflow-dark.png and b/docs/diagrams/release-workflow-dark.png differ diff --git a/docs/diagrams/release-workflow.html b/docs/diagrams/release-workflow.html index a50b5f6..3e2578d 100644 --- a/docs/diagrams/release-workflow.html +++ b/docs/diagrams/release-workflow.html @@ -5083,10 +5083,10 @@

Signed XPI release workflow

-

sign-xpi.yml gets the add-on approved by addons.thunderbird.net; release.yml publishes it

+

sign-xpi.yml gets the add-on approved by addons.thunderbird.net; release.yml publishes it to GitHub and npm

- + @@ -5582,7 +5583,8 @@

Publish

  • • release.yml fails if no *-tb.xpi is committed
  • -
  • • Unsigned build is attached alongside for reference
  • +
  • • Attaches the highest signed *-tb.xpi plus the unsigned build
  • +
  • • npm trusted publishing (OIDC) on Node 22, no stored token
@@ -5872,190 +5874,6 @@

Publish

return getComputedStyle(document.body).backgroundColor || '#ffffff'; } - function cleanExportClone(clone) { - // View transforms and neighborhood focus are HTML exploration state, - // never part of a downloaded full-diagram artifact. - clone.style.removeProperty('transform'); - clone.style.removeProperty('clip-path'); - clone.removeAttribute('data-view-scale'); - clone.removeAttribute('data-focus-active'); - clone.removeAttribute('data-reach-active'); - clone.removeAttribute('data-lens-active'); - clone.removeAttribute('data-lens-flow-count'); - clone.removeAttribute('data-lens-flow-density'); - clone.removeAttribute('data-legend-preview-active'); - clone.removeAttribute('data-relationship-preview-active'); - clone.removeAttribute('data-relationship-direct-active'); - clone.removeAttribute('data-relationship-pin-active'); - clone.removeAttribute('data-intent-trace-active'); - clone.removeAttribute('data-route-picking'); - clone.removeAttribute('data-route-active'); - clone.removeAttribute('data-route-journey'); - clone.removeAttribute('data-share-route'); - clone.removeAttribute('data-share-reach'); - clone.removeAttribute('data-story-active'); - clone.removeAttribute('data-story-playing'); - clone.removeAttribute('data-story-beat'); - clone.removeAttribute('data-story-next'); - clone.removeAttribute('data-story-follow'); - clone.removeAttribute('data-chapter-handoff'); - clone.removeAttribute('data-chapter-anchor'); - clone.removeAttribute('data-chapter-preview'); - Array.prototype.forEach.call(clone.querySelectorAll('[data-chapter-handoff-overlay]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-chapter-role]'), function (el) { - el.removeAttribute('data-chapter-role'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-chapter-preview-role]'), function (el) { - el.removeAttribute('data-chapter-preview-role'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-story-overlay], [data-story-carrier-overlay]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-intent-trace-overlay]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-route-probe-overlay]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-route-journey-overlay]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-semantic-lens-overlay]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-legend-bridge-runtime]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-legend-kind]'), function (el) { - el.removeAttribute('data-legend-kind'); - el.removeAttribute('data-legend-label'); - el.removeAttribute('data-legend-count'); - el.removeAttribute('data-legend-zero'); - el.removeAttribute('data-legend-selected'); - el.removeAttribute('role'); - el.removeAttribute('tabindex'); - el.removeAttribute('aria-label'); - el.removeAttribute('aria-pressed'); - el.removeAttribute('aria-haspopup'); - el.removeAttribute('aria-controls'); - el.removeAttribute('aria-expanded'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-legend-bridge]'), function (el) { - el.removeAttribute('data-legend-bridge'); - el.removeAttribute('role'); - el.removeAttribute('aria-label'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-relationship-pulse-overlay]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-relationship-hit-overlay]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-source-evidence-beacon]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-source-evidence-count]'), function (el) { - var originalLabel = el.getAttribute('data-source-evidence-original-label'); - if (originalLabel == null || originalLabel === '') el.removeAttribute('aria-label'); - else el.setAttribute('aria-label', originalLabel); - el.removeAttribute('data-source-evidence-count'); - el.removeAttribute('data-source-evidence-original-label'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-story-step], [data-story-beat-state], [data-story-beat-step]'), function (el) { - el.removeAttribute('data-story-step'); - el.removeAttribute('data-story-beat-state'); - el.removeAttribute('data-story-beat-step'); - el.style.removeProperty('--story-step'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-focus-match], [data-focus-selected]'), function (el) { - el.removeAttribute('data-focus-match'); - el.removeAttribute('data-focus-selected'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-reach-match], [data-reach-origin], [data-reach-depth]'), function (el) { - el.removeAttribute('data-reach-match'); - el.removeAttribute('data-reach-origin'); - el.removeAttribute('data-reach-depth'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-lens-match], [data-lens-selected], [data-lens-peer]'), function (el) { - el.removeAttribute('data-lens-match'); - el.removeAttribute('data-lens-selected'); - el.removeAttribute('data-lens-peer'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-legend-preview-match], [data-legend-preview-selected], [data-legend-preview-peer]'), function (el) { - el.removeAttribute('data-legend-preview-match'); - el.removeAttribute('data-legend-preview-selected'); - el.removeAttribute('data-legend-preview-peer'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-relationship-preview], [data-relationship-preview-node], [data-relationship-preview-source], [data-relationship-preview-target]'), function (el) { - el.removeAttribute('data-relationship-preview'); - el.removeAttribute('data-relationship-preview-node'); - el.removeAttribute('data-relationship-preview-source'); - el.removeAttribute('data-relationship-preview-target'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-intent-trace-match], [data-intent-trace-selected]'), function (el) { - el.removeAttribute('data-intent-trace-match'); - el.removeAttribute('data-intent-trace-selected'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-route-match], [data-route-start], [data-route-end], [data-route-step], [data-route-candidate], [data-route-journey-state], [data-route-journey-current]'), function (el) { - el.removeAttribute('data-route-match'); - el.removeAttribute('data-route-start'); - el.removeAttribute('data-route-end'); - el.removeAttribute('data-route-step'); - el.removeAttribute('data-route-candidate'); - el.removeAttribute('data-route-journey-state'); - el.removeAttribute('data-route-journey-current'); - el.style.removeProperty('--route-step'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-share-route-match], [data-share-route-step], [data-share-route-start], [data-share-route-end], [data-share-route-middle]'), function (el) { - el.removeAttribute('data-share-route-match'); - el.removeAttribute('data-share-route-step'); - el.removeAttribute('data-share-route-start'); - el.removeAttribute('data-share-route-end'); - el.removeAttribute('data-share-route-middle'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-share-reach-match], [data-share-reach-origin], [data-share-reach-depth]'), function (el) { - el.removeAttribute('data-share-reach-match'); - el.removeAttribute('data-share-reach-origin'); - el.removeAttribute('data-share-reach-depth'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-detail], [data-detail-anchor]'), function (el) { - el.removeAttribute('data-detail'); - el.removeAttribute('data-detail-anchor'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-node-id][aria-pressed]'), function (el) { - el.setAttribute('aria-pressed', 'false'); - }); - return !clone.hasAttribute('data-view-scale') && - !clone.hasAttribute('data-focus-active') && - !clone.hasAttribute('data-reach-active') && - !clone.hasAttribute('data-lens-active') && - !clone.hasAttribute('data-lens-flow-count') && - !clone.hasAttribute('data-lens-flow-density') && - !clone.hasAttribute('data-legend-preview-active') && - !clone.hasAttribute('data-relationship-preview-active') && - !clone.hasAttribute('data-relationship-direct-active') && - !clone.hasAttribute('data-intent-trace-active') && - !clone.hasAttribute('data-route-picking') && - !clone.hasAttribute('data-route-active') && - !clone.hasAttribute('data-route-journey') && - !clone.hasAttribute('data-share-route') && - !clone.hasAttribute('data-share-reach') && - !clone.hasAttribute('data-story-active') && - !clone.hasAttribute('data-story-playing') && - !clone.hasAttribute('data-story-beat') && - !clone.hasAttribute('data-story-next') && - !clone.hasAttribute('data-story-follow') && - !clone.hasAttribute('data-chapter-handoff') && - !clone.hasAttribute('data-chapter-anchor') && - !clone.hasAttribute('data-chapter-preview') && - !clone.style.getPropertyValue('transform') && - !clone.style.getPropertyValue('clip-path') && - clone.querySelectorAll('[data-story-overlay], [data-story-carrier-overlay], [data-story-carrier-token], [data-story-step], [data-story-beat-state], [data-story-beat-step], [data-chapter-handoff-overlay], [data-chapter-role], [data-chapter-preview-role], [data-focus-match], [data-focus-selected], [data-reach-match], [data-reach-origin], [data-reach-depth], [data-semantic-lens-overlay], [data-lens-match], [data-lens-selected], [data-lens-peer], [data-legend-bridge], [data-legend-kind], [data-legend-bridge-runtime], [data-legend-count], [data-legend-zero], [data-legend-selected], [data-legend-preview-match], [data-legend-preview-selected], [data-legend-preview-peer], [data-relationship-hit-overlay], [data-relationship-pulse-overlay], [data-relationship-preview], [data-relationship-preview-node], [data-relationship-preview-source], [data-relationship-preview-target], [data-intent-trace-overlay], [data-intent-trace-match], [data-intent-trace-selected], [data-route-probe-overlay], [data-route-journey-overlay], [data-route-match], [data-route-start], [data-route-end], [data-route-step], [data-route-candidate], [data-route-journey-state], [data-route-journey-current], [data-share-route-match], [data-share-route-step], [data-share-route-start], [data-share-route-end], [data-share-route-middle], [data-share-reach-match], [data-share-reach-origin], [data-share-reach-depth], [data-source-evidence-beacon], [data-source-evidence-count], [data-source-evidence-original-label], [data-detail], [data-detail-anchor]').length === 0; - } - - /** * Serialize the main SVG into a standalone document. * @@ -6241,7 +6059,186 @@

Publish

var svg = document.querySelector('.diagram-container svg'); var clone = svg.cloneNode(true); - var canonicalStateClean = cleanExportClone(clone); + // View transforms and neighborhood focus are HTML exploration state, + // never part of a downloaded full-diagram artifact. + clone.style.removeProperty('transform'); + clone.style.removeProperty('clip-path'); + clone.removeAttribute('data-view-scale'); + clone.removeAttribute('data-focus-active'); + clone.removeAttribute('data-reach-active'); + clone.removeAttribute('data-lens-active'); + clone.removeAttribute('data-lens-flow-count'); + clone.removeAttribute('data-lens-flow-density'); + clone.removeAttribute('data-legend-preview-active'); + clone.removeAttribute('data-relationship-preview-active'); + clone.removeAttribute('data-relationship-direct-active'); + clone.removeAttribute('data-relationship-pin-active'); + clone.removeAttribute('data-intent-trace-active'); + clone.removeAttribute('data-route-picking'); + clone.removeAttribute('data-route-active'); + clone.removeAttribute('data-route-journey'); + clone.removeAttribute('data-share-route'); + clone.removeAttribute('data-share-reach'); + clone.removeAttribute('data-story-active'); + clone.removeAttribute('data-story-playing'); + clone.removeAttribute('data-story-beat'); + clone.removeAttribute('data-story-next'); + clone.removeAttribute('data-story-follow'); + clone.removeAttribute('data-chapter-handoff'); + clone.removeAttribute('data-chapter-anchor'); + clone.removeAttribute('data-chapter-preview'); + Array.prototype.forEach.call(clone.querySelectorAll('[data-chapter-handoff-overlay]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-chapter-role]'), function (el) { + el.removeAttribute('data-chapter-role'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-chapter-preview-role]'), function (el) { + el.removeAttribute('data-chapter-preview-role'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-story-overlay], [data-story-carrier-overlay]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-intent-trace-overlay]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-route-probe-overlay]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-route-journey-overlay]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-semantic-lens-overlay]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-legend-bridge-runtime]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-legend-kind]'), function (el) { + el.removeAttribute('data-legend-kind'); + el.removeAttribute('data-legend-label'); + el.removeAttribute('data-legend-count'); + el.removeAttribute('data-legend-zero'); + el.removeAttribute('data-legend-selected'); + el.removeAttribute('role'); + el.removeAttribute('tabindex'); + el.removeAttribute('aria-label'); + el.removeAttribute('aria-pressed'); + el.removeAttribute('aria-haspopup'); + el.removeAttribute('aria-controls'); + el.removeAttribute('aria-expanded'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-legend-bridge]'), function (el) { + el.removeAttribute('data-legend-bridge'); + el.removeAttribute('role'); + el.removeAttribute('aria-label'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-relationship-pulse-overlay]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-relationship-hit-overlay]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-source-evidence-beacon]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-source-evidence-count]'), function (el) { + var originalLabel = el.getAttribute('data-source-evidence-original-label'); + if (originalLabel == null || originalLabel === '') el.removeAttribute('aria-label'); + else el.setAttribute('aria-label', originalLabel); + el.removeAttribute('data-source-evidence-count'); + el.removeAttribute('data-source-evidence-original-label'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-story-step], [data-story-beat-state], [data-story-beat-step]'), function (el) { + el.removeAttribute('data-story-step'); + el.removeAttribute('data-story-beat-state'); + el.removeAttribute('data-story-beat-step'); + el.style.removeProperty('--story-step'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-focus-match], [data-focus-selected]'), function (el) { + el.removeAttribute('data-focus-match'); + el.removeAttribute('data-focus-selected'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-reach-match], [data-reach-origin], [data-reach-depth]'), function (el) { + el.removeAttribute('data-reach-match'); + el.removeAttribute('data-reach-origin'); + el.removeAttribute('data-reach-depth'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-lens-match], [data-lens-selected], [data-lens-peer]'), function (el) { + el.removeAttribute('data-lens-match'); + el.removeAttribute('data-lens-selected'); + el.removeAttribute('data-lens-peer'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-legend-preview-match], [data-legend-preview-selected], [data-legend-preview-peer]'), function (el) { + el.removeAttribute('data-legend-preview-match'); + el.removeAttribute('data-legend-preview-selected'); + el.removeAttribute('data-legend-preview-peer'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-relationship-preview], [data-relationship-preview-node], [data-relationship-preview-source], [data-relationship-preview-target]'), function (el) { + el.removeAttribute('data-relationship-preview'); + el.removeAttribute('data-relationship-preview-node'); + el.removeAttribute('data-relationship-preview-source'); + el.removeAttribute('data-relationship-preview-target'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-intent-trace-match], [data-intent-trace-selected]'), function (el) { + el.removeAttribute('data-intent-trace-match'); + el.removeAttribute('data-intent-trace-selected'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-route-match], [data-route-start], [data-route-end], [data-route-step], [data-route-candidate], [data-route-journey-state], [data-route-journey-current]'), function (el) { + el.removeAttribute('data-route-match'); + el.removeAttribute('data-route-start'); + el.removeAttribute('data-route-end'); + el.removeAttribute('data-route-step'); + el.removeAttribute('data-route-candidate'); + el.removeAttribute('data-route-journey-state'); + el.removeAttribute('data-route-journey-current'); + el.style.removeProperty('--route-step'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-share-route-match], [data-share-route-step], [data-share-route-start], [data-share-route-end], [data-share-route-middle]'), function (el) { + el.removeAttribute('data-share-route-match'); + el.removeAttribute('data-share-route-step'); + el.removeAttribute('data-share-route-start'); + el.removeAttribute('data-share-route-end'); + el.removeAttribute('data-share-route-middle'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-share-reach-match], [data-share-reach-origin], [data-share-reach-depth]'), function (el) { + el.removeAttribute('data-share-reach-match'); + el.removeAttribute('data-share-reach-origin'); + el.removeAttribute('data-share-reach-depth'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-detail], [data-detail-anchor]'), function (el) { + el.removeAttribute('data-detail'); + el.removeAttribute('data-detail-anchor'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-node-id][aria-pressed]'), function (el) { + el.setAttribute('aria-pressed', 'false'); + }); + var canonicalStateClean = !clone.hasAttribute('data-view-scale') && + !clone.hasAttribute('data-focus-active') && + !clone.hasAttribute('data-reach-active') && + !clone.hasAttribute('data-lens-active') && + !clone.hasAttribute('data-lens-flow-count') && + !clone.hasAttribute('data-lens-flow-density') && + !clone.hasAttribute('data-legend-preview-active') && + !clone.hasAttribute('data-relationship-preview-active') && + !clone.hasAttribute('data-relationship-direct-active') && + !clone.hasAttribute('data-intent-trace-active') && + !clone.hasAttribute('data-route-picking') && + !clone.hasAttribute('data-route-active') && + !clone.hasAttribute('data-route-journey') && + !clone.hasAttribute('data-share-route') && + !clone.hasAttribute('data-share-reach') && + !clone.hasAttribute('data-story-active') && + !clone.hasAttribute('data-story-playing') && + !clone.hasAttribute('data-story-beat') && + !clone.hasAttribute('data-story-next') && + !clone.hasAttribute('data-story-follow') && + !clone.hasAttribute('data-chapter-handoff') && + !clone.hasAttribute('data-chapter-anchor') && + !clone.hasAttribute('data-chapter-preview') && + !clone.style.getPropertyValue('transform') && + !clone.style.getPropertyValue('clip-path') && + clone.querySelectorAll('[data-story-overlay], [data-story-carrier-overlay], [data-story-carrier-token], [data-story-step], [data-story-beat-state], [data-story-beat-step], [data-chapter-handoff-overlay], [data-chapter-role], [data-chapter-preview-role], [data-focus-match], [data-focus-selected], [data-reach-match], [data-reach-origin], [data-reach-depth], [data-semantic-lens-overlay], [data-lens-match], [data-lens-selected], [data-lens-peer], [data-legend-bridge], [data-legend-kind], [data-legend-bridge-runtime], [data-legend-count], [data-legend-zero], [data-legend-selected], [data-legend-preview-match], [data-legend-preview-selected], [data-legend-preview-peer], [data-relationship-hit-overlay], [data-relationship-pulse-overlay], [data-relationship-preview], [data-relationship-preview-node], [data-relationship-preview-source], [data-relationship-preview-target], [data-intent-trace-overlay], [data-intent-trace-match], [data-intent-trace-selected], [data-route-probe-overlay], [data-route-journey-overlay], [data-route-match], [data-route-start], [data-route-end], [data-route-step], [data-route-candidate], [data-route-journey-state], [data-route-journey-current], [data-share-route-match], [data-share-route-step], [data-share-route-start], [data-share-route-end], [data-share-route-middle], [data-share-reach-match], [data-share-reach-origin], [data-share-reach-depth], [data-source-evidence-beacon], [data-source-evidence-count], [data-source-evidence-original-label], [data-detail], [data-detail-anchor]').length === 0; var vb = svg.viewBox.baseVal; var finiteSvgDimensions = Number.isFinite(vb.x) && Number.isFinite(vb.y) && @@ -6387,11 +6384,8 @@

Publish

clone.insertBefore(style, clone.firstChild); clone.insertBefore(bgRect, style.nextSibling); - // The XML declaration pins UTF-8: without it, consumers that guess an - // encoding instead of defaulting to UTF-8 mangle non-ASCII text. return { - svgString: '\n' + - new XMLSerializer().serializeToString(clone), + svgString: new XMLSerializer().serializeToString(clone), width: vb.width * scale, height: vb.height * scale, canonicalStateClean: canonicalStateClean, @@ -8041,8 +8035,8 @@

Publish

} function relationshipHitRecords() { var recordsByKey = {}; - var recordsById = Object.create(null); - var byId = Object.create(null); + var recordsById = {}; + var byId = {}; nodes().forEach(function (node) { byId[node.getAttribute('data-node-id')] = node; }); var records = []; edges().forEach(function (edge) { @@ -8792,7 +8786,7 @@

Publish

Archify.routeProbe.clear({ updateUrl: false, restoreFocus: false }); } var nodeList = nodes(); - var byId = Object.create(null); + var byId = {}; nodeList.forEach(function (node) { byId[node.getAttribute('data-node-id')] = node; }); var normalized = []; (ids || []).forEach(function (id) { @@ -8806,8 +8800,8 @@

Publish

clear({ updateUrl: false, preserveView: true }); activeIds = normalized; - var selected = Object.create(null); - var related = Object.create(null); + var selected = {}; + var related = {}; var seenEdges = {}; var matchedEdges = 0; normalized.forEach(function (id) { selected[id] = true; related[id] = true; }); @@ -9158,7 +9152,7 @@

Publish

clear({ announce: false }); var nodeList = nodes(); var edgeList = edges(); - var byId = Object.create(null); + var byId = {}; nodeList.forEach(function (node) { byId[node.getAttribute('data-node-id')] = node; }); var selected = byId[id]; if (!selected) return false; @@ -9167,7 +9161,7 @@

Publish

overlay.setAttribute('class', 'intent-trace-overlay'); overlay.setAttribute('data-intent-trace-overlay', ''); overlay.setAttribute('aria-hidden', 'true'); - var related = Object.create(null); + var related = {}; var seen = {}; var counts = { out: 0, in: 0, loop: 0 }; related[id] = true; @@ -9338,12 +9332,12 @@

Publish

try { views = JSON.parse(data.textContent || '[]'); } catch (_) { views = []; } if (!views.length) return { count: 0, active: function () { return null; } }; - var canonicalNodeIds = Object.create(null); + var canonicalNodeIds = {}; Array.prototype.forEach.call(svg.querySelectorAll('[data-node-id]'), function (node) { canonicalNodeIds[node.getAttribute('data-node-id')] = true; }); views.forEach(function (view) { - var seen = Object.create(null); + var seen = {}; view.focus = (view.focus || []).filter(function (id) { if (!canonicalNodeIds[id] || seen[id]) return false; seen[id] = true; @@ -9372,8 +9366,8 @@

Publish

function chapterDelta(previous, destination) { var previousFocus = previous ? previous.focus : []; var destinationFocus = destination ? destination.focus : []; - var previousIds = Object.create(null); - var destinationIds = Object.create(null); + var previousIds = {}; + var destinationIds = {}; previousFocus.forEach(function (id) { previousIds[id] = true; }); destinationFocus.forEach(function (id) { destinationIds[id] = true; }); return { @@ -9385,7 +9379,7 @@

Publish

function chapterAnchor(previous, destination, outgoingBeatIndex, delta) { if (!previous || !destination) return ''; - var stayIds = Object.create(null); + var stayIds = {}; delta.stay.forEach(function (id) { stayIds[id] = true; }); var activeBeat = outgoingBeatIndex >= 0 ? previous.focus[outgoingBeatIndex] : ''; if (activeBeat && stayIds[activeBeat]) return activeBeat; @@ -9403,7 +9397,7 @@

Publish

} function classifyHandoff(delta, anchor) { - var roles = Object.create(null); + var roles = {}; delta.stay.forEach(function (id) { roles[id] = 'stay'; }); delta.enter.forEach(function (id) { roles[id] = 'enter'; }); delta.leave.forEach(function (id) { roles[id] = 'leave'; }); @@ -9720,7 +9714,7 @@

Publish

if (activePreviewIndex === index && svg.getAttribute('data-chapter-preview') === views[index].id) return true; clearChapterPreviewPresentation(); var delta = chapterDelta(activeIndex >= 0 ? views[activeIndex] : null, views[index]); - var roles = Object.create(null); + var roles = {}; delta.stay.forEach(function (id) { roles[id] = 'stay'; }); delta.enter.forEach(function (id) { roles[id] = 'enter'; }); delta.leave.forEach(function (id) { roles[id] = 'leave'; }); @@ -10184,9 +10178,7 @@

Publish

function centerStoryStop(stop) { if (!stop || !trail) return false; - var bounds = stop.getBoundingClientRect(); - var viewportLeft = trail.getBoundingClientRect().left + trail.clientLeft; - var target = Math.max(0, trail.scrollLeft + bounds.left - viewportLeft - (trail.clientWidth - bounds.width) / 2); + var target = Math.max(0, stop.offsetLeft - (trail.clientWidth - stop.offsetWidth) / 2); trail.scrollLeft = target; return true; } @@ -10374,7 +10366,7 @@

Publish

var nodeList = Array.prototype.slice.call(svg.querySelectorAll('[data-node-id]')); var edgeList = Array.prototype.slice.call(svg.querySelectorAll('[data-edge-from][data-edge-to]')); - var byId = Object.create(null); + var byId = {}; nodeList.forEach(function (node) { byId[node.getAttribute('data-node-id')] = node; }); storySteps = view.focus.map(function (_, index) { return storyStep(view, index, edgeList, byId); }); var labels = []; @@ -10936,22 +10928,6 @@

Publish

}); storyMotionObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-motion'] }); } - if (typeof ResizeObserver === 'function') { - var chapterViewportWidth = chapterList.clientWidth; - var trailViewportWidth = trail.clientWidth; - var storyScrollObserver = new ResizeObserver(function () { - var chapterWidth = chapterList.clientWidth; - var trailWidth = trail.clientWidth; - if (chapterWidth !== chapterViewportWidth && activeIndex >= 0) centerChapterButton(chapterButtons[activeIndex]); - if (trailWidth !== trailViewportWidth && storyBeatIndex >= 0) { - centerStoryStop(trail.querySelectorAll('[data-story-node]')[storyBeatIndex]); - } - chapterViewportWidth = chapterWidth; - trailViewportWidth = trailWidth; - }); - storyScrollObserver.observe(chapterList); - storyScrollObserver.observe(trail); - } window.addEventListener('beforeprint', function () { clearChapterPreview({ clearIntents: true }); if (playing) pausePlayback(); @@ -11825,8 +11801,8 @@

Publish

return true; } function semanticIds(ids, includeNeighbors) { - var seeds = Object.create(null); - var wanted = Object.create(null); + var seeds = {}; + var wanted = {}; (ids || []).forEach(function (id) { seeds[id] = true; wanted[id] = true; }); if (includeNeighbors) { Array.prototype.forEach.call(svg.querySelectorAll('[data-edge-from][data-edge-to]'), function (edge) { @@ -13294,7 +13270,7 @@

Publish

} function outgoingByNode() { var byId = nodesById(); - var outgoing = Object.create(null); + var outgoing = {}; edges().forEach(function (edge) { var from = edge.getAttribute('data-edge-from'); var to = edge.getAttribute('data-edge-to'); @@ -13306,7 +13282,7 @@

Publish

} function reachableFrom(source) { var outgoing = outgoingByNode(); - var reached = Object.create(null); + var reached = {}; var queue = [source]; reached[source] = true; for (var cursor = 0; cursor < queue.length; cursor += 1) { @@ -13321,7 +13297,7 @@

Publish

} function hopDistancesFrom(source) { var outgoing = outgoingByNode(); - var distances = Object.create(null); + var distances = {}; var queue = [source]; distances[source] = 0; for (var cursor = 0; cursor < queue.length; cursor += 1) { @@ -13337,7 +13313,7 @@

Publish

function shortestDirectedPath(source, target) { if (source === target) return null; var outgoing = outgoingByNode(); - var previous = Object.create(null); + var previous = {}; var queue = [source]; previous[source] = null; for (var cursor = 0; cursor < queue.length && !Object.prototype.hasOwnProperty.call(previous, target); cursor += 1) { @@ -13867,7 +13843,7 @@

Publish

var sourceIds = nodes().map(function (node) { return node.getAttribute('data-node-id'); }).filter(function (id) { return outgoing[id] && outgoing[id].length; }); - var sourceBadges = Object.create(null); + var sourceBadges = {}; sourceIds.forEach(function (id) { sourceBadges[id] = viewerText('viewer.route.finder.source.badge'); }); return { kind: 'route-source', @@ -13883,7 +13859,7 @@

Publish

if (mode === 'target' && startId && byId[startId]) { var distances = hopDistancesFrom(startId); var targetIds = Object.keys(distances).filter(function (id) { return id !== startId && byId[id]; }); - var targetBadges = Object.create(null); + var targetBadges = {}; targetIds.forEach(function (id) { targetBadges[id] = viewerCount('viewer.route.hop', distances[id]); }); @@ -14092,7 +14068,7 @@

Publish

var MAX_LENS_FLOW_EDGES = 24; function nodesById() { - var byId = Object.create(null); + var byId = {}; Array.prototype.forEach.call(svg.querySelectorAll('[data-node-id][data-node-kind]'), function (node) { var id = node.getAttribute('data-node-id'); if (id && !byId[id]) byId[id] = node; @@ -14259,7 +14235,7 @@

Publish

return (byId[id].getAttribute('data-node-kind') || 'neutral') === kind; }); if (!matches.length) return false; - var chosen = Object.create(null); + var chosen = {}; matches.forEach(function (id) { chosen[id] = true; byId[id].setAttribute('data-legend-preview-match', ''); @@ -14435,7 +14411,7 @@

Publish

options = options || {}; cleanSvgState(); var byId = nodesById(); - var nodeKind = Object.create(null); + var nodeKind = {}; Object.keys(byId).forEach(function (id) { nodeKind[id] = byId[id].getAttribute('data-node-kind') || 'neutral'; }); if (!selectedKinds.length) { status.textContent = viewerText('viewer.lens.choose'); diff --git a/docs/diagrams/release-workflow.png b/docs/diagrams/release-workflow.png index c51528a..a87f5db 100644 Binary files a/docs/diagrams/release-workflow.png and b/docs/diagrams/release-workflow.png differ diff --git a/docs/diagrams/roadmap-dark.png b/docs/diagrams/roadmap-dark.png index 3a5d258..bb11b91 100644 Binary files a/docs/diagrams/roadmap-dark.png and b/docs/diagrams/roadmap-dark.png differ diff --git a/docs/diagrams/roadmap.html b/docs/diagrams/roadmap.html index 8ea6198..492126d 100644 --- a/docs/diagrams/roadmap.html +++ b/docs/diagrams/roadmap.html @@ -4,7 +4,7 @@ - Roadmap: mail today, PIM next Diagram + Roadmap: mail and PIM shipped Diagram + @@ -5800,190 +5799,6 @@

Parity target

return getComputedStyle(document.body).backgroundColor || '#ffffff'; } - function cleanExportClone(clone) { - // View transforms and neighborhood focus are HTML exploration state, - // never part of a downloaded full-diagram artifact. - clone.style.removeProperty('transform'); - clone.style.removeProperty('clip-path'); - clone.removeAttribute('data-view-scale'); - clone.removeAttribute('data-focus-active'); - clone.removeAttribute('data-reach-active'); - clone.removeAttribute('data-lens-active'); - clone.removeAttribute('data-lens-flow-count'); - clone.removeAttribute('data-lens-flow-density'); - clone.removeAttribute('data-legend-preview-active'); - clone.removeAttribute('data-relationship-preview-active'); - clone.removeAttribute('data-relationship-direct-active'); - clone.removeAttribute('data-relationship-pin-active'); - clone.removeAttribute('data-intent-trace-active'); - clone.removeAttribute('data-route-picking'); - clone.removeAttribute('data-route-active'); - clone.removeAttribute('data-route-journey'); - clone.removeAttribute('data-share-route'); - clone.removeAttribute('data-share-reach'); - clone.removeAttribute('data-story-active'); - clone.removeAttribute('data-story-playing'); - clone.removeAttribute('data-story-beat'); - clone.removeAttribute('data-story-next'); - clone.removeAttribute('data-story-follow'); - clone.removeAttribute('data-chapter-handoff'); - clone.removeAttribute('data-chapter-anchor'); - clone.removeAttribute('data-chapter-preview'); - Array.prototype.forEach.call(clone.querySelectorAll('[data-chapter-handoff-overlay]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-chapter-role]'), function (el) { - el.removeAttribute('data-chapter-role'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-chapter-preview-role]'), function (el) { - el.removeAttribute('data-chapter-preview-role'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-story-overlay], [data-story-carrier-overlay]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-intent-trace-overlay]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-route-probe-overlay]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-route-journey-overlay]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-semantic-lens-overlay]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-legend-bridge-runtime]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-legend-kind]'), function (el) { - el.removeAttribute('data-legend-kind'); - el.removeAttribute('data-legend-label'); - el.removeAttribute('data-legend-count'); - el.removeAttribute('data-legend-zero'); - el.removeAttribute('data-legend-selected'); - el.removeAttribute('role'); - el.removeAttribute('tabindex'); - el.removeAttribute('aria-label'); - el.removeAttribute('aria-pressed'); - el.removeAttribute('aria-haspopup'); - el.removeAttribute('aria-controls'); - el.removeAttribute('aria-expanded'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-legend-bridge]'), function (el) { - el.removeAttribute('data-legend-bridge'); - el.removeAttribute('role'); - el.removeAttribute('aria-label'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-relationship-pulse-overlay]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-relationship-hit-overlay]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-source-evidence-beacon]'), function (el) { - el.remove(); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-source-evidence-count]'), function (el) { - var originalLabel = el.getAttribute('data-source-evidence-original-label'); - if (originalLabel == null || originalLabel === '') el.removeAttribute('aria-label'); - else el.setAttribute('aria-label', originalLabel); - el.removeAttribute('data-source-evidence-count'); - el.removeAttribute('data-source-evidence-original-label'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-story-step], [data-story-beat-state], [data-story-beat-step]'), function (el) { - el.removeAttribute('data-story-step'); - el.removeAttribute('data-story-beat-state'); - el.removeAttribute('data-story-beat-step'); - el.style.removeProperty('--story-step'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-focus-match], [data-focus-selected]'), function (el) { - el.removeAttribute('data-focus-match'); - el.removeAttribute('data-focus-selected'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-reach-match], [data-reach-origin], [data-reach-depth]'), function (el) { - el.removeAttribute('data-reach-match'); - el.removeAttribute('data-reach-origin'); - el.removeAttribute('data-reach-depth'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-lens-match], [data-lens-selected], [data-lens-peer]'), function (el) { - el.removeAttribute('data-lens-match'); - el.removeAttribute('data-lens-selected'); - el.removeAttribute('data-lens-peer'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-legend-preview-match], [data-legend-preview-selected], [data-legend-preview-peer]'), function (el) { - el.removeAttribute('data-legend-preview-match'); - el.removeAttribute('data-legend-preview-selected'); - el.removeAttribute('data-legend-preview-peer'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-relationship-preview], [data-relationship-preview-node], [data-relationship-preview-source], [data-relationship-preview-target]'), function (el) { - el.removeAttribute('data-relationship-preview'); - el.removeAttribute('data-relationship-preview-node'); - el.removeAttribute('data-relationship-preview-source'); - el.removeAttribute('data-relationship-preview-target'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-intent-trace-match], [data-intent-trace-selected]'), function (el) { - el.removeAttribute('data-intent-trace-match'); - el.removeAttribute('data-intent-trace-selected'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-route-match], [data-route-start], [data-route-end], [data-route-step], [data-route-candidate], [data-route-journey-state], [data-route-journey-current]'), function (el) { - el.removeAttribute('data-route-match'); - el.removeAttribute('data-route-start'); - el.removeAttribute('data-route-end'); - el.removeAttribute('data-route-step'); - el.removeAttribute('data-route-candidate'); - el.removeAttribute('data-route-journey-state'); - el.removeAttribute('data-route-journey-current'); - el.style.removeProperty('--route-step'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-share-route-match], [data-share-route-step], [data-share-route-start], [data-share-route-end], [data-share-route-middle]'), function (el) { - el.removeAttribute('data-share-route-match'); - el.removeAttribute('data-share-route-step'); - el.removeAttribute('data-share-route-start'); - el.removeAttribute('data-share-route-end'); - el.removeAttribute('data-share-route-middle'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-share-reach-match], [data-share-reach-origin], [data-share-reach-depth]'), function (el) { - el.removeAttribute('data-share-reach-match'); - el.removeAttribute('data-share-reach-origin'); - el.removeAttribute('data-share-reach-depth'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-detail], [data-detail-anchor]'), function (el) { - el.removeAttribute('data-detail'); - el.removeAttribute('data-detail-anchor'); - }); - Array.prototype.forEach.call(clone.querySelectorAll('[data-node-id][aria-pressed]'), function (el) { - el.setAttribute('aria-pressed', 'false'); - }); - return !clone.hasAttribute('data-view-scale') && - !clone.hasAttribute('data-focus-active') && - !clone.hasAttribute('data-reach-active') && - !clone.hasAttribute('data-lens-active') && - !clone.hasAttribute('data-lens-flow-count') && - !clone.hasAttribute('data-lens-flow-density') && - !clone.hasAttribute('data-legend-preview-active') && - !clone.hasAttribute('data-relationship-preview-active') && - !clone.hasAttribute('data-relationship-direct-active') && - !clone.hasAttribute('data-intent-trace-active') && - !clone.hasAttribute('data-route-picking') && - !clone.hasAttribute('data-route-active') && - !clone.hasAttribute('data-route-journey') && - !clone.hasAttribute('data-share-route') && - !clone.hasAttribute('data-share-reach') && - !clone.hasAttribute('data-story-active') && - !clone.hasAttribute('data-story-playing') && - !clone.hasAttribute('data-story-beat') && - !clone.hasAttribute('data-story-next') && - !clone.hasAttribute('data-story-follow') && - !clone.hasAttribute('data-chapter-handoff') && - !clone.hasAttribute('data-chapter-anchor') && - !clone.hasAttribute('data-chapter-preview') && - !clone.style.getPropertyValue('transform') && - !clone.style.getPropertyValue('clip-path') && - clone.querySelectorAll('[data-story-overlay], [data-story-carrier-overlay], [data-story-carrier-token], [data-story-step], [data-story-beat-state], [data-story-beat-step], [data-chapter-handoff-overlay], [data-chapter-role], [data-chapter-preview-role], [data-focus-match], [data-focus-selected], [data-reach-match], [data-reach-origin], [data-reach-depth], [data-semantic-lens-overlay], [data-lens-match], [data-lens-selected], [data-lens-peer], [data-legend-bridge], [data-legend-kind], [data-legend-bridge-runtime], [data-legend-count], [data-legend-zero], [data-legend-selected], [data-legend-preview-match], [data-legend-preview-selected], [data-legend-preview-peer], [data-relationship-hit-overlay], [data-relationship-pulse-overlay], [data-relationship-preview], [data-relationship-preview-node], [data-relationship-preview-source], [data-relationship-preview-target], [data-intent-trace-overlay], [data-intent-trace-match], [data-intent-trace-selected], [data-route-probe-overlay], [data-route-journey-overlay], [data-route-match], [data-route-start], [data-route-end], [data-route-step], [data-route-candidate], [data-route-journey-state], [data-route-journey-current], [data-share-route-match], [data-share-route-step], [data-share-route-start], [data-share-route-end], [data-share-route-middle], [data-share-reach-match], [data-share-reach-origin], [data-share-reach-depth], [data-source-evidence-beacon], [data-source-evidence-count], [data-source-evidence-original-label], [data-detail], [data-detail-anchor]').length === 0; - } - - /** * Serialize the main SVG into a standalone document. * @@ -6169,7 +5984,186 @@

Parity target

var svg = document.querySelector('.diagram-container svg'); var clone = svg.cloneNode(true); - var canonicalStateClean = cleanExportClone(clone); + // View transforms and neighborhood focus are HTML exploration state, + // never part of a downloaded full-diagram artifact. + clone.style.removeProperty('transform'); + clone.style.removeProperty('clip-path'); + clone.removeAttribute('data-view-scale'); + clone.removeAttribute('data-focus-active'); + clone.removeAttribute('data-reach-active'); + clone.removeAttribute('data-lens-active'); + clone.removeAttribute('data-lens-flow-count'); + clone.removeAttribute('data-lens-flow-density'); + clone.removeAttribute('data-legend-preview-active'); + clone.removeAttribute('data-relationship-preview-active'); + clone.removeAttribute('data-relationship-direct-active'); + clone.removeAttribute('data-relationship-pin-active'); + clone.removeAttribute('data-intent-trace-active'); + clone.removeAttribute('data-route-picking'); + clone.removeAttribute('data-route-active'); + clone.removeAttribute('data-route-journey'); + clone.removeAttribute('data-share-route'); + clone.removeAttribute('data-share-reach'); + clone.removeAttribute('data-story-active'); + clone.removeAttribute('data-story-playing'); + clone.removeAttribute('data-story-beat'); + clone.removeAttribute('data-story-next'); + clone.removeAttribute('data-story-follow'); + clone.removeAttribute('data-chapter-handoff'); + clone.removeAttribute('data-chapter-anchor'); + clone.removeAttribute('data-chapter-preview'); + Array.prototype.forEach.call(clone.querySelectorAll('[data-chapter-handoff-overlay]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-chapter-role]'), function (el) { + el.removeAttribute('data-chapter-role'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-chapter-preview-role]'), function (el) { + el.removeAttribute('data-chapter-preview-role'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-story-overlay], [data-story-carrier-overlay]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-intent-trace-overlay]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-route-probe-overlay]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-route-journey-overlay]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-semantic-lens-overlay]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-legend-bridge-runtime]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-legend-kind]'), function (el) { + el.removeAttribute('data-legend-kind'); + el.removeAttribute('data-legend-label'); + el.removeAttribute('data-legend-count'); + el.removeAttribute('data-legend-zero'); + el.removeAttribute('data-legend-selected'); + el.removeAttribute('role'); + el.removeAttribute('tabindex'); + el.removeAttribute('aria-label'); + el.removeAttribute('aria-pressed'); + el.removeAttribute('aria-haspopup'); + el.removeAttribute('aria-controls'); + el.removeAttribute('aria-expanded'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-legend-bridge]'), function (el) { + el.removeAttribute('data-legend-bridge'); + el.removeAttribute('role'); + el.removeAttribute('aria-label'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-relationship-pulse-overlay]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-relationship-hit-overlay]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-source-evidence-beacon]'), function (el) { + el.remove(); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-source-evidence-count]'), function (el) { + var originalLabel = el.getAttribute('data-source-evidence-original-label'); + if (originalLabel == null || originalLabel === '') el.removeAttribute('aria-label'); + else el.setAttribute('aria-label', originalLabel); + el.removeAttribute('data-source-evidence-count'); + el.removeAttribute('data-source-evidence-original-label'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-story-step], [data-story-beat-state], [data-story-beat-step]'), function (el) { + el.removeAttribute('data-story-step'); + el.removeAttribute('data-story-beat-state'); + el.removeAttribute('data-story-beat-step'); + el.style.removeProperty('--story-step'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-focus-match], [data-focus-selected]'), function (el) { + el.removeAttribute('data-focus-match'); + el.removeAttribute('data-focus-selected'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-reach-match], [data-reach-origin], [data-reach-depth]'), function (el) { + el.removeAttribute('data-reach-match'); + el.removeAttribute('data-reach-origin'); + el.removeAttribute('data-reach-depth'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-lens-match], [data-lens-selected], [data-lens-peer]'), function (el) { + el.removeAttribute('data-lens-match'); + el.removeAttribute('data-lens-selected'); + el.removeAttribute('data-lens-peer'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-legend-preview-match], [data-legend-preview-selected], [data-legend-preview-peer]'), function (el) { + el.removeAttribute('data-legend-preview-match'); + el.removeAttribute('data-legend-preview-selected'); + el.removeAttribute('data-legend-preview-peer'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-relationship-preview], [data-relationship-preview-node], [data-relationship-preview-source], [data-relationship-preview-target]'), function (el) { + el.removeAttribute('data-relationship-preview'); + el.removeAttribute('data-relationship-preview-node'); + el.removeAttribute('data-relationship-preview-source'); + el.removeAttribute('data-relationship-preview-target'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-intent-trace-match], [data-intent-trace-selected]'), function (el) { + el.removeAttribute('data-intent-trace-match'); + el.removeAttribute('data-intent-trace-selected'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-route-match], [data-route-start], [data-route-end], [data-route-step], [data-route-candidate], [data-route-journey-state], [data-route-journey-current]'), function (el) { + el.removeAttribute('data-route-match'); + el.removeAttribute('data-route-start'); + el.removeAttribute('data-route-end'); + el.removeAttribute('data-route-step'); + el.removeAttribute('data-route-candidate'); + el.removeAttribute('data-route-journey-state'); + el.removeAttribute('data-route-journey-current'); + el.style.removeProperty('--route-step'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-share-route-match], [data-share-route-step], [data-share-route-start], [data-share-route-end], [data-share-route-middle]'), function (el) { + el.removeAttribute('data-share-route-match'); + el.removeAttribute('data-share-route-step'); + el.removeAttribute('data-share-route-start'); + el.removeAttribute('data-share-route-end'); + el.removeAttribute('data-share-route-middle'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-share-reach-match], [data-share-reach-origin], [data-share-reach-depth]'), function (el) { + el.removeAttribute('data-share-reach-match'); + el.removeAttribute('data-share-reach-origin'); + el.removeAttribute('data-share-reach-depth'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-detail], [data-detail-anchor]'), function (el) { + el.removeAttribute('data-detail'); + el.removeAttribute('data-detail-anchor'); + }); + Array.prototype.forEach.call(clone.querySelectorAll('[data-node-id][aria-pressed]'), function (el) { + el.setAttribute('aria-pressed', 'false'); + }); + var canonicalStateClean = !clone.hasAttribute('data-view-scale') && + !clone.hasAttribute('data-focus-active') && + !clone.hasAttribute('data-reach-active') && + !clone.hasAttribute('data-lens-active') && + !clone.hasAttribute('data-lens-flow-count') && + !clone.hasAttribute('data-lens-flow-density') && + !clone.hasAttribute('data-legend-preview-active') && + !clone.hasAttribute('data-relationship-preview-active') && + !clone.hasAttribute('data-relationship-direct-active') && + !clone.hasAttribute('data-intent-trace-active') && + !clone.hasAttribute('data-route-picking') && + !clone.hasAttribute('data-route-active') && + !clone.hasAttribute('data-route-journey') && + !clone.hasAttribute('data-share-route') && + !clone.hasAttribute('data-share-reach') && + !clone.hasAttribute('data-story-active') && + !clone.hasAttribute('data-story-playing') && + !clone.hasAttribute('data-story-beat') && + !clone.hasAttribute('data-story-next') && + !clone.hasAttribute('data-story-follow') && + !clone.hasAttribute('data-chapter-handoff') && + !clone.hasAttribute('data-chapter-anchor') && + !clone.hasAttribute('data-chapter-preview') && + !clone.style.getPropertyValue('transform') && + !clone.style.getPropertyValue('clip-path') && + clone.querySelectorAll('[data-story-overlay], [data-story-carrier-overlay], [data-story-carrier-token], [data-story-step], [data-story-beat-state], [data-story-beat-step], [data-chapter-handoff-overlay], [data-chapter-role], [data-chapter-preview-role], [data-focus-match], [data-focus-selected], [data-reach-match], [data-reach-origin], [data-reach-depth], [data-semantic-lens-overlay], [data-lens-match], [data-lens-selected], [data-lens-peer], [data-legend-bridge], [data-legend-kind], [data-legend-bridge-runtime], [data-legend-count], [data-legend-zero], [data-legend-selected], [data-legend-preview-match], [data-legend-preview-selected], [data-legend-preview-peer], [data-relationship-hit-overlay], [data-relationship-pulse-overlay], [data-relationship-preview], [data-relationship-preview-node], [data-relationship-preview-source], [data-relationship-preview-target], [data-intent-trace-overlay], [data-intent-trace-match], [data-intent-trace-selected], [data-route-probe-overlay], [data-route-journey-overlay], [data-route-match], [data-route-start], [data-route-end], [data-route-step], [data-route-candidate], [data-route-journey-state], [data-route-journey-current], [data-share-route-match], [data-share-route-step], [data-share-route-start], [data-share-route-end], [data-share-route-middle], [data-share-reach-match], [data-share-reach-origin], [data-share-reach-depth], [data-source-evidence-beacon], [data-source-evidence-count], [data-source-evidence-original-label], [data-detail], [data-detail-anchor]').length === 0; var vb = svg.viewBox.baseVal; var finiteSvgDimensions = Number.isFinite(vb.x) && Number.isFinite(vb.y) && @@ -6315,11 +6309,8 @@

Parity target

clone.insertBefore(style, clone.firstChild); clone.insertBefore(bgRect, style.nextSibling); - // The XML declaration pins UTF-8: without it, consumers that guess an - // encoding instead of defaulting to UTF-8 mangle non-ASCII text. return { - svgString: '\n' + - new XMLSerializer().serializeToString(clone), + svgString: new XMLSerializer().serializeToString(clone), width: vb.width * scale, height: vb.height * scale, canonicalStateClean: canonicalStateClean, @@ -7969,8 +7960,8 @@

Parity target

} function relationshipHitRecords() { var recordsByKey = {}; - var recordsById = Object.create(null); - var byId = Object.create(null); + var recordsById = {}; + var byId = {}; nodes().forEach(function (node) { byId[node.getAttribute('data-node-id')] = node; }); var records = []; edges().forEach(function (edge) { @@ -8720,7 +8711,7 @@

Parity target

Archify.routeProbe.clear({ updateUrl: false, restoreFocus: false }); } var nodeList = nodes(); - var byId = Object.create(null); + var byId = {}; nodeList.forEach(function (node) { byId[node.getAttribute('data-node-id')] = node; }); var normalized = []; (ids || []).forEach(function (id) { @@ -8734,8 +8725,8 @@

Parity target

clear({ updateUrl: false, preserveView: true }); activeIds = normalized; - var selected = Object.create(null); - var related = Object.create(null); + var selected = {}; + var related = {}; var seenEdges = {}; var matchedEdges = 0; normalized.forEach(function (id) { selected[id] = true; related[id] = true; }); @@ -9086,7 +9077,7 @@

Parity target

clear({ announce: false }); var nodeList = nodes(); var edgeList = edges(); - var byId = Object.create(null); + var byId = {}; nodeList.forEach(function (node) { byId[node.getAttribute('data-node-id')] = node; }); var selected = byId[id]; if (!selected) return false; @@ -9095,7 +9086,7 @@

Parity target

overlay.setAttribute('class', 'intent-trace-overlay'); overlay.setAttribute('data-intent-trace-overlay', ''); overlay.setAttribute('aria-hidden', 'true'); - var related = Object.create(null); + var related = {}; var seen = {}; var counts = { out: 0, in: 0, loop: 0 }; related[id] = true; @@ -9266,12 +9257,12 @@

Parity target

try { views = JSON.parse(data.textContent || '[]'); } catch (_) { views = []; } if (!views.length) return { count: 0, active: function () { return null; } }; - var canonicalNodeIds = Object.create(null); + var canonicalNodeIds = {}; Array.prototype.forEach.call(svg.querySelectorAll('[data-node-id]'), function (node) { canonicalNodeIds[node.getAttribute('data-node-id')] = true; }); views.forEach(function (view) { - var seen = Object.create(null); + var seen = {}; view.focus = (view.focus || []).filter(function (id) { if (!canonicalNodeIds[id] || seen[id]) return false; seen[id] = true; @@ -9300,8 +9291,8 @@

Parity target

function chapterDelta(previous, destination) { var previousFocus = previous ? previous.focus : []; var destinationFocus = destination ? destination.focus : []; - var previousIds = Object.create(null); - var destinationIds = Object.create(null); + var previousIds = {}; + var destinationIds = {}; previousFocus.forEach(function (id) { previousIds[id] = true; }); destinationFocus.forEach(function (id) { destinationIds[id] = true; }); return { @@ -9313,7 +9304,7 @@

Parity target

function chapterAnchor(previous, destination, outgoingBeatIndex, delta) { if (!previous || !destination) return ''; - var stayIds = Object.create(null); + var stayIds = {}; delta.stay.forEach(function (id) { stayIds[id] = true; }); var activeBeat = outgoingBeatIndex >= 0 ? previous.focus[outgoingBeatIndex] : ''; if (activeBeat && stayIds[activeBeat]) return activeBeat; @@ -9331,7 +9322,7 @@

Parity target

} function classifyHandoff(delta, anchor) { - var roles = Object.create(null); + var roles = {}; delta.stay.forEach(function (id) { roles[id] = 'stay'; }); delta.enter.forEach(function (id) { roles[id] = 'enter'; }); delta.leave.forEach(function (id) { roles[id] = 'leave'; }); @@ -9648,7 +9639,7 @@

Parity target

if (activePreviewIndex === index && svg.getAttribute('data-chapter-preview') === views[index].id) return true; clearChapterPreviewPresentation(); var delta = chapterDelta(activeIndex >= 0 ? views[activeIndex] : null, views[index]); - var roles = Object.create(null); + var roles = {}; delta.stay.forEach(function (id) { roles[id] = 'stay'; }); delta.enter.forEach(function (id) { roles[id] = 'enter'; }); delta.leave.forEach(function (id) { roles[id] = 'leave'; }); @@ -10112,9 +10103,7 @@

Parity target

function centerStoryStop(stop) { if (!stop || !trail) return false; - var bounds = stop.getBoundingClientRect(); - var viewportLeft = trail.getBoundingClientRect().left + trail.clientLeft; - var target = Math.max(0, trail.scrollLeft + bounds.left - viewportLeft - (trail.clientWidth - bounds.width) / 2); + var target = Math.max(0, stop.offsetLeft - (trail.clientWidth - stop.offsetWidth) / 2); trail.scrollLeft = target; return true; } @@ -10302,7 +10291,7 @@

Parity target

var nodeList = Array.prototype.slice.call(svg.querySelectorAll('[data-node-id]')); var edgeList = Array.prototype.slice.call(svg.querySelectorAll('[data-edge-from][data-edge-to]')); - var byId = Object.create(null); + var byId = {}; nodeList.forEach(function (node) { byId[node.getAttribute('data-node-id')] = node; }); storySteps = view.focus.map(function (_, index) { return storyStep(view, index, edgeList, byId); }); var labels = []; @@ -10864,22 +10853,6 @@

Parity target

}); storyMotionObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-motion'] }); } - if (typeof ResizeObserver === 'function') { - var chapterViewportWidth = chapterList.clientWidth; - var trailViewportWidth = trail.clientWidth; - var storyScrollObserver = new ResizeObserver(function () { - var chapterWidth = chapterList.clientWidth; - var trailWidth = trail.clientWidth; - if (chapterWidth !== chapterViewportWidth && activeIndex >= 0) centerChapterButton(chapterButtons[activeIndex]); - if (trailWidth !== trailViewportWidth && storyBeatIndex >= 0) { - centerStoryStop(trail.querySelectorAll('[data-story-node]')[storyBeatIndex]); - } - chapterViewportWidth = chapterWidth; - trailViewportWidth = trailWidth; - }); - storyScrollObserver.observe(chapterList); - storyScrollObserver.observe(trail); - } window.addEventListener('beforeprint', function () { clearChapterPreview({ clearIntents: true }); if (playing) pausePlayback(); @@ -11753,8 +11726,8 @@

Parity target

return true; } function semanticIds(ids, includeNeighbors) { - var seeds = Object.create(null); - var wanted = Object.create(null); + var seeds = {}; + var wanted = {}; (ids || []).forEach(function (id) { seeds[id] = true; wanted[id] = true; }); if (includeNeighbors) { Array.prototype.forEach.call(svg.querySelectorAll('[data-edge-from][data-edge-to]'), function (edge) { @@ -13222,7 +13195,7 @@

Parity target

} function outgoingByNode() { var byId = nodesById(); - var outgoing = Object.create(null); + var outgoing = {}; edges().forEach(function (edge) { var from = edge.getAttribute('data-edge-from'); var to = edge.getAttribute('data-edge-to'); @@ -13234,7 +13207,7 @@

Parity target

} function reachableFrom(source) { var outgoing = outgoingByNode(); - var reached = Object.create(null); + var reached = {}; var queue = [source]; reached[source] = true; for (var cursor = 0; cursor < queue.length; cursor += 1) { @@ -13249,7 +13222,7 @@

Parity target

} function hopDistancesFrom(source) { var outgoing = outgoingByNode(); - var distances = Object.create(null); + var distances = {}; var queue = [source]; distances[source] = 0; for (var cursor = 0; cursor < queue.length; cursor += 1) { @@ -13265,7 +13238,7 @@

Parity target

function shortestDirectedPath(source, target) { if (source === target) return null; var outgoing = outgoingByNode(); - var previous = Object.create(null); + var previous = {}; var queue = [source]; previous[source] = null; for (var cursor = 0; cursor < queue.length && !Object.prototype.hasOwnProperty.call(previous, target); cursor += 1) { @@ -13795,7 +13768,7 @@

Parity target

var sourceIds = nodes().map(function (node) { return node.getAttribute('data-node-id'); }).filter(function (id) { return outgoing[id] && outgoing[id].length; }); - var sourceBadges = Object.create(null); + var sourceBadges = {}; sourceIds.forEach(function (id) { sourceBadges[id] = viewerText('viewer.route.finder.source.badge'); }); return { kind: 'route-source', @@ -13811,7 +13784,7 @@

Parity target

if (mode === 'target' && startId && byId[startId]) { var distances = hopDistancesFrom(startId); var targetIds = Object.keys(distances).filter(function (id) { return id !== startId && byId[id]; }); - var targetBadges = Object.create(null); + var targetBadges = {}; targetIds.forEach(function (id) { targetBadges[id] = viewerCount('viewer.route.hop', distances[id]); }); @@ -14020,7 +13993,7 @@

Parity target

var MAX_LENS_FLOW_EDGES = 24; function nodesById() { - var byId = Object.create(null); + var byId = {}; Array.prototype.forEach.call(svg.querySelectorAll('[data-node-id][data-node-kind]'), function (node) { var id = node.getAttribute('data-node-id'); if (id && !byId[id]) byId[id] = node; @@ -14187,7 +14160,7 @@

Parity target

return (byId[id].getAttribute('data-node-kind') || 'neutral') === kind; }); if (!matches.length) return false; - var chosen = Object.create(null); + var chosen = {}; matches.forEach(function (id) { chosen[id] = true; byId[id].setAttribute('data-legend-preview-match', ''); @@ -14363,7 +14336,7 @@

Parity target

options = options || {}; cleanSvgState(); var byId = nodesById(); - var nodeKind = Object.create(null); + var nodeKind = {}; Object.keys(byId).forEach(function (id) { nodeKind[id] = byId[id].getAttribute('data-node-kind') || 'neutral'; }); if (!selectedKinds.length) { status.textContent = viewerText('viewer.lens.choose'); diff --git a/docs/diagrams/roadmap.png b/docs/diagrams/roadmap.png index 7e89547..6090a84 100644 Binary files a/docs/diagrams/roadmap.png and b/docs/diagrams/roadmap.png differ diff --git a/docs/diagrams/src/access-control.json b/docs/diagrams/src/access-control.json index 5587053..c4c86ca 100644 --- a/docs/diagrams/src/access-control.json +++ b/docs/diagrams/src/access-control.json @@ -115,7 +115,7 @@ "id": "handler", "type": "backend", "label": "Handler", - "sublabel": "messenger.* call", + "sublabel": "mail / PIM call", "stage": 4, "row": 0, "tag": "allowed", @@ -128,7 +128,7 @@ "sublabel": "off / unclassified", "stage": 4, "row": 2, - "tag": "no mail access", + "tag": "no data access", "width": 136 } ], @@ -199,7 +199,7 @@ "dot": "emerald", "title": "Always available", "items": [ - "Read routes (search, list, read, stats, sync) have no switch", + "Read routes (search, list, read, calendar/task/contact lists) have no switch", "tb access shows the loaded policy" ] }, @@ -207,8 +207,8 @@ "dot": "amber", "title": "Switches", "items": [ - "compose, send, move, copy, archive, mark, tag, folder ops default on", - "delete and folderDelete default off (ODIAA-2304)" + "16 switches; mail writes (compose, send, move, tag, folder ops, ...) default on", + "delete, folderDelete, contactsWrite, calendarWrite, tasksWrite default off" ] }, { diff --git a/docs/diagrams/src/architecture.json b/docs/diagrams/src/architecture.json index 9fbffe6..78c3e6e 100644 --- a/docs/diagrams/src/architecture.json +++ b/docs/diagrams/src/architecture.json @@ -2,15 +2,15 @@ "schema_version": 1, "diagram_type": "architecture", "meta": { - "title": "thunderbird-cli-enhanced — System Architecture", + "title": "Thunderbird CLI Enhanced — System Architecture", "subtitle": "Every request stays on 127.0.0.1; credentials never leave Thunderbird", "quality_profile": "showcase", "views": [ { "id": "request-path", "label": "Request path", - "focus": ["agent", "mcp", "cli", "bridge", "ext", "accounts"], - "note": "Follow a CLI command or MCP tool call from the caller to the mailbox." + "focus": ["agent", "mcp", "cli", "bridge", "ext", "accounts", "notes"], + "note": "Follow a CLI command or MCP tool call from the caller to the mailbox, or to the local notes workspace." }, { "id": "policy-gate", @@ -23,15 +23,16 @@ "components": [ { "id": "agent", "type": "external", "label": "AI agent", "sublabel": "Claude Desktop / Code", "pos": [40, 150], "size": [150, 64] }, { "id": "shell", "type": "external", "label": "Shell / scripts", "sublabel": "terminal, cron, CI", "pos": [40, 350], "size": [150, 64] }, - { "id": "mcp", "type": "frontend", "label": "tb-mcp", "sublabel": "13 MCP tools (stdio)", "pos": [280, 150], "size": [160, 64] }, - { "id": "cli", "type": "frontend", "label": "tb CLI", "sublabel": "41 commands, JSON out", "pos": [280, 350], "size": [160, 64] }, + { "id": "mcp", "type": "frontend", "label": "tb-mcp", "sublabel": "42 MCP tools (stdio)", "pos": [280, 150], "size": [160, 64] }, + { "id": "cli", "type": "frontend", "label": "tb CLI", "sublabel": "74 commands, JSON out", "pos": [280, 350], "size": [160, 64] }, { "id": "bridge", "type": "backend", "label": "Bridge daemon", "sublabel": "HTTP :7700 / WS :7701", "pos": [530, 250], "size": [170, 64], "tag": "auto-start (#13)" }, - { "id": "ext", "type": "backend", "label": "WebExtension", "sublabel": "route handlers", "pos": [790, 250], "size": [160, 64] }, - { "id": "policy", "type": "security", "label": "Access policy", "sublabel": "build-time switches", "pos": [790, 80], "size": [160, 64], "tag": "fail closed" }, - { "id": "accounts", "type": "database", "label": "Accounts + folders", "sublabel": "IMAP / POP / local", "pos": [790, 420], "size": [160, 64] } + { "id": "ext", "type": "backend", "label": "WebExtension", "sublabel": "handlers + calendar APIs", "pos": [790, 250], "size": [160, 64] }, + { "id": "policy", "type": "security", "label": "Access policy", "sublabel": "16 build-time switches", "pos": [790, 80], "size": [160, 64], "tag": "fail closed" }, + { "id": "accounts", "type": "database", "label": "Mail + PIM data", "sublabel": "calendars, tasks, contacts", "pos": [790, 420], "size": [160, 64] }, + { "id": "notes", "type": "database", "label": "Notes workspace", "sublabel": "local Markdown files", "pos": [530, 470], "size": [170, 64], "tag": "never via bridge" } ], "boundaries": [ - { "kind": "region", "label": "localhost (127.0.0.1)", "wraps": ["mcp", "cli", "bridge", "ext", "policy", "accounts"] }, + { "kind": "region", "label": "localhost (127.0.0.1)", "wraps": ["mcp", "cli", "bridge", "ext", "policy", "accounts", "notes"] }, { "kind": "security-group", "label": "Thunderbird process", "wraps": ["ext", "policy", "accounts"] } ], "connections": [ @@ -41,11 +42,12 @@ { "id": "cli-bridge", "from": "cli", "to": "bridge", "label": "HTTP + bearer" }, { "id": "bridge-ext", "from": "bridge", "to": "ext", "label": "WebSocket", "variant": "emphasis" }, { "id": "ext-policy", "from": "ext", "to": "policy", "label": "classify route", "variant": "security" }, - { "id": "ext-accounts", "from": "ext", "to": "accounts", "label": "messenger.* APIs", "variant": "emphasis", "labelDy": 24 } + { "id": "ext-accounts", "from": "ext", "to": "accounts", "label": "messenger.* APIs", "variant": "emphasis", "labelDy": 24 }, + { "id": "cli-notes", "from": "cli", "to": "notes", "label": "fs read/write" } ], "cards": [ - { "dot": "cyan", "title": "Entry points", "items": ["tb CLI for shells and scripts", "tb-mcp for MCP clients such as Claude Desktop", "Both spawn the bridge on first use if it is down"] }, + { "dot": "cyan", "title": "Entry points", "items": ["tb CLI for shells and scripts", "tb-mcp for MCP clients such as Claude Desktop", "Both spawn the bridge on first use if it is down", "Notes live on disk; the CLI and MCP server read them directly"] }, { "dot": "emerald", "title": "Bridge", "items": ["Stateless HTTP to WebSocket proxy", "Bearer token checked with timingSafeEqual", "Ping/pong drops dead extension sockets"] }, - { "dot": "rose", "title": "Thunderbird", "items": ["Owns all credentials and sync state", "Unclassified routes are refused", "delete and folderDelete are off by default"] } + { "dot": "rose", "title": "Thunderbird", "items": ["Owns all credentials, mail, calendars, tasks and contacts", "Calendar and tasks via Experiment APIs (calendar_*)", "Unclassified routes are refused", "delete, folderDelete and the contacts/calendar/tasks write switches are off by default"] } ] } diff --git a/docs/diagrams/src/release-workflow.json b/docs/diagrams/src/release-workflow.json index 6a403ab..7519202 100644 --- a/docs/diagrams/src/release-workflow.json +++ b/docs/diagrams/src/release-workflow.json @@ -3,7 +3,7 @@ "diagram_type": "workflow", "meta": { "title": "Signed XPI release workflow", - "subtitle": "sign-xpi.yml gets the add-on approved by addons.thunderbird.net; release.yml publishes it", + "subtitle": "sign-xpi.yml gets the add-on approved by addons.thunderbird.net; release.yml publishes it to GitHub and npm", "quality_profile": "showcase", "views": [ { @@ -26,7 +26,7 @@ "tag", "release" ], - "note": "A version tag attaches the committed signed XPI to a GitHub Release." + "note": "A version tag attaches the highest committed signed XPI (plus the unsigned build) to a GitHub Release, then publishes the npm package." }, { "id": "failures", @@ -38,7 +38,7 @@ "resume", "commit" ], - "note": "A red gate stops the run; a slow ATN review is resumed by re-running." + "note": "A red gate stops the run; an ATN manual review (Experiment APIs) is resumed by re-running." } ] }, @@ -141,7 +141,7 @@ "col": 3, "type": "security", "label": "ATN review", - "sublabel": "validate + sign", + "sublabel": "validate + manual review", "tag": "polled", "width": 140 }, @@ -170,7 +170,7 @@ "col": 5, "type": "cloud", "label": "GitHub Release", - "sublabel": "tests + signed XPI", + "sublabel": "XPIs, then npm (OIDC)", "width": 140 }, { @@ -245,7 +245,7 @@ "id": "review-resume", "from": "review", "to": "resume", - "label": "timeout", + "label": "still in review", "variant": "security", "role": "error" } @@ -256,7 +256,8 @@ "title": "Gate", "items": [ "Same gate as the pre-push hook: npm run verify", - "Signing needs MOZILLA_HUB_JWT_* repository secrets" + "Signing needs MOZILLA_HUB_JWT_* repository secrets", + "Experiment APIs (calendar_*) need an ATN manual review" ] }, { @@ -272,7 +273,8 @@ "title": "Publish", "items": [ "release.yml fails if no *-tb.xpi is committed", - "Unsigned build is attached alongside for reference" + "Attaches the highest signed *-tb.xpi plus the unsigned build", + "npm trusted publishing (OIDC) on Node 22, no stored token" ] } ] diff --git a/docs/diagrams/src/roadmap.json b/docs/diagrams/src/roadmap.json index abb0ff3..38966d5 100644 --- a/docs/diagrams/src/roadmap.json +++ b/docs/diagrams/src/roadmap.json @@ -2,8 +2,8 @@ "schema_version": 1, "diagram_type": "lifecycle", "meta": { - "title": "Roadmap: mail today, PIM next", - "subtitle": "Phases 01–03 are on main; 04–05 and the parity target are planned (docs/PLAN.md)", + "title": "Roadmap: mail and PIM shipped", + "subtitle": "Phases 01–05 shipped in v1.3.0; the stability pass and atbridge parity remain (docs/PLAN.md)", "viewBox": [ 980, 680 @@ -17,20 +17,20 @@ "baseline", "integration", "mail", - "stability" + "calendar", + "notes" ], - "note": "Upstream baseline plus integrated fork work, all merged on main." + "note": "Upstream baseline plus fork work, mail, calendar, contacts, notes, tasks, skills and Fast Actions, all released in v1.3.0." }, { "id": "planned", "label": "Planned", "focus": [ - "mail", - "calendar", "notes", + "stability", "parity" ], - "note": "Calendar, contacts, notes and tasks (ODIAA-2306), then atbridge.ai feature parity." + "note": "An extension stability pass, then the remaining atbridge.ai parity gaps." } ] }, @@ -72,50 +72,50 @@ { "id": "mail", "type": "active", - "label": "Mail today", + "label": "Mail", "sublabel": "CLI + MCP", "lane": "main", "col": 2, "step": "03", - "tag": "you are here" + "tag": "shipped" }, { "id": "calendar", - "type": "waiting", + "type": "active", "label": "Calendar", "sublabel": "+ contacts write", "lane": "main", "col": 3, "step": "04", - "tag": "planned" + "tag": "shipped" }, { "id": "notes", - "type": "waiting", + "type": "active", "label": "Notes, tasks", - "sublabel": "ODIAA-2306", + "sublabel": "skills, Fast Actions", "lane": "main", "col": 4, "step": "05", - "tag": "planned" + "tag": "v1.3.0 · you are here" }, { "id": "stability", - "type": "active", + "type": "waiting", "label": "Stability pass", "sublabel": "extension audit", "lane": "hardening", "col": 0, - "tag": "in progress" + "tag": "planned" }, { "id": "parity", "type": "success", "label": "atbridge parity", - "sublabel": "actions + skills", + "sublabel": "remaining gaps", "lane": "terminal", "col": 2, - "tag": "goal" + "tag": "planned" } ], "transitions": [ @@ -145,27 +145,26 @@ "cards": [ { "dot": "emerald", - "title": "Shipped on main", + "title": "Shipped in v1.3.0", "items": [ - "41 CLI commands, 13 MCP tools", - "Access policy, signed XPI CI, auto-start, server-side search", - "extension-reload, opt-in v2 output, read-only contacts" + "74 CLI commands, 42 MCP tools", + "Calendar with clash detection, tasks, contacts write, notes + transcription", + "Deterministic skills, Fast Actions (email to note/task/event/contact)" ] }, { "dot": "amber", - "title": "Planned (ODIAA-2306)", + "title": "Planned", "items": [ - "Calendar events with clash detection", - "Contact create/update, notes, tasks" + "Extension stability pass with regression tests", + "Remaining atbridge.ai parity items (docs/PLAN.md)" ] }, { "dot": "cyan", "title": "Parity target", "items": [ - "Email to note/task/event/contact actions", - "Deterministic slash-command skills", + "Close the remaining atbridge.ai gaps", "Writes stay opt-in and local-first" ] } diff --git a/mcp/README.md b/mcp/README.md index 838ad86..608e3ab 100644 --- a/mcp/README.md +++ b/mcp/README.md @@ -11,7 +11,9 @@ Part of [thunderbird-cli-enhanced](https://github.com/odience-network/thunderbir ## What it does -Exposes 13 email management tools to Claude Desktop: +Exposes 42 tools to Claude Desktop (and any other MCP client): + +**Email (14)** | Tool | Description | |---|---| @@ -28,8 +30,67 @@ Exposes 13 email management tools to Claude Desktop: | `email_archive` | Archive, move, or delete messages | | `email_attachments` | List + download attachments (base64) | | `email_folders` | List folders, get info, trigger sync | +| `email_action_items` | Extract candidate action items from a message as a Markdown checklist (deterministic, no LLM) | + +**Contacts (4)** + +| Tool | Description | +|---|---| +| `contact_search` | Search or list contacts across all address books | +| `address_book_list` | List address books (id, name) — pick a target for `contact_create` / `email_to_contact` | +| `contact_create` | Create a contact (requires `contactsWrite`) | +| `contact_update` | Update contact properties by id (requires `contactsWrite`) | + +**Notes (7)** — local Markdown workspace, no Thunderbird round-trip except where noted + +| Tool | Description | +|---|---| +| `note_list` | List notes (title, created, source message, size) | +| `note_read` | Read a note's Markdown body and metadata ("Use as Context") | +| `note_save` | Save/overwrite a note ("Save to Notes") | +| `note_append` | Append to a note, creating it if missing | +| `note_transcribe` | Transcribe a local audio file with a local STT engine (whisper.cpp / faster-whisper) into a note | +| `note_to_draft` | Render a note to HTML and open it as a new email draft (default: draft) | +| `notes_listen_once` | Wait for a "Save to Notes" click in Thunderbird, then save the note | + +**Tasks (3)** + +| Tool | Description | +|---|---| +| `task_list` | List calendar tasks (VTODO), per calendar or by completion state | +| `task_create` | Create a task (requires `tasksWrite`) | +| `task_update` | Update task properties by id (requires `tasksWrite`) | + +**Calendar (6)** -**Safe defaults:** compose/reply/forward/edit all default to **draft mode**. Claude must explicitly pass `mode: "send"` to actually send anything. Permanent delete requires `confirm: true`. +| Tool | Description | +|---|---| +| `calendar_list` | List calendars (id, name, type, read-only/enabled, color) | +| `calendar_events` | List events in a date range; recurring events expanded | +| `calendar_event_create` | Create an event (requires `calendarWrite`) | +| `calendar_event_update` | Update event properties by id (requires `calendarWrite`) | +| `calendar_event_delete` | Delete an event by id (requires `calendarWrite`) | +| `calendar_clashes` | Detect overlapping events across all calendars (DST- and all-day-aware) | + +**Skills (4)** — deterministic, return ready-to-show Markdown + +| Tool | Description | +|---|---| +| `skill_today` | Today's events plus unread/flagged counts | +| `skill_week` | Next 7 days of events, grouped by day | +| `skill_clashes` | Overlapping events in the next N days (default 7) | +| `skill_from` | Recent mail from a sender address or domain, grouped into threads | + +**Fast Actions (4)** + +| Tool | Description | +|---|---| +| `email_to_note` | Save an email to the notes workspace (no access switch) | +| `email_to_task` | Create a task from an email via action-item extraction (requires `tasksWrite`) | +| `email_to_event` | Create an event from an email via deterministic date/time/location parsing (requires `calendarWrite`) | +| `email_to_contact` | Add the sender as a contact, deduped by address (requires `contactsWrite`) | + +**Safe defaults:** compose/reply/forward/edit all default to **draft mode**. Claude must explicitly pass `mode: "send"` to actually send anything; `note_to_draft` has no send mode at all. Permanent delete requires `confirm: true`, and `delete` is refused unless the add-on was built with `delete: true`. Contact, calendar and task writes are gated behind the `contactsWrite`, `calendarWrite` and `tasksWrite` access switches, all **off** by default — see [ACCESS-CONTROL.md](https://github.com/odience-network/thunderbird-cli-enhanced/blob/main/docs/ACCESS-CONTROL.md). Calendar and task tools need Thunderbird's calendar Experiment APIs, which ship in add-on 2.4.0+. ## Architecture @@ -127,6 +188,9 @@ Then in Claude Desktop config: |---|---| | macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` | | Windows | `%APPDATA%\Claude\claude_desktop_config.json` | +| Linux | `~/.config/Claude/claude_desktop_config.json` | + +After editing, **restart Claude Desktop**. You should see "thunderbird" in the MCP servers list when you click the tool icon. ## Recommended: also install the companion skill @@ -154,9 +218,6 @@ cd skills && zip -r thunderbird-cli.zip thunderbird-cli Full skill docs: [`skills/thunderbird-cli/SKILL.md`](https://github.com/odience-network/thunderbird-cli-enhanced/blob/main/skills/thunderbird-cli/SKILL.md). Skipping the skill is fine — the MCP still works — but with it, Claude's defaults get noticeably safer (never-auto-send, junk-excluded, truncated bodies) without the user having to re-prompt. -| Linux | `~/.config/Claude/claude_desktop_config.json` | - -After editing, **restart Claude Desktop**. You should see "thunderbird" in the MCP servers list when you click the tool icon. ## Configuration @@ -204,7 +265,8 @@ Once configured, try these in Claude Desktop: ## Safety - **Compose/reply/forward default to draft mode.** Claude cannot send emails without explicitly requesting `mode: "send"`. -- **Permanent delete is gated** behind `confirm: true`. +- **Permanent delete is gated** behind `confirm: true`, and refused outright unless the add-on was built with `delete: true`. +- **Contact, calendar and task writes are off by default** (`contactsWrite`, `calendarWrite`, `tasksWrite` access switches). - **Search excludes junk/spam by default** to prevent prompt injection from adversarial emails. - **All traffic stays on localhost.** Bridge listens on `127.0.0.1` only. - **No credentials are exposed.** Thunderbird handles all IMAP/SMTP — your passwords never leave its config. @@ -236,7 +298,7 @@ See [SECURITY.md](https://github.com/odience-network/thunderbird-cli-enhanced/bl git clone https://github.com/odience-network/thunderbird-cli-enhanced cd thunderbird-cli-enhanced npm install -npm run test:mcp # 34 integration tests against mock bridge +npm run test:mcp # integration tests against a mock bridge ``` ## License diff --git a/mcp/package.json b/mcp/package.json index f59e281..c789c49 100644 --- a/mcp/package.json +++ b/mcp/package.json @@ -2,7 +2,7 @@ "name": "thunderbird-cli-mcp", "version": "1.3.0", "private": true, - "description": "MCP server that gives Claude Desktop full access to your email through Mozilla Thunderbird. 12 high-level tools: search, read, compose, reply, attachments, folders.", + "description": "MCP server that gives Claude Desktop full access to your email, calendar, tasks and contacts through Mozilla Thunderbird. 42 tools: search, read, compose, reply, attachments, folders, contacts, calendar, tasks, notes and Fast Actions.", "type": "module", "bin": { "tb-mcp": "./src/server.js" diff --git a/mcp/src/client.js b/mcp/src/client.js index 2230321..b906502 100644 --- a/mcp/src/client.js +++ b/mcp/src/client.js @@ -193,7 +193,7 @@ export async function api(method, path, body = null, timeout = 30000) { new Error( "Cannot connect to Thunderbird bridge at " + BASE_URL + - ". Is the bridge daemon running? See https://github.com/vitalio-sh/thunderbird-cli#quick-start" + ". Is the bridge daemon running? See https://github.com/odience-network/thunderbird-cli-enhanced#quick-start" ), { code: "BRIDGE_UNREACHABLE" } ); diff --git a/package.json b/package.json index 21c37e8..e417a70 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@odience-network/thunderbird-cli-enhanced", "mcpName": "io.github.odience-network/thunderbird-cli-enhanced", "version": "1.3.0", - "description": "Thunderbird email for AI agents: the tb CLI, the tb-bridge daemon and the tb-mcp MCP server in one package. Enhanced fork of thunderbird-cli by Vitalii Ionov.", + "description": "Thunderbird email, calendar, tasks and contacts for AI agents: the tb CLI, the tb-bridge daemon and the tb-mcp MCP server in one package. Enhanced fork of thunderbird-cli by Vitalii Ionov.", "type": "module", "workspaces": [ "cli", @@ -71,6 +71,10 @@ "email-client", "imap", "smtp", + "contacts", + "notes", + "tasks", + "calendar", "cli", "ai-agent", "ai-tools", diff --git a/server.json b/server.json index 2f6880d..b0011ce 100644 --- a/server.json +++ b/server.json @@ -1,7 +1,7 @@ { "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", "name": "io.github.odience-network/thunderbird-cli-enhanced", - "description": "Give AI agents full read/write email access via Mozilla Thunderbird. Zero credentials.", + "description": "Give AI agents access to Mozilla Thunderbird email, calendar, tasks and contacts. Zero credentials.", "version": "1.3.0", "repository": { "url": "https://github.com/odience-network/thunderbird-cli-enhanced", diff --git a/skills/thunderbird-cli/README.md b/skills/thunderbird-cli/README.md index 76e0553..ed837dc 100644 --- a/skills/thunderbird-cli/README.md +++ b/skills/thunderbird-cli/README.md @@ -4,7 +4,7 @@ > > This is a [Claude Skill](https://agentskills.io) that teaches Claude how to drive the [`tb-mcp`](../../mcp/) server effectively — handling token-efficient field selection, draft-by-default safety, trust metadata, and common email workflows across all the user's Thunderbird accounts. -Use it alongside the MCP server: the MCP gives Claude the capability (12 email tools), this skill gives Claude the recipes for using them well. +Use it alongside the MCP server: the MCP gives Claude the capability (42 tools across email, contacts, notes, tasks, calendar, skills and Fast Actions), this skill gives Claude the recipes for using them well. --- @@ -44,7 +44,7 @@ Skills are supported on the API via the Code Execution Tool beta. See Anthropic' The skill itself is just instructions — it needs the MCP server to do anything. Before enabling, set up the full stack: 1. **Install Thunderbird 128+** with your email accounts configured (normal Thunderbird install). -2. **Install the signed WebExtension** from [`dist/releases/`](https://github.com/odience-network/thunderbird-cli-enhanced/tree/main/dist/releases) → *Install Add-on From File…* in Thunderbird. +2. **Install the WebExtension** → *Install Add-on From File…* in Thunderbird. The signed XPI in [`dist/releases/`](https://github.com/odience-network/thunderbird-cli-enhanced/tree/main/dist/releases) is 2.1.0 (no calendar/tasks/Fast Actions); for the full v1.3.0 surface use the unsigned 2.4.0 XPI from the [v1.3.0 release](https://github.com/odience-network/thunderbird-cli-enhanced/releases/tag/v1.3.0) until signed 2.4.0 clears ATN review. 3. **Install the CLI, bridge and MCP server:** `npm i -g @odience-network/thunderbird-cli-enhanced`, or from a clone with `./setup.sh` (or `.\setup.ps1`), linking `tb-bridge` and `tb-mcp` when asked. The bridge auto-starts on first use. 4. **Configure the MCP server** in `claude_desktop_config.json` (or equivalent): ```json @@ -65,11 +65,13 @@ Full setup: mode="chec ## CLI fallback (for power users) -If the user says "from the terminal" or asks about scripting, the same capabilities are available via the `tb` CLI (47 commands, JSON output). Full reference: `tb --help` or https://github.com/odience-network/thunderbird-cli-enhanced/blob/main/docs/COMMANDS.md. +If the user says "from the terminal" or asks about scripting, the same capabilities are available via the `tb` CLI (74 commands, JSON output). Full reference: `tb --help` or https://github.com/odience-network/thunderbird-cli-enhanced/blob/main/docs/COMMANDS.md. MCP tool → CLI command mapping: @@ -356,7 +362,14 @@ MCP tool → CLI command mapping: | `contact_search` | `tb contacts-search ` | | `contact_create` | `tb contacts create --book ...` | | `contact_update` | `tb contacts update ...` | -| `calendar_list` | `tb calendars` | +| `calendar_list` | `tb calendar list` | +| `calendar_events` | `tb calendar events --start --end ` | +| `calendar_event_create` | `tb calendar create --calendar --title --start <date> --end <date>` | +| `calendar_event_update` | `tb calendar update <eventId> --calendar <calendarId> ...` | +| `calendar_event_delete` | `tb calendar delete <eventId> --calendar <calendarId>` | +| `calendar_clashes` | `tb calendar clashes` | +| `skill_today` / `skill_week` / `skill_clashes` | `tb today` / `tb week` / `tb clashes` | +| `skill_from` | `tb from <address>` | | `task_list` | `tb tasks list` | | `task_create` | `tb tasks create --calendar <calendarId> --title <title> ...` | | `task_update` | `tb tasks update <taskId> --calendar <calendarId> ...` | @@ -370,4 +383,4 @@ MCP tool → CLI command mapping: ## Version -This skill tracks the `tb-mcp` server on `main` of thunderbird-cli-enhanced (package version 1.1.0). The tool surface (26 tools, parameter names, defaults) is stable within the 1.x line. Check [CHANGELOG](https://github.com/odience-network/thunderbird-cli-enhanced/blob/main/CHANGELOG.md) for additions. +This skill tracks the `tb-mcp` server on `main` of thunderbird-cli-enhanced (package version 1.3.0). The tool surface (42 tools, parameter names, defaults) is stable within the 1.x line. Check [CHANGELOG](https://github.com/odience-network/thunderbird-cli-enhanced/blob/main/CHANGELOG.md) for additions.