Skip to content

assets: zn asset CLI, MCP get_asset, desktop save dialog - #736

Open
gtheys wants to merge 2 commits into
ZenNotes:mainfrom
gtheys:feat/asset-cli-mcp
Open

assets: zn asset CLI, MCP get_asset, desktop save dialog#736
gtheys wants to merge 2 commits into
ZenNotes:mainfrom
gtheys:feat/asset-cli-mcp

Conversation

@gtheys

@gtheys gtheys commented Sep 4, 2026

Copy link
Copy Markdown

Follow-up to #735 — closes the remaining surfaces of #716 (CLI + MCP + desktop save dialog). Stacked on #735: review that first; this diff shrinks to the CLI/MCP/desktop work once it merges.

1. CLI: zn asset list / zn asset get

zn asset list [--json]
zn asset get assets/pic.png                    # binary to stdout (pipe-friendly)
zn asset get assets/pic.png --output ~/x.png [--quiet]
zn asset get assets/pic.png --server home      # remote/self-hosted vault

Backed by a new readAsset() on the VaultBackend seam, so it works identically against a local folder or a server:

  • localvault-ops.readAsset(), vault-root escape-guarded
  • remoteGET /api/assets/raw with Bearer auth, raw bytes (no JSON envelope), proper RemoteRequestError handling (401 gives the existing token hint)

asset is registered as a subcommand group (like folder/task/base) and documented in zn --help under a new ASSETS heading.

2. MCP: get_asset tool

Sibling to list_assets: path in → { path, size, mimeType, base64 } out. MIME is guessed from the extension (same table as the desktop's protocol handler). A 10 MB cap rejects huge binaries with a message pointing agents at zn asset get — a base64 payload over that size isn't useful inside one JSON-RPC message anyway.

3. Desktop: native save dialog

bridge.downloadAsset(relPath) (optional method, so web stays clean):

  • main process opens the native save dialog (suggested name = basename, Documents default — same pattern as PDF/DOCX export)
  • local vault: fs.copyFile
  • remote/self-hosted vault: streams fetchAssetResponse(rel)'s web body to the file via stream/promises pipeline — bytes never land in the renderer
  • preload passthrough; the shared Assets-view Download action prefers the dialog on desktop and keeps the PR-1 blob path for web

Verification

  • vitest: 19 new/updated tests across CLI commands, MCP tool (mime guess, size cap, backend errors, pinned tool list updated), and the helper's desktop-vs-web branching — all green
  • npm run typecheck (all TS workspaces) + go vet/go test — green; web + desktop builds pass
  • Live end-to-end: built the actual cli.js bundle and ran it — local vault list/get (file + stdout piping), and against a running server: remote list/get/--output, plus a wrong-token run producing the clean 401 hint

Server:
- GET /api/assets/raw accepts ?download=1, serving the asset with an
  attachment Content-Disposition (RFC 5987 filename*) so plain browsers
  and <a href> links save the file under its own name.
- The SPA fallback now returns a real 404 for unknown file-looking paths
  (anything with a non-.html extension, e.g. /files/assets/img.png)
  instead of index.html with HTTP 200, which made naive download attempts
  'succeed' while yielding an HTML file. Unknown app routes still fall
  through to the SPA shell.

Web + Desktop UI (shared app-core Assets view):
- New 'Download' context-menu item and a keyboard 'd' on focused asset
  rows. The shared downloadAsset() helper resolves the asset URL exactly
  like embedded images do (same-origin HTTP on web, zen-asset:// scheme
  in the desktop app for local and remote vaults) and saves it via a
  blob + <a download> click, with no bridge-contract change.

Tests: Go coverage for the download flag and the fallback 404; vitest
coverage for the download helper (resolve failure, read failure, and
anchor naming).
CLI:
- zn asset list [--json] and zn asset get <path> [--output <file>]
  [--quiet], backed by a new readAsset() on the VaultBackend seam:
  local vaults read under the root (vault-ops readAsset, path-escape
  guarded), remote vaults stream GET /api/assets/raw with Bearer auth.
  Without --output the bytes go to stdout, so piping works.

MCP:
- New get_asset tool next to list_assets: path in, { path, size,
  mimeType, base64 } out, with a 10 MB cap that points agents at
  zn asset get for bigger files.

Desktop:
- Bridge contract gains optional downloadAsset(relPath): IPC handler
  shows the native save dialog (suggested name = basename), then copies
  the file for local vaults or streams the server's raw-asset response
  for remote/self-hosted vaults. Preload passthrough included; the
  shared Assets-view Download action prefers it and keeps the web blob
  path as fallback.

Tests: vitest for the CLI commands, the MCP tool, and the shared
helper's desktop-vs-web branching. Verified live against a running
server: local list/get, remote list/get/output/401.
@gtheys
gtheys requested a review from adibhanna as a code owner September 4, 2026 08:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant