Skip to content

Latest commit

 

History

History
4139 lines (3163 loc) · 184 KB

File metadata and controls

4139 lines (3163 loc) · 184 KB
last_edited 2026-10-05
title CLI Reference
description Complete command reference for all msgvault commands.

Find a command by task below, or use msgvault COMMAND --help for the flags in your installed binary. This reference follows current main; see the 0.21.0 changelog for features and upgrade notes.

Task Commands and guides
Add and sync a source Choose a source, sync, sync-full
Import local exports import-eml, import-mbox, import-maildir, import-emlx, import-pst, import-slackdump, import-imazing-csv, text imports
Search and browse search, tui, show-message, documents, embeddings, multimodal, eval
Maintain people and contacts person, people guide, CardDAV
Organize accounts identity, collection, update-account
Read meeting evidence meetings, meeting workflow
Export export-messages, export-eml, export-attachments, create-subset
Review and remove mail stage-delete, delete-staged, deduplicate, gc
Back up and manage attachment storage backup, pack-attachments, purge-excluded-media
Repair older records repair-identity, repair-senders, repair-message, repair-derived, repair-labels, repair-list-ids, repair-dates
Operate or integrate setup, credentials, daemon, serve, activity, mcp, query, openapi, agent-token

meetings

Read archived meeting evidence through the selected daemon. Requires API schema 2.27.0 or newer. See the meeting workflow for coverage and source limits.

msgvault meetings context --id 42 --id 43 --format markdown --output context.md
msgvault meetings actions --status pending --assignee alex@example.com --json
msgvault meetings metrics --domain example.com --after 2026-01-01 --json

meetings context

Flag Contract
--id Required meeting message IDs, repeatable; at most 100
--format markdown (default) or json
--include-transcript Include archived transcript evidence; off by default
--max-bytes UTF-8 content budget, default 131072; range 4096–1048576
--output, -o Output file; omitted or - writes the exact packet content to stdout

Mixed or invalid selections fail. Truncated packets identify omitted content and meetings.

meetings actions and meetings metrics

These commands share the following scope flags. Values within a filter group are alternatives; different groups intersect.

Flag Contract
--id Meeting message ID, repeatable
--source-id Source ID, repeatable
--domain Exact participant domain, repeatable
--participant-id Exact participant ID, repeatable
--person-id Durable person ID; mutually exclusive with --participant-id
--after, --before YYYY-MM-DD at UTC midnight; after inclusive, before exclusive
--deletion Source deletion state: any (default), active, or deleted
--json Structured response including coverage or duration bases

ID lists and domain lists accept at most 100 values. IDs must be positive JavaScript-safe integers. Locally deleted records are always excluded.

meetings actions also accepts:

Flag Contract
--status pending, completed, cancelled, or unknown; default all
--assignee Exact assignee email
--query Literal title or description substring, at most 256 characters
--limit Action rows per page, default 50; range 1–200
--cursor Opaque continuation cursor from the previous page

Actions reflect the current archived source snapshot. Keep filters unchanged when continuing a page; refresh from page one to see newer evidence.

Global Flags

Flag Description
--config Path to config file (default: ~/.msgvault/config.toml)
--home Home directory for all data (overrides MSGVAULT_HOME)
-v, --verbose Verbose output (implies --log-level=debug)
--local Use the local daemon instead of a configured remote for archive-access commands
--log-file <path> Override log file path (default: <data_dir>/logs/msgvault-YYYY-MM-DD.log)
--log-level <level> Log level: debug, info, warn, error (default: info)
--no-log-file Disable file logging for this run (stderr output stays on)
--log-sql Log every SQL query at info level (verbose, for debugging)
--log-sql-slow-ms <ms> Slow query threshold in ms (default: 100; 0 uses built-in default)
--help Show help
--agent-url <url> Daemon base URL for delegated mode; must be supplied together with --agent-token-file
--agent-token-file <path> Path to a file containing a restricted agent token secret; must be supplied together with --agent-url
--agent-allow-insecure Allow a plain-HTTP --agent-url; rejected by default

HTTP-Backed CLI Behavior

Commands that access archive state keep their usual stdout/stderr output while using the same API path as remote access:

  1. If [remote].url is configured and --local is not passed, the CLI talks to that remote server.
  2. Otherwise, archive-access commands discover or start the local background daemon and talk to it over HTTP. With [server].daemon_auto_start = false, they use a daemon that is already running or starting and never start one.
  3. --local selects the local daemon even when [remote].url is configured; it is not a request to open SQLite in the CLI process.
  4. With both --agent-url and --agent-token-file, the CLI connects to a remote daemon as a restricted caller. draft-reply, draft-compose, draft-get, draft-edit, draft-delete, and draft-recover are available in this mode. The CLI rejects owner configuration (--config, --home, --local) and never writes the token to logs or argv. It sends the token in the X-Msgvault-Agent-Token header; generated OpenAPI clients do not model this transport detail.

This makes local and remote msgvault behavior the same from the CLI's point of view and avoids opening a large SQLite database from foreground CLI processes.

The local daemon publishes its binary version and API schema version. By default, a newer compatible CLI restarts an older local daemon before issuing the request; configure [server].daemon_auto_restart as newer, never, or always to control that lifecycle behavior. Remote servers are not restarted by clients, so compatibility is negotiated from the API schema version exposed in the OpenAPI document.


init-db

Initialize the archive schema through the configured remote server or the local daemon. When no remote is configured, the CLI starts the local daemon if needed and the daemon owns the database initialization work.

msgvault init-db

add-account

Add a Gmail account and authorize via OAuth.

msgvault add-account <email>
msgvault add-account <email> --headless
msgvault add-account <email> --oauth-app <name>
Flag Description
--headless Show instructions for headless server setup
--oauth-app Use a named OAuth app from [oauth.apps.<name>] in config
--force Delete existing token and re-authorize
--readonly Request Gmail read-only access instead of read + write. Refused if the account already holds write access — see OAuth Setup
--display-name Set a display name for the account
--no-default-identity Do not auto-confirm the email address as this account's "me" identity. Saved across syncs and re-authorization; only explicit --no-default-identity=false clears the choice. See saved identity choice

If [oauth].service_account_key or [oauth.apps.<name>].service_account_key is configured, add-account authorizes via Google service account domain-wide delegation instead of browser OAuth. Service-account accounts do not use --headless, --force, or --readonly; their scope comes from the domain-wide delegation grant in the Admin Console.

Saved default identity choice

add-account (including service accounts), add-imap, add-o365 (IMAP and --graph), and add-teams save --no-default-identity for later syncs. Omitting the flag when adding or re-authorizing an existing account keeps its saved choice. Use --no-default-identity=false to re-enable automatic defaults. New accounts confirm their default identity unless the flag is set.

The flag does not remove existing identities or prevent the one-time legacy [identity] migration from adding configured addresses. If the saved sync configuration cannot be read, automatic identity confirmation logs a warning and skips the write.

Removing the last confirmed identity also saves the no-default choice for future syncs. To restore automatic confirmation, run the source's add command with --no-default-identity=false.


add-imap

Add an IMAP account for syncing mail from any standard IMAP server.

msgvault add-imap --host <hostname> --username <email>

The command prompts interactively for your password (never accepted as a flag to avoid shell history exposure). For scripting or Docker, set MSGVAULT_IMAP_PASSWORD or pipe via stdin:

MSGVAULT_IMAP_PASSWORD="..." msgvault add-imap --host imap.example.com --username user@example.com
# or
echo "$PASS" | msgvault add-imap --host imap.example.com --username user@example.com

It tests the connection before saving credentials.

Flag Default Description
--host (required) IMAP server hostname
--username (required) IMAP username or email address
--port 993 IMAP server port (993 for TLS, 143 for STARTTLS/plain)
--starttls false Use STARTTLS instead of implicit TLS
--no-tls false Disable TLS entirely (plaintext, not recommended)
--no-default-identity false Do not auto-confirm the username as this account's "me" identity. Saved across syncs and re-authorization; only explicit --no-default-identity=false clears the choice. See saved identity choice

Credentials are stored in tokens/imap_<hash>.json with restricted file permissions (0600). Use app-specific passwords when your provider supports them.


After adding an account, sync it with msgvault sync-full. IMAP accounts use the same sync and sync-full commands as Gmail. See Setup Guide for a walkthrough.


draft-reply

Create one reply draft from an archived message to an authorized IMAP destination, or reply within its original Gmail account. The daemon requires the matching operator grant. --from is optional when exactly one confirmed identity is eligible.

msgvault draft-reply <message-id> --body <text>
msgvault draft-reply <message-id> --all --account <account> --from <address> --body <text>
msgvault draft-reply <message-id> --source-id 42 --body= --json

Use --all to include the parent sender, To, and Cc recipients. Msgvault deduplicates them in order, removes the destination account's confirmed identities, and never copies the parent's Bcc recipients. Use --account or --source-id to choose a destination. An offline parent requires one of those selectors; msgvault never treats imported provenance as provider credentials.

The daemon appends the composed message to the configured literal mailbox with the \Draft flag, then stores the local message and its (mailbox, uidvalidity, uid) receipt. The server must advertise UIDPLUS. An accepted APPEND without a receipt returns accepted_unidentified, and a lost connection returns remote_unknown; inspect the mailbox before retrying either result. A request made while that source is syncing returns sync_active; retry after the sync finishes.

Configure the grant by editing the daemon host's config.toml, then restart the daemon:

[[imap.drafts]]
source_id = 42
enabled = true
mailbox = "Drafts"

Requests, Settings, source JSON, environment variables, and client config do not grant access or change the mailbox. The host policy applies per source. For owner callers (API key, browser session, or keyless loopback), any owner caller that can reach the daemon can create drafts on a granted source. A delegated caller authenticated with a restricted agent token additionally requires that the target source and selected sender appear in the token's frozen grant; see agent-token. A draft From choice is local header selection. It does not prove provider send-as permission. Draft creation never sends mail. A later sync reconciles the saved membership when the mailbox's UIDVALIDITY changes. Draft creation never moves an IMAP cursor.

For Gmail, enable drafting per source in the daemon host's config.toml and restart the daemon:

[[gmail.drafts]]
source_id = 42
enabled = true

Gmail accepts draft writes with gmail.modify, mail.google.com, or gmail.compose. Creation also lists send-as entries. That read accepts gmail.settings.basic, gmail.modify, gmail.readonly, or mail.google.com. The daemon checks saved scope information when available; for older tokens without it, Gmail decides whether to allow the request. The daemon accepts --from when it is the primary address or an accepted alias. Service-account sources use the scopes in their delegated assertion.

Gmail writes use one provider request. A 429 or rate-limit 403 is a rejection; retry the command after the limit clears. A transport failure, response-read failure, or 5xx returns remote_unknown. For an uncertain creation, inspect Gmail using the reported RFC822 Message-ID before creating another draft. Managed edits and deletes reconcile on retry as described below. The archive stores the Gmail draft and its DRAFT label after a confirmed response. draft-reply never sends mail.


draft-compose

Create a new plain-text IMAP draft on one selected live source. Provide at least one --to, --cc, or --bcc value. The flags are repeatable and keep their envelope roles.

msgvault draft-compose --account you@example.com \
  --from you@example.com --to recipient@example.com \
  --subject 'Project update' --body 'Draft text'
msgvault draft-compose --source-id 42 --to recipient@example.com \
  --bcc private@example.com --json

The source selector is required. The daemon applies the same host draft policy, confirmed identity check, delegated sender grant, UIDPLUS requirement, and structured provider outcomes as draft-reply. It stores the Bcc envelope in the draft so the mail application can use it. It never sends the message or validates provider send-as rights.

Beeper chat drafts

For a Beeper source, draft-compose leaves a text draft in the composer of an existing chat. --to takes the Beeper chat ID, and --body the text; no other fields apply. Enable it per source in the daemon host's config.toml, the same way as Gmail, and keep Beeper Desktop running:

[[beeper.drafts]]
source_id = 42
enabled = true
msgvault draft-compose --source-id 42 --to '!room:beeper.local' --body 'Draft text'

The chat's composer must be empty. Each chat has at most one managed draft; draft_exists reports the existing one. draft-get, draft-edit, and draft-delete work on the returned draft ID, and draft-delete clears the composer. The edit body must be nonblank. Delegated tokens need draft.create on the Beeper source to create; draft-get accepts draft.edit or draft.delete, draft-edit needs draft.edit, and draft-delete needs draft.delete. Delete-only agents receive draft metadata without committed or pending text, including in error responses. A creator's draft_exists result carries the draft ID without its text. draft-get shows the draft as Beeper last reported it, which can differ from the text sent because Beeper formats it. Edit and delete read the composer first and return draft_conflict when someone changed it in Beeper. Beeper has no conditional write, so text typed between that read and the write can still be cleared. A write Beeper refuses before anything changed returns provider_rejected; a new draft whose text Beeper refused is discarded, so the chat is free for another draft-compose. A write with an unknown outcome returns remote_unknown and keeps the draft's pending operation; the next edit or delete settles it from what Beeper shows. An edit that finds an earlier delete settled, or a new draft's text never delivered, returns draft_discarded. When Beeper shows text msgvault cannot attribute, the command returns pending_operation; clear the composer in Beeper and retry. not_supported means the installed Beeper Desktop does not report chat drafts, chat_not_found that Beeper has no chat with that ID, provider_unavailable that Beeper Desktop or its saved token cannot be reached, and provider_identity_mismatch that Beeper answered for another chat or account.


draft-forward

Create an IMAP draft that forwards an archived message and reuses its retained attachment files. Choose exactly one destination with --account or --source-id, and provide at least one explicit --to, --cc, or --bcc. The parent message's Bcc recipients are never copied. This command requires owner access; agent tokens cannot invoke it.

msgvault draft-forward <message-id> --account you@example.com \
  --from you@example.com --to recipient@example.com --body 'Please review'
msgvault draft-forward <message-id> --source-id 42 \
  --cc team@example.com --json

Every nonempty attachment must have a retained, readable file in the archive's catalog, even when its bytes remain in the original message. Forwarding reuses those catalog references; it does not restore skipped or missing files from raw MIME. Empty attachment parts need no stored file.

A pending, skipped, failed, unavailable, missing, unreadable, or unsupported occurrence stops the command before APPEND. It never creates a partial draft. The IMAP source must have an enabled [[imap.drafts]] entry, and the selected sender must be a confirmed identity on that source.

The draft has two inline plain-text parts: your editable note, then a ---------- Forwarded message ---------- separator with the original headers and text. The original attachments follow, retaining their names and Content-ID values when present. Attached emails (message/rfc822) keep their bytes with 8-bit encoding. An attached email that needs binary encoding, such as one containing NUL bytes, bare line breaks, or lines longer than 998 bytes, is refused. Unknown MIME dispositions are treated as attachments.

The draft has a new Date and Message-ID. Its subject has one Fwd: prefix; existing Fw: and Fwd: prefixes are removed. The X-Msgvault-Forward: 1 header identifies the layout for note editing. A mail client that preserves custom headers may include it when you send the message. msgvault only creates the draft.

When the server supplies a numeric APPENDLIMIT, msgvault checks the encoded size before uploading; otherwise the server's APPEND response decides. This upload check does not limit the memory used to build the draft.

With --json, attachment preflight refusals emit a JSON object on stderr with status: "attachment_preflight_failed" and a problems array. Each problem has a reason, plus filename, part_key, and detail when available:

Reason Meaning
missing_catalog_reference An original attachment has no matching catalog row.
attachment_pending, attachment_skipped, attachment_failed, attachment_unavailable The catalog records a file that is not stored. detail includes its skip reason when available.
unreadable_file Opening, reading, verifying, or closing a stored file failed.
catalog_size_mismatch The stored file's length differs from the original MIME part.
unrepresented_attachment A catalog row has no corresponding original attachment.
attachment_reader_unavailable The daemon cannot access the attachment store.
unrepresentable_attachment An attachment's metadata or bytes cannot be forwarded, or the generated draft cannot retain its catalog reference.

draft-get, draft-edit, draft-delete, and draft-recover

Read, edit, or delete a managed draft created by draft-reply, draft-compose, or draft-forward:

msgvault draft-get <draft-id> [--json]
msgvault draft-edit <draft-id> --revision <n> --body <text> [--json]
msgvault draft-delete <draft-id> --revision <n> [--json]
msgvault draft-recover <draft-id> --revision <n> [--json]

The creation result supplies the opaque draft_id and initial revision.

  • --revision is required for edit, delete, and recover; use the current positive revision.
  • --body is required for edit; --body= sets an empty plain-text body.
  • --json emits one JSON result.

draft-edit --body replaces the note in a draft created by draft-forward and keeps the quoted text and attachments. Other drafts must be text/plain; any other multipart draft returns invalid_draft before the provider draft changes.

draft-get reads retained archive content, including discarded drafts, without connecting to a provider or requiring the source's draft mutation grant. For IMAP drafts, edit and delete continue to require the configured [[imap.drafts]] policy. For Gmail drafts, edit and delete require the configured [[gmail.drafts]] policy and one of gmail.modify, mail.google.com, or gmail.compose. They do not list send-as entries. Delete removes the provider draft and retains its archived content. Gmail edit and delete inspect the current provider message ID before making a change. A Gmail web edit advances the local revision and returns changed_externally; the next mutation must name that revision. These commands never send mail.

Retry a pending Gmail edit or delete with its current revision. The daemon checks Gmail before writing again. If the original message is unchanged, it clears the pending claim and runs the requested operation. If Gmail holds a recorded replacement, the daemon finishes publishing it locally and returns recovered with revision_mismatch; review the new revision before retrying. Other provider edits are adopted with changed_externally. A pending delete can finish locally when Gmail confirms absence, or retry deletion when the original draft is still present. Confirmed deletions with unfinished local cleanup retry that cleanup without another provider request. If Gmail no longer has a draft during an edit retry, the command returns provider_absent and keeps the candidate content. Use draft-delete to finish discarding it.

Recovery applies to IMAP drafts only. draft-recover refuses a Gmail draft ID with not_supported for the owner and not_permitted for delegated tokens. The delegated refusal does not reveal whether the draft exists. Gmail reconciliation uses edit and delete retries. Delegated tokens with draft.create can create Gmail reply drafts. For a Gmail or IMAP draft, delegated draft-get accepts draft.create, draft.edit, or draft.delete. draft-edit requires draft.edit, and draft-delete requires draft.delete. Each command requires the source's exact type and identifier and the draft's archived From sender in the grant. The frozen sender selection applies to get, edit, and delete, including when the owner issued the token with --sender. If Gmail reports an external edit, the daemon checks the adopted draft's sender again before returning it.

A grant with only draft.delete receives lifecycle metadata, including the revision, from get and delete responses. These responses omit content, raw_mime, and candidate_content in JSON and human-readable output, including pending and refused deletes. A matching draft.create or draft.edit grant allows content reads. Delegated recovery continues to return metadata only.

A missing command permission returns HTTP 400 command_not_allowed. A permitted command targeting another source or an unknown draft ID streams not_permitted before revision checks, draft policy, source locking, or any provider request. draft-send-as remains owner-only. For IMAP drafts, recovery resumes a pending operation from recorded receipts. It can publish a known replacement or finish confirmed removal without APPEND. Delegated recovery requires draft.edit for an edit or active repeat and draft.delete for a delete or discarded repeat, scoped to the source in the grant. An active draft with no pending operation requires draft.edit, including after a delete was aborted before writing. Recovery output keeps the saved pending_code. Refused results use refusal_code; pending cleanup results describe the current provider result in observation.code. See Manage a created draft for revision, provider checks, retention, retry behavior, and recovery limits.

Local chat drafts

Slack, Teams, and Discord conversations get drafts that live only in msgvault. They never appear in the provider's composer and no command makes a provider request.

msgvault draft-compose --conversation <conversation-id> --body <text> [--reply-to <message-id>] [--json]
msgvault draft-get --conversation <conversation-id> [--json]

The conversation's source owns the draft. --reply-to names an archived message in the same conversation. draft-get --conversation lists that conversation's drafts oldest first, so a lost draft ID can be found again; --json returns one array. draft-get, draft-edit, and draft-delete take the returned chat-draft- ID and report location as msgvault. A stale revision returns revision_mismatch and leaves the draft unchanged. draft-recover returns not_supported. Delegated tokens use the same permissions as mail drafts, scoped to the conversation's source, except that listing and retrieval need draft.edit or draft.delete. Removing the source or conversation deletes its local drafts.

draft-send-as

List Gmail send-as identities for an owner-invoked Gmail account:

msgvault draft-send-as <account> [--json]

Gmail accepts this read with gmail.settings.basic, gmail.modify, gmail.readonly, or mail.google.com. Saved scope information is checked when available. The command reports the address, display name, primary and default flags, verification status, and whether the address is a confirmed msgvault identity. Delegated agent tokens cannot run this command. It does not require [[gmail.drafts]] and never changes Gmail.


list-folders

List the selectable folders in one or all configured IMAP accounts, including an approximate message count for each folder.

msgvault list-folders [account]

Use the folder names in repeated --folder or --skip-folder flags on sync-full and sync. When the account argument is omitted, the command lists folders for every configured IMAP account. See IMAP Folder Sync for examples and matching rules.


add-o365

Add a Microsoft 365 or Outlook.com account via OAuth2 with XOAUTH2 IMAP authentication.

msgvault add-o365 <email>

The command opens your browser for Microsoft OAuth consent, then configures IMAP with XOAUTH2 automatically. The correct IMAP host is auto-detected: outlook.office.com for personal accounts (hotmail.com, outlook.com, live.com, msn.com) and outlook.office365.com for organizational accounts.

Requires a [microsoft] section with client_id in config.toml. See the OAuth Setup guide for Azure AD app registration.

Flag Default Description
--tenant common Azure AD tenant ID (restricts which accounts can authorize)
--headless false Sign in with a device code instead of a local browser
--no-default-identity false Do not auto-confirm the email address as this account's "me" identity. Saved across syncs and re-authorization; only explicit --no-default-identity=false clears the choice. See saved identity choice
--graph false Sync through the Microsoft Graph mail API instead of IMAP. Creates an msmail account. Needs the Mail.Read permission. delete-staged asks for Mail.ReadWrite on first use
--as — With --graph: sign in as this user and sync <email> as a shared or delegated mailbox the user can open. Needs the Mail.Read.Shared permission. delete-staged does not support these accounts yet

After adding the account, sync it with msgvault sync-full. For a --graph account, use msgvault sync. See Microsoft Graph mail sync.


add-teams

Authorize a Microsoft Teams account through delegated Microsoft Graph OAuth and register a teams source.

msgvault add-teams <email>
msgvault add-teams <email> --tenant <tenant-id>

This stores a Teams Graph token under tokens/teams_<email>.json, separate from the Microsoft IMAP token used by add-o365. Requires [microsoft].client_id in config.toml and the Graph permissions documented in Microsoft Teams.

Flag Default Description
--tenant common Azure AD tenant ID to use for authorization
--headless false Sign in with a device code instead of a local browser
--no-default-identity false Do not auto-confirm the email address as this source's "me" identity. Saved across syncs and re-authorization; only explicit --no-default-identity=false clears the choice. See saved identity choice

After adding the account, sync it with msgvault sync-teams.


add-discord

Validate a Discord bot token and register one or more guilds as discord sources. The token is read from a masked prompt or piped stdin; there is no token flag.

msgvault add-discord
msgvault add-discord --guild 123456789012345678
msgvault add-discord --oauth-app archive-bot \
  --guild 123456789012345678 --guild 234567890123456789

If the bot can access exactly one guild, it is selected automatically and echoed for confirmation. Otherwise, repeat --guild to select exact guild IDs. One guild becomes one source. Setup also reports likely Message Content Intent, channel-history, and private-thread access issues.

Flag Default Description
--guild sole accessible guild Guild ID to register (repeatable/comma-separated)
--oauth-app sole/default bot Discord token binding label; no [oauth.apps] entry is required

After registering a guild, sync it with msgvault sync-discord. See Discord for least-privilege bot setup and multi-bot binding rules.


sync-full

Download all messages from a Gmail or IMAP account. The optional account can be an identifier or display name. When it is omitted, the command syncs all configured syncable accounts.

msgvault sync-full [account] [flags]
Flag Description
--limit N Maximum messages to download
--after YYYY-MM-DD Only messages after this date
--before YYYY-MM-DD Only messages before this date
--query Gmail search query filter
--noresume Ignore checkpoints, start fresh
--folder NAME Scan this IMAP folder (repeatable)
--skip-folder NAME Skip this IMAP folder (repeatable)
--source-id ID Sync exactly one source by numeric ID; mutually exclusive with the account argument
--build-cache Queue a cache refresh after the sync, even inside min_rebuild_interval
--no-build-cache Skip the post-sync cache refresh
--verbose Detailed progress output

An account token can select more than one matching source. Use --source-id when Gmail and IMAP, or two other sources, share the same identifier.

The CLI sends the sync request to the configured remote server or local daemon and streams the daemon's stdout/stderr back to the terminal. This keeps local and remote full sync behavior aligned and avoids running a separate direct SQLite writer beside msgvault serve.

sync-full does not infer source deletion from messages absent from its result set. This is especially important with --query, --limit, or date filters, where the listing is intentionally incomplete. Gmail source-deletion reconciliation happens only during the complete mailbox recovery described under sync.


sync

Sync new and changed messages. Gmail accounts use the Gmail History API; IMAP accounts perform a mailbox scan and skip messages already in the database. The optional account can be an identifier or display name. When it is omitted, the command syncs all accounts that have completed an initial full sync.

msgvault sync [account] [flags]
Flag Description
--folder NAME Scan this IMAP folder (repeatable)
--skip-folder NAME Skip this IMAP folder (repeatable)
--source-id ID Sync exactly one source by numeric ID; mutually exclusive with the account argument
--build-cache Queue a cache refresh after the sync, even inside min_rebuild_interval
--no-build-cache Skip the post-sync cache refresh

The CLI sends the incremental sync request to the configured remote server or local daemon and streams the daemon's stdout/stderr back to the terminal. The daemon serializes this work with other archive mutations.

The same cache flags apply to message-writing sync-* commands. By default, manual syncs leave a usable stale cache in place until the minimum rebuild interval expires. The daemon owns any refresh after the sync command returns. The two flags are mutually exclusive.

For sync and sync-full, either cache flag requires daemon API schema 2.31.0 or newer. The CLI reports an upgrade error before starting sync against an older daemon.

Folder filters are applied only to IMAP accounts. See IMAP Folder Sync for examples and matching rules.

For Gmail, normal incremental sync consumes History API deletion events and marks those messages as deleted from the source. If Gmail says the saved history cursor has expired, msgvault recovers with an unfiltered, unlimited snapshot of the complete mailbox, then consumes changes made while that snapshot was in progress. Only a successfully completed snapshot is used to mark missing messages as source-deleted; interrupted or partial listings do not reconcile absence.

Source deletion updates archive metadata rather than deleting local message or raw MIME data. Search excludes source-deleted messages by default; use msgvault search ... --deletion-scope deleted or --deletion-scope any to find retained history. See Source-Deleted Messages.


sync-teams

Sync Microsoft Teams chats and channels for an authorized account.

msgvault sync-teams <email>
msgvault sync-teams <email> --no-channels
msgvault sync-teams <email> --limit 100
msgvault sync-teams <email> --full

Full versus incremental sync is detected from stored cursors and checkpoints. --full ignores those cursors and re-fetches messages, upserting rows in place so importer upgrades can repair existing data without creating duplicates.

Flag Default Description
--no-channels false Sync chats only and skip team channels
--limit 0 Maximum messages per conversation (0 = unlimited)
--full false Ignore stored cursor and re-fetch every message

See Microsoft Teams for setup, scheduling, search, and inline media backfill.


sync-discord

Sync one registered Discord guild, or every registered guild when the selector is omitted.

msgvault sync-discord [guild-id-or-name]
msgvault sync-discord 123456789012345678 --after 2025-01-01
msgvault sync-discord 123456789012345678 --full

One-guild selection accepts an exact guild ID or an unambiguous display name. Without a selector, sources run sequentially in stable source order. One guild failure does not block later guilds, but the overall command returns an aggregate error.

Flag Default Description
--full false Ignore stored cursors and re-fetch all available history, repairing rows and historical deletions
--after — Exclusive lower bound in YYYY-MM-DD or RFC3339 form

Normal runs re-scan the configured trailing edit window, seven days by default. --full --after bounds both re-fetch and deletion repair and leaves earlier rows untouched. See Discord for per-channel/thread checkpoint and deletion semantics.


add-granola

Register a configured Granola account and validate its API key with a live API call.

msgvault add-granola [identifier]

Reads the key from the matching [[granola]] entry in config.toml. With a single configured entry the identifier may be omitted. Granola API keys are created in the desktop app's settings and require a Business plan. The command also confirms the source's effective account_email, even when other aliases already exist. Meeting-source identity confirmation is mandatory.

After adding the account, sync it with msgvault sync-granola. If you later change account_email or add/remove an alias with msgvault identity, run msgvault sync-granola <identifier> --full to repair existing is_from_me attribution.


sync-granola

Sync Granola meeting notes and transcripts.

msgvault sync-granola [identifier]
msgvault sync-granola --limit 5
msgvault sync-granola --full --after 2024-01-01

Incremental by default: only notes updated since the last successful run are fetched. With no identifier, every configured [[granola]] source is synced. Re-fetched notes are upserted in place, so --full repairs existing rows without creating duplicates. A partial run with one or more failed notes is recorded and returned as an error without advancing the successful cursor. If other notes were added or updated first, the cache is refreshed before the error is returned. Scheduled sync refuses a configured source that has been removed from the archive and directs you to run add-granola again.

Flag Default Description
--limit 0 Maximum notes per run (0 = unlimited)
--after — Full-sync only notes created after this date (YYYY-MM-DD; implies --full)
--full false Ignore stored cursor and re-fetch every note

See Meeting Transcripts for setup and what gets stored.


add-notion-meetings

Validate a configured read-only Notion integration and register its meeting source without printing meeting content.

msgvault add-notion-meetings [identifier]

The matching [[notion_meetings]] entry requires account_email and token. With one entry, the identifier may be omitted.


sync-notion-meetings

Sync Notion AI Meeting Notes from the attendee-visible recent window.

msgvault sync-notion-meetings [identifier]
msgvault sync-notion-meetings --limit 3
msgvault sync-notion-meetings --full --after 2026-01-01
msgvault sync-notion-meetings --probe

Notion returns at most 50 meeting notes and exposes no discovery cursor. Every run hydrates and checksum-verifies the selected visible meetings. --full also sends matching snapshots through archive upsert so cursor/archive divergence can be repaired; --after filters the visible set locally. Partial coverage is reported when Notion says more records exist. Due transcript maintenance runs outside --limit.

Flag Default Description
--limit 0 Maximum visible meetings hydrated and verified; due maintenance is additional
--after — Local visible-set lower bound in YYYY-MM-DD form; implies --full
--full false Force selected snapshots through archive upsert instead of skipping matching checksums
--probe false Validate capabilities and result shape without printing meeting content

See Meeting Transcripts for setup, privacy, retry behavior, and stored evidence.


add-plaud

Authorize and register a configured Plaud cloud account using browser OAuth.

msgvault add-plaud [identifier]

With one configured [[plaud]] entry, omit the identifier. The browser callback runs on the daemon host at localhost:8091/callback/plaud. A configured remote refuses before proxying; use SSH with that port forwarded and run msgvault --local add-plaud <identifier> on the daemon host. Credentials are stored in tokens/plaud_<identifier>.json and bound to the exact MCP endpoint.

The live account email must equal configured account_email before source registration. A source's confirmed owner cannot change under the same identifier. Use a new identifier for another account. See Plaud configuration.

sync-plaud

Archive Plaud cloud recordings, complete transcripts, and every note tab. Cloud Sync and upstream transcription must already be enabled. Audio is not downloaded; no changes propagate to Plaud.

msgvault sync-plaud [identifier]
msgvault sync-plaud work --limit 20
msgvault sync-plaud work --full --after 2025-01-01
msgvault sync-plaud work --probe
Flag Description
--limit n Hydrate at most n recordings; newest first, then rotate through least recently attempted. Failed recordings retry on their next turn. 0 is unlimited; negatives fail
--full Force archive repair, preserving stable source and file IDs
--after YYYY-MM-DD Filter recording dates locally; implies --full and retains rotation progress
--probe Print tool names, input schemas, and first-page counts without personal content or archive writes; requires an identifier when multiple accounts are configured
--build-cache Refresh analytics cache after sync
--no-build-cache Skip analytics cache refresh; mutually exclusive with --build-cache

Without an identifier, sync every configured Plaud entry. Every run validates the live account owner and checks complete content for edits. Sync refuses an unregistered source before authentication; run add-plaud first.

Missing pending transcripts and notes preserve previous evidence. Deleted recordings remain archived. Failed and canceled runs save rotation progress and refresh committed changes before returning the error. See the meeting guide for pagination and consistency limits.

add-circleback

Authorize a configured Circleback account using browser OAuth (their MCP server uses OAuth with dynamic client registration).

msgvault add-circleback [identifier]

The token is stored under tokens/circleback_<identifier>.json. With a single configured [[circleback]] entry the identifier may be omitted. The command always confirms the source's effective account_email; there is no identity opt-out flag.

Circleback redirects OAuth to localhost:8090. A CLI configured for a remote daemon refuses to proxy this command: run it on the daemon host, where the token is stored. Over SSH, use ssh -L 8090:localhost:8090 user@daemon-host, run msgvault --local add-circleback <identifier> in that remote shell, and open the printed authorization URL in your local browser if necessary. On the daemon host, --local selects that host's archive; on a workstation it would target a separate local archive.

After adding the account, sync it with msgvault sync-circleback. Run a --full sync after changing the primary email or confirmed aliases.


sync-circleback

Sync Circleback meetings, notes, action items, and transcripts.

msgvault sync-circleback [identifier]
msgvault sync-circleback --limit 5
msgvault sync-circleback --full --after 2024-01-01
msgvault sync-circleback --probe

Incremental by default: each run enumerates meeting IDs without a date bound so newly created backfills are discovered even when their scheduled date is old. Unknown meetings and known meetings created within the 48-hour refresh overlap are fetched in detail; unchanged snapshots are skipped without invalidating the search cache. With no identifier, every configured [[circleback]] source is synced. Missing or recognized-empty transcripts enter a bounded pending state and retry every six hours, normally until seven days after the scheduled meeting time (48 hours when no usable time exists). Expired retries become unavailable; a later --full run can check them again.

Due transcript retries are maintenance work and run outside the new-meeting limit. Provider, contract, missing-result, ingest, archive-recovery, and cancellation failures fail the sync and preserve the prior successful cursor.

Flag Default Description
--limit 0 Maximum newly searched meetings; due maintenance items are additional (0 = unlimited)
--after — Full-sync only meetings after this date (YYYY-MM-DD; implies --full)
--full false Ignore stored cursor and re-fetch every meeting
--probe false Print the MCP tool inventory and a sample result instead of syncing

See Meeting Transcripts for setup and what gets stored.


add-muesli

Check a configured local Muesli database and register it as a meeting source.

msgvault add-muesli [identifier]

The matching [[muesli]] entry requires account_email; db_path defaults to ~/Library/Application Support/Muesli/muesli.db. With one entry, the identifier may be omitted. The command fails unless the file opens read-only as a Muesli database on the daemon's host.


sync-muesli

Archive completed meetings from a local Muesli database.

msgvault sync-muesli [identifier]
msgvault sync-muesli mac --limit 5
msgvault sync-muesli --after 2026-01-01
msgvault sync-muesli --full

Every run reads the whole database read-only and updates changed meetings in place; unchanged meetings are skipped. Attendees are resolved through Apple Contacts unless contacts = false; the summary reports whether Contacts was complete, partial, unavailable, or off. Meetings still recording or processing wait for a later run, and meetings deleted in Muesli stay archived. With no identifier, every configured [[muesli]] source is synced.

Flag Default Description
--limit 0 Maximum meetings processed per run (0 = unlimited)
--after — Only meetings that start on or after this UTC date (YYYY-MM-DD)
--full false Rewrite every archived meeting, even unchanged ones, to refresh attribution

See Meeting Transcripts for setup and what gets stored.


archive-remote-images

Download remote <img src> images from existing email for offline viewing. This is separate from the default-off [sync].archive_remote_images setting.

msgvault archive-remote-images --allow-tracking
msgvault archive-remote-images --allow-tracking --source-id 1 --limit 100
Flag Default Description
--allow-tracking false Required consent to image requests that can activate tracking pixels
--source-id 0 Restrict to one source; 0 includes all email sources
--limit 0 Maximum messages to scan; 0 scans all matching email

Requests originate from the archive server, not the browser. Private-network destinations are blocked, but public hosts can still track requests. The command requires --allow-tracking even when automatic archiving is enabled.

Already archived images are reused. Failed downloads can be retried by running the command again; successful downloads are preserved if other images fail. The summary reports messages scanned, images downloaded or reused, and errors. Any image errors produce a nonzero exit status. Original messages are unchanged.


backfill-teams-media

Re-fetch Microsoft Teams inline hosted-content media for already imported messages.

msgvault backfill-teams-media <email>
msgvault backfill-teams-media <email> --only-incomplete

The command scans stored Teams HTML bodies for Graph hostedContents URLs and downloads those images into the attachment store. It is idempotent because attachments are content-addressed.

Flag Default Description
--only-incomplete false Retry only messages whose inline media is still missing

backfill-discord-media

Refresh Discord message attachment metadata and retry pending downloads for one guild or every registered guild.

msgvault backfill-discord-media [guild-id-or-name]
msgvault backfill-discord-media 123456789012345678 --only-incomplete

By default, all archived Discord messages with attachments are scanned; already-complete messages require no download work. With no selector, guilds run sequentially using the same aggregate-failure behavior as sync-discord.

Flag Default Description
--only-incomplete false Select only messages with pending attachment rows

Backfill re-fetches each selected source message that has pending attachments to obtain fresh signed CDN URLs. An incomplete attachment is unrecoverable if the source message has since been deleted. See Discord.


add-beeper

Register the chat accounts connected to a locally running Beeper Desktop as beeper sources, one per network.

msgvault add-beeper
msgvault add-beeper --token-file ~/beeper-token.txt
MSGVAULT_BEEPER_TOKEN="..." msgvault add-beeper

Requires Beeper Desktop running on the same machine and an access token minted in Beeper Desktop (Settings → Developer). The token is stored at tokens/beeper.json. Accounts filtered out by [beeper].accounts / exclude_accounts in config.toml are skipped.

Networks Beeper serves natively instead of bridging — iMessage — are absent from its accounts API, so they are found from chat data and reported as found via chats. Re-run the command after connecting a network in Beeper Desktop.

Flag Default Description
--token-file Read the access token from a file instead of prompting
--no-default-identity false Do not auto-confirm each account's own identity as that source's "me" identity

After adding, sync with msgvault sync-beeper.


sync-beeper

Sync chats from Beeper Desktop for every registered Beeper account (all connected networks). The first run backfills full locally-available history and is resumable; later runs are incremental. Per-account failures do not stop the run: remaining accounts still sync, the analytics cache is rebuilt for the successful ones, and the command exits non-zero listing the failures. Without --account, the [beeper] config accounts/exclude_accounts filters select which registered sources sync. See Beeper.

msgvault sync-beeper
msgvault sync-beeper --account signal --account telegram
msgvault sync-beeper --full
Flag Default Description
--account all registered Beeper accountID to sync (repeatable)
--limit 0 Max messages per chat this run (0 = no limit; limited backfills resume next run)
--full false Ignore stored cursors and re-fetch every message (repairs rows in place)
--no-media false Skip attachment downloads for this run

backfill-beeper-media

Retry pending Beeper attachment downloads (media that failed or exceeded the size cap during sync-beeper). Idempotent: attachments are content-addressed. If the source message is gone (deleted in Beeper), its pending markers are cleared permanently — already-downloaded attachments are kept. Transient fetch errors keep the marker and count as still pending, so re-running retries them.

msgvault backfill-beeper-media
msgvault backfill-beeper-media --account signal
Flag Default Description
--account all registered Beeper accountID to backfill (repeatable)

add-slack

Register a Slack workspace as a slack source. Requires a user token (xoxp-…) from an internal Slack app you create (see the usage guide for setup and scope choices). The token is validated with auth.test and stored at tokens/slack_<team-id>_<user-id>.json. Public-channel-only tokens need no search, file, or reaction permissions; sync revisits history for thread replies when search:read is absent.

msgvault add-slack
msgvault add-slack --token-file ~/slack-token.txt
MSGVAULT_SLACK_TOKEN="xoxp-..." msgvault add-slack
Flag Default Description
--token-file Read the user token from a file instead of prompting
--no-default-identity false Do not auto-confirm the workspace user ID as the source's "me" identity

After adding, sync with msgvault sync-slack.


import-slackdump

Import a Slackdump directory or ZIP without contacting Slack. --me resolves your account from users.json by exact Slack user ID or unique profile email. The import preserves conversations, threads, reactions, raw JSON, identities, and exported files. The path must be local to the daemon host; exports are not uploaded through a configured remote. See Slack.

msgvault import-slackdump --me you@example.com /path/to/export
msgvault import-slackdump --me U0123456789 --limit 100 /path/to/export.zip
Flag Default Description
--me required Slack user ID or unique profile email in the export
--limit 0 Max messages imported per conversation (0 = no limit)
--max-media-mb configured/default limit Max exported file size in MiB (0 = configured/default limit)
--no-default-identity false Do not auto-confirm the workspace user ID as the source's "me" identity

sync-slack

Sync Slack conversations for registered workspaces. Public-channel-only tokens can archive all public channels, including unjoined ones. Broader tokens archive your channel memberships, group DMs, and 1:1 DMs. The first run backfills full history and is resumable; later runs are incremental and sweep for thread replies created since the last run (any thread age). Per-workspace failures do not stop the run: remaining workspaces still sync and the command exits non-zero listing the failures. The [slack] config channels/exclude_channels filters select which channels sync. The private_channels, dms, and group_dms settings independently select whether private channels, DMs, and group DMs sync. See Slack.

msgvault sync-slack
msgvault sync-slack T0123456789
msgvault sync-slack --full
msgvault sync-slack --private-channels=false --dms=false --group-dms=false
Flag Default Description
--limit 0 Max messages of work per conversation this run, thread replies included; the reply sweep gets the same budget workspace-wide (0 = no limit; every phase resumes next run so standing limited schedules converge; only the maintenance rescan is skipped)
--dms configured Include one-to-one DMs for this run (true or false)
--full false Start (or continue) a repair session: re-fetch every message, upserting in place (catches old thread replies and edits). Interrupted or --limit-scoped repairs resume across later runs of any kind until complete
--group-dms configured Include group DMs for this run (true or false)
--private-channels configured Include private channels for this run (true or false)
--no-threads false Skip thread-reply fetching for this run (a later threaded run pays the debt automatically)
--maintenance false Repair edits/reaction changes on recent messages (ignored by default after capture)
--no-media false Skip file downloads for this run (files become pending markers; backfill-slack-media fetches them later)

backfill-slack-media

Retry pending Slack file downloads (files that failed or exceeded the size cap during sync-slack). Idempotent: files are content-addressed and already-downloaded ones are never re-fetched. Files hosted outside files.slack.com are metadata-only link rows and are never downloaded.

msgvault backfill-slack-media
msgvault backfill-slack-media T0123456789

add-calendar

Authorize read-only Google Calendar access for an account and register its calendars for sync. If the account already has a Gmail token, re-consent bundles Gmail + Calendar so Gmail access is not dropped — keep both checked on the consent screen. The Calendar API must be enabled on the OAuth project. By default only owned/writable calendars are registered.

msgvault add-calendar <email> [flags]
Flag Description
--write Also request calendar.events, preserving existing Google scopes; source write permissions remain required
--oauth-app Named OAuth app to use
--headless Print token-copy instructions for a headless host instead of opening a browser
--all-calendars Include reader/freeBusyReader (subscribed, holiday) calendars
--min-access-role Minimum access role: owner, writer, or reader
--calendars Comma-separated calendar IDs to register

sync-calendar

Sync Google Calendar events for an account. The account is resolved from a [[gcal]] config entry (by name or email) or used directly as an email. The first run (or --full) does a full sync that registers calendars; later runs are incremental via the Calendar syncToken. Events are stored as searchable records (message_type = calendar_event) and become eligible for semantic search when the embedding worker runs. Cancelled events are retained and marked cancelled, never deleted. Sync is read-only.

msgvault sync-calendar <name|email> [flags]
Flag Description
--full Force a full sync (ignore stored sync tokens)
--limit Max events per calendar (0 = unlimited)
--after / --before Bound a full sync to a date range (YYYY-MM-DD); full sync only
--calendar Restrict to specific calendar IDs
--all-calendars Include reader/freeBusyReader calendars
--min-access-role Minimum access role: owner, writer, or reader
--oauth-app Named OAuth app to use
--noresume Do not resume an interrupted full sync

calendar

Control live Google Calendar events through the daemon. This is unreleased functionality. Follow Calendar event setup for consent and source permissions.

msgvault calendar create <calendar-id> --account <name|email> --summary <title> --from <start> --to <end>
msgvault calendar update <calendar-id> <event-id> --account <name|email> [field flags]
msgvault calendar delete <calendar-id> <event-id> --account <name|email>
msgvault calendar move <calendar-id> <event-id> <destination-calendar-id> --account <name|email>
msgvault calendar respond <calendar-id> <event-id> --account <name|email> --status accepted
msgvault calendar freebusy <calendar-id> --account <name|email> --from <start> --to <end>
msgvault calendar conflicts <calendar-id> --account <name|email> --from <start> --to <end> --calendars <ids>
Flag Applies to Contract
--account All Required OAuth account or configured source name, separate from the target calendar
--dry-run All Verify live access and return the proposed writes without applying them; availability still reads Google
--read-only All Reject all mutations
--json All Print complete results and archive receipts; dry runs and availability always print JSON
--send-updates Writes none (default), all, or externalOnly
--summary, --description, --location Create/update Omitted update fields are preserved; explicit empty clears
--from, --to Create/update/availability RFC3339 with offset, or local YYYY-MM-DDTHH:MM with --tz
--tz Create/update/availability IANA time zone; event edits require a time bound
--all-day Create/update Date-only YYYY-MM-DD; end is exclusive
--attendees Create/update Replace comma-separated guest list; explicit empty clears
--add-attendee Update Add guest emails while preserving existing guests and RSVP state; cannot combine with --attendees
--rrule Create/update Repeatable RRULE; explicit empty clears recurrence
--reminder Create/update Repeatable popup:minutes or email:minutes (0–40320, at most five); default or none cannot combine with overrides
--scope Update/delete/respond single (default), all, or future; future supports update/delete only
--original-start Update/delete/respond Original occurrence start, RFC3339 or all-day date; required for single on a series ID and future edits
--destination Move Required calendar ID or configured alias; standalone events only
--status Respond Required self RSVP: accepted, declined, or tentative
--calendars Availability Selected IDs or aliases; default is the positional calendar; at most 50

Commands return completed Google writes with archive message IDs. A partial remote failure or an archive failure exits with an error after printing the receipts. Reconcile the reported event; do not repeat a completed mutation.


import-eml

Import RFC 5322 .eml files inside MailMate-style .mailbox directories. Arbitrary loose .eml files are not discovered. The directory structure provides mailbox labels. The command resumes an active checkpoint by default.

msgvault import-eml /path/to/mail --identifier user@example.com
Flag Default Description
--identifier required Source identifier for imported messages
--source-type eml Source type stored for this import
--no-resume false Start a new import rather than resume its checkpoint
--checkpoint-interval 200 Messages between saved checkpoints
--no-attachments false Do not write attachment files
--no-default-identity false Do not confirm the identifier as this source's identity

See local email import for file discovery and repeat-import behavior, and remote images for the separate opt-in that can cause network requests during email imports.


import-mbox

Import a local MBOX archive into msgvault.

msgvault import-mbox <identifier> <export-file>

The export file may be a plain mbox file (any extension) or a .zip containing one or more .mbox/.mbx files.

Flag Default Description
--source-type mbox Source type recorded in database (e.g., hey or google-groups)
--label — Label(s) to apply to imported messages (repeatable, or comma-separated)
--no-resume false Start fresh, ignoring interrupted progress
--checkpoint-interval 200 Save progress every N messages
--no-attachments false Skip writing attachments to disk
--no-default-identity false Do not auto-confirm the identifier as this source's "me" identity

For Google Groups Takeout, use --source-type google-groups. This preserves Groups thread IDs and labels, and does not auto-confirm the group identifier as "me". Import a Groups-only ZIP or an extracted group MBOX; non-message files are skipped.

See Importing Local Email for usage examples.


import-maildir

Import a Maildir archive without changing its files:

msgvault import-maildir ~/Maildir --identifier you@example.com

Reads regular message files in cur and new; skips tmp, symlinks, and mailbox bookkeeping files. The root mailbox receives the INBOX label. Nested directories and Maildir++ folders such as .Projects.Go become slash-separated labels (Projects/Go). Use a stable snapshot of the mailbox so delivery and flag renames cannot race the import.

Stores raw MIME, bodies, recipients, and attachments. As with other local email imports, attachment-write failures are logged; the original attachment bytes remain in raw MIME, but a rerun does not retry those writes. Exact duplicate raw messages within the same source are stored once and collect all folder and flag labels. Rerunning the import adds missing messages and labels; it does not remove archived messages or labels. Moving a message from new to cur or changing its filename flags does not create another copy.

Maildir flags become DRAFT, STARRED, PASSED, REPLIED, and TRASH labels. Messages without the seen (S) flag receive UNREAD. Trashed messages are still archived.

Flag Default Description
--identifier — Required account identifier
--source-type maildir Source type stored for the archive
--no-resume false Start a fresh run instead of resuming a checkpoint
--checkpoint-interval 200 Save progress after this many messages
--no-attachments false Skip writing attachment files
--no-default-identity false Skip confirming the identifier as the source identity

Interrupted imports resume by rescanning the tree and skipping stored messages. Messages larger than 128 MiB are reported as errors and left unimported.


import-emlx

Import Apple Mail .emlx files into msgvault. Can auto-discover accounts from macOS Accounts4.sqlite or accept explicit arguments.

# Auto-discover accounts (reads ~/Library/Accounts/Accounts4.sqlite)
msgvault import-emlx

# Specify mail directory
msgvault import-emlx <mail-dir>

# Legacy form: explicit identifier and directory
msgvault import-emlx <identifier> <mail-dir>

The mail directory should be an Apple Mail mailbox tree containing .mbox or .imapmbox directories, each with a Messages/ subdirectory of .emlx files. You can also point directly at a single .mbox directory. Labels are derived from directory names.

Apple Mail's N.partial.emlx files are also imported. Apple Mail keeps the attachments of these messages outside the MIME payload, in a sibling Attachments/N/ directory. The importer restores cached attachments directly inside the message's outer multipart, within the message size limit. Attachments in nested parts, such as inside some forwarded messages, are not restored. An attachment without a cached file stays absent; unreadable files or directories produce a warning. Re-importing a partial message adds newly cached attachments to the existing message without creating another copy. When the archived message already holds every cached attachment, the rerun skips it instead of rewriting it. If both N.emlx and N.partial.emlx exist, the complete N.emlx copy wins. The command summary reports the number of partial files read and how many attachments the run added to the archive.

Flag Default Description
--source-type apple-mail Source type recorded in database
--account — Filter to specific account(s) during auto-discover (repeatable)
--accounts-db — Custom path to macOS Accounts4.sqlite
--identifier — Manual identifier when auto-discover is not suitable
--no-resume false Start fresh, ignoring interrupted progress
--checkpoint-interval 200 Save progress every N messages
--no-attachments false Skip writing attachments to disk
--no-default-identity false Do not auto-confirm the identifier as this source's "me" identity

See Importing Local Email for usage examples.


import-pst

Import a Microsoft Outlook PST archive into msgvault.

msgvault import-pst <identifier> <pst-file>

The importer preserves PST folder structure as labels, imports email messages, and skips non-email PST items such as calendar entries, contacts, tasks, and notes.

Flag Default Description
--source-type pst Source type recorded in database
--skip-folder — Folder name to skip, case-insensitive; repeat for multiple folders
--no-resume false Start fresh, ignoring interrupted progress
--checkpoint-interval 200 Save progress every N messages
--no-attachments false Skip writing attachments to disk

See Importing Local Email for usage examples.


import-whatsapp

Import messages from a decrypted Android msgstore.db or Apple ChatStorage.sqlite WhatsApp database. Apple support currently imports text, participants, and available names; native Apple attachments are not imported.

msgvault import-whatsapp <msgstore.db> --phone <your-number>

The --phone flag is required and must be in E.164 format (e.g., +447700900000).

Flag Required Description
--phone Yes Your phone number in E.164 format (must start with +)
--contacts No Path to contacts .vcf file for name resolution
--media-dir No Path to decrypted Media folder for attachments
--limit No Limit number of messages (for testing)
--display-name No Display name for the phone owner
--no-default-identity No Do not auto-confirm the phone number as this source's "me" identity

See Text Messages for usage examples.


import-imessage

Import messages from the local iMessage database on macOS. Requires Full Disk Access in System Settings.

msgvault import-imessage

Reads from ~/Library/Messages/chat.db by default. This is a read-only operation.

Flag Default Description
--db-path ~/Library/Messages/chat.db Path to chat.db
--before — Only messages before this date (YYYY-MM-DD)
--after — Only messages after this date (YYYY-MM-DD)
--limit 0 Limit number of messages (for testing)
--me — Your phone/email for recipient tracking
--contacts — Path to contacts .vcf file for display-name backfill

See Text Messages for usage examples.


import-imazing-csv

Import iMessage and SMS history from an iMazing Messages CSV export. Pass the export root containing csv/ and optional attachments/, or pass its csv/ directory directly.

msgvault import-imazing-csv ~/Downloads/messages-export \
  --me +14155550100 --timezone America/Los_Angeles
Flag Default Description
--me (required) Your phone number or email address
--timezone local IANA zone (required on Windows) Timezone used for dates that do not include an offset
--contacts — vCard file used to fill empty participant names

The importer accepts comma, tab, and semicolon CSV files with named iMazing headers. It is deterministic across reruns. Available referenced files are stored up to 100 MiB each. Larger files are recorded as skipped; missing files and ambiguous filenames remain visible as missing attachment occurrences. These outcomes do not fail the import or prevent contact enrichment, and a later rerun can fill missing files. Reply links are added only when the exported reply text identifies exactly one earlier message.

CSV exports do not share stable message IDs with chat.db. Importing the same history through both commands can create cross-source duplicates.

See Text Messages for the full format and rerun behavior.


import-gvoice

Import texts, calls, and voicemails from a Google Voice Takeout export.

msgvault import-gvoice <takeout-voice-dir>

The directory must be the "Voice" folder from a Google Takeout export, containing Calls/ and Phones.vcf.

Flag Default Description
--before — Only messages before this date (YYYY-MM-DD)
--after — Only messages after this date (YYYY-MM-DD)
--limit 0 Limit number of messages (for testing)
--no-default-identity false Do not auto-confirm the phone number as this source's "me" identity

See Google Voice imports for voicemail audio behavior and usage examples.


import-messenger

Import Facebook Messenger conversations from a Download Your Information export.

msgvault import-messenger --me <you@facebook.messenger> <dyi-export-dir>
Flag Default Description
--me (required) Your synthetic Messenger identifier, e.g. test.user@facebook.messenger
--format auto Export format: auto, json, html, or both
--limit 0 Limit number of messages (for testing)
--no-resume false Start fresh, ignoring interrupted progress
--checkpoint-interval 200 Save progress every N messages

See Text Messages for usage examples.


import-synctech-sms

Import SMS Backup & Restore XML or ZIP backups.

msgvault import-synctech-sms <path> --owner-phone <your-number>
Flag Default Description
--owner-phone (required) Your phone number in E.164 format
--sms true Import SMS records
--mms true Import MMS records
--calls true Import call logs
--attachments true Import MMS attachments

See Text Messages for usage examples.


add-synctech-sms-drive

Configure a Google Drive source for SMS Backup & Restore backups.

msgvault add-synctech-sms-drive <name> --owner-phone <number> --folder-id <id> --google-account <email>
Flag Default Description
--owner-phone (required) Your phone number in E.164 format
--folder-id (required) Google Drive folder ID containing backups
--google-account (required) Google account used for Drive access
--schedule 30 4 * * * Cron schedule used by msgvault serve
--oauth-app — Named Google OAuth app to use

sync-synctech-sms

Run one configured SMS Backup & Restore source immediately.

msgvault sync-synctech-sms <name>

backup

Create, list, verify, and restore incremental archive snapshots in a backup repository.

msgvault backup init --repo ~/Backups/msgvault
msgvault backup create --repo ~/Backups/msgvault
msgvault backup list --repo ~/Backups/msgvault
msgvault backup verify --repo ~/Backups/msgvault
msgvault backup restore --target ~/msgvault-restored --repo ~/Backups/msgvault

Every backup subcommand requires a repository: pass --repo, or set [backup].repo in config.toml to omit it. backup init initializes the repository directory but does not modify config.toml. backup create is routed through the selected daemon so the daemon can freeze a consistent SQLite snapshot while it scans pages and attachments. backup verify and backup restore run locally against the repository because they do not write the live archive.

backup init

msgvault backup init --repo <dir>
Flag Description
--repo <dir> Backup repository directory

backup create

msgvault backup create [flags]
Flag Description
--repo <dir> Backup repository directory
--include-config Include config.toml verbatim; may contain API keys
--include-tokens Include OAuth token files
--allow-plaintext-secrets Allow config/tokens in an unencrypted repository
--tag <text> Optional label recorded on the snapshot manifest
--force-unlock Break a stale exclusive repository lock before creating
--jobs N Concurrent attachment capture workers; 0 uses one per CPU

backup list

msgvault backup list [--repo <dir>]

Prints snapshot ID, creation time, message count, bytes added, and tag.

backup verify

msgvault backup verify [snapshot] [flags]
Flag Description
--repo <dir> Backup repository directory
--all Verify every snapshot instead of only the latest
--quick Skip reading and hash-verifying content blobs
--force-unlock Break a stale exclusive repository lock before verifying
--jobs N Concurrent pack readers; 0 uses one per CPU

backup restore

msgvault backup restore [snapshot] --target <dir> [flags]
Flag Description
--repo <dir> Backup repository directory
--target <dir> Directory to restore into (required)
--overwrite Allow restoring into a non-empty target directory
--integrity-check Run SQLite's full integrity check after restoring (slow for large databases)
--loose-attachments Restore attachments as individual loose files instead of installing compatible packs
--force-unlock Break a stale exclusive repository lock before restoring
--jobs N Concurrent pack readers; 0 uses one per CPU

By default, restore installs compatible repository packs directly into the restored attachment store. Every selected attachment is still read and SHA-256 verified; entries that exceed the target store's maintenance limits, or packs that use an incompatible representation, are restored loose instead. The summary reports the resulting packed/loose split and any fallback reasons. Every restore verifies database pages and content blobs by hash and compares the restored database statistics with the snapshot manifest. Pass --integrity-check to additionally run SQLite's full PRAGMA integrity_check scan; it is optional because it can dominate restore time for large databases.

Use --loose-attachments for downgrade or recovery. Restore into a fresh target with that flag to guarantee a fully loose result. An overwritten target can retain uncataloged old pack files, and unpack-attachments processes only cataloged packs, so overwrite cannot currently make the same guarantee. Restoring into the live archive home of a running daemon is refused. See Backup for repository format, scheduling, verification, and privacy details.


pack-attachments

Move every eligible loose content-addressed attachment into sealed immutable pack files:

msgvault pack-attachments

The daemon serializes packing against sync and backup operations. Reads remain available from loose, packed, or mixed storage, and the command is safe to rerun as new loose content arrives. Bounded packing also runs after successful attachment-producing commands sent to the daemon. Scheduled syncs never pack inline: when one writes new loose blobs, the daemon's attachment-pack job (every 6 hours) packs them, and the daily attachment-maintenance job packs and repacks. Each automatic pack pass has a 256 MiB soft raw-byte budget and finishes its current blob before stopping for that byte limit. Scheduled packing and daily attachment maintenance also stop after one minute, which lets queued scheduled work run. Each scheduled pack pass verifies at most 128 existing packed blobs or 32 MiB of their raw content, allowing one oversized blob. Its durable hash cursor resumes verification after a restart, while new loose blobs can be packed during each pass. Manual unpacking reads the full catalog. Cancellation preserves committed packs. A pass with committed progress resumes behind waiting work; runtime expiry without a checkpoint records an error and waits for the next trigger. The first pack tick after a restart also checks for blobs left by the previous daemon. This command processes the complete eligible backlog immediately.

With [data].loose_attachments = true, automatic packing is disabled and this command refuses to run.


repack-attachments

Reclaim dead bytes from sparse attachment packs:

msgvault repack-attachments

Repack always runs through the selected daemon so it can atomically replace live blob mappings, retire shared readers, and remove old pack files. It is safe to retry after interruption or a Windows file-sharing error. It refuses to run when [data].loose_attachments = true because repacking creates replacement pack files.


unpack-attachments

Restore every cataloged packed attachment to a loose file and remove its pack:

msgvault daemon stop
msgvault unpack-attachments

Each object is SHA-256 verified as it is written. This is the downgrade and recovery escape hatch because msgvault versions before packed attachment support cannot read the packs. The command is local-only and refuses to run while a daemon holds pack readers open. With [remote] configured, run it on the archive host or pass --local there to select that host's local archive.


search

Search the archive with Gmail-like query syntax. Supports keyword (FTS5), semantic, and hybrid modes.

msgvault search <query> [flags]
Flag Description
-n, --limit N Maximum number of results (default: 50)
--offset N Skip first N results (only valid for --mode fts)
--json Output results as JSON
--account Limit results to a specific account
--collection Limit results to all member accounts of a collection
--message-type Limit results to one or more message types, e.g. email, teams, discord, calendar_event, sms
--deletion-scope Source-deletion scope: active (default), deleted, or any. Non-active scopes require --mode fts.
--mode Search mode: fts (default), vector, or hybrid. vector and hybrid require vector search to be configured.
--explain Include per-signal scores (RRF, BM25, vector) in the output. Only applies to --mode vector and --mode hybrid.

Without an explicit message-type filter, search intentionally returns all matching cached message types, including meeting transcripts and chats. Ordinary aggregate views and statistics still default to email-only; use --message-type (or message_type: in the query) when you need an explicit search scope.

The query accepts list: and list-id: as equivalent RFC 2919 List-Id filters. Values are case-insensitive literal substrings; quote values that contain spaces. Repeating either alias requires every value to match.

--mode vector and --mode hybrid require at least one free-text term in the query (filter-only queries use --mode fts). They do not support pagination (--offset is rejected) or non-active deletion scopes because the vector index covers active messages only. Bump --limit to retrieve a larger candidate pool instead. See Searching for the operator reference and Vector Search for semantic setup.

Search tables show the subject, or the message snippet when the subject is blank. Redirected or piped output keeps the full sender and subject/snippet on one line, with terminal controls removed and whitespace collapsed. Terminal tables fit the available width by shortening sender and subject text, including Unicode characters and emoji. Fixed IDs, dates, sizes and explanation scores stay complete; very narrow terminals may wrap. An unknown message size appears as -.

With --json, each result also includes web_url when the selected daemon can provide a browser link for that message.

The daemon checks full-text index completeness in the background. The CLI warns that results may be incomplete when the daemon finds an index gap or is rebuilding the index. The warning also applies while a rebuild waits for other daemon work to finish. A completeness check alone prints no warning. API clients can still observe index_state="checking" while that check runs.


repair-list-ids

Re-derive archived email List-Id values from stored raw MIME without contacting a provider.

msgvault repair-list-ids [--apply]

The default is a dry run and does not modify the archive. Pass --apply to write changed values and mark derived analytics stale for the normal rebuild path.

The analytics cache exports invalid UTF-8 List-IDs as NULL so mailing-list filters cannot match unrelated messages through replacement characters. Use --apply to recover these values when the stored raw MIME has an intact List-Id header. This cannot recover a header that is itself damaged.


repair-labels

Rebuild an IMAP source's message labels from its stored mailbox memberships, without contacting the provider.

msgvault repair-labels [identifier] [--apply]

An add-only label merge — used when a sync's mailbox snapshot is incomplete or a dedup match is not fully confirmed — never removes a label. If the message's stored membership never changes again, nothing later revisits it, and a stray label can persist. repair-labels rebuilds a message's labels straight from imap_message_memberships, the same rebuild a full mailbox enumeration performs, without a live IMAP connection.

With no identifier, every IMAP source is repaired. An identifier scopes the repair to one matching source. The default is a dry run and does not modify the archive. Pass --apply to write changed labels and mark derived analytics stale for the normal rebuild path.

Flag Description
--apply Write repaired labels to the archive.

documents

Manage hosted extraction and local full-text indexing for standalone document attachments. Provider operations require a private authenticated capability manifest and exact recorded consent; local search and removal operations do not contact the provider.

msgvault documents probe-mistral --fixtures <private-dir> [--validate-only]
msgvault documents consent-mistral --capabilities <manifest> [--yes]
msgvault documents build --capabilities <manifest> [--limit N] [--full-rebuild] [--yes]
msgvault documents resume --capabilities <manifest> [--limit N] [--yes]
msgvault documents search <query> [flags]
msgvault documents status --capabilities <manifest> [--json]
msgvault documents retry --capabilities <manifest> --hash <sha256>
msgvault documents retire <profile-id> [--yes]
msgvault documents purge-derived --hash <sha256> [--yes]

probe-mistral --validate-only checks the complete synthetic fixture set without credentials or network access. An authenticated probe writes the manifest to stdout. Run consent and build commands once without --yes to review their disclosure and preflight.

documents search accepts --source-id, --message-type, --attachment-id, --message-id, -n/--limit, --cursor, and --json. Its cursor is opaque and bound to a stable index revision; restart pagination after a stale-cursor error.

In v0.21.0, build failures include available local causes alongside the document hash and reason code. documents status lists up to 20 current failed documents, including failed replacements in an active rebuild. JSON output adds status.failures and status.failures_exhausted. Each failure has canonical_blob_hash, reason_code, detail, and state; detail is a bounded single line and may be empty for historical failures. Use documents retry and then build again to obtain fresh diagnostics for an older failure.

To include inline documents, set [attachments.documents.scope].include_inline = true and record exact consent again. The default is false. See the scope configuration for the contract.

See Document Attachment Indexing for fixture generation, configuration, privacy boundaries, scheduling, and recovery.


tui

Launch the interactive terminal interface.

msgvault tui [flags]
Flag Description
--local Use the local daemon instead of the configured remote server

Analytics engine and cache behavior are daemon-managed. Configure [analytics].engine and [analytics].auto_build_cache in config.toml to force live SQL, require DuckDB, or disable automatic cache builds. See Configuration: analytics.

Deprecated in 0.17.0: the older TUI-only --force-sql, --no-cache-build, and --no-sqlite-scanner flags are hidden and no longer control the foreground CLI. Use [analytics].engine = "sql" for live SQL, [analytics].auto_build_cache = false to skip daemon cache builds, or msgvault build-cache to prebuild cache files on the daemon host.


export-messages

Export a bounded, provider-neutral message window as deterministic JSON Lines. The command reads the archive through the configured daemon and never contacts a provider.

msgvault export-messages \
  --start <RFC3339> \
  --end <RFC3339> \
  [--message-type <type>] \
  [--source <type:identifier>] \
  [--person-id <id>] \
  [--format jsonl]
Flag Default Description
--start required Inclusive RFC3339 lower bound
--end required Exclusive RFC3339 upper bound
--message-type all Exact message type to include; repeatable
--source all Exact typed source selector; repeatable
--person-id all Durable person whose bound participants scope the export
--format jsonl Output format; v1 accepts only jsonl

Use --person-id N to export messages involving a durable person's bound participants. Linked participants outside those bindings contribute no messages. Exported authors use the sender's curated person name when present, keeping the address unchanged.

The stream schema is msgvault-message-export/1. Records appear as one manifest, all sources, all conversations, all messages, and one completion record with counts. A missing completion record means the stream is partial and must be rejected. Stdout contains JSONL only; diagnostics use stderr.

See Exporting Data for the full record, identity, ordering, and validation contract.


export-discord

Export a bounded interval from one Discord guild using the older msgvault-discord-export/1 JSON envelope.

msgvault export-discord <guild-id-or-name> \
  --start 2026-01-01T00:00:00Z \
  --end 2026-02-01T00:00:00Z
Flag Description
--start <RFC3339> Inclusive lower bound (required)
--end <RFC3339> Exclusive upper bound (required)
--format json Output format; json is the only supported value

This compatibility command reads only the archive and does not contact Discord. New integrations should use export-messages, which emits the provider-neutral msgvault-message-export/1 JSONL schema. See the Discord export guide.


export-eml

Export a message as a .eml file. Accepts an internal message ID or a provider message ID. A numeric reference selects a live internal ID first, then falls back to a provider ID if that internal ID does not exist. This also applies with --thread.

msgvault export-eml <id> [flags]
Flag Description
-o, --output <path> Output file (default: <source_message_id>.eml, use - for stdout). With --thread, the output directory (default: current directory)
--thread Write visible archived messages in the conversation that have stored MIME as <n>-<source_message_id>.eml, numbered oldest first. Reports skipped messages and when the account last synced. Needs a daemon with API schema 2.33.0 or newer
--account <email> With --thread, the account that holds a provider message ID found in more than one account

Thread export captures its message list once. Messages added afterward belong to the next export. It reports and skips messages that disappear or lose their stored MIME before they are read; other read or write errors stop the command. Hidden duplicate copies are excluded. The last-sync time describes account freshness, not conversation completeness.


export-attachment

Export an attachment by its SHA-256 content hash.

msgvault export-attachment <content-hash> [flags]
Flag Description
-o, --output <path> Output file path (use - for stdout)
--base64 Output raw base64 to stdout
--json Output as JSON with base64-encoded data

The --json, --base64, and --output flags are mutually exclusive.

See Exporting Data for usage examples.


export-attachments

Export all attachments from a message as individual files.

msgvault export-attachments <message-id> [flags]
Flag Description
-o, --output <dir> Output directory (default: current directory)

Accepts internal numeric IDs or Gmail message IDs. See Exporting Data for usage examples.


create-subset

Create a new SQLite archive containing the requested number of most recent messages and the records they reference. The destination receives its own msgvault.db and can be opened as a separate msgvault home.

msgvault create-subset --output ./subset-vault --rows 1000
MSGVAULT_HOME=./subset-vault msgvault tui
Flag Description
-o, --output <directory> Destination directory (required)
--rows <count> Number of most recent messages to copy; must be positive (required)
--include-identity Copy complete identity clusters for included participants
--include-attributes Copy current and historical person and organization attribute values
--include-profiles Copy profiles, profile history and media, relationships, employment history, and referenced organizations
--include-vcard-resources Copy complete native vCards and retired UID aliases; requires --include-profiles

The command is SQLite-only. The optional identity, attribute, profile, and vCard flags can copy personal records that have no message in the subset; read the command's warning before sharing its output.


export-token

Export a browser-created OAuth refresh token to a remote msgvault instance.

Use this for headless deployments (NAS, cloud VM, any remote server) that cannot run a browser flow.

msgvault export-token <email> [flags]
Flag Description
--to <url> Remote msgvault URL (or MSGVAULT_REMOTE_URL)
--api-key <key> API key (or MSGVAULT_REMOTE_API_KEY)
--allow-insecure Allow HTTP for trusted networks (for example Tailscale)

export-token uploads ~/.msgvault/tokens/<email>.json to /api/v1/auth/token/<email>, saves it in the remote token store, and posts account metadata to /api/v1/accounts.


verify

Verify archive integrity against Gmail through the configured remote server or local daemon. The command streams the daemon's stdout/stderr back to the terminal.

msgvault verify <email> [flags]
Flag Description
--sample N Messages to sample (default: 100)
--skip-db-check Skip SQLite integrity check
--json Emit machine-readable JSON summary

stats

Show archive statistics.

msgvault stats [flags]
Flag Description
--account Show stats for a specific account
--collection Show stats for all member accounts of a collection

People command guide

Use the people guide for the workflow. Observed contacts use participant IDs; saved profiles use person IDs. Each command below takes the ID named in its arguments.

identity matches

Review archive-derived identity suggestions with a token from the current review snapshot:

Command Purpose
identity matches list [--state candidate] [--limit 100] [--offset 0] [--json] List candidate, accepted, rejected, conflict, or all match records
identity matches show <id> [--json] Inspect evidence, blockers, and the current review token
`identity matches accept --review-token [--notes-file --notes-stdin] [--json]`
`identity matches reject --review-token [--notes-file --notes-stdin] [--json]`

Always inspect the match before deciding. Evidence or endpoint changes make a review token stale; fetch the match again and decide with its new token. A successful acceptance records the decision and applies the link. Review notes are private data. See people and profiles for how to review candidates safely.

person scoring

Run a bounded scoring batch to create identity review suggestions and journal the results. Scoring never accepts matches. Configure the disabled-by-default people.identity_scoring settings, inspect the provider disclosure and data fields, then consent to its fingerprint:

msgvault person scoring status
msgvault person scoring consent <disclosure-fingerprint>
msgvault person scoring run --limit 20
msgvault person scoring history --limit 20
Command Purpose
person scoring status [--json] Show readiness, credential availability, consent, blockers, and the provider disclosure
person scoring consent <disclosure-fingerprint> [--json] Consent to the exact current disclosure
person scoring revoke <disclosure-fingerprint> [--json] Withdraw that disclosure's consent without changing configuration
person scoring run [--limit <n>] [--json] Score and journal one batch; local blockers prevent provider requests for those pairs
person scoring history [--candidate-id <id>] [--limit <n>] [--before-id <id>] [--json] Read redacted judgments, optionally for one candidate or below a history cursor

run --limit defaults to the configured batch size and cannot exceed it. History defaults to 100 rows and accepts limits from 1 through 100. Candidate and cursor IDs must be nonnegative; zero means no filter. Use the returned next_before_id with --before-id for older history.

Each run result includes its candidate's review token, proposed action, and blockers. If a batch stops early, completed results remain in the response and history, and the command reports the batch error. Inspect the candidate's current evidence before accepting a proposal. See the people guide for the consent and provider boundaries.

person notes

Read, replace, or append private Notes while retaining earlier values:

Command Purpose
person notes get <person-id> Read the current Notes text
person notes set <person-id> --text <text> Replace Notes
person notes append <person-id> --text <text> Append a fragment atomically on a new line

--text accepts inline text, @path, or - for standard input. All commands accept --json. set --expected-value-id <id> rejects concurrent changes. See private notes for how Notes interact with search, MCP, and CardDAV.

person search and files

Command Purpose
person search <free-text> [--limit 20] [--json] Search curated profiles semantically; requires person embeddings and consent
person files <person-id> [flags] Find archived attachments related to the person's linked identities

Person search accepts --limit from 1–100. File lookup defaults to the metadata lane and 100 results. Its main filters are --direction, --filename, --mime-family, --after, and --before. --lane documents, visual, or all requires --query; a --cursor belongs to one lane and cannot be used with all. See person search and person files for index requirements, supported filters, and pagination.

person track, provider, sweep, and facts

Tracking chooses which profiles automatic maintenance may process. It does not grant consent to a provider or enroll a person in briefs.

Command Purpose
person track <person-id> / person untrack <person-id> Enable or stop future profile maintenance for a person
person provider add <name> [flags] Configure and synthetically check a named model provider
person provider list List configured model profiles
person provider use <name> Select a checked profile and enable sweeps
person provider check [name] Run fixed synthetic input without granting consent
person provider consent [name] --yes Grant consent to the exact checked policy
person provider revoke [name] Revoke that policy's consent; --all revokes all stored sweep policies
person provider remove <name> Remove a configured profile
person provider history [name] [--person <id>] Inspect redacted runs and attempts
person sweep run [--person <id>] [--limit 25] Run a bounded maintenance pass for tracked people
person sweep status Read redacted progress and usage
person sweep history [--person <id>] [--limit 20] Read recent maintenance attempts
person facts catalog [--include-sensitive] List eligible fields, their keys, and revisions
person facts evidence <person-id> Inspect saved supporting evidence
person facts evidence-status <person-id> Inspect changes to evidence support
person facts claims <person-id> Inspect proposed values
person facts decisions <person-id> Inspect resolver decisions
person facts pins <person-id> List fields protected from automatic replacement
person facts pin <person-id> <kind> <key> Protect a field's current or empty value
person facts unpin <person-id> <kind> <key> Allow automatic resolution for the field again

These commands accept --json. Use catalog keys, not slugs, for fact pins. Evidence, claims, and decisions accept an exact --target; fact-history commands accept --limit (1–200) and --offset. sweep run --backstop revisits older evidence in a bounded pass.

Provider configuration edits run on the daemon host. Restart the daemon after changing its active provider or policy. Profile removal uses the running daemon to revoke consent and clear checks for both the saved and running policies. If the daemon is incompatible, stop it before removing a profile. With no daemon, removal updates the local archive directly.

For setup, privacy controls, budgets, and recovery, see profile automation. Provider status, reverify, set, and briefs have additional details below.

person enrichment

Look up public person information through independently configured and consented Exa or Sixtyfour policies:

Command Purpose
person enrichment profiles List saved policy fingerprints
person enrichment status [--limit 20] Inspect policies, consent, and bounded suppression history
person enrichment consent <fingerprint> Grant consent for that exact enrichment policy
person enrichment revoke [fingerprint] Revoke an exact policy; --all revokes all enrichment policies
person enrichment run --person <id> --provider <name> --idempotency-key <key> Request a lookup for one person
person enrichment suppress --person <id> --reason <reason> Record an opt-out for the person's current identifiers
person enrichment suppress --provider <name> --identifier-class <class> --reason <reason> < identifier.txt Suppress an identifier read from standard input

Status, profiles, consent, revoke, and run accept --json. Suppression reasons are opt_out and data_subject_request. The suppression forms are mutually exclusive: --person does not accept --provider or --identifier-class.

For the identifier form, <class> is email, phone, public_profile_url, provider_person_id, or name_company. Supply one non-empty line on standard input, except for name_company, which requires two: the name, then the company. Do not put identifier values in command arguments.

See external enrichment for required identity details, configuration, request limits, and the persistent suppression key.

organization and employment

Keep organization details and current or historical jobs in structured records. org is an alias for organization.

Command Purpose
organization list [--query <text>] [--include-retired] Find organizations
organization create <name> [--domain <domain>] Create an organization
organization show <id> [--history] Read an organization and its profile data
organization set <id> --name <name> [flags] Replace mutable organization fields
organization retire <id> / organization unretire <id> Change active status while keeping the record
organization merge <survivor-id> <losing-id> Consolidate duplicate organizations
organization delete <id> Permanently delete an organization
organization attribute list <id> Read attributes; --include-superseded adds history
organization attribute set <id> --definition <slug> --text <value> Set a text attribute
organization attribute clear <id> <slug> Close an attribute value while retaining history
employment add --person <id> --organization <id> [flags] Connect a person to an organization
employment list --person <id> List a person's jobs; --organization <id> lists an organization's jobs
employment show <id> / employment set <id> [flags] Inspect or edit one job
employment end <id> [--end <date>] End a job while keeping its history
employment set-primary <id> Select the primary current job
employment delete <id> Permanently delete a job

These commands accept --json. Employment creation accepts --title, --role, --department, --location, --description, --start, --end, and --primary. Dates can be partial (YYYY, YYYY-MM, or YYYY-MM-DD). Attribute writes support --dry-run and --expected-value-id for validation and concurrent-edit checks. See employment and relationships.

relationship-type and person relationship

Record typed relationships between two saved profiles:

Command Purpose
relationship-type list Read forward and reverse labels for available types
relationship-type create <slug> <forward-label> <reverse-label> Create a user-owned type; --symmetric requires identical labels
relationship-type update <type-id> [flags] Change labels and presentation
relationship-type delete <type-id> Delete an eligible user-owned type
person relationship add <source-person-id> <type-slug> <target-person-id> Add a relationship; accepts --from, --until, and --notes
person relationship list <person-id> [--include-ended] Read relationships from that person's perspective
person relationship end <relationship-id> <until-date> End a relationship while keeping history
person relationship delete <relationship-id> Permanently delete a relationship
person relationship reviews [--status <status>] Inspect relationship review candidates

All accept --json. The forward label means “source is the ___ of target.” See relationships for direction and date examples.

add-carddav, sync-carddav, and carddav

Connect CardDAV accounts, choose their address-book roles, and resolve sync conflicts. Publishing or resolving a conflict can change the external address book; see the CardDAV guide.

Command Purpose
add-carddav <base-url> <username> [--connection <name>] [--schedule <cron>] [--disabled] Discover and save an account; password is prompted or read from piped stdin
add-carddav --google <email> [--connection <name>] [--oauth-app <name>] [--schedule <cron>] [--disabled] Connect Google Contacts using an authorized account token
carddav authorize-google <email> [--oauth-app <name>] [--no-browser] Authorize Google Contacts in the browser, preserving existing Google permissions
sync-carddav [--connection <name>] [--full] Synchronize all enabled connections, or the selected connection; --full reconciles complete books
carddav connections List connection names, enablement, runtime availability and orphaned accounts
person publish <person-id> / person unpublish <person-id> Publish a saved profile or remove its remote card
person publish <person-id> --preview Print the exact vCard and approval token as JSON without publishing
person publish <person-id> --approve <token> Approve reviewed changes; conflict previews also need explicit keep_local resolution
carddav books [--connection <name>] List discovered books, owning connection names and roles
carddav books set-role <book-id> [--write-target] [--subscribed] [--lookup-source] Replace all three roles; omitted flags become false
carddav conflicts list List unresolved conflicts
carddav conflicts show <conflict-id> Compare bounded base, local, and remote summaries
carddav conflicts resolve <conflict-id> <keep_local|keep_remote> Choose the local or remote side explicitly

Unqualified add-carddav saves default. Explicit sync selects one saved connection, including a disabled connection for manual repair. Unqualified sync with no enabled connection fails as unavailable. Aggregate sync prints counts and safe failure codes per connection, and exits nonzero for partial or failed outcomes. Book IDs, publication commands and conflict commands remain global; network operations use the owning connection's credentials.

Directory and integrations can also use the publication API. CardDAV commands do not expose a general --json flag; role changes, publication previews, and conflict list/show commands already print JSON.


identity

Manage the confirmed "me" identifiers for each account.

The identity subcommands use the configured remote server or local daemon by default. --local uses the local daemon even when a remote is configured.

msgvault identity list [--account <account> | --collection <name> | --source-id <id>]
msgvault identity show [<account>] [--source-id <id>]
msgvault identity add [<account>] <identifier> [--source-id <id>]
msgvault identity remove [<account>] <identifier> [--source-id <id>]
msgvault identity discover [<account>] [--source-id <id>] [--apply]
msgvault identity import [<account>] [--source-id <id>] (--file <path> | --stdin)
Command Description
identity list List confirmed identifiers across accounts
identity show <account> Show one account's identity in detail
identity add <account> <identifier> Add a confirmed identifier
identity remove <account> <identifier> Remove a confirmed identifier
identity discover <account> Preview archived source evidence; --apply confirms strong candidates
identity import <account> Preview or apply source-scoped identifiers from text or JSON
Flag Applies to Description
--account list Restrict to a single account
--collection list Restrict to all member accounts of a collection
--source-id all subcommands Select one source unambiguously by numeric ID; mutually exclusive with an account argument or list scope
--json list, show, discover, import Output structured JSON; discovery also suppresses progress
--signal add Evidence signal name (default manual)
--apply discover After the complete preview scan, confirm strong evidence
--provider discover Include the authenticated Gmail profile or the source's configured [[fastmail]] alias inventory
--confirm <address> discover Explicitly confirm one weak candidate; repeatable and requires --apply
--file <path> / --stdin import Read a text or JSON identity list from exactly one input
--signal import Evidence signal recorded for imported identities (default manual)
--apply import Confirm every validated imported identity; without it the command only previews

Email sync enriches identities already confirmed for the source with strong sender evidence from trusted Sent metadata; it does not confirm first-time aliases. Review candidates with msgvault identity discover and apply strong candidates with msgvault identity discover --apply. Recipient-only evidence stays review-only. See People, Profiles, and Source Identities for classifications, Fastmail inventory, and import formats.


person

Manage durable person profiles and their typed, historized attributes. A profile can be created by explicitly promoting an observed participant's identity cluster or by importing contacts from a subscribed CardDAV address book.

msgvault person promote <participant-id>
msgvault person list [--json]
msgvault person directory [flags]
msgvault person get <person-id> [--json]
msgvault person set-display-name <person-id> <display-name> [--json]
msgvault person set-display-name <person-id> --clear [--json]
msgvault person delete <person-id>
msgvault person merge <survivor-id> <absorbed-id> [flags]
msgvault person split <source-person-id> [flags]
msgvault person merge-history <person-id> [--json]
msgvault person merge-show <merge-id> [--snapshot] [--json]
msgvault person merge-candidate <candidate-id> [flags]

msgvault person attributes list <person-id> [--slug <slug>] [--history] [--json]
msgvault person attributes set <person-id> <slug> (--value <scalar> | --value-json <json|@path|->) [flags]
msgvault person attributes clear <person-id> <slug> [flags]

promote seeds a new profile with the first nonblank observed name in its linked cluster, ordered by participant ID. It leaves the name empty when no member has an observed name. Repeating promotion preserves the saved name, including an edited or cleared value. set-display-name preserves the profile's stable ID and vCard UID. delete permanently retires that UID and removes the profile's participant bindings. A person with active merge lineage cannot be deleted until that lineage is fully split.

merge keeps the survivor's ID and vCard UID, moves the absorbed profile into it, and records a reversible merge packet. Profiles with active CardDAV publication are rejected. split creates a new person and UID; repeat --participant to select multiple absorbed lineages, or omit it for a merge whose absorbed profile had no participants.

Merge flag Applies to Description
--survivor-revision <n> merge Required expected revision of the surviving person
--absorbed-revision <n> merge Required expected revision of the absorbed person
--idempotency-key <key> merge, split Required retry key; reusing it with a different request is rejected
--merge-id <id> split Required merge record to reverse
--participant <id> split Absorbed participant lineage to move; repeat as needed
--revision <n> split, merge-candidate Required expected revision of the current person
--person-id <id> merge-candidate Person that owns the review candidate
--decision <accepted|rejected> merge-candidate Resolve a conflicting single-value attribute
--snapshot merge-show Read and verify the immutable merge snapshot
--json all merge commands Output structured JSON

Exact splits restore the pre-merge profiles when their lineage and referenced rows remain available. For partial splits, use --json to inspect ambiguous or unrestored rows. Complete merge packets retain merge-time profile values even after later redaction and require the strongest profile-data options when copied into a subset. See People, Profiles, and Source Identities for the workflow and lifecycle boundaries.

Attribute flag Applies to Description
--slug <slug> list Restrict output to one definition
--history list Include superseded values instead of current values only
--value <scalar> set Coerce text, integer, real, boolean, date, or timestamp input to the definition's type
--value-json <json|@path|-> set Supply a structured typed-value envelope
--ordinal <n> set, clear Address one slot of a multi-valued definition
--source <name> set Provenance: user, carddav_import, vcard_import, archive_observation, extraction, enrichment, or system
--source-ref <value> set Resource or message reference that produced the value
--confidence <0..1> set Confidence for a derived or suggested value
--actor <value> set Actor recorded with the value
--expected-value-id <id> set, clear Compare-and-swap guard for the current value
--dry-run set, clear Validate and preview without writing
--json list, set, clear Output structured JSON

Setting or clearing a value closes the current history row rather than deleting it. See People, Profiles, and Source Identities for the shipped definitions and complete workflow.


person agenda

Read and organize a person's live Kata tasks. Configure [integrations.kata] on the daemon first. Tasks belong to one person and stay in the configured Kata project.

msgvault person agenda list <person-id> [--json]
msgvault person agenda create <person-id> --title "Discuss the proposal" [flags]
msgvault person agenda link <person-id> <ref> [--list agenda] [--json]
msgvault person agenda edit <person-id> <ref> --list follow-up [--json]
msgvault person agenda unlink <person-id> <ref> [--json]

<ref> is a task reference returned by Kata or an agenda command. create creates a task and links it to the person; link attaches an existing task. edit changes only its list. unlink removes the person link without deleting the task. Complete or reopen tasks, or edit their title, body, priority, and labels, in Kata.

Flag Commands Default Description
--title create — Required task title
--body create — Task body
--priority create — Kata priority from 0 through 4
--label create — Kata label; repeat for multiple labels
--list create, link, edit agenda for create/link List name; required for edit
--idempotency-key create Generated Stable key to reuse when retrying the same creation
--json All agenda commands false Print the response as JSON

create prints its generated retry key to stderr before sending the request. To retry, pass that key with --idempotency-key and keep the task details the same. You can also choose the key in advance:

msgvault person agenda create 42 --title "Discuss the proposal" --idempotency-key proposal-discussion-1

list returns up to 100 open tasks and recognizes the person's current vCard UID and aliases. JSON includes truncated when more items remain; use Kata to view the rest. Oversized Kata responses produce an explicit error. See the Kata configuration for metadata and response limits.


person directory

Browse promoted people by last contact through the selected local or configured remote daemon. The default order is most recent first, last_contact_desc. Each page uses the daemon's default of 50 people.

msgvault person directory
msgvault person directory --last-contact-after 2026-06-01 --last-contact-before 2026-07-01 --json
msgvault person directory --sort last_contact_asc
msgvault person directory --sort name
msgvault person directory --last-contact-after 2026-06-01 --cursor "<next_cursor>"
Flag Default Description
--last-contact-after <date> absent Inclusive lower bound, YYYY-MM-DD or RFC3339
--last-contact-before <date> absent Inclusive upper bound, YYYY-MM-DD or RFC3339
--sort <order> last_contact_desc name, last_contact_desc, or last_contact_asc
--cursor <cursor> absent Opaque next_cursor from the previous page
--json false Output the full Directory page envelope

Date-only bounds mean midnight UTC on that date. RFC3339 bounds accept offsets and fractional seconds. Both bounds include the specified instant, so --last-contact-before 2026-07-01 includes midnight at the start of July 1. Use a timestamp when you need a different cutoff.

Human output shows ID, DISPLAY NAME, and LAST CONTACT in daemon order. Timestamps use UTC RFC3339 at seconds precision; JSON preserves fractional seconds. Missing display names and timestamps show -. A page with more results prints Next cursor. Pass that value unchanged to --cursor and repeat the same bounds and sort to continue.

JSON contains a people array and optional next_cursor. Each person retains the Directory fields, including categories, organizations, contact state, ID, and revision. Optional fields stay absent when the daemon omits them, including last_contact_at for people without a contact timestamp. person list continues to return the full unpaginated profile collection, with its existing human columns and JSON array output.


person provider add

Create and synthetically check a named people inference profile. Select a built-in preset to bind its protocol, endpoint, and authentication scheme:

msgvault person provider add primary --provider openrouter --model <model> \
  --credential-env OPENROUTER_API_KEY --retention-posture <assertion> \
  --training-posture <assertion> --source conversation_text \
  --source-since 2026-01-01 --allow-sensitive=true --yes
msgvault person provider consent primary --yes
msgvault person provider use primary

--provider accepts openai, openrouter, or venice and requires an explicit model and privacy policy. It cannot be combined with --custom or --accept-catalog-prices; conflicting protocol, endpoint, or auth overrides are rejected. --credential-env reads only the named host variable. Alternatively, --api-key-stdin reads a key from standard input, or an interactive terminal prompts for it. A successful synthetic check does not grant consent or select the profile. See profile automation for custom protocol profiles and policy fields.

person provider enroll-codex

Create, check, consent to, and select a new Codex profile through the daemon. Codex enrollment is unavailable in this release: no Codex build is approved. The daemon returns HTTP 503 before changing credentials or consent. This command requires a terminal and never starts a noninteractive device login.

msgvault person provider enroll-codex <new-name> \
  --retention-posture <assertion> --training-posture <assertion> \
  --source conversation_text --source-since 2026-01-01 --allow-sensitive=true

The gated flow prints a verification URL, user code, and local deadline, waits for sign-in, then prompts for an available model and reasoning effort. Existing profile names are rejected. After saving, a synthetic check and separate consent are required before selection. Declining consent leaves the saved profile unselected. Restart the daemon when the reported running profile differs.

Flag Default Description
--model prompt Codex model ID available to the signed-in account
--reasoning-effort prompt Supported effort for the chosen model
--retention-posture, --training-posture required Explicit operator assertions
--source required Repeatable: conversation_text, meeting_text, or document_text
--source-since required Earliest disclosed date, YYYY-MM-DD
--source-until absent Latest disclosed date, on or after --source-since
--allow-sensitive explicit decision required true permits archive evidence; false permits only synthetic checks
--yes false Confirm the displayed check disclosure and select the profile without a consent prompt

Host-side person provider login reauthenticates the selected existing Codex profile; person provider models lists its models and reasoning efforts. Both accept --json and remain unavailable behind the same release gate. See Codex configuration.

person provider status

Show the exact people inference provider policy and its check and consent state. The JSON output includes the selected profile name and adds stale_program_check or stale_program_consent when a verified historical record matches every policy field except the extraction program and supplies a missing current gate. Human output identifies a different extraction program and gives the recovery command without provider credentials or response content.

msgvault person provider status [name] [--json]

person provider reverify

Run the existing synthetic provider check, then grant consent for the same current exact provider profile. The check must succeed before consent is granted. The command prints the provider disclosure and asks for confirmation unless --yes is supplied.

msgvault person provider reverify [name] --yes [--json]

Omit name to use the active provider, as with check and consent. --yes confirms the disclosure. Human output includes the disclosure even with --yes; --json returns the final exact check and consent status. The named profile can be disabled in configuration, and this command still selects it for the operation without enabling scheduled sweeps.


person brief

Read a short summary before your next conversation with someone, or generate a new one from their recent supported chat and text messages.

msgvault person brief enroll <person-id> [--track] [--json]
msgvault person brief generate <person-id> [--json]
msgvault person brief show <person-id> [--json]
msgvault person brief history <person-id> [--limit 20] [--json]
msgvault person brief reject <person-id> [--reason "..."] [--json]
msgvault person brief unenroll <person-id> [--json]

Enrollment requires tracking; --track enables both. Generation uses the consented provider and its budget. History accepts limits from 1 to 200; rejection hides the current version but preserves it in history. Unenrolling stops future generation without deleting saved versions.

See person briefs for provider setup, supported sources, and a first-run example.


person provider set

Update the mutable policy fields of an existing named people inference provider profile in place. The protocol, endpoint, auth scheme, credential source, executable, execution boundary, selection, and enablement stay unchanged.

msgvault person provider set <name> --model <model> [flags]

The command reruns the exact synthetic provider check and revokes consent for the previous fingerprint. Grant fresh consent with msgvault person provider consent <name> --yes after reviewing the updated policy. A running local daemon requires msgvault daemon restart before its scheduled sweeps observe the change. Configured remote daemons refuse this mutation; run it on the daemon host or pass --local.

Flag Default Description
--model unchanged Provider model identifier
--retention-posture unchanged Provider retention assertion
--training-posture unchanged Provider training assertion
--source unchanged Allowed source class; repeatable, replaces the existing list
--source-since unchanged Earliest disclosed source date
--source-until unchanged Latest disclosed source date
--allow-sensitive unchanged Allow sensitive text in provider packets
--reasoning-effort unchanged Explicit reasoning effort
--reasoning-mode unchanged Explicit reasoning mode
--request-timeout unchanged Provider request timeout
--yes false Confirm the final provider and privacy values; required
--json false Output structured JSON

attribute-definition

Manage portable field metadata. Definitions add no runtime database columns; their universal IDs and slugs remain stable while labels and descriptions may change.

msgvault attribute-definition list [--object-type person|organization] [--include-hidden] [--json]
msgvault attribute-definition get <definition-id> [--json]
msgvault attribute-definition create --definition <json|@path|-> [--dry-run] [--json]
msgvault attribute-definition rename <definition-id> [--label <text>] [--description <text> | --clear-description] [--json]
msgvault attribute-definition delete <definition-id>

create --dry-run performs local structural validation but cannot detect a conflict with definitions already stored by the daemon. rename does not change the immutable slug or universal ID. Deletion is limited to user-created, deletable definitions that have no stored values.


collection

Manage named groups of accounts.

The collection subcommands use the configured remote server or local daemon by default. --local uses the local daemon even when a remote is configured.

msgvault collection create <name> --accounts <account1,account2,...>
msgvault collection list
msgvault collection show <name>
msgvault collection add <name> --accounts <account1,account2,...>
msgvault collection remove <name> --accounts <account1,account2,...>
msgvault collection delete <name>

Deleting a collection does not delete sources or messages.


deduplicate

Find and merge duplicate messages within an account or collection.

msgvault deduplicate [flags]

By default, each source is deduplicated independently. --collection is the explicit opt-in for cross-source deduplication.

Flag Description
--dry-run Scan and report only; do not hide duplicates
--account Scope dedup to one account
--collection Dedup across every member account of a collection
--content-hash Also detect duplicates by normalized raw MIME content
--prefer Comma-separated source type preference order
--undo <batch-id> Restore rows hidden by a previous dedup run; repeatable
--delete-dups-from-source-server Stage same-source pruned duplicates for remote deletion
--no-backup Skip the database backup before merging
-y, --yes Skip confirmation prompt

delete-deduped

Permanently delete dedup-hidden messages from the selected msgvault archive.

msgvault delete-deduped --batch <batch-id>
msgvault delete-deduped --all-hidden

The CLI sends the request to the configured remote daemon, or to the auto-started local daemon when no remote is configured. It no longer opens the SQLite database directly. The delete cannot be undone with deduplicate --undo; when backups are enabled, the daemon writes the backup next to the database it owns before deleting. Repeated --batch selections commit as one transaction; if the request is canceled, none of the selected batches are left half-deleted.

Flag Description
--batch Delete rows hidden by this dedup batch ID; repeatable
--all-hidden Delete every dedup-hidden row regardless of batch
--no-backup Skip database backup before deleting
-y, --yes Skip confirmation prompt (--all-hidden still prompts)

list-senders

List top senders by message count.

msgvault list-senders [flags]
Flag Description
-n, --limit N Number of results (default: 50)
--after YYYY-MM-DD Only messages after this date
--before YYYY-MM-DD Only messages before this date
--json Output as JSON

list-domains

List top sender domains by message count.

msgvault list-domains [flags]
Flag Description
-n, --limit N Number of results (default: 50)
--after YYYY-MM-DD Only messages after this date
--before YYYY-MM-DD Only messages before this date
--json Output as JSON

list-labels

List all labels with message counts.

msgvault list-labels [flags]
Flag Description
-n, --limit N Number of results (default: 50)
--after YYYY-MM-DD Only messages after this date
--before YYYY-MM-DD Only messages before this date
--json Output as JSON

build-cache

Build or update the Parquet analytics cache through the configured remote server or the local daemon.

msgvault build-cache [flags]
Flag Description
--full-rebuild Rebuild all cache files from scratch

The CLI sends the request over HTTP and streams the daemon's stdout/stderr back to the terminal. A local daemon runs the DuckDB export in an isolated child process so DuckDB's bundled SQLite library never opens the archive inside the long-lived daemon process. With [remote].url configured, the remote daemon builds its own cache; use --local only to target this machine's local daemon.

For automatic cache rebuilds after daemon-owned syncs, configure [analytics].auto_build_cache in config.toml. See analytics settings for snapshot freshness, builder memory, threads, and temporary disk limits.

Build warnings count exported text values repaired with U+FFFD and identity values exported as unknown. Invalid source message IDs, source conversation IDs, and source identifiers export as empty strings; valid keys stay unchanged. Invalid List-IDs export as NULL; see List-Id recovery. The counts cover only datasets written by that build or refresh, on every platform. A source value written to two output columns counts twice; repeated identity comparisons do not add to the count. See encoding repair limits for archive recovery.


activity

Refresh contact activity and last-contact dates from archived messages.

msgvault activity build
msgvault activity build --backstop

The normal build resumes from its watermark. --backstop rescans the complete archive. msgvault serve also runs this projection on the schedule configured under [activity].


rebuild-fts

Rebuild the SQLite FTS5 search index.

msgvault rebuild-fts

Use this if verify reports FTS5 shadow-table corruption such as a malformed inverted index. The command rebuilds the search index from the canonical messages table.


embeddings

Manage the vector embedding index used by --mode vector and --mode hybrid search. Requires a build with a vector backend (sqlite_vec for SQLite archives, pgvector for PostgreSQL archives) and a configured [vector.embeddings] endpoint. See Vector Search for prerequisites, model rotation, and troubleshooting.

msgvault embeddings <subcommand> [flags]
Subcommand Description
build Build or update the index. Incremental by default; --full-rebuild starts a new generation.
resume Continue scan-and-fill embedding for the building or active generation. Incremental by default; --backstop also scans below the watermark.
list List index generations with their state, model, dimension, and pending count.
optimize [generation-id] Build or resume the local SQLite search accelerator, or remove it with --drop.
prune Remove embeddings for hard-deleted messages.
activate <generation-id> Activate a completed building generation, retiring the current active one.
retire <generation-id> Retire a generation.
prune Remove embeddings whose messages were hard-deleted.

embeddings build

msgvault embeddings build [flags]
Flag Description
--full-rebuild Create a new index generation and rebuild from scratch. The new generation is activated atomically once coverage reaches zero. Same-model rebuilds keep serving the previous active generation in the meantime, but active-generation top-ups are frozen until activation; model or dimension changes return index_stale for vector/hybrid search until the new generation activates.
--yes Skip the confirmation prompt that --full-rebuild otherwise requires.
--backstop Run a full-scan pass that ignores the per-generation watermark to recover missing coverage skipped by incremental scans. Already-covered messages are skipped.
--account <identifier> Limit embedding to this account, by identifier or display name — numeric source IDs are rejected (repeatable). Overrides [vector.embed.scope] accounts for this run; configured message_types still apply. After activating this one-off scope, add the equivalent accounts to config and restart the daemon before searching.
--collection <name> Limit embedding to this collection's accounts (repeatable). Can be combined with --account; the scope is the union. This is a one-run override; persist the resolved accounts in config before restarting the daemon.

Without --full-rebuild, the command is incremental: it resumes any in-flight rebuild that matches the configured embedding settings, otherwise scans for live messages still missing coverage in the active generation, then exits. Safe to schedule via cron (or let msgvault serve do it via [vector.embed.schedule]).

The account scope is part of the generation fingerprint, so building with a different --account/--collection set than the active generation requires --full-rebuild, exactly like changing the model. See Scoped Generations.

embeddings resume

msgvault embeddings resume [flags]

Continue embedding work and finish the current generation. If a generation matching the configured embedding settings is building, this embeds its remaining rows and activates it once coverage reaches zero; otherwise it tops up the active generation. Equivalent to msgvault embeddings build with no flags, but never starts a full rebuild. Accepts the same --account/--collection scope flags as embeddings build.

Pass --backstop to scan for missing coverage below the per-generation watermark as well. This fills gaps in the selected generation without starting a full rebuild; it only activates a generation if that generation is building.

msgvault embeddings resume --backstop

embeddings list

msgvault embeddings list

Print one row per index generation: ID, generation state, model, dimension, coverage, accelerator state and row count, accelerator timestamps and last error, fingerprint, and generation timestamps.

embeddings optimize

msgvault embeddings optimize [generation-id] [--drop]

Build or resume the SQLite approximate-search accelerator from vectors already stored for a generation. The active generation is used when the ID is omitted. The command never calls the embedding provider. It is safe to interrupt and rerun; the accelerator is not used for search until verification and atomic publication complete. The daemon's operation gate pauses scheduled embedding and other gated writes until the command finishes. Searches remain available. PostgreSQL does not need this command.

Use --drop to remove the accelerator and its build state, including for a retired generation. Exact vectors remain intact. Freed database pages become reusable; the database file does not shrink. Re-run optimization after substantial archive growth to retrain its fixed bucket count.

embeddings prune

msgvault embeddings prune

Remove stored message embeddings whose source messages were hard-deleted.

embeddings activate

msgvault embeddings activate <generation-id> [flags]

Activate a completed building generation and retire the currently active one. By default this refuses to activate a generation that still has messages missing coverage or whose fingerprint does not match the current config.

Flag Description
--yes Skip the confirmation prompt.
--force Activate even with missing coverage or a fingerprint mismatch.

embeddings retire

msgvault embeddings retire <generation-id> [flags]

Mark a generation as retired. Retiring the active generation requires --force-active, since it leaves no generation serving vector/hybrid search.

Flag Description
--yes Skip the confirmation prompt.
--force-active Allow retiring the generation that is currently active.

embeddings prune

msgvault embeddings prune

Remove vector rows whose source messages no longer exist. The configured vector backend must support orphan pruning.

msgvault build-embeddings remains as a deprecated alias for msgvault embeddings build (same --full-rebuild and --yes flags).


multimodal

Build, inspect, and search the optional visual attachment index. The workflow requires [vector.multimodal] configuration, a probed capability manifest, and explicit consent for hosted processing. See Visual attachment search.

Subcommand Purpose
probe --seeds <dir> --out <file> --yes Send synthetic fixtures to the configured provider and write a capability manifest. --fixtures keeps the generated fixtures instead of using a temporary directory.
build --yes Record consent for the configured capability profile and build the visual index.
resume Continue a consented build.
status [--json] Report generation state and coverage.
retry --message <id> --hash <sha256> Retry one attachment occurrence.
retire <generation-id> --yes Retire one generation and delete its vectors. Original attachments remain archived.

Search by text or by one local JPEG, PNG, or WebP image:

msgvault multimodal search "a whiteboard timeline"
msgvault multimodal search --image ./reference.png
Search flag Default Description
--image <path> — Use a local query image of at most 20 MiB instead of text
--limit <count> 20 Results to return; must be 1–100
--cursor <value> — Continue from an opaque result cursor
--sender-person <id> — Attachments sent by one durable person
--person <id> — Attachments related to one durable person
--participant <id> — Attachments related to one observed participant
--direction <value> — from_person, to_person, or group; requires --person or --participant
--source <id> — Restrict to one source
--message <id> — Restrict to one owning message
--filename <text> — Case-insensitive filename substring
--mime-prefix <text> — Case-insensitive MIME prefix
--after, --before — YYYY-MM-DD sent-date bounds
--json false Emit JSON

Supply exactly one text query or --image. --person and --participant are mutually exclusive. --sender-person cannot be combined with either of those or with --direction.


eval

Compare how well search modes find messages you have rated. The command runs the same full-text, vector, and hybrid retrieval paths used by production search. It reports ranking quality, recall, latency, configuration, and input diagnostics.

# topics.tsv: qid<TAB>query<TAB>optional-category
q1	quarterly planning	pointed
# qrels.txt: qid iteration document-id relevance (1 or greater means relevant)
# Replace example-message-001 with a real source_message_id, not a local numeric ID.
q1 0 example-message-001 2
msgvault eval \
  --topics topics.tsv \
  --qrels qrels.txt \
  --modes fts,vector,hybrid \
  --limit 100
Flag Default Description
--topics <path> required Tab-separated topics: qid, query, and optional category
--qrels <path> required Whitespace-separated judgments: qid iteration docid relevance; relevance of 1 or greater means relevant
--modes <list> fts,vector,hybrid Comma-separated modes to evaluate
--doc-key <kind> message Match judgments to message source IDs or conversation source IDs
-n, --limit <count> 100 Distinct documents retrieved per query
--json false Emit the report as JSON
--rerank-jev <shapes> disabled Opt in to per-candidate, batched, or both Jev shapes
--rerank-top <count> 30 Between 2 and 30 candidates; cannot exceed --limit
--rerank-max-requests <count> 1000 Maximum provider requests for the whole invocation
--rerank-input-usd-per-million <amount> required when enabled Input price supplied for this run
--rerank-output-usd-per-million <amount> required when enabled Output price supplied for this run

eval opens the archive selected by local configuration directly; it does not use [remote]. Vector and hybrid evaluation currently require a SQLite archive, an sqlite_vec build, enabled vector configuration, and a compatible active generation. On PostgreSQL, run --modes fts.

Optional Jev reranking

--rerank-jev adds an evaluation arm after retrieval. It sends the topic query and up to 30 message candidates to TypeSafe's fixed Jev endpoint. The command uses TYPESAFE_API_KEY from the process environment. Selecting a shape is the per-invocation consent to send that archived text. The reranked arms accept --doc-key=message because TREC judgments identify messages. Conversation judgments remain available for the baseline run.

The command prepares candidate text with the same body selection and cleanup used for embeddings. It sends only the query and the cleaned subject/body. Candidate text is capped at 2048 UTF-8 bytes and query text at 4096 bytes. Msgvault calls TypeSafe through Docbank's document/typesafe client, which sets the request and response size limits and runs at most eight calls at once. Msgvault gives each ranking 40 seconds for all of its calls, enough for four waves of 10-second calls at 30 candidates. A ranking's full elapsed time contributes to the reported latency. The per-candidate and batched shapes use the same retrieved messages in one invocation.

Cost is the returned input and output tokens times the prices supplied on the command line. Before opening the archive, the command refuses a run whose worst-case request count, judged topics times modes times requests per ranking, exceeds --rerank-max-requests. Msgvault has no local spend limit, so before a live study, verify an account or order limit that TypeSafe enforces by rejecting charges above the cap. A displayed balance or alert does not establish that behavior. A response without token usage fails that arm. The first provider failure ends reranking for the run. The report keeps the baseline and the token subtotals observed before the failure, usage_complete=false marks them as partial, and unknown cost prints unknown in the table and null in JSON. Requests and tokens count completed rankings only. The command returns a nonzero result.

Example with placeholder prices:

TYPESAFE_API_KEY=replace-me msgvault eval \
  --topics topics.tsv --qrels qrels.txt --modes fts,vector,hybrid \
  --limit 100 --rerank-jev per-candidate,batched --rerank-top 30 \
  --rerank-input-usd-per-million 1 \
  --rerank-output-usd-per-million 2

Reranked p95 includes retrieval, shared candidate preparation, and that shape's provider calls. A live TREC Legal 2010 study also needs a permitted matching message collection, topics, qrels, source-id mapping, and the enforced account cap. Consider a search integration only when a complete hybrid shape improves Hit@10 by 0.05 absolute and has end-to-end p95 at or below 2000 ms under the same corpus, topics, qrels, retrieval settings, and top N of 30. Report the paired topic denominator and changes for both shapes. The local threaded fixture checks wiring only.


cache-stats

Show statistics about the analytics cache.

msgvault cache-stats

The command queries the configured msgvault server over HTTP. With local configuration, the CLI auto-starts or reuses the local daemon, and the daemon reads the analytics cache files. With remote configuration, the remote server reports its own cache state.


query

Run arbitrary SQL against the Parquet analytics cache using an in-memory DuckDB engine.

msgvault query <sql> [flags]

A usable stale cache remains queryable while refresh work runs. The JSON result includes cache.published_at, and includes stale_reason and pending_additions when known. If a new publication is required before rows can be returned, the command reports the build job on stderr, waits for it, then returns query results. A failed build or interrupted wait exits with an error. --fresh waits for a check that includes archive writes committed before the request, rebuilding if needed, before returning rows.

Flag Default Description
--format json Output format: json, csv, or table
--fresh false Wait for a freshness check and any required rebuild before returning rows

See SQL Queries for available views and example queries.


credentials

On unreleased main, manage provider keys on the daemon host without the Web UI:

msgvault credentials set <id> --from-file PATH [--endpoint URL]
msgvault credentials set <id> --stdin [--endpoint URL]
msgvault credentials list [--json]
msgvault credentials import-env

Supply exactly one input source. Supported IDs are vector.embeddings, vector.multimodal, people.enrichment/<name>, and people.enrichment/suppression. The configured provider supplies the default endpoint. Suppression keys have no endpoint. Listing prints IDs and bound origins, never key values. Import copies present configured environment keys and preserves stored keys. See stored credentials for input security, runtime precedence, and sweep-provider commands.

mcp

Discover running HTTP listeners

Run msgvault mcp status --json to list HTTP MCP listeners started by this version. The command reads local runtime records without starting a server. Each entry includes transport, url, pid, backend_url when known, and token_path when the listener requires a bearer token. Read the token from that private file; the status output does not print it.

The listener publishes its actual bound port after startup, including when started with port zero, and removes its record on orderly shutdown. Status omits records whose process has exited or whose process-start identity cannot be verified, including when another process reuses the PID. Stdio sessions are not listening endpoints and do not appear. An empty JSON list means no HTTP listeners were found in this application's configured data directory.

Start the Model Context Protocol server for AI assistant integration.

msgvault mcp [flags]
Flag Default Description
--force-sql false Deprecated in 0.17.0; use [analytics].engine = "sql" in config.toml instead. See Configuration: analytics.
--no-sqlite-scanner false Deprecated in 0.17.0; cache engine selection is daemon-managed. Use [analytics].engine = "sql" for live SQL.
--http — Serve MCP over StreamableHTTP on this address instead of stdio. Bare ports bind to loopback, e.g. 8080 becomes 127.0.0.1:8080. Non-loopback addresses require an effective inbound key or --http-allow-insecure.
--http-token-file — On unreleased main, read an independent inbound bearer key from an owner-only file; takes priority over --http-token-env. Requires --http.
--http-token-env — On unreleased main, name the environment variable holding an independent inbound bearer key. Requires --http.
--http-allow-insecure false Allow non-loopback HTTP binding without an effective inbound key. A configured key is still enforced; without one, use only behind a trusted network boundary or authenticated reverse proxy.
--http-allow-writes false Expose Saved View management, attachment export, and deletion staging tools over StreamableHTTP. Calendar event mutations also require --allow-calendar-writes. Enable only for trusted, authenticated clients.
--allow-calendar-writes false Expose calendar event mutation tools. HTTP also requires --http-allow-writes; only enable for sessions where the user explicitly authorizes calendar writes.

See MCP Server for configuration and tool reference.

For Kata person agendas, get_person_agenda reads the live open tasks and returns the person's canonical vCard UID and aliases. Use Kata's MCP tools to create or change tasks; msgvault's agenda tool is read-only. Use the returned canonical UID as the scalar msgvault.person metadata value and msgvault.list for the list name (agenda by default). See Kata configuration for setup and limits.


skills

Install or remove the bundled msgvault agent skills:

msgvault skills install
msgvault skills uninstall

Install detects Claude Code and Codex from ~/.claude and ~/.codex, then writes the msgvault-search, msgvault-attachments, and msgvault-analytics skills to their user-level skill directories. Existing generated copies are updated in place; files without msgvault's generation marker are preserved unless --force is supplied.

Install flag Description
--agent claude / --agent codex Restrict installation to one or more detected agents; repeat or comma-separate values
--dir <path> Install into an explicit skill directory instead of detected agents
--force Overwrite skill files that no longer carry the msgvault generation marker

skills uninstall accepts --agent and --dir with the same target semantics, and removes only generated copies that still carry the marker. See Agent Skills for the workflow and safety model.


openapi

Print the checked-in msgvault OpenAPI contract without starting the daemon or opening the archive database.

msgvault openapi [flags]
Flag Default Description
--version 3.1 OpenAPI version to emit: 3.1 or 3.0
--format yaml Output format: yaml or json

The OpenAPI info.version is the API schema version, not the msgvault binary version. Use it to reason about forward/backward compatibility when a local or remote CLI talks to a server. The running daemon also serves the same contract at /openapi.json; generated client artifacts are built from the OpenAPI 3.0 form for tool compatibility.


daemon

Manage the local background daemon used by HTTP-backed CLI commands.

msgvault daemon start
msgvault daemon status
msgvault daemon stop
msgvault daemon restart

start launches the daemon in the background, status reports its recorded URL/PID/version/API schema/uptime, stop shuts it down, and restart performs a stop followed by a start. Starting a newer compatible binary replaces an older recorded daemon when [server].daemon_auto_restart = "newer"; incompatible running daemons are reported with a prompt to stop them first.

The lifecycle commands have no command-specific flags. Configuration comes from config.toml with the runtime environment overrides. See Web UI & API Server for endpoint documentation, run msgvault openapi, or fetch /openapi.json from a running server for the generated OpenAPI contract. See Configuration for config options. When vector search is enabled, the daemon can also run the embed worker on a cron and/or after every successful sync, see Configuration: vector.embed.schedule.

Background daemons started by daemon start or auto-started by a CLI command shut down after [server].daemon_idle_timeout with no requests. The default is 20m; set it to "0s" to disable idle shutdown. MSGVAULT_DAEMON_IDLE_TIMEOUT can override the value for a lifecycle-managed background daemon.

[server].daemon_auto_restart controls local daemon replacement when the CLI and recorded daemon versions differ. The default newer restarts only older compatible daemons, never leaves lifecycle to the operator or supervisor, and always restarts on any version mismatch that is safe for the current API schema.

For compatibility with existing scripts, msgvault serve start|status|stop|restart remains accepted without warnings, but these aliases are hidden from help and shell completion.


serve

Start the Web UI and API server with optional background sync scheduling in the foreground.

msgvault serve

msgvault serve stays in the foreground until interrupted and is not idle-stopped. Use it for externally supervised, Docker, and NAS deployments; use msgvault daemon for local background lifecycle management.

On unreleased main, --bind ADDRESS and --port PORT override environment, TOML, and defaults. A port of 0 selects an open port. --bind iface:NAME resolves a named network interface at startup and fails before binding if it cannot find a usable address. See runtime configuration for environment-only deployment and persisted API keys.

msgvault serve --bind 0.0.0.0 --port 8080

setup

Run the first-run setup wizard. It writes config.toml, optionally stores a Google OAuth credential (needed only for Gmail and Google Calendar; press Enter to skip), and optionally configures a remote deployment.

msgvault setup

If configured for a remote server, this command generates <MSGVAULT_HOME>/nas-bundle with:

  • config.toml ready for container deployment
  • client_secret.json, if a Google OAuth credential is configured
  • docker-compose.yml

The wizard also stores remote URL/API key in remote config block so export-token can use it without extra flags.

setup providers

Configure optional search and people features from the available API keys. The command reads VOYAGE_API_KEY, OPENAI_API_KEY, and the document provider's configured key variable (default MISTRAL_API_KEY). For an unset text feature, it chooses Voyage contextual embeddings, then OpenAI embeddings, then an available loopback Ollama model at [chat].server. Hosted people inference requires an explicit --provider and --model; an OpenAI key alone does not select it. Without an OpenAI key or explicit provider choice, setup can offer the configured local Ollama chat model. The running daemon does not fall back to another provider after a failure.

Setup prints a plan, asks once per hosted provider, writes config.toml, and prints the remaining commands. It onboards a new people-sweep provider through the same check and consent gates as person provider. Other feature-specific consents remain separate. Visual search needs a valid probe manifest before setup enables it; document extraction remains manual. See Recommended Configuration.

Local people-sweep setup stays pending unless --allow-sensitive is supplied. An explicit --provider requires --allow-sensitive=true|false and explicit --retention-posture and --training-posture assertions. Only true permits sending sensitive archive excerpts and inferring sensitive personal attributes; false permits the synthetic check but real sweeps cannot process evidence. --yes alone does not grant this permission. If a sweep is already enabled, --provider is rejected; use person provider add and person provider use to choose another profile. Vector lanes also stay pending when the binary lacks the backend required by the configured database; setup prints rebuild guidance.

Saved retention and training postures on disabled lanes are preserved unless their corresponding posture flags are explicitly supplied. Custom hosted text endpoints require explicit configuration of dependent lanes. Document semantic search requires both documents vectors consent --yes and documents vectors consent --purpose queries --yes; the latter authorizes query-text uploads. The status report tracks the two purposes separately.

An existing text model or endpoint is preserved even if the feature is disabled. Setup does not replace it when a different key appears. Explicit text and visual schedule keys, including an empty cron and run_after_sync = false, are also preserved. Setup does not configure source syncs, track people, enroll briefs, or set provider prices and monetary caps.

msgvault setup providers --dry-run
msgvault setup providers
msgvault setup providers --allow-sensitive
msgvault setup providers --yes --document-retention zdr --document-training opted-out
Flag Default Description
--provider absent Explicit people inference preset: openai, openrouter, or venice; requires model and privacy decisions
--model absent Required model ID for the selected preset
--credential-env absent Host environment-variable name for the preset key; mutually exclusive with --api-key-stdin
--api-key-stdin false Read the preset API key from standard input; otherwise a terminal can prompt for it
--yes false Accept every provider disclosure without prompting. Required to apply a plan with hosted-provider prompts when stdin is not a terminal or --json is used. It does not grant the separate sensitive-data opt-in.
--allow-sensitive false Allow the people sweep to send sensitive archive excerpts and infer sensitive personal attributes
--dry-run false Print the plan, the disclosures, and the current lane report without writing
--document-retention standard Mistral retention posture to record: standard or zdr
--document-training default-opt-out Mistral training posture to record: default-opt-out or opted-out
--retention-posture provider-declared Retention assertion recorded for embedding and inference providers
--training-posture provider-declared Training assertion recorded for embedding and inference providers
--json false Output the plan, applied flag, follow-ups, and report as JSON

The command edits the config file on this machine and refuses to run against a configured remote daemon; run it on the daemon host or pass --local.

setup status

Report the setup state of each feature (text search, semantic people search, visual attachments, documents, document vectors, people sweep, activity projection, media policy): state, provider, model, recorded consent, schedule, the reason a lane is off, and the command that turns it on. Reads config.toml, the environment, and the local archive's consent records; never contacts a provider. Stored provider credentials are checked too. An on state means the setup checks passed; it does not prove that an index has been built or that a provider will answer. The MCP summary is a partial list derived from enabled configuration flags, not the live server's tools/list response. Use the feature's status command or Web Operations to inspect index progress and coverage.

msgvault setup status
msgvault setup status --json

show-message

Show full message details.

msgvault show-message <id> [flags]
Flag Description
--json Output as JSON

JSON output includes web_url when the selected daemon can provide a browser link for the message.


list-accounts

List synced email accounts.

msgvault list-accounts [flags]
Flag Description
--json Output as JSON

update-account

Update account settings through the configured remote server or local daemon. Use --local to force the local daemon when a remote is configured.

msgvault update-account [account] [flags]
Flag Description
--display-name Set a display name for the account
--source-id ID Update exactly one source by numeric ID; mutually exclusive with the account argument

The account argument accepts an identifier or a unique display name. Supply either an account or --source-id.


remove-account

Remove an account or source and all its archived data from the selected msgvault archive. Deletes messages, labels, sync state, credentials no longer shared by another source, and attachment files unique to this source. This is irreversible but does not touch the remote provider.

msgvault remove-account [account] [flags]
msgvault remove-account 123456789012345678 --type discord
msgvault remove-account --source-id 42
Flag Description
-y, --yes Skip the confirmation prompt (and allow removal when an active sync is in progress)
--type Source type to remove when the same identifier exists across source types (gmail, imap, mbox, discord, etc.)
--source-id ID Remove exactly one source by numeric ID; mutually exclusive with the account argument and --type

The account argument accepts an identifier or a unique display name.

Attachment files are only deleted when no other account references the same content hash. A Discord bot token is preserved while another registered guild uses it, and deleted after its last source is removed. The shared Parquet analytics cache is rebuilt automatically.


stage-delete

Stage active messages as a pending deletion batch using exactly one selector: Gmail-like search criteria or a comma-separated list of explicit internal message IDs. The query form uses the daemon's search and preflight checks before staging; the ID form sends the existing message_ids field directly to the daemon. The command does not delete messages from a provider.

In query mode, staging is refused while the daemon is still verifying or rebuilding its full-text search index, or while its analytical cache is unavailable, because an incomplete index could silently omit matching messages. Both states resolve in the background; retry when they finish. These search-index and analytical-cache requirements apply to query mode.

In query mode, like deletion staging in the TUI and Web UI, the selection is resolved against the committed analytical snapshot, so messages synced after the most recent cache build are not included until the daemon refreshes the cache (it does so automatically after each sync). Re-run the command after a refresh to stage newly synced matches.

msgvault stage-delete <query>
msgvault stage-delete --ids 123,456,789

Exactly one of <query> and --ids IDS is required. The selectors are mutually exclusive, and --ids is also mutually exclusive with --source-id.

Flag Description
--dry-run Show the staged subset and skipped counts without creating a deletion batch
--source-id ID Restrict staging to one exact source ID
--ids IDS Stage positive, unique, comma-separated internal message IDs instead of a query

Query staging requires daemon API schema 2.18.0 or newer so the preflight reports the exact deletable subset. Upgrade the daemon if the CLI rejects its schema version.

The query resolves with the same search semantics as msgvault search, and --dry-run prints the set that staging would create.

Deletion staging covers email from Gmail and Microsoft Graph mail (msmail) sources only. A search that also matches chats, meetings, calendar entries, or mail from other sources such as IMAP or Apple Mail imports stages the deletable subset and reports how many items it skipped. Only a search with nothing deletable in it is refused. Legacy Gmail messages imported before message types existed carry a blank type and count as email, so message_type:email stages them too.

In ID mode, the daemon resolves live targets and source boundaries for the requested IDs; IDs that do not resolve to live deletable messages with provider message IDs are omitted, so the reported count is the number of targets resolved by the daemon; the CLI does not search, probe FTS readiness, call Explore or preflight, or require analytical-cache readiness. A newly started local daemon may initialize its cache in the background for later query consumers.

For a query that resolves to one source, the source selector is optional. For a multi-source query, provide --source-id for each source:

msgvault stage-delete --source-id 42 "from:newsletter@example.com older_than:1y"

Review a created batch with msgvault show-deletion <batch-id>, then execute it with msgvault delete-staged <batch-id>.


list-deletions

List pending and recent deletion batches.

msgvault list-deletions

show-deletion

Show details of a deletion batch.

msgvault show-deletion <batch-id>

cancel-deletion

Cancel pending or in-progress deletion batches. When called without a batch ID, lists available batches.

msgvault cancel-deletion [batch-id]
msgvault cancel-deletion --all
Flag Description
--all Cancel all pending and in-progress batches

delete-staged

Execute staged remote deletions. Gmail and IMAP move messages to Trash by default, and Microsoft Graph mail moves them to Deleted Items. --permanent uses Gmail batch deletion, IMAP UID EXPUNGE, or Graph permanentDelete; the IMAP permanent path requires UIDPLUS. The first deletion for a Graph mail account asks to upgrade its token to Mail.ReadWrite. Recovery from Trash depends on the provider. See deletion behavior.

msgvault delete-staged [batch-id] [flags]
Flag Description
-y, --yes Skip confirmation prompt
--permanent Permanently delete through Gmail batch deletion, IMAP UID EXPUNGE, or Graph permanentDelete instead of moving to Trash
--headless Use device-code sign-in for Microsoft Graph permission upgrades; open the printed URL on another device
--dry-run Show what would be deleted without deleting
-l, --list List staged deletion batches
--account Filter to one source by identifier or unique display name
--source-id ID Filter to exactly one source by numeric ID; mutually exclusive with --account

Starting in v0.20.0, remote deletion remains permanently opt-in. The invoking CLI may enable it durably with [deletion] remote_enabled = true or for one command with MSGVAULT_ENABLE_REMOTE_DELETE=1. Both mechanisms are permanent; there is no planned automatic removal of the guardrail. A remote daemon's own [deletion] section is not server policy for a command invoked elsewhere. Staging, --list, and --dry-run remain ungated. --permanent and --yes are mutually exclusive because permanent deletion always requires the destructive confirmation prompt.


repair-identity

Confirm a source's own email address and recompute is_from_me for matching archived messages. This writes immediately; it has no dry-run flag. Sources whose account address is already confirmed are skipped.

msgvault repair-identity user@example.com
msgvault repair-identity --type imap

The optional identifier narrows the repair. --type selects a source type and defaults to gmail. Only plain, valid email addresses are confirmed. For reviewing other aliases, use identity discovery.

repair-senders

Recover missing sender metadata from archived email MIME. The default is a read-only report; --apply writes repairs and refreshes analytics. This repairs messages that have neither sender_id nor a From-recipient snapshot and does not replace existing sender evidence or contact a provider.

msgvault repair-senders
msgvault repair-senders --apply

repair-message

Repair one Gmail message snapshot, or audit stored Gmail MIME without changing it. The repair form can fetch the message from Gmail; the audit is local.

msgvault repair-message --audit
msgvault repair-message --audit --source-id 42 --json
msgvault repair-message 123 --source-id 42
Flag Description
--audit Inspect stored MIME; accepts no message argument
--source-id Limit to one positive source ID
--json Emit newline-delimited audit results; requires --audit

Without --audit, supply exactly one internal numeric message ID or Gmail ID.

repair-derived

Recompute derived text and metadata from retained provider payloads without contacting the provider. The command writes immediately and refreshes analytics; it leaves raw payloads, downloaded media, and sync cursors in place.

msgvault repair-derived
msgvault repair-derived --source-type beeper

Repeat --source-type or --identifier to narrow the source set. Only source types with a registered re-derivation pass are supported; an unknown requested type is an error. Source syncs also run pending re-derivation passes, so use this command for on-demand repair or retrying an interrupted pass.

gc

Permanently purge all source-deleted messages from a SQLite archive, compact the database, and remove loose attachment blobs no remaining message references. Active messages and messages hidden only by deduplication remain. This does not contact providers and does not run on PostgreSQL.

msgvault gc
Flag Default Description
--yes, -y false Skip interactive confirmation
--no-backup false Skip the automatic SQLite database backup

There is no source/date selector or dry-run flag. The automatic backup covers the database, not attachment bytes removed afterward. Create a complete backup first. After a purge, follow the printed instructions to rebuild each enabled analytics or embedding cache. See local purge for the difference from remote deletion.

purge-excluded-media

Remove locally stored provider media excluded by the current media policy, replacing its occurrences with typed skip markers. Messages remain. Shared blobs stay live while a retained occurrence references them; packed dead bytes are reclaimed by the daemon's normal repack maintenance.

msgvault purge-excluded-media --dry-run
msgvault purge-excluded-media

--dry-run previews occurrence, blob, and logical-byte counts without changing the archive. Applying requires confirmation or --yes (-y). This never removes media from a provider. Review media policy and backup before applying.


repair-encoding

Fix UTF-8 encoding issues in existing messages through the configured remote server or local daemon. The command streams the daemon's stdout/stderr back to the terminal, and the daemon serializes the repair with other archive mutations. For SQLite archives, it also rebuilds the analytics cache. It reports invalid RFC 822 Message-ID values and leaves their original bytes unchanged to avoid making distinct identifiers collide. The analytics cache exports these IDs as NULL. Recover the original values separately from a verified source before rebuilding the cache.

It does not repair source message IDs, source identifiers, recorded sender/recipient envelope addresses, account identity addresses, or participant identifiers. Those fields need separate recovery from verified original values; rebuilding the analytics cache alone cannot recover them.

msgvault repair-encoding

repair-dates

Report email messages whose canonical sent_at is missing, before 1990, or more than 30 days in the future. The command resolves replacements from a plausible Date header, the oldest plausible Received timestamp, or stored source metadata, in that order.

The default is a read-only report. Pass --apply to update the archive and write a JSON audit ledger under the data directory. SQLite archives also rebuild the Parquet analytics cache; PostgreSQL archives do not use that cache. Original source files and remote servers are never modified.

msgvault repair-dates
msgvault repair-dates --apply
Flag Description
--apply Write repaired dates to the archive

update

Update msgvault to the latest version.

msgvault update [flags]
Flag Description
--check Check for updates without installing
-y, --yes Skip confirmation prompt
-f, --force Force update even if already on the latest version

version

Print version, commit, build date, and platform information.

msgvault version

completion

Generate a shell completion script.

msgvault completion [bash|zsh|fish|powershell]

To load completions:

Bash:

source <(msgvault completion bash)

# Permanent (Linux):
msgvault completion bash > /etc/bash_completion.d/msgvault

# Permanent (macOS with Homebrew):
msgvault completion bash > $(brew --prefix)/etc/bash_completion.d/msgvault

Zsh:

msgvault completion zsh > "${fpath[1]}/_msgvault"

If shell completion is not already enabled, add autoload -U compinit; compinit to your ~/.zshrc first.

Fish:

msgvault completion fish > ~/.config/fish/completions/msgvault.fish

PowerShell:

msgvault completion powershell | Out-String | Invoke-Expression

logs

View and tail structured log files from the selected daemon. With [remote].url configured, this shows remote daemon logs; otherwise it starts or contacts the local daemon. File logging must be enabled first (see Configuration: Log).

msgvault logs [flags]
Flag Default Description
-f, --follow false Follow today's log file as new lines are written
-n, --lines 50 Number of trailing lines to show before following
--run-id <id> — Filter to a single run (matches on prefix)
--level <level> — Filter by log level: debug, info, warn, error
--grep <string> — Substring filter applied to the raw JSON record
--all false Read every log file in the logs directory, not just today's
--path false Print the selected daemon's log directory path and exit

Examples:

# Last 50 lines of today's log
msgvault logs

# Follow live
msgvault logs -n 200 -f

# Filter to a single run by its correlation ID
msgvault logs --run-id a1b2c3

# Only errors
msgvault logs --level error

# Substring search across all log files
msgvault logs --all --grep deduplicate

quickstart

Print a quickstart guide for AI agents.

msgvault quickstart

agent-token

Manage restricted agent grants. The daemon must be started with [server] agent_access = true and a non-empty [server] api_key. All three subcommands require owner authentication (the owner API key; keyless loopback is not sufficient); a delegated caller cannot issue or modify grants.

What a token does and does not defend against

A restricted agent token scopes and revokes access for an agent process. It is not a boundary against an agent that holds shell access as the daemon owner: such an agent can read provider credentials, rewrite config.toml, or replace the daemon binary. What tokens buy is scoping and revocability for an agent already constrained by its deployment environment, plus a usable way to run the daemon somewhere the agent cannot reach.

A token does not defend against a local agent process running under the same user account as the daemon. The supported model keeps owner credentials, provider credentials, daemon configuration, and daemon replacement outside the agent's authority — a remote user-managed daemon achieves that, and so does an isolated local agent environment, while a second daemon or data directory under the same unrestricted user does not.

Tokens are in-memory and process-scoped. All grants are invalidated when the daemon restarts. A grant is valid until it is revoked or the daemon restarts. There is no persistence to disk and no migration needed.

agent-token issue

Issue a new restricted grant for one agent. The secret is printed once and not stored by the daemon (only its SHA-256 digest is kept in memory). Write it to a file that the agent can read, never pass it as a flag or environment variable.

msgvault agent-token issue --label <name> \
  --permissions draft.create \
  --source-ids <id>[,<id>...] \
  --sender <source-id>=<address>
Flag Description
--label <name> (required) Human-readable name for the grant
--permissions <perms> Comma-separated permissions: calendar.read for availability, calendar.event.read for provider-derived event details in delegated plans and write receipts, calendar.write for calendar mutations, and additional calendar.invite for guest changes; draft.create for draft-reply, draft-compose, and draft-get; draft.edit for draft-get, draft-edit, and draft-recover; draft.delete for draft-get, draft-delete, and draft-recover (see managed drafts)
--source-ids <ids> Comma-separated source IDs that the permissions apply to
--sender <source-id>=<address> Restrict a source to one confirmed sender identity; repeat for multiple choices

The grant is valid until revoked or until the daemon restarts.

When --sender is omitted for a selected source, issuance snapshots every currently confirmed valid mailbox identity. Sender selections are stored as canonical mailbox keys and remain fixed until the token is revoked. Adding an alias later does not expand an existing grant. A source with no selected sender has no delegated draft sender authority. Calendar grants use exact calendar source identities and do not require a draft sender selection. Delegated callers can run calendar commands and mcp over stdio; the delegated MCP bridge exposes only calendar tools.

The response includes the daemon address, the secret, and the granted source references. Pass --agent-url <address> and the file path to --agent-token-file when invoking delegated commands. The address comes from the issuing request. On a default local install it is an HTTP loopback URL, which works only on that machine and requires --agent-allow-insecure. For an agent on another machine, use a reachable HTTPS address instead. Plain HTTP requires --agent-allow-insecure and sends the token without transport encryption; use it only on a trusted network.

agent-token list

List all active grants. Secrets and digests are never returned. Agent-token commands return an error when server.agent_access is disabled.

msgvault agent-token list

Each row shows the grant ID, label, permissions, sources (as id/type/identifier), frozen sender keys, and creation time.

agent-token revoke

Revoke a grant by ID. Returns 204 whether or not the ID existed, so grant IDs cannot be enumerated by probing revoke.

msgvault agent-token revoke <id>

The grant is removed from the in-memory registry immediately. Any request in flight that already passed authentication completes, but the next authentication attempt with the revoked secret is denied without fallback.