proton-drive-mcp 1.0.39 → 1.2.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,67 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.0 — 2026-09-28
4
+
5
+ Setup, packaging and reliability round. Five features were built in parallel on separate branches, merged, then verified live (real account, 3 concurrent servers), on Node 22/24 and on the new Ubuntu + macOS CI matrix, plus an independent code review. Review and CI findings were fixed before release.
6
+
7
+ **Breaking:** Node 20 is no longer supported (`engines` is `>=22`; Node 20 is past end-of-life). It may still start, but it is not tested and `doctor` fails on it.
8
+
9
+ ### Added
10
+ - **`proton-drive-cli doctor`**: checks Node, the `proton-drive` CLI, login state, sync folder and the Claude Desktop config; `--json` output; exits 1 on failure. It only reports on our own config entry, never other servers' entries.
11
+ - **`proton-drive-cli setup-claude-desktop`**: prints (default) or writes (`--write`) the Claude Desktop entry with absolute paths. Touches only `mcpServers["proton-drive-mcp"]`, keeps a timestamped `.bak`, writes atomically, skips the write if nothing changed, and refuses to run from a temporary `npx` cache or on invalid JSON.
12
+ - **MCPB bundle** (`npm run package:mcpb`): one-click Claude Desktop extension attached to each GitHub release (macOS and Linux only; installing it into Claude Desktop is untested).
13
+ - **`PROTON_DRIVE_TOOL_TIER=core`**: exposes 16 everyday tools (~13 KB of tool definitions instead of ~29 KB). Hidden tools are refused at call time. Tool descriptions were also shortened; every safety note was kept.
14
+ - **Retry of read-only calls** on rate limiting, timeouts and network errors (max 3 attempts, bounded by the call timeout). Mutating and transfer commands are never retried.
15
+ - **CI**: Ubuntu and macOS × Node 22/24, non-blocking Windows probe; publish workflow is ready for npm Trusted Publishing (the `NPM_TOKEN` fallback stays until one OIDC publish has succeeded).
16
+
17
+ ### Fixed
18
+ - An empty `PROTON_DRIVE_BIN` (some clients pass `""` for an optional setting) is now treated as unset.
19
+ - The README Claude Desktop snippets use the same server key as `setup-claude-desktop` (`proton-drive-mcp`), so `doctor` finds it.
20
+ - Local-path guard: an unreadable ancestor directory (such as `/var/root` for a non-root user) no longer aborts the denylist check with a raw `EACCES`.
21
+
22
+ ### Known limitations
23
+ - Windows is not supported (tests never passed there; only probed in CI).
24
+ - `drive_share_leave`, `drive_invitation_accept` and `drive_invitation_reject` have never been tested live (no second Proton account).
25
+ - The MCPB bundle and the npm OIDC publish path have not been exercised against the real Claude Desktop / npm yet.
26
+
27
+ ## 1.1.0 — 2026-09-28
28
+
29
+ A "test it like Mail Bridge" round: 6 agents live-tested every tool group against a real account for the first time at scale — full-byte round trips up to 200 MB, the real Proton Drive sync folder, real photo uploads/albums, three MCP servers sharing one CLI session concurrently, official MCP clients (SDK, Inspector, Node 20/22/24, a packed-and-installed tarball), and adversarial edge cases. 3 high-severity and 7 medium-severity bugs were found, fixed by 5 agents working in parallel on separate branches, then the merged result was re-verified live by another 5 agents (one an independent code review) before anything shipped. Every fix below was reproduced from the original bug, not assumed from a commit message.
30
+
31
+ **Behavior changes:** `drive_list`/`drive_list_trash`/`photos_list_timeline`/`photos_list_album_photos` now return items in a stable sorted order; `drive_list`'s `size` is the real file size (the old value is `storageSize`); `drive_restore`/`drive_delete` accept an optional `uid`; `drive_read_file`/`drive_write_file` interpret a `/my-files/...` path as documented.
32
+
33
+ ### Fixed — high severity
34
+ - **`drive_list` pagination lost and duplicated items.** Each page re-ran the CLI, whose item order isn't stable, so a page slice could return the same item twice or skip one entirely while `total` still looked correct. Confirmed live: walking a 1,000-file folder returned 998 of 1,000 unique items. `drive_list`, `drive_list_trash`, `photos_list_timeline` and `photos_list_album_photos` now sort deterministically before paginating (name, then trash time, then capture time — each with a uid tie-break) and their descriptions say each page is a fresh read.
35
+ - **`drive_restore`/`drive_delete` could act on the wrong item when a trash name repeated.** `drive_list_trash` documents using `uid` to tell duplicates apart, but neither tool accepted one. Confirmed live: restoring/deleting `/trash/x.txt` with two items named `x.txt` picked an arbitrary one — the delete case is irreversible. Both tools now accept `uid`, refuse an ambiguous path (listing every matching uid and trash time instead of guessing), and the same check now also covers `/photos-trash`. The underlying CLI can only address a trashed item by name, so a genuinely ambiguous duplicate still can't be targeted — resolve it in the Proton Drive web or desktop app.
36
+ - **`drive_read_file`/`drive_write_file` didn't map the documented `/my-files/...` path.** Following the example in the tool description created a local folder literally named `my-files`, which then synced to Drive as `/my-files/my-files/...`. `/my-files/<rest>` now maps to the sync root correctly; every other Drive root (`/photos`, `/trash`, ...) is rejected with a clear message, since only `/my-files` is synced locally.
37
+
38
+ ### Fixed — medium severity
39
+ - **`photos_delete_album` deleted non-empty albums** despite its description promising a refusal without `force` — the check never existed. Now enforced, and fails closed (refuses) if the album's photo count can't be looked up at all, rather than assuming empty.
40
+ - **`photos_download` advertised `/albums/<album>/<photo>` paths that the CLI can't resolve.** Now rejected up front with a message pointing at the `/photos/<name>` form.
41
+ - **`drive_list`'s `size` was the encrypted storage size summed across every revision**, not the file's real size (a 1-byte file showed 79; it doubled after a new revision). Real size is now `size`; the old value is `storageSize`.
42
+ - **`drive_move` misreported a missing source as "Destination already exists"**, because the destination was checked before the source. Now reports "Source not found" correctly, and hints at the fix when the "existing destination" is actually the target folder itself (a path/parent mix-up seen in live testing).
43
+ - **`drive_share_status` on a Drive root (`/my-files`, etc.) failed with a raw CLI decryption error**, which also broke this project's own live smoke test. Roots now get a clear "cannot be shared" error without calling the CLI.
44
+ - **A folder name ending in a backslash made its children unaddressable** (`tail\` + child `\/` is read as an escaped slash by the CLI, which has no way to escape a literal backslash). No fix exists upstream; documented as a known limitation.
45
+ - **The Claude Desktop config example didn't set `PROTON_DRIVE_BIN`**, and Claude Desktop launches servers with a minimal `PATH` that usually can't find `proton-drive`. The example now sets it, and the "CLI not found" error mentions the variable.
46
+
47
+ ### Also fixed
48
+ - Non-UTF-8 text read through the sync folder was silently corrupted (replacement characters, no warning) — now a clear error.
49
+ - The invite-message limit was 2000 characters; the CLI enforces 500 — now matched.
50
+ - `drive_mkdir` accepted a spaces-only name while `drive_rename` rejected one — now consistent.
51
+ - `drive_share_remove_all` said "Removed all access" while an active public link stayed live — now says so, and how to remove it.
52
+ - Raw upstream error codes (`InvalidRequirementsAPIError` 2000, `APICodeError` 2511/2500) are now mapped to a readable sentence, with the code kept in parentheses; bundled CLI source lines and bare `Error details: {}` are stripped from error text.
53
+ - Read-only CLI calls now retry (up to twice) on the CLI's own `database is locked` error, seen live when multiple MCP clients share one CLI session concurrently; writes are never retried.
54
+ - `photos_list_album_photos` without `loadDetails` has no `captureTime`, so its description now says the order is by `nodeUid`, not capture time.
55
+
56
+ ### Verified with no changes needed
57
+ Data integrity (sha256 round-trips up to 200 MB, 500+ file trees, every upload/download conflict strategy, `drive_copy`), the real sync folder end to end, 3 concurrent MCP server processes sharing one CLI session, a 20+ minute long-running session (no memory or process leaks), server-kill-mid-upload recovery, the official MCP SDK client and MCP Inspector, Node 20/22/24, a packed-and-installed npm tarball launched via its real `.bin` symlink, and dozens of adversarial edge cases (special Drive roots, Unicode name tricks, 40-level nesting, etc.).
58
+
59
+ ### Known, not fixed (upstream CLI limitations, now documented in README)
60
+ Big folders can't be copied; a `"` in a name becomes `_` on local download; a public link's expiration caps at ~90 days; sharing inherited from a parent folder isn't reported by `drive_share_status`; upload/download counts include folders.
61
+
62
+ ### Testing status
63
+ `drive_share_leave`, `drive_invitation_accept` and `drive_invitation_reject` have never been tested against a real account — they need a second Proton account, which the maintainer doesn't have. Only unit-tested against a fake CLI. Documented in README.
64
+
3
65
  ## 1.0.39 — 2026-09-26
4
66
 
5
67
  Test-suite hardening only — no `src/` changes, no behavior change.
package/README.md CHANGED
@@ -12,12 +12,12 @@
12
12
  [![npm version](https://img.shields.io/npm/v/proton-drive-mcp?color=%236d4aff&label=npm)](https://www.npmjs.com/package/proton-drive-mcp)
13
13
  [![CI](https://github.com/googlarz/proton-drive-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/googlarz/proton-drive-mcp/actions/workflows/ci.yml)
14
14
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
15
- [![Node.js 20+](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](https://nodejs.org)
15
+ [![Node.js 22+](https://img.shields.io/badge/node-%3E%3D22-brightgreen)](https://nodejs.org)
16
16
  [![TypeScript](https://img.shields.io/badge/TypeScript-5-3178c6?logo=typescript&logoColor=white)](https://www.typescriptlang.org)
17
17
  [![MCP](https://img.shields.io/badge/MCP-compatible-blueviolet)](https://modelcontextprotocol.io)
18
18
  [![GitHub stars](https://img.shields.io/github/stars/googlarz/proton-drive-mcp?style=social)](https://github.com/googlarz/proton-drive-mcp)
19
19
  [![Last commit](https://img.shields.io/github/last-commit/googlarz/proton-drive-mcp?color=brightgreen&label=last%20commit)](https://github.com/googlarz/proton-drive-mcp/commits/main)
20
- [![Platforms](https://img.shields.io/badge/platform-macOS%20%7C%20Linux%20%7C%20Windows-lightgrey)](https://github.com/googlarz/proton-drive-mcp)
20
+ [![Platforms](https://img.shields.io/badge/platform-macOS%20%7C%20Linux-lightgrey)](https://github.com/googlarz/proton-drive-mcp)
21
21
  [![proton-drive-mcp MCP server](https://glama.ai/mcp/servers/googlarz/proton-drive-mcp/badges/score.svg)](https://glama.ai/mcp/servers/googlarz/proton-drive-mcp)
22
22
 
23
23
  </div>
@@ -60,7 +60,7 @@ proton-drive auth login
60
60
 
61
61
  This opens a browser for Proton's standard sign-in flow. Credentials are stored in your OS keychain — not on disk, not in config files.
62
62
 
63
- **3. Node.js 20 or later** — `node --version` to check.
63
+ **3. Node.js 22 or later** — `node --version` to check.
64
64
 
65
65
  ---
66
66
 
@@ -91,24 +91,31 @@ Add to your `claude_desktop_config.json`:
91
91
  ```json
92
92
  {
93
93
  "mcpServers": {
94
- "proton-drive": {
94
+ "proton-drive-mcp": {
95
95
  "command": "npx",
96
- "args": ["-y", "proton-drive-mcp"]
96
+ "args": ["-y", "proton-drive-mcp"],
97
+ "env": { "PROTON_DRIVE_BIN": "/absolute/path/to/proton-drive" }
97
98
  }
98
99
  }
99
100
  }
100
101
  ```
101
102
 
102
- Restart Claude Desktop. Check **`+` → Connectors → proton-drive** to confirm the server is connected.
103
+ Claude Desktop starts servers with a minimal `PATH`, so a `proton-drive` in `~/.local/bin` is usually not found. Set `PROTON_DRIVE_BIN` to the output of `which proton-drive` (the default install location is `~/.local/bin/proton-drive`, written out in full, e.g. `/Users/you/.local/bin/proton-drive`). If `npx` itself is not found, use its absolute path as `command` (`which npx`).
104
+
105
+ Restart Claude Desktop. Check **`+` → Connectors → proton-drive-mcp** to confirm the server is connected.
103
106
 
104
107
  > **Tip:** Make sure `proton-drive auth login` has been run at least once before starting Claude Desktop.
105
108
 
109
+ ### One-click install (MCPB bundle)
110
+
111
+ Instead of editing JSON, download `proton-drive-mcp-<version>.mcpb` from the [latest GitHub release](https://github.com/googlarz/proton-drive-mcp/releases/latest) and open it (or drag it into **Settings → Extensions** in Claude Desktop). In the extension settings, set **proton-drive CLI path** to the output of `which proton-drive` (usually `~/.local/bin/proton-drive`); optionally set the **Proton Drive sync folder** to enable `drive_read_file` / `drive_write_file`. You still need the official `proton-drive` CLI installed and `proton-drive auth login` done once.
112
+
106
113
  ### If installed globally
107
114
 
108
115
  ```json
109
116
  {
110
117
  "mcpServers": {
111
- "proton-drive": {
118
+ "proton-drive-mcp": {
112
119
  "command": "proton-drive-mcp"
113
120
  }
114
121
  }
@@ -117,6 +124,29 @@ Restart Claude Desktop. Check **`+` → Connectors → proton-drive** to confirm
117
124
 
118
125
  ---
119
126
 
127
+ ## Setup and diagnostics
128
+
129
+ Two commands of the companion CLI help when Claude Desktop reports "proton-drive CLI not found" (it starts servers with a minimal `PATH`):
130
+
131
+ ```bash
132
+ # Read-only checks: Node >= 22, proton-drive CLI + version, auth, PROTON_DRIVE_SYNC_PATH, your Claude Desktop entry
133
+ proton-drive-cli doctor [--json] [--config <path>]
134
+
135
+ # Dry run: prints the entry it would add and the target file, changes nothing
136
+ proton-drive-cli setup-claude-desktop [--config <path>] [--sync-path <dir>]
137
+
138
+ # Apply it (timestamped backup first; only mcpServers["proton-drive-mcp"] is touched)
139
+ proton-drive-cli setup-claude-desktop --sync-path "$HOME/Proton Drive" --write
140
+ ```
141
+
142
+ `doctor` exits 1 if any check fails (warnings do not fail). It only ever reports on the `proton-drive-mcp` entry of the config, never on other servers. `setup-claude-desktop` writes the absolute path of the running `node`, of this package's `dist/index.js`, and of the resolved `proton-drive` binary (`PROTON_DRIVE_BIN`). It refuses to run if the CLI cannot be found, the config file is not valid JSON, or the package is running from a temporary `npx` cache (install it first with `npm install -g proton-drive-mcp`). The entry pins the current `node` binary, so re-run it after switching Node versions (e.g. with nvm). Re-running with unchanged settings leaves the file alone; otherwise the file is replaced atomically and a timestamped `.bak-…` copy is kept.
143
+
144
+ The default config location is per OS (macOS `~/Library/Application Support/Claude/`, Windows `%APPDATA%\Claude\`, Linux `$XDG_CONFIG_HOME` or `~/.config/Claude/`); use `--config` or the `CLAUDE_DESKTOP_CONFIG` environment variable to point elsewhere. On Windows the CLI lookup honours `PATHEXT`.
145
+
146
+ **Claude Desktop only reads its config at startup: fully quit and restart it afterwards** (closing the window is not enough).
147
+
148
+ ---
149
+
120
150
  ## Try it: example Claude prompts
121
151
 
122
152
  **Backup a build artifact**
@@ -330,6 +360,38 @@ proton-drive-cli share status /my-files/Projects
330
360
  - Paths are always Drive-absolute: `/my-files/folder/file.pdf`. Relative paths are not supported.
331
361
  - All calls include `--json` automatically, except `drive_version`, whose underlying CLI command ignores `--json` and always prints plain text — this MCP parses it directly.
332
362
 
363
+ ### Token cost
364
+
365
+ `tools/list` is sent to the model in every session. Measured payload (JSON bytes): `full` 29.5 KB (38 tools; 39.5 KB before description trimming), `core` 13.3 KB (16 tools). Set `PROTON_DRIVE_TOOL_TIER=core` to load only:
366
+
367
+ `drive_auth_status`, `drive_version`, `drive_list`, `drive_info`, `drive_list_trash`, `drive_mkdir`, `drive_upload`, `drive_download`, `drive_rename`, `drive_move`, `drive_copy`, `drive_trash`, `drive_restore`, `drive_share_status`, `photos_list_timeline`, `photos_download`.
368
+
369
+ Left out of `core` (use `full`): permanent deletion (`drive_delete`, `drive_empty_trash`), `drive_auth_logout`, public links, invitations and invites, album management, `photos_upload` and the sync-file tools. A call to a hidden tool returns an error asking for `PROTON_DRIVE_TOOL_TIER=full`; no CLI command runs.
370
+
371
+ ## Known limitations
372
+
373
+ These come from the upstream `proton-drive` CLI (v0.8.0), not from this server:
374
+
375
+ - Big folders cannot be copied yet (`drive_copy` fails with `InvalidRequirementsAPIError` code 2000).
376
+ - A `"` in a name becomes `_` when the item is downloaded locally.
377
+ - Public-link expiration can be at most ~90 days ahead, to the minute.
378
+ - `drive_share_status` only reports sharing set directly on the item — access inherited from a shared parent folder is not shown.
379
+ - Roots (`/my-files`, `/photos`, …) cannot be shared; `drive_share_status` on a root returns an error.
380
+ - `photos_list_timeline` pages are not a snapshot: photos added or removed between calls shift later pages.
381
+ - Upload/download counts include folders, not only files.
382
+ - `/albums/...` paths cannot be downloaded — download a photo via `/photos/<name>`.
383
+ - A name ending in a backslash (e.g. `tail\`, created by another client) cannot be used as a parent in a path: the CLI reads `tail\/child` as an escaped `/`, and has no escape for a literal backslash (`\\` does not work either). The folder itself is reachable; its children are not reachable by path.
384
+ - Trashed items are addressed by name only. When two items in `/trash` or `/photos-trash` share a name, `drive_restore` and `drive_delete` refuse (listing the uids) instead of acting on an arbitrary one — restore or delete that item in the Proton Drive web or desktop app.
385
+ - When several programs use the CLI at the same moment (e.g. Claude Desktop and Claude Code), its local cache can briefly report `database is locked`. Read-only calls are retried automatically; writes are not (a retry could repeat the change), so just run the write again. The same read-only retry (up to two more attempts, short jittered backoff, honoring a `Retry-After` of at most 5 s and the call's overall timeout) also covers rate limiting (HTTP 429), single-request timeouts (`Request timed out`) and transient network resets; auth and not-found errors are never retried.
386
+
387
+ ## Platform support
388
+
389
+ Developed and live-tested on **macOS**. CI runs the test suite on Ubuntu and macOS (Node 22 and 24). **Windows is not supported yet**: the test suite has never passed there (CI runs it only as a non-blocking probe), and the Windows paths in `doctor` / `setup-claude-desktop` (`%APPDATA%`, `PATHEXT` lookup) are untested. The system-PATH warning in `doctor` only knows POSIX directories. Linux is covered by CI with the fake CLI but has not been live-tested against a real Proton account.
390
+
391
+ ## Testing status
392
+
393
+ Every tool group was live-tested on 2026-09-28 against a real Proton account, **except** `drive_share_leave`, `drive_invitation_accept` and `drive_invitation_reject`. Those three have never been tested against a real account, because the maintainer has no second Proton account to share from; they are only unit-tested against a fake CLI.
394
+
333
395
  ---
334
396
 
335
397
  ## Environment variables
@@ -337,9 +399,12 @@ proton-drive-cli share status /my-files/Projects
337
399
  | Variable | Required | Description |
338
400
  |---|---|---|
339
401
  | `PROTON_DRIVE_SYNC_PATH` | Optional | Absolute path to your local Proton Drive sync folder root (e.g. `/Users/you/Proton Drive`). Required only for `drive_read_file` and `drive_write_file`. The Proton Drive desktop app must be running to sync written files to the cloud. |
340
- | `PROTON_DRIVE_BIN` | Optional | Override the `proton-drive` binary name or path (default: `proton-drive`). Useful for non-standard installations. |
402
+ | `PROTON_DRIVE_BIN` | Optional | Override the `proton-drive` binary name or path (default: `proton-drive`; an empty value counts as unset). Useful for non-standard installations. |
341
403
  | `PROTON_DRIVE_LOCAL_ROOT` | Optional | Path-delimiter-separated list of local directories that upload/download/photos tools may touch. Unset = any path except the built-in credential denylist. |
342
404
  | `PROTON_DRIVE_ALLOW_SENSITIVE_PATHS` | Optional | Set to `1` to disable the built-in credential-location denylist (not recommended). |
405
+ | `CLAUDE_DESKTOP_CONFIG` | Optional | Path of the Claude Desktop config that `doctor` and `setup-claude-desktop` read/write instead of the per-OS default. |
406
+ | `PROTON_DRIVE_RETRY_BASE_MS` | Optional | Test hook: base backoff in ms for retrying read-only calls (default 250). |
407
+ | `PROTON_DRIVE_TOOL_TIER` | Optional | `full` (default, all 38 tools) or `core` (16 everyday tools; see [Token cost](#token-cost)). Tools outside the active tier are hidden from `tools/list` and refused at call time. Read once at startup; an unknown value falls back to `full` with a warning on stderr. |
343
408
 
344
409
  ---
345
410
 
@@ -355,7 +420,7 @@ Download from [proton.me/download/drive/cli](https://proton.me/download/drive/cl
355
420
  Run `proton-drive auth login` in your terminal. Auth state is stored in your OS keychain and persists across sessions.
356
421
 
357
422
  **Claude can't see the connector**
358
- Restart Claude Desktop fully after changing the MCP config. Check **`+` → Connectors → proton-drive**. The Proton Drive CLI must be in the `PATH` that Claude Desktop inherits (on macOS this may differ from your shell PATH — use the full binary path in config if needed).
423
+ Restart Claude Desktop fully after changing the MCP config. Check **`+` → Connectors → proton-drive-mcp**. The Proton Drive CLI must be in the `PATH` that Claude Desktop inherits (on macOS this may differ from your shell PATH — use the full binary path in config if needed).
359
424
 
360
425
  **Upload fails on image files**
361
426
  The CLI generates WebP thumbnails by default using Bun's image API. If Bun isn't installed or doesn't support thumbnails on your platform, the MCP passes `--skip-thumbnails` to bypass this. No action needed.
@@ -369,7 +434,7 @@ Or in Claude Desktop config:
369
434
  ```json
370
435
  {
371
436
  "mcpServers": {
372
- "proton-drive": {
437
+ "proton-drive-mcp": {
373
438
  "command": "npx",
374
439
  "args": ["-y", "proton-drive-mcp"],
375
440
  "env": { "PROTON_DRIVE_BIN": "/usr/local/bin/proton-drive" }
@@ -383,7 +448,7 @@ Use the full path to the `proton-drive.exe` binary in your Claude Desktop config
383
448
  ```json
384
449
  {
385
450
  "mcpServers": {
386
- "proton-drive": {
451
+ "proton-drive-mcp": {
387
452
  "command": "C:\\path\\to\\proton-drive-mcp.cmd"
388
453
  }
389
454
  }
package/dist/cli.js CHANGED
@@ -9,6 +9,9 @@
9
9
  import { DriveService, } from "./services/drive.js";
10
10
  import { DriveCliError, DriveCliNotFoundError, DriveNotAuthenticatedError, DriveParseError } from "./utils/errors.js";
11
11
  import { checkCliAvailable } from "./utils/subprocess.js";
12
+ import { runDoctor, formatDoctor } from "./utils/doctor.js";
13
+ import { buildEntry, defaultConfigPath, isDirectory, resolveDriveCli, writeEntry, SERVER_KEY } from "./utils/claudeConfig.js";
14
+ import { resolve } from "node:path";
12
15
  import { validateRemotePath, validateLocalPath, validateEmail, validateMessage, validateName, validateFlagValue } from "./utils/validation.js";
13
16
  const drive = new DriveService();
14
17
  const args = process.argv.slice(2);
@@ -32,7 +35,7 @@ Commands:
32
35
  'remove' deletes the existing LOCAL item and needs --confirm)
33
36
  rename <path> <new-name> Rename in place, no move
34
37
  move <src> <dst> Move and/or rename
35
- delete <path> --confirm Delete a file/folder already in trash, permanently
38
+ delete <path>|--uid <uid> --confirm Delete a file/folder already in trash, permanently
36
39
  share status <path> Show sharing info
37
40
  share invite <path> <email> <role> Invite user (viewer/editor/admin)
38
41
  share revoke <path> <email> Revoke one user's access
@@ -44,7 +47,7 @@ Commands:
44
47
  trash <path> Move to trash
45
48
  trash list List trash contents
46
49
  trash empty --confirm Permanently delete all trash
47
- restore <path> Restore from trash
50
+ restore <path>|--uid <uid> Restore from trash (uid from 'trash list')
48
51
  invitation list List pending invitations
49
52
  invitation accept <uid> Accept an invitation
50
53
  invitation reject <uid> Reject an invitation
@@ -59,6 +62,12 @@ Commands:
59
62
  photo download <photo>... <local> [--conflict X] [--confirm] Download photos (skip/rename/remove; 'remove' needs --confirm)
60
63
  photo upload <local>... [--conflict X] Upload photos to your library (skip/rename)
61
64
 
65
+ doctor [--config <path>] Read-only diagnostics: Node, proton-drive CLI, auth, sync path, Claude Desktop config
66
+ (exit 1 if any check fails)
67
+ setup-claude-desktop [--config <path>] [--sync-path <dir>] [--write]
68
+ Print the Claude Desktop entry for this server (dry run); --write adds it to the
69
+ config (timestamped backup first). Restart Claude Desktop afterwards.
70
+
62
71
  Flags:
63
72
  --json Machine-readable JSON output (one line)
64
73
  `);
@@ -100,6 +109,28 @@ function getFlag(flag) {
100
109
  function print(data) {
101
110
  console.log(jsonMode ? JSON.stringify(data) : JSON.stringify(data, null, 2));
102
111
  }
112
+ function setupClaudeDesktop() {
113
+ const configPath = resolve(getFlag("--config") ?? defaultConfigPath());
114
+ const rawSync = getFlag("--sync-path");
115
+ const cliPath = resolveDriveCli();
116
+ if (!cliPath) {
117
+ console.error("Error: proton-drive CLI not found. Install it from https://proton.me/download/drive/cli/index.html, or set PROTON_DRIVE_BIN to its absolute path (`which proton-drive`).");
118
+ process.exit(1);
119
+ }
120
+ const syncPath = rawSync !== undefined ? resolve(rawSync) : undefined;
121
+ if (syncPath && !isDirectory(syncPath)) {
122
+ console.error(`Error: --sync-path ${syncPath} is not an existing directory.`);
123
+ process.exit(1);
124
+ }
125
+ const entry = buildEntry(cliPath, syncPath);
126
+ const shown = JSON.stringify({ mcpServers: { [SERVER_KEY]: entry } }, null, 2);
127
+ if (!args.includes("--write")) {
128
+ console.log(`Dry run: nothing changed. Would set this entry in ${configPath}:\n${shown}\nRe-run with --write to apply.`);
129
+ return;
130
+ }
131
+ const backup = writeEntry(configPath, entry);
132
+ console.log(`Wrote ${SERVER_KEY} to ${configPath}${backup ? ` (backup: ${backup})` : ""}.\n${shown}\nFully quit and restart Claude Desktop for the change to take effect.`);
133
+ }
103
134
  async function run() {
104
135
  // Strip --json from positional parsing
105
136
  const positional = args.filter((a) => a !== "--json");
@@ -115,11 +146,21 @@ async function run() {
115
146
  usage();
116
147
  return;
117
148
  }
149
+ if (cmd === "doctor") {
150
+ const result = await runDoctor(getFlag("--config"));
151
+ console.log(jsonMode ? JSON.stringify(result) : formatDoctor(result));
152
+ process.exitCode = result.ok ? 0 : 1;
153
+ return;
154
+ }
155
+ if (cmd === "setup-claude-desktop") {
156
+ setupClaudeDesktop();
157
+ return;
158
+ }
118
159
  const cliCheck = await checkCliAvailable();
119
160
  if (!cliCheck.available) {
120
161
  const msg = cliCheck.reason === "not_executable"
121
162
  ? "Error: proton-drive CLI found but not executable.\nRun: chmod +x $(which proton-drive)"
122
- : "Error: proton-drive CLI not found in PATH.\nDownload from https://proton.me/download/drive/cli/index.html";
163
+ : "Error: proton-drive CLI not found in PATH.\nDownload from https://proton.me/download/drive/cli/index.html, or set PROTON_DRIVE_BIN to its absolute path (find it with `which proton-drive`).";
123
164
  console.error(msg);
124
165
  process.exit(1);
125
166
  }
@@ -241,13 +282,15 @@ async function run() {
241
282
  console.log("Folder created.");
242
283
  break;
243
284
  case "delete": {
244
- const delPath = requirePath(sub, "delete <path> --confirm");
285
+ const delUid = getFlag("--uid");
286
+ const delPath = delUid && (!sub || sub.startsWith("--")) ? undefined : requirePath(sub, "delete <path>|--uid <uid> --confirm");
287
+ const delLabel = delPath ?? `uid ${delUid}`;
245
288
  if (!args.includes("--confirm")) {
246
- console.error(`This permanently deletes an item already in trash: ${delPath}\nPass --confirm to proceed.`);
289
+ console.error(`This permanently deletes an item already in trash: ${delLabel}\nPass --confirm to proceed.`);
247
290
  process.exit(1);
248
291
  }
249
- await drive.delete(delPath);
250
- console.log(`Deleted: ${delPath}`);
292
+ await drive.delete(delPath, delUid);
293
+ console.log(`Deleted: ${delLabel}`);
251
294
  break;
252
295
  }
253
296
  case "share":
@@ -288,10 +331,11 @@ async function run() {
288
331
  console.error(`This removes everyone's access to: ${removeAllPath}\nPass --confirm to proceed.`);
289
332
  process.exit(1);
290
333
  }
291
- const removedCount = await drive.shareRemoveAll(removeAllPath);
292
- console.log(removedCount === 0
334
+ const removeAll = await drive.shareRemoveAll(removeAllPath);
335
+ console.log((removeAll.removed === 0
293
336
  ? `Nothing to remove: ${removeAllPath} has no members or pending invitations.`
294
- : `Removed all access (${removedCount} member/invitation(s)) to: ${removeAllPath}`);
337
+ : `Removed all members and invitations (${removeAll.removed}) from: ${removeAllPath}`) +
338
+ (removeAll.publicLink ? " A public link is still active — remove it with: share remove-url <path>" : ""));
295
339
  }
296
340
  else if (sub === "set-url") {
297
341
  const setUrlPath = requirePath(rest[0], "share set-url <path> [--role] [--password] [--expiration]");
@@ -342,10 +386,13 @@ async function run() {
342
386
  process.exit(1);
343
387
  }
344
388
  break;
345
- case "restore":
346
- await drive.restore(requirePath(sub, "restore <path>"));
389
+ case "restore": {
390
+ const restoreUid = getFlag("--uid");
391
+ const restorePath = restoreUid && (!sub || sub.startsWith("--")) ? undefined : requirePath(sub, "restore <path>|--uid <uid>");
392
+ await drive.restore(restorePath, restoreUid);
347
393
  console.log("Restored.");
348
394
  break;
395
+ }
349
396
  case "copy": {
350
397
  const copySrc = requirePath(sub, "copy <src> <dst>");
351
398
  const copyDst = requirePath(rest[0], "copy <src> <dst>");
@@ -452,7 +499,7 @@ async function run() {
452
499
  return;
453
500
  }
454
501
  await drive.updateAlbum(albumPath, newName, coverPhotoUid);
455
- console.log(`Album updated: ${albumPath}`);
502
+ console.log(`Album updated: ${newName ? `/albums/${newName}` : albumPath}`);
456
503
  }
457
504
  else if (sub === "delete") {
458
505
  const albumPath = requirePath(rest[0], "album delete <path> [--force] [--save] --confirm");