proton-mail-bridge-client 2.9.1 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +23 -21
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +36 -4
  4. package/dist/cli.js.map +1 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +79 -24
  7. package/dist/index.js.map +1 -1
  8. package/dist/scripts/install-claude-desktop.d.ts.map +1 -1
  9. package/dist/scripts/install-claude-desktop.js +21 -2
  10. package/dist/scripts/install-claude-desktop.js.map +1 -1
  11. package/dist/services/account-manager.d.ts.map +1 -1
  12. package/dist/services/account-manager.js +1 -0
  13. package/dist/services/account-manager.js.map +1 -1
  14. package/dist/services/local-index-service.d.ts.map +1 -1
  15. package/dist/services/local-index-service.js +3 -0
  16. package/dist/services/local-index-service.js.map +1 -1
  17. package/dist/services/simple-imap-service.d.ts +9 -1
  18. package/dist/services/simple-imap-service.d.ts.map +1 -1
  19. package/dist/services/simple-imap-service.js +69 -19
  20. package/dist/services/simple-imap-service.js.map +1 -1
  21. package/dist/services/smtp-service.d.ts +3 -0
  22. package/dist/services/smtp-service.d.ts.map +1 -1
  23. package/dist/services/smtp-service.js +32 -5
  24. package/dist/services/smtp-service.js.map +1 -1
  25. package/dist/types/index.d.ts +10 -0
  26. package/dist/types/index.d.ts.map +1 -1
  27. package/dist/utils/helpers.d.ts.map +1 -1
  28. package/dist/utils/helpers.js +23 -1
  29. package/dist/utils/helpers.js.map +1 -1
  30. package/dist/utils/pdf-text.d.ts +23 -0
  31. package/dist/utils/pdf-text.d.ts.map +1 -0
  32. package/dist/utils/pdf-text.js +122 -0
  33. package/dist/utils/pdf-text.js.map +1 -0
  34. package/dist/utils/runtime-policy.d.ts.map +1 -1
  35. package/dist/utils/runtime-policy.js +2 -1
  36. package/dist/utils/runtime-policy.js.map +1 -1
  37. package/dist/utils/schedule-templates.d.ts +8 -0
  38. package/dist/utils/schedule-templates.d.ts.map +1 -1
  39. package/dist/utils/schedule-templates.js +29 -0
  40. package/dist/utils/schedule-templates.js.map +1 -1
  41. package/package.json +3 -2
package/README.md CHANGED
@@ -35,8 +35,8 @@ Give Claude Desktop (or Cline, or any MCP client) full access to your Proton Mai
35
35
  > **No terminal?** Follow the [step-by-step guide](docs/install-for-non-developers.md) instead.
36
36
 
37
37
  1. **Install and sign in to [Proton Mail Bridge](https://proton.me/mail/bridge)**, and leave it running. In the Bridge app, open your account and copy the **Bridge password** (it is not your Proton password).
38
- 2. **Connect Claude Desktop**, either way:
39
- - **Bundle:** download the `proton-mail-bridge-client-<your-os>.mcpb` for macOS (`darwin-arm64` Apple silicon, `darwin-x64` Intel), Linux (`linux-x64`, `linux-arm64`) or Windows (`win32-x64`) from the [latest release](https://github.com/googlarz/proton-mail-bridge-client/releases/latest) and open it. Claude Desktop asks for your Proton address and the Bridge password, plus optional settings (Bridge IMAP/SMTP host and port, read-only, allow sending, signature, extra accounts, data and download folders, tool tier); the defaults match a standard Bridge install.
38
+ 2. **Connect Claude Desktop**, either way (quit Claude Desktop completely first, so it does not overwrite the config on exit):
39
+ - **Bundle** (needs Node.js 24 or later on the machine): download the `proton-mail-bridge-client-<your-os>.mcpb` for macOS (`darwin-arm64` Apple silicon, `darwin-x64` Intel), Linux (`linux-x64`, `linux-arm64`) or Windows (`win32-x64`) from the [latest release](https://github.com/googlarz/proton-mail-bridge-client/releases/latest) and open it. Claude Desktop asks for your Proton address and the Bridge password, plus optional settings (Bridge IMAP/SMTP host and port, read-only, allow sending, signature, extra accounts, data and download folders, tool tier); the defaults match a standard Bridge install.
40
40
  - **npm:** `npm install -g proton-mail-bridge-client`, then `proton-mail-bridge-client setup-claude-desktop` (it checks your Bridge ports and writes the config). Fully quit and reopen Claude Desktop afterwards.
41
41
  3. **Check it works:** run `proton-mail-bridge-client doctor`, or ask Claude "run the Proton Mail doctor".
42
42
  4. **Try these prompts:**
@@ -81,7 +81,7 @@ Download: [proton.me/mail/bridge](https://proton.me/mail/bridge)
81
81
 
82
82
  **3. Your Bridge credentials** — from the Bridge app:
83
83
  - IMAP host/port (default: `127.0.0.1:1143`)
84
- - SMTP host/port (default: `127.0.0.1:1025`)
84
+ - SMTP host/port (default: `127.0.0.1:1025`); the IMAP and SMTP password is the same Bridge password
85
85
  - Username (your Proton email address)
86
86
  - Bridge password (see note above)
87
87
 
@@ -290,11 +290,11 @@ Reload the Cline extension after saving. Proton Mail tools will appear in Cline'
290
290
 
291
291
  ## Multi-account support (v2.1.0+)
292
292
 
293
- Support multiple Proton Mail addresses configured in Bridge (using Bridge's **Split Addresses** feature). Each address gets its own isolated service stack, local index, and drafts storage.
293
+ Use more than one Proton mailbox from the same server: either several addresses of one account (Bridge's **Split Addresses** feature) or separate Proton accounts signed in to the same Bridge. Each one gets its own isolated service stack, local index, and drafts storage.
294
294
 
295
295
  ### Setup
296
296
 
297
- First, enable **Split Addresses** in Proton Bridge and configure each address as a separate account. Bridge will generate a separate password for each address. Then set `PROTONMAIL_ACCOUNTS_JSON`:
297
+ In Proton Bridge, make each address or account its own login: with **Split Addresses** on, every address has a login; separate accounts each have their own. Bridge generates a separate password for each login, and the same password works for IMAP and SMTP. The first account is `PROTONMAIL_USERNAME` / `PROTONMAIL_PASSWORD`; **`PROTONMAIL_ACCOUNTS_JSON` lists only the additional ones**, as a JSON array on one line:
298
298
 
299
299
  ```json
300
300
  {
@@ -304,27 +304,22 @@ First, enable **Split Addresses** in Proton Bridge and configure each address as
304
304
  "env": {
305
305
  "PROTONMAIL_USERNAME": "primary@proton.me",
306
306
  "PROTONMAIL_PASSWORD": "primary-bridge-password",
307
- "PROTONMAIL_IMAP_HOST": "127.0.0.1",
308
- "PROTONMAIL_IMAP_PORT": "1143",
309
- "PROTONMAIL_IMAP_SECURE": "false",
310
- "PROTONMAIL_SMTP_HOST": "127.0.0.1",
311
- "PROTONMAIL_SMTP_PORT": "1025",
312
- "PROTONMAIL_ACCOUNTS_JSON": "[
313
- { \"address\": \"primary@proton.me\", \"password\": \"primary-bridge-password\" },
314
- { \"address\": \"alias@proton.me\", \"password\": \"alias-bridge-password\" },
315
- { \"address\": \"business@custom.com\", \"password\": \"business-bridge-password\" }
316
- ]"
307
+ "PROTONMAIL_ACCOUNTS_JSON": "[{\"address\": \"alias@proton.me\", \"password\": \"alias-bridge-password\"}, {\"address\": \"business@custom.com\", \"password\": \"business-bridge-password\"}]"
317
308
  }
318
309
  }
319
310
  }
320
311
  }
321
312
  ```
322
313
 
323
- **Note:** The password is the **Bridge password for each address**, not your Proton account password. Bridge generates a unique password for each Split Address.
314
+ Instead of `password` an entry can use `passwordFile` (path to a file with the Bridge password) or `passwordCommand` (a command that prints it), so the password need not sit in the client config; use only one of the three. `autoSyncFolder` sets that account's folders for background sync (default `PROTONMAIL_AUTO_SYNC_FOLDER`). Folders in a list that an account does not have are skipped and reported as `skippedFolders`.
315
+
316
+ Listing the primary address again is refused at startup ("duplicate address"). Host, ports and the TLS mode default to the primary account's; an entry can override them with `imapHost`, `imapPort`, `imapSecure`, `smtpHost`, `smtpPort` and `smtpSecure`.
317
+
318
+ **Note:** The password is the **Bridge password for each login**, not your Proton account password.
324
319
 
325
320
  ### How it works
326
321
 
327
- - **Primary account:** the first entry in `PROTONMAIL_ACCOUNTS_JSON` (or the main `PROTONMAIL_USERNAME`/`PROTONMAIL_PASSWORD` pair if not set). Email IDs from the primary account have no prefix.
322
+ - **Primary account:** the `PROTONMAIL_USERNAME`/`PROTONMAIL_PASSWORD` pair. Email IDs from the primary account have no prefix.
328
323
  - **Additional accounts:** each has an auto-generated slug (e.g., `alias`, `business`). Email IDs carry an account prefix: `alias::message-id`, `business::message-id`.
329
324
  - **Sending:** `send_email`, `reply_to_email`, etc. route through the account matching the `from` address. If no account matches, the primary connection is used with a header override (for non-configured aliases).
330
325
  - **Search and read:** all read tools (search, threads, analytics, digest) automatically fan out across all configured accounts and merge results, preserving the account prefix in returned IDs.
@@ -449,7 +444,7 @@ If you would rather not combine the flags below yourself, pick how much the assi
449
444
  | `assisted` | yes | yes | yes | yes | yes, with `confirmed:true` | no |
450
445
  | `full` (the default) | yes | yes | yes | yes | yes | yes |
451
446
 
452
- A mode only sets where the individual flags start. Any flag you set explicitly still wins (for example `PROTONMAIL_MODE=drafts` with `PROTONMAIL_ALLOW_SEND=true` allows sending). An unknown value is an error that lists the valid ones. The mode in force is shown by `get_runtime_status`.
447
+ A mode is a floor: the individual flags below can make things stricter (`PROTONMAIL_MODE=full` with `PROTONMAIL_READ_ONLY=true` is read-only; `assisted` with `PROTONMAIL_ALLOWED_ACTIONS=mark_read` allows only that) but never looser (`drafts` with `PROTONMAIL_ALLOW_SEND=true` still does not send). If you want a combination a mode does not give you, leave `PROTONMAIL_MODE` unset and use the flags. An unknown value is an error that lists the valid ones. The mode in force is shown by `get_runtime_status`, and the Claude Desktop bundle has a *Safety mode* setting for it.
453
448
 
454
449
  All flags work in both the MCP server and CLI:
455
450
 
@@ -484,7 +479,7 @@ PROTONMAIL_IMAP_PORT='1143'
484
479
  PROTONMAIL_IMAP_SECURE='false'
485
480
  PROTONMAIL_SMTP_HOST='127.0.0.1'
486
481
  PROTONMAIL_SMTP_PORT='1025'
487
- PROTONMAIL_SMTP_SECURE='true' # Bridge's local SMTP port requires implicit TLS from the first byte; set false only for a non-Bridge SMTP relay
482
+ PROTONMAIL_SMTP_SECURE='true' # true = implicit TLS, false = STARTTLS. Unset, the server tries the other mode itself if Bridge's SMTP mode differs
488
483
  PROTONMAIL_IMAP_USERNAME='' # optional: log in to IMAP as a different user than PROTONMAIL_USERNAME (default: the same). The send identity, and RESTRICT_OUTBOUND_TO_SELF, still use PROTONMAIL_USERNAME.
489
484
  PROTONMAIL_IMAP_PASSWORD='' # optional: a different IMAP password than PROTONMAIL_PASSWORD (default: the same)
490
485
 
@@ -528,6 +523,11 @@ PROTONMAIL_SYNC_INTERVAL_MINUTES='5'
528
523
  PROTONMAIL_IDLE_WATCH='true'
529
524
  PROTONMAIL_IDLE_MAX_SECONDS='30'
530
525
 
526
+ # Attachments
527
+ PROTONMAIL_MAX_INLINE_KB='40' # in KB (older name PROTONMAIL_MAX_INLINE_BYTES still works, also in KB): largest attachment get_attachment_content returns inline as base64
528
+ PROTONMAIL_PDF_MAX_MB='10' # largest PDF get_attachment_text extracts text from (1-100 MB); bigger ones are refused with a message
529
+ PROTONMAIL_PDF_MAX_PAGES='50' # pages get_attachment_text reads from a PDF (1-2000); a longer PDF is cut off with a note saying how many pages are missing
530
+
531
531
  # Tuning and diagnostics
532
532
  PROTONMAIL_OP_DELAY_MS='0' # minimum gap in ms between IMAP mailbox operations (0-5000); raise it to be gentler on a slow Bridge. 0 = no throttling
533
533
  PROTONMAIL_DEBUG='false' # true: get_connection_status and run_doctor include the raw underlying connection error text (otherwise only a classified cause and suggestion)
@@ -548,7 +548,7 @@ With several accounts or a signature, the server sends from whichever `from` the
548
548
  | Search and read | Native Claude UX | IMAP + local index |
549
549
  | Send email | No | Yes |
550
550
  | Draft workflows | Better first-party UX | Full control incl. remote draft sync |
551
- | Attachment content | Limited | Fetch and save to disk |
551
+ | Attachment content | Limited | Fetch, save to disk, read text from PDFs |
552
552
  | Mailbox actions | Limited | Full (star, move, archive, trash, restore, delete, batch) |
553
553
  | Folder management | No | Yes (create, rename, delete) |
554
554
  | CLI access | No | Full parity with MCP |
@@ -594,6 +594,8 @@ With several accounts or a signature, the server sends from whichever `from` the
594
594
  ### Import/export & attachments
595
595
  `export_email` · `import_email` · `get_attachment_text` · `get_emails_by_ids`
596
596
 
597
+ `get_attachment_text` reads text-like attachments and PDFs. A PDF comes back page by page, each page headed `--- Page N of M ---`, and the result has a `pdf` field with `totalPages`, `extractedPages`, `omittedPages` and `truncated`. It is limited by `PROTONMAIL_PDF_MAX_MB` (default 10) and `PROTONMAIL_PDF_MAX_PAGES` (default 50); past the page limit the text ends with a line saying how many pages were left out. A password-protected PDF, a damaged file and a scan without a text layer each fail with a short explanation. The PDF is read in memory with [unpdf](https://github.com/unjs/unpdf) (pure JavaScript, no native module): nothing is written to disk, nothing is downloaded, and a document that takes longer than 30 seconds is stopped.
598
+
597
599
  ---
598
600
 
599
601
  ## Using it as a library
@@ -666,7 +668,7 @@ Measured in October 2026 (v2.7.0) on a copy of the same ~57,000-message index. "
666
668
  - List and search results are compact by default: attachments are metadata only, and empty or derivable fields are left out. Pass `fields: ["subject","from","date"]` to trim further — 10 results is roughly 800 tokens that way, versus about 11,000 for 50 full results.
667
669
  - Defaults are bounded: `search_emails`/`search_indexed_emails` return 50 (pass `limit` for more), `list_drafts` 20, `list_scheduled_sends` 50 newest first. Each reports `hasMore`; page with `offset`.
668
670
  - Draft and queued-message bodies are shortened to a preview in list and update responses (`bodyTruncated`, `bodyLength`); use `get_draft` for one full draft. Attachment content is never inlined in a listing.
669
- - Large attachments: set `PROTONMAIL_ALLOW_FILE_DOWNLOAD_DIR` and pass `saveTo`, rather than raising `PROTONMAIL_MAX_INLINE_BYTES`.
671
+ - Large attachments: set `PROTONMAIL_ALLOW_FILE_DOWNLOAD_DIR` and pass `saveTo`, rather than raising `PROTONMAIL_MAX_INLINE_KB`.
670
672
  - **`PROTONMAIL_TOOL_TIER=core`** exposes 25 tools instead of 100: about 7k tokens of tool definitions instead of about 21k (roughly 67% less; the earlier measurement with a reference tokenizer, 7k against 20k for 96 tools, scaled by the 3.7% the four newest tool definitions add), paid on every conversation that loads the server.
671
673
  - Repeating an `update_draft` that changes nothing on a draft whose remote copy is already in sync does no store write and no IMAP upload. (A draft whose last sync failed, or that was edited with `syncToRemote:false`, still syncs.)
672
674
 
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AA6BA,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC;AAEjD,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,KAAK,EAAE,QAAQ,CAAC;CACjB;AAiBD,qBAAa,aAAc,SAAQ,KAAK;CAAG;AAM3C,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,aAAa,CAuD1D;AA0ID,UAAU,WAAW;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAOD,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAoErD,CAAC;AAoBF,eAAO,MAAM,iBAAiB,YAAI,KAAK,EAAE,MAAM,EAAE,MAAM,CAAU,CAAC;AAGlE,wBAAgB,kBAAkB,IAAI,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAQlG;AAID,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CA+DtD;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAcnE;AAyKD,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAY5F;AAk0CD,UAAU,eAAe;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IAIb,SAAS,CAAC,EAAE,MAAM,CAAC;IAMnB,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,eAAO,MAAM,kBAAkB,EAAE,eAAe,EAmE/C,CAAC;AA2EF,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAqO1C;AA2CD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAGzD"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AA8BA,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC;AAEjD,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,KAAK,EAAE,QAAQ,CAAC;CACjB;AAiBD,qBAAa,aAAc,SAAQ,KAAK;CAAG;AAM3C,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,aAAa,CAuD1D;AA0ID,UAAU,WAAW;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAOD,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAoErD,CAAC;AAoBF,eAAO,MAAM,iBAAiB,YAAI,KAAK,EAAE,MAAM,EAAE,MAAM,CAAU,CAAC;AAGlE,wBAAgB,kBAAkB,IAAI,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAQlG;AAID,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CA+DtD;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAcnE;AAyKD,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAY5F;AAo0CD,UAAU,eAAe;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IAIb,SAAS,CAAC,EAAE,MAAM,CAAC;IAMnB,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,eAAO,MAAM,kBAAkB,EAAE,eAAe,EAmE/C,CAAC;AA2EF,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAqO1C;AA4ED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAGzD"}
package/dist/cli.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { execFile } from "node:child_process";
3
+ import { accessSync, constants as fsConstants, realpathSync } from "node:fs";
3
4
  import { readFile } from "node:fs/promises";
4
5
  import { promisify } from "node:util";
5
6
  import { fileURLToPath } from "node:url";
@@ -10,7 +11,7 @@ import { isMainModule } from "./is-main.js";
10
11
  import { SimpleIMAPService } from "./services/simple-imap-service.js";
11
12
  import { ensureDestructiveConfirmed, ensureEmailActionAllowed, ensureMailboxWriteAllowed, sanitizeRuntimeConfig } from "./utils/runtime-policy.js";
12
13
  import { isValidEmail, parseEmails } from "./utils/helpers.js";
13
- import { buildSchedule, defaultSchedulerSystem, parseTimeOfDay, SCHEDULER_SYSTEMS } from "./utils/schedule-templates.js";
14
+ import { buildSchedule, defaultSchedulerSystem, findStableAlias, parseTimeOfDay, SCHEDULER_SYSTEMS } from "./utils/schedule-templates.js";
14
15
  import { getClaudeDesktopInstallStatus } from "./scripts/check-claude-desktop.js";
15
16
  import { installClaudeDesktopConfig } from "./scripts/install-claude-desktop.js";
16
17
  import { runClaudeDesktopSetupWizard } from "./scripts/setup-claude-desktop.js";
@@ -576,7 +577,11 @@ async function withMcpClient(run) {
576
577
  const transport = new StdioClientTransport({
577
578
  command: process.execPath,
578
579
  args: [cliEntryPath()],
579
- env: Object.fromEntries(Object.entries(process.env).filter((entry) => typeof entry[1] === "string")),
580
+ env: {
581
+ ...Object.fromEntries(Object.entries(process.env).filter((entry) => typeof entry[1] === "string")),
582
+ // The CLI reports a failed tool on stderr with its own exit codes; MCP clients get an isError result instead.
583
+ PROTONMAIL_TOOL_ERRORS: "protocol",
584
+ },
580
585
  stderr: "ignore",
581
586
  });
582
587
  const client = new Client({
@@ -2170,6 +2175,33 @@ export async function main() {
2170
2175
  }
2171
2176
  }
2172
2177
  }
2178
+ // The path of this Node and of this CLI that a scheduler job should hold: the link on PATH when there is one
2179
+ // (Homebrew's /opt/homebrew/bin/node), because the real path carries the version and breaks on an upgrade.
2180
+ function pathDirectories() {
2181
+ return (process.env.PATH ?? "").split(process.platform === "win32" ? ";" : ":");
2182
+ }
2183
+ function stableAlias(target, names, access) {
2184
+ return (findStableAlias({
2185
+ target,
2186
+ names,
2187
+ dirs: pathDirectories(),
2188
+ realpath: (path) => realpathSync(path),
2189
+ isExecutable: (path) => {
2190
+ accessSync(path, access);
2191
+ return true;
2192
+ },
2193
+ separator: process.platform === "win32" ? "\\" : "/",
2194
+ }) ?? target);
2195
+ }
2196
+ function stableNodePath() {
2197
+ return stableAlias(process.execPath, process.platform === "win32" ? ["node.exe"] : ["node"], fsConstants.X_OK);
2198
+ }
2199
+ function stableScriptPath() {
2200
+ const script = fileURLToPath(import.meta.url);
2201
+ // On Windows the bin entry is a .cmd shim, which node cannot run as a script.
2202
+ // Node only has to read the script, so readable is enough (a checkout's dist/cli.js has no execute bit).
2203
+ return process.platform === "win32" ? script : stableAlias(script, ["proton-mail-bridge-client"], fsConstants.R_OK);
2204
+ }
2173
2205
  // `schedule`: builds the text only; nothing is installed or written.
2174
2206
  function scheduleText(parsed) {
2175
2207
  const { hour, minute } = parseTimeOfDay(getStringFlag(parsed.flags, "at") ?? "08:00");
@@ -2185,8 +2217,8 @@ function scheduleText(parsed) {
2185
2217
  system,
2186
2218
  hour,
2187
2219
  minute,
2188
- node: process.execPath,
2189
- script: fileURLToPath(import.meta.url),
2220
+ node: stableNodePath(),
2221
+ script: stableScriptPath(),
2190
2222
  output,
2191
2223
  username: process.env.PROTONMAIL_USERNAME?.trim() || "you@proton.me",
2192
2224
  passwordFile,