proton-drive-mcp 1.2.0 → 1.3.1

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,39 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.3.1 — 2026-10-01
4
+
5
+ ### Changed
6
+ - Trusted Publishing is now configured on npm for this package; the publish workflow keeps the `NPM_TOKEN` fallback until an OIDC publish has been confirmed.
7
+
8
+ ### Fixed
9
+ - `drive_find_duplicates` with `verify`: the "verify budget reached" note now names the limit that was actually hit (bytes or file count) instead of always listing both.
10
+
11
+ ## 1.3.0 — 2026-09-30
12
+
13
+ Proton Drive has no search, and its end-to-end encryption rules out server-side indexing. This release adds the client-side layer: find things, understand what uses your space, and clean up safely. 4 builders worked in parallel, then 3 verifiers (live on a real account, an independent security review, packaging/CI) and a re-check of every fix. Review findings fixed before release include two ReDoS vectors in search and a local-path guard gap.
14
+
15
+ ### Added
16
+ - **`drive_tree`** and **`drive_search`** (also in the `core` tier): a bounded, parallel walk of the drive (8 concurrent calls, `.git` and `node_modules` skipped, call cap, cancellable) with a 5-minute in-memory cache that the server's own writes invalidate. Search by name, glob (relative to the search folder when it contains `/`), extension, media type, size and date, with stable pagination. The first walk of a large drive takes minutes (about 25 s per 1,200 files); later calls are instant.
17
+ - **`drive_usage`**: largest files and folders, breakdown by type, old files and trash stats (sums of file sizes, not the account quota).
18
+ - **`drive_find_duplicates`**: groups by the uploader-claimed sha1 or by size; `verify` downloads and hashes candidates within a size and total budget. It never deletes.
19
+ - **`drive_sharing_audit`**: public links (role, expiry, password; the URL is never printed), invitees, pending invitations and risk flags.
20
+ - **`drive_sync_plan`**: compares a local folder with a Drive folder (plan only).
21
+ - **`drive_bulk_move`** and **`drive_bulk_trash`**: up to 200 items in one batched call. Always two steps: without `confirmed` you get a plan and any problems; `confirmed:true` applies only if there are none. Trash stays reversible.
22
+ - **MCP prompts** (`organise-folder`, `find-files`, `storage-audit`, `sharing-audit`), listed only when the tools they need are in the active tier; also declared in the MCPB manifest.
23
+ - **Human confirmation through elicitation** for clients that support it (Claude Code CLI; Claude Desktop is unverified): gated calls and bulk applies ask the person instead of only failing. Clients without it behave as before. It does not protect against a model that passes `confirmed:true` itself.
24
+ - `drive_list` and `drive_info` now return `mtime`, `uploadedAt` and `sha1` (claimed, often absent); `drive_list` takes a `type` filter; `drive_share_status` adds `shareUrlRole`.
25
+ - Tool titles on every tool, corrected annotations (for example `drive_trash` is not destructive), `maxResultSizeChars` hints on the large-result tools.
26
+
27
+ ### Changed
28
+ - The full tool list is now 46 tools (about 39 KB, was 29.5 KB); `core` is 18 tools (16.8 KB). Use `PROTON_DRIVE_TOOL_TIER=core` to keep the token cost down.
29
+ - Folder uploads and `drive_sync_plan` refuse a local folder that contains protected locations (home directory, `/`, `~/.ssh`, ...); scans skip secret-named files and report `protectedSkipped`. Downloads into your home folder still work. Note that for uploads the CLI does the recursion, so a `.env` inside an uploaded subfolder is not filtered.
30
+ - `.gitignore` covers `node_modules` as a symlink too.
31
+
32
+ ### Known limitations
33
+ - Windows is not supported. The CLI has no search, version history, quota or change notifications, so none of those are offered.
34
+ - The walk cache does not see changes made by other clients until it expires or `refresh:true`.
35
+ - `drive_share_leave`, `drive_invitation_accept` and `drive_invitation_reject` have never been tested live (no second Proton account). Elicitation and prompts are verified with the official SDK client and Claude Code semantics, not with Claude Desktop.
36
+
3
37
  ## 1.2.0 — 2026-09-28
4
38
 
5
39
  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.
package/README.md CHANGED
@@ -30,8 +30,8 @@ Give Claude Desktop (or any MCP client) full access to your Proton Drive and Pro
30
30
 
31
31
  - **Claude manages your Proton Drive** — list, upload, download, move, share, trash, restore
32
32
  - **Proton Photos album management** — list albums, create/delete albums, add and remove photos
33
- - **Full CLI** — same 38 operations, scriptable and pipeable, works in cron and shell scripts
34
- - **100% CLI coverage** — every scriptable Proton Drive CLI command has a matching tool (verified against the CLI's own source; `auth login` is the one command excluded, since it's an interactive browser flow)
33
+ - **Companion CLI** — 44 of the 46 operations, scriptable and pipeable, works in cron and shell scripts (the two sync-folder tools, `drive_read_file` and `drive_write_file`, are MCP-only)
34
+ - **Full Proton Drive CLI coverage** — every scriptable Proton Drive CLI command has a matching tool (verified against the CLI's own source; `auth login` is the one command excluded, since it's an interactive browser flow)
35
35
  - **Zero credential exposure** — auth is handled entirely by the official Proton Drive CLI; this MCP never touches your password or session token
36
36
  - **Shell injection safe** — all CLI calls use `execFile` with discrete argument arrays, never string interpolation
37
37
  - **Privacy-native** — end-to-end encryption is handled by Proton's own CLI; this server is just a thin MCP wrapper
@@ -229,6 +229,28 @@ proton-drive-cli restore /my-files/old-draft.pdf # restore from trash
229
229
  proton-drive-cli trash empty --confirm # permanently delete all trashed items
230
230
  ```
231
231
 
232
+ ### Search, analytics and cleanup
233
+ `drive_search` · `drive_tree` · `drive_usage` · `drive_find_duplicates` · `drive_sharing_audit` · `drive_sync_plan` · `drive_bulk_move` · `drive_bulk_trash`
234
+
235
+ ```bash
236
+ proton-drive-cli tree /my-files --depth 2
237
+ proton-drive-cli search /my-files --ext pdf --min-size 1000000
238
+ proton-drive-cli usage /my-files --top 20 --older-than 365
239
+ proton-drive-cli duplicates /my-files --min-size 1000000 [--verify]
240
+ proton-drive-cli share audit /my-files
241
+ proton-drive-cli sync-plan ./local /my-files/backup --direction up
242
+ proton-drive-cli bulk-move /my-files/Archive /my-files/a.pdf /my-files/b.pdf # plan only
243
+ proton-drive-cli bulk-trash /my-files/old1.txt /my-files/old2.txt --confirm # apply
244
+ ```
245
+
246
+ - The first walk of a large drive can take minutes (measured ~25 s per 1,200 files at concurrency 8). Later calls reuse a 5-minute cache; `refresh` bypasses it.
247
+ - Walks skip `.git` and `node_modules` by default.
248
+ - Read-only tools (`drive_search`, `drive_tree`, `drive_usage`, `drive_find_duplicates`, `drive_sharing_audit`, `drive_sync_plan`) change nothing. `drive_bulk_move` and `drive_bulk_trash` are two-step: call without `confirmed` for the plan, then with `confirmed: true` to apply. Bulk trash is reversible with `drive_restore`.
249
+ - Duplicate detection uses the sha1 the uploader claimed, which is unverified. `verify: true` downloads the candidates and hashes them.
250
+ - The sharing audit flags an invitee as external when the address is outside Proton domains, so Proton users on a custom domain are over-flagged.
251
+ - Sizes are sums of file sizes, not your account quota.
252
+ - `drive_usage` trash stats always make one uncached `/trash` listing (~2.5 s), even when the walk is cached.
253
+
232
254
  ### Photos
233
255
 
234
256
  ```bash
@@ -260,13 +282,19 @@ proton-drive-cli share status /my-files/Projects
260
282
 
261
283
  ---
262
284
 
285
+ ## Prompts
286
+
287
+ The server offers four MCP prompts (also declared in the MCPB manifest): `organise-folder` (`path`), `find-files` (`description`), `storage-audit` and `sharing-audit`. In Claude Code they are listed as `/servername:promptname (MCP)`, and `/mcp__servername__promptname` also runs them. Other clients show them as prompts. A prompt is hidden when a tool it needs is outside the active tool tier: with `PROTON_DRIVE_TOOL_TIER=core` only `find-files` is offered.
288
+
289
+ ---
290
+
263
291
  ## Tool surface
264
292
 
265
293
  ### Auth
266
294
  `drive_auth_status` · `drive_auth_logout` · `drive_version`
267
295
 
268
296
  ### Filesystem
269
- `drive_list` · `drive_info` · `drive_mkdir` · `drive_upload` · `drive_download` · `drive_rename` · `drive_move` · `drive_delete`
297
+ `drive_list` · `drive_info` · `drive_tree` · `drive_search` · `drive_mkdir` · `drive_upload` · `drive_download` · `drive_rename` · `drive_move` · `drive_delete`
270
298
 
271
299
  ### Sharing
272
300
  `drive_share_status` · `drive_share_invite` · `drive_share_revoke` · `drive_share_remove_all` · `drive_share_set_url` · `drive_share_remove_url`
@@ -280,6 +308,9 @@ proton-drive-cli share status /my-files/Projects
280
308
  ### Copy
281
309
  `drive_copy`
282
310
 
311
+ ### Plan & bulk
312
+ `drive_sync_plan` · `drive_bulk_move` · `drive_bulk_trash`
313
+
283
314
  ### Invitations
284
315
  `drive_list_invitations` · `drive_invitation_accept` · `drive_invitation_reject` · `drive_share_leave`
285
316
 
@@ -297,6 +328,8 @@ proton-drive-cli share status /my-files/Projects
297
328
  | `drive_version` | CLI and SDK version info | — |
298
329
  | `drive_list` | List files and folders at a path (paginated, default 200; `/` lists the roots) | `path`, `limit?`, `offset?` |
299
330
  | `drive_info` | Get metadata for one file/folder, including revision details (noise trimmed) | `path`, `verbose?` (raw CLI node) |
331
+ | `drive_tree` | Folder overview: per-folder file counts and size totals (largest first), depth-limited, cached 5 min | `path?`, `depth?` (default 2, max 10), `limit?`, `foldersOnly?`, `refresh?` |
332
+ | `drive_search` | Find files/folders under a path in one cached walk: name (substring/glob; a glob containing `/` matches the path relative to `path`), type, extension, MIME prefix, size, modified date; sortable, paginated. Reports `walk.complete` — false means the walk was cut short and matches may be missing | `query?`, `glob?`, `path?`, `type?`, `mediaType?`, `extensions?`, `minSize?`, `maxSize?`, `modifiedAfter?`, `modifiedBefore?`, `sort?`, `limit?` (default 50, max 500), `offset?`, `refresh?` |
300
333
  | `drive_mkdir` | Create a new empty folder | `path` |
301
334
  | `drive_upload` | Upload local file or folder | `localPath`, `remotePath`, `fileConflictStrategy?` (skip/create-new-revision/rename/replace), `folderConflictStrategy?` (skip/merge/rename/replace), `confirmed?` (**required for `replace`** — it trashes the existing remote item) |
302
335
  | `drive_download` | Download to local path | `remotePath`, `localPath`, `fileConflictStrategy?` (skip/rename/remove), `folderConflictStrategy?` (skip/merge/rename/remove), `confirmed?` (**required for `remove`** — it deletes the existing local item) |
@@ -306,12 +339,18 @@ proton-drive-cli share status /my-files/Projects
306
339
  | `drive_delete` | Permanently delete an item already in trash ⚠️ | `path`, `confirmed: true` |
307
340
  | `drive_list_trash` | List items currently in trash (paginated, default 100; includes `uid` — names are not unique in trash) | `limit?`, `offset?` |
308
341
  | `drive_share_status` | Get sharing members and URL | `path` |
342
+ | `drive_usage` | Storage analytics for a subtree: totals, largest files/folders, extension and media-type breakdown, old files, trash stats (sum of file sizes, not the account quota) | `path?`, `top?`, `olderThanDays?`, `refresh?` |
343
+ | `drive_find_duplicates` | Likely duplicate groups (claimed sha1 / same size; `verify` downloads and sha256s candidates), wasted bytes, suggested keeper — never deletes | `path?`, `minSize?`, `verify?`, `maxVerifyBytes?`, `maxVerifyTotalBytes?` (default 500 MB, max 2 GB), `limit?`, `refresh?` |
344
+ | `drive_sharing_audit` | Public links (no URL), invitees, pending invitations and risk flags for shared items (max 100) | `path?`, `refresh?` |
309
345
  | `drive_share_invite` | Invite a user (sends an email) ⚠️ | `path`, `email`, `role` (viewer/editor/admin), `message?`, `confirmed: true` |
310
346
  | `drive_share_revoke` | Revoke one person's access (fails if not a member) ⚠️ | `path`, `email`, `confirmed: true` |
311
347
  | `drive_share_remove_all` | Remove every member + pending invitation at once ⚠️ | `path`, `confirmed: true` |
312
348
  | `drive_share_set_url` | Create/replace a public share link ⚠️ (re-running without `password`/`expiration` removes them; expiry max ~90 days) | `path`, `role?` (viewer/editor), `password?`, `expiration?`, `confirmed: true` |
313
349
  | `drive_share_remove_url` | Remove the public share link ⚠️ | `path`, `confirmed: true` |
314
350
  | `drive_trash` | Move to trash | `path` |
351
+ | `drive_sync_plan` | Read-only diff of a local folder vs a Drive folder (nothing transferred) | `localPath`, `drivePath`, `direction?` (up/down/both), `ignore?`, `compare?` (size-mtime/sha1), `limit?` |
352
+ | `drive_bulk_move` | Move up to 200 items into an existing folder; without `confirmed` returns the plan and problems only | `sources`, `destinationFolder`, `confirmed?` (**required to apply**) |
353
+ | `drive_bulk_trash` | Trash up to 200 items (recoverable); without `confirmed` returns the plan and problems only | `paths`, `confirmed?` (**required to apply**) |
315
354
  | `drive_restore` | Restore from trash | `path` |
316
355
  | `drive_empty_trash` | Permanently delete all trash ⚠️ | `confirmed: true` |
317
356
  | `drive_read_file` | Read text file from local sync folder | `path` |
@@ -356,15 +395,17 @@ proton-drive-cli share status /my-files/Projects
356
395
  - Error messages come from the CLI's own output; the command line (and therefore any `--password`) is never echoed back.
357
396
  - `drive_move` accepts a full destination path (parent + new name) for a familiar interface, but the underlying CLI only has separate `move` (change parent) and `rename` (change name) commands — this MCP translates automatically, issuing one or both as needed.
358
397
  - `drive_delete` only works on items already in `/trash` or `/photos-trash` — the CLI rejects live paths. Trash an item first with `drive_trash`, or use `drive_empty_trash` to clear everything at once.
398
+ - If the client declares MCP elicitation (form mode), a refused `confirmed` gate is put to the human instead, and `drive_bulk_move` / `drive_bulk_trash` ask before applying a `confirmed: true` call. Only an explicit approval runs the call; decline, cancel, timeout or error keep the refusal, and after 5 declined prompts in a minute the server stops prompting. This only upgrades refusals and bulk applies: a compromised model that supplies `confirmed: true` on any other tool is **not** stopped by it. Clients without elicitation behave as before. Confirmation prompts need a client that supports elicitation (Claude Code CLI today; Claude Desktop is unverified).
399
+ - Walk-based tools (`drive_tree`, `drive_search`, `drive_usage`, `drive_find_duplicates`) read from a cache up to 5 minutes old; changes made by other clients or processes are not seen until it expires or you pass `refresh: true`. The first walk of a large drive can take minutes (measured about 25 s per 1200 files at concurrency 8).
359
400
  - `drive_auth_status` has no native CLI equivalent — it probes by resolving `/my-files` and reports authenticated based on whether that succeeds.
360
401
  - Paths are always Drive-absolute: `/my-files/folder/file.pdf`. Relative paths are not supported.
361
402
  - All calls include `--json` automatically, except `drive_version`, whose underlying CLI command ignores `--json` and always prints plain text — this MCP parses it directly.
362
403
 
363
404
  ### Token cost
364
405
 
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:
406
+ `tools/list` is sent to the model in every session. Measured payload (JSON bytes): `full` 39.0 KB (46 tools), `core` 16.8 KB (18 tools). Set `PROTON_DRIVE_TOOL_TIER=core` to load only:
366
407
 
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`.
408
+ `drive_auth_status`, `drive_version`, `drive_list`, `drive_info`, `drive_list_trash`, `drive_search`, `drive_tree`, `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
409
 
369
410
  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
411
 
@@ -386,7 +427,7 @@ These come from the upstream `proton-drive` CLI (v0.8.0), not from this server:
386
427
 
387
428
  ## Platform support
388
429
 
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.
430
+ 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 and CI does not run it, 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
431
 
391
432
  ## Testing status
392
433
 
@@ -404,7 +445,8 @@ Every tool group was live-tested on 2026-09-28 against a real Proton account, **
404
445
  | `PROTON_DRIVE_ALLOW_SENSITIVE_PATHS` | Optional | Set to `1` to disable the built-in credential-location denylist (not recommended). |
405
446
  | `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
447
  | `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. |
448
+ | `PROTON_DRIVE_WALK_TTL_MS` | Optional | How long `drive_tree`/`drive_search` reuse a cached folder walk, in ms (default 300000; 0 disables the cache). Results can be up to this old, and changes made by other clients or processes are not seen until it expires or `refresh: true`. The first walk of a large drive can take minutes. The cache is dropped for any path this server writes to. |
449
+ | `PROTON_DRIVE_TOOL_TIER` | Optional | `full` (default, all 46 tools) or `core` (18 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. |
408
450
 
409
451
  ---
410
452
 
package/dist/cli.js CHANGED
@@ -11,8 +11,12 @@ import { DriveCliError, DriveCliNotFoundError, DriveNotAuthenticatedError, Drive
11
11
  import { checkCliAvailable } from "./utils/subprocess.js";
12
12
  import { runDoctor, formatDoctor } from "./utils/doctor.js";
13
13
  import { buildEntry, defaultConfigPath, isDirectory, resolveDriveCli, writeEntry, SERVER_KEY } from "./utils/claudeConfig.js";
14
+ import { syncPlan, planBulkMove, planBulkTrash, loadListing, foldersToList } from "./services/plan.js";
15
+ import { invalidatePath } from "./services/walk.js";
14
16
  import { resolve } from "node:path";
15
- import { validateRemotePath, validateLocalPath, validateEmail, validateMessage, validateName, validateFlagValue } from "./utils/validation.js";
17
+ import { driveSearch, driveTree } from "./services/find.js";
18
+ import { driveUsage, driveFindDuplicates, driveSharingAudit } from "./services/analytics.js";
19
+ import { validateRemotePath, validateRemotePathList, validateLocalPath, validateEmail, validateMessage, validateName, validateFlagValue } from "./utils/validation.js";
16
20
  const drive = new DriveService();
17
21
  const args = process.argv.slice(2);
18
22
  const jsonMode = args.includes("--json");
@@ -26,6 +30,12 @@ Commands:
26
30
  version Show CLI/SDK version
27
31
  list <path> List files at path
28
32
  info <path> [--verbose] Show node metadata (--verbose = raw CLI node)
33
+ tree [path] [--depth N] [--limit N] [--folders-only] [--refresh]
34
+ Folder overview with per-folder counts and sizes (default /my-files, depth 2)
35
+ search [path] [--query S] [--glob G] [--type file|folder] [--media-type P] [--ext pdf,docx]
36
+ [--min-size B] [--max-size B] [--after DATE] [--before DATE] [--sort name|size|mtime]
37
+ [--limit N] [--offset N] [--refresh]
38
+ Find files/folders under path (default /my-files) in one walk
29
39
  mkdir <path> Create a new folder
30
40
  upload <local> <remote> [--file-conflict X] [--folder-conflict X] [--confirm]
31
41
  Upload file/folder (files: skip/create-new-revision/rename/replace; folders: skip/merge/rename/replace;
@@ -37,6 +47,7 @@ Commands:
37
47
  move <src> <dst> Move and/or rename
38
48
  delete <path>|--uid <uid> --confirm Delete a file/folder already in trash, permanently
39
49
  share status <path> Show sharing info
50
+ share audit [path] [--refresh] Audit public links, invitees and risk flags under a path (default /my-files)
40
51
  share invite <path> <email> <role> Invite user (viewer/editor/admin)
41
52
  share revoke <path> <email> Revoke one user's access
42
53
  share remove-all <path> --confirm Remove everyone's access + pending invitations
@@ -45,7 +56,15 @@ Commands:
45
56
  share leave <path> Leave a folder shared with you
46
57
  copy <src> <dst-parent-folder> [--name N] Copy file/folder into a folder (optionally under a new name)
47
58
  trash <path> Move to trash
59
+ sync-plan <local> <remote> [--direction up|down|both] [--compare size-mtime|sha1] [--ignore g1,g2] [--limit N]
60
+ Read-only diff of a local folder vs a Drive folder (nothing transferred)
61
+ bulk-move <dst-folder> <src>... [--confirm] Move many items into an existing folder; without --confirm prints the plan only
62
+ bulk-trash <path>... [--confirm] Trash many items; without --confirm prints the plan only
48
63
  trash list List trash contents
64
+ usage [path] [--top N] [--older-than DAYS] [--refresh]
65
+ Storage analytics: totals, largest files/folders, breakdowns, trash (sum of file sizes, not the quota)
66
+ duplicates [path] [--min-size B] [--verify] [--max-verify-bytes B] [--max-verify-total-bytes B] [--limit N] [--refresh]
67
+ Likely duplicate files; --verify downloads and hashes candidates. Never deletes.
49
68
  trash empty --confirm Permanently delete all trash
50
69
  restore <path>|--uid <uid> Restore from trash (uid from 'trash list')
51
70
  invitation list List pending invitations
@@ -86,6 +105,19 @@ function requirePath(value, usage) {
86
105
  }
87
106
  return ""; // unreachable; process.exit(1) above always terminates
88
107
  }
108
+ function pathOrRoot(value) {
109
+ return value && !value.startsWith("--") ? requirePath(value, "<command> [path]") : "/my-files";
110
+ }
111
+ function intFlag(flag) {
112
+ const v = getFlag(flag);
113
+ if (v === undefined)
114
+ return undefined;
115
+ if (!/^\d+$/.test(v)) {
116
+ console.error(`${flag} must be a non-negative integer`);
117
+ process.exit(1);
118
+ }
119
+ return Number(v);
120
+ }
89
121
  function requireEmail(value) {
90
122
  try {
91
123
  return validateEmail(value);
@@ -106,6 +138,19 @@ function getFlag(flag) {
106
138
  }
107
139
  return args[idx + 1];
108
140
  }
141
+ function numFlag(flag) {
142
+ const v = getFlag(flag);
143
+ if (v === undefined)
144
+ return undefined;
145
+ if (!/^\d+$/.test(v)) {
146
+ console.error(`${flag} must be a non-negative integer`);
147
+ process.exit(1);
148
+ }
149
+ return Number(v);
150
+ }
151
+ function numFlags(...names) {
152
+ return Object.fromEntries(names.map((n) => [n, numFlag(`--${n}`)]));
153
+ }
109
154
  function print(data) {
110
155
  console.log(jsonMode ? JSON.stringify(data) : JSON.stringify(data, null, 2));
111
156
  }
@@ -184,6 +229,21 @@ async function run() {
184
229
  case "list":
185
230
  print(await drive.list(requirePath(sub, "list <path>")));
186
231
  break;
232
+ case "tree":
233
+ print(await driveTree(drive, { path: sub && !sub.startsWith("--") ? sub : undefined, ...numFlags("depth", "limit"), foldersOnly: args.includes("--folders-only") || undefined, refresh: args.includes("--refresh") || undefined }));
234
+ break;
235
+ case "search":
236
+ print(await driveSearch(drive, {
237
+ path: sub && !sub.startsWith("--") ? sub : undefined,
238
+ query: getFlag("--query"), glob: getFlag("--glob"),
239
+ type: getFlag("--type"), mediaType: getFlag("--media-type"),
240
+ extensions: getFlag("--ext")?.split(","),
241
+ ...numFlags("limit", "offset"),
242
+ minSize: numFlag("--min-size"), maxSize: numFlag("--max-size"),
243
+ modifiedAfter: getFlag("--after"), modifiedBefore: getFlag("--before"),
244
+ sort: getFlag("--sort"), refresh: args.includes("--refresh") || undefined,
245
+ }));
246
+ break;
187
247
  case "info":
188
248
  print(await drive.info(requirePath(sub, "info <path> [--verbose]"), args.includes("--verbose")));
189
249
  break;
@@ -195,7 +255,7 @@ async function run() {
195
255
  }
196
256
  let local;
197
257
  try {
198
- local = validateLocalPath(rawLocal);
258
+ local = validateLocalPath(rawLocal, { scan: true });
199
259
  }
200
260
  catch (e) {
201
261
  console.error(e instanceof Error ? e.message : String(e));
@@ -297,6 +357,9 @@ async function run() {
297
357
  if (sub === "status") {
298
358
  print(await drive.shareStatus(requirePath(rest[0], "share status <path>")));
299
359
  }
360
+ else if (sub === "audit") {
361
+ print(await driveSharingAudit(drive, { path: pathOrRoot(rest[0]), refresh: args.includes("--refresh") }));
362
+ }
300
363
  else if (sub === "invite") {
301
364
  const [rawPath, rawEmail, role] = rest;
302
365
  if (!rawPath || !rawEmail || !role) {
@@ -365,6 +428,25 @@ async function run() {
365
428
  process.exit(1);
366
429
  }
367
430
  break;
431
+ case "usage":
432
+ print(await driveUsage(drive, {
433
+ path: pathOrRoot(sub),
434
+ top: intFlag("--top"),
435
+ olderThanDays: intFlag("--older-than"),
436
+ refresh: args.includes("--refresh"),
437
+ }));
438
+ break;
439
+ case "duplicates":
440
+ print(await driveFindDuplicates(drive, {
441
+ path: pathOrRoot(sub),
442
+ minSize: intFlag("--min-size"),
443
+ verify: args.includes("--verify"),
444
+ maxVerifyBytes: intFlag("--max-verify-bytes"),
445
+ maxVerifyTotalBytes: intFlag("--max-verify-total-bytes"),
446
+ limit: intFlag("--limit"),
447
+ refresh: args.includes("--refresh"),
448
+ }));
449
+ break;
368
450
  case "trash":
369
451
  if (sub === "empty") {
370
452
  if (!args.includes("--confirm")) {
@@ -386,6 +468,66 @@ async function run() {
386
468
  process.exit(1);
387
469
  }
388
470
  break;
471
+ case "sync-plan": {
472
+ const direction = getFlag("--direction"), compare = getFlag("--compare"), ignore = getFlag("--ignore"), limit = getFlag("--limit");
473
+ if (direction !== undefined && !["up", "down", "both"].includes(direction)) {
474
+ console.error("--direction must be up, down or both");
475
+ process.exit(1);
476
+ }
477
+ if (compare !== undefined && !["size-mtime", "sha1"].includes(compare)) {
478
+ console.error("--compare must be size-mtime or sha1");
479
+ process.exit(1);
480
+ }
481
+ if (!sub || !rest[0]) {
482
+ console.error("Usage: sync-plan <local> <remote> [--direction X] [--compare X] [--ignore g1,g2] [--limit N]");
483
+ process.exit(1);
484
+ }
485
+ print(await syncPlan(drive, {
486
+ localPath: sub, drivePath: rest[0], direction: direction, compare: compare,
487
+ ignore: ignore?.split(","), limit: limit !== undefined ? Number(limit) : undefined,
488
+ }));
489
+ break;
490
+ }
491
+ case "bulk-move": {
492
+ const dst = requirePath(sub, "bulk-move <dst-folder> <src>... [--confirm]");
493
+ const srcs = validateRemotePathList(rest.filter((r) => !r.startsWith("--")), "sources");
494
+ const plan = planBulkMove(srcs, dst, await loadListing(drive, foldersToList(srcs, dst)));
495
+ if (!args.includes("--confirm") || plan.problems.length) {
496
+ print({ applied: false, ...plan });
497
+ if (plan.problems.length)
498
+ process.exit(1);
499
+ console.error("Plan only. Pass --confirm to apply.");
500
+ break;
501
+ }
502
+ try {
503
+ await drive.bulkMove(srcs, dst);
504
+ }
505
+ finally {
506
+ invalidatePath(dst);
507
+ srcs.forEach(invalidatePath);
508
+ }
509
+ console.log(`Moved ${srcs.length} item(s) to ${dst}.`);
510
+ break;
511
+ }
512
+ case "bulk-trash": {
513
+ const paths = validateRemotePathList([sub, ...rest].filter((r) => r !== undefined && !r.startsWith("--")), "paths");
514
+ const plan = planBulkTrash(paths, await loadListing(drive, foldersToList(paths)));
515
+ if (!args.includes("--confirm") || plan.problems.length) {
516
+ print({ applied: false, ...plan });
517
+ if (plan.problems.length)
518
+ process.exit(1);
519
+ console.error("Plan only. Pass --confirm to apply.");
520
+ break;
521
+ }
522
+ try {
523
+ await drive.bulkTrash(paths);
524
+ }
525
+ finally {
526
+ paths.forEach(invalidatePath);
527
+ }
528
+ console.log(`Moved ${paths.length} item(s) to trash.`);
529
+ break;
530
+ }
389
531
  case "restore": {
390
532
  const restoreUid = getFlag("--uid");
391
533
  const restorePath = restoreUid && (!sub || sub.startsWith("--")) ? undefined : requirePath(sub, "restore <path>|--uid <uid>");
@@ -577,7 +719,7 @@ async function run() {
577
719
  const localPaths = [];
578
720
  for (const p of positionals) {
579
721
  try {
580
- localPaths.push(validateLocalPath(p));
722
+ localPaths.push(validateLocalPath(p, { scan: true }));
581
723
  }
582
724
  catch (e) {
583
725
  console.error(e instanceof Error ? e.message : String(e));