Omagma agent CLI#
Experimental: verify account capabilities and operation receipts before live tasks. API behavior and commands may change as the terminal client develops. The feature catalogue groups the main capabilities and smaller workflow conveniences.
This guide describes the current CLI. Release bundles need no compiler; build-info reports compiler/build information. Agent setup and the setup workflow cover installation.
Authorize live features through full TUI/CLI permissions. A user's own configured account can be used after consent; fixtures are an optional disconnected preview, and a dedicated test mailbox is not a user setup prerequisite. An agent still needs the user's task authorization before sending mail or changing mailbox/contact data. Read access is not permission to widen grants or perform unrelated writes.
Fixture accounts expose mock send, mailbox-change, contact and RSVP capabilities without OAuth or Google access. Inspect accounts.list when selecting capabilities for an actual account.
Discover commands#
The executable reveals details as you ask for them. Help needs no account configuration, authorization or network access.
| Command | Shows |
|---|---|
omagma --help |
Short overview of command families |
omagma cli --help |
JSONL quickstart and the actual command names grouped by namespace |
omagma cli --help commands |
Every supported JSONL cmd with a description |
omagma cli --help mail |
Only the JSONL commands in that namespace |
omagma mail --help |
Mail operations with short descriptions |
omagma mail read --help |
That operation's flags, JSONL fields and example |
omagma mail read --help-all |
Scoped operation help plus shared defaults and advanced flags |
omagma draft create --help-all |
Compose flags plus the complete bare Draft file schema |
omagma contacts upsert --help-all |
Contact flags plus the complete bare Contact file schema |
omagma cli --help-options |
Launch/config/cache/fixture/resource options |
omagma cli --help mail.read |
Complete request fields, nested objects, conditions, limits and examples |
omagma cli --schema mail.read |
Machine-readable JSON Schema for that request, with all referenced types included |
omagma cli --schema |
Machine-readable schemas for all public JSONL commands |
omagma cli --help-all |
Complete offline JSONL reference, including shared object definitions |
omagma --help-all |
Extended command overview |
Start with accounts.list, then use a returned enabled account and its
capabilities. Follow message IDs from list replies into read requests, and check
ok on every response. The command catalogue and schemas are embedded in the
executable and work offline without configuration or credentials; the printed
documentation link supplies additional background. Command schemas describe
the supported public request fields. Stateful permissions, byte quotas,
content hashes and provider checks still apply when executing a request.
CLI convenience operations can perform several JSONL calls: Spam/Not spam uses
mail.batch, and conversation actions resolve mail.triage-scope before applying
an explicit batch. Contextual help describes those differences.
Verify agent access#
On a new Mac setup, choose Interactive or Agent-enabled
before consent. Interactive uses native Keychain by default in a local unlocked
terminal. Agent-enabled uses the same executable with private owner-only,
unencrypted token files. Preserve an existing explicit choice during repairs.
Changing the store needs no Google reauthorization: use the documented
credential migration from a context able to read the source, then restart open
sessions. omagma credentials status --json reports the selected store without
token contents; it is a standalone command, not a JSONL account operation.
File credentials avoid Keychain audit-session restrictions, but the agent sandbox must still permit the selected config, grant registry, credential and cache files and Google network access. Setup does not grant those permissions automatically. Use the agent's supported permission mechanism when access is denied; a local TUI working in another terminal is insufficient evidence.
For Agent-enabled setup, run real CLI subprocesses from the intended agent context, using the installed executable and actual private paths:
omagma credentials status --json --config /absolute/private/config.json
omagma terminal-auth status --account personal@example.com \
--config /absolute/private/config.json
omagma cli --config /absolute/private/config.json <<'EOF'
{"id":"accounts","cmd":"accounts.list"}
{"id":"mail","cmd":"mail.list","account":"personal@example.com","label":"INBOX","limit":1}
{"id":"contacts","cmd":"contacts.list","account":"personal@example.com","limit":1}
EOF
omagma mail read --account personal@example.com --message-id RETURNED_MESSAGE_ID \
--config /absolute/private/config.json
Use a message ID returned by the live list. Repeat per requested account and
include a custom --grant-file consistently when configured. Skip the mail
read if the mailbox page is empty; skip contacts only when that access was not
requested. A successful empty page is valid. accounts.list checks configured
accounts/capabilities; successful live mail/contact responses establish provider
access. Require ok:true for each JSONL response; process exit alone does not
prove that every request succeeded. Do not use --fixtures, --cached or
cacheOnly:true for this check.
Keep stdout and agent logs private because successful responses contain real
mail/contact data. Verification uses reads and needs no test send or mutation.
For Interactive setup, verify in the user's local unlocked terminal and state
when agent subprocess access has not been checked.
CLI and TUI coverage#
The JSONL agent CLI and TUI share the same backend operations, terminal grant, account checks, cache and mutation receipts. Reading full messages/threads, cache/server search, reply/reply-all, forwarding, sending attachments, archive/Trash/restore, stars/read state, label assignment and collection management, bulk actions/undo, contact list/search/create/update, custom-label colors, private browser preview, local draft discard, Spam/Not spam and invitation replies are available to both. The complete permission setup enables both interfaces for the authorized account.
The TUI adds interactive conveniences: Vim/mouse navigation, styled HTML and
tables, pane layouts, action discovery, current-message find, unread navigation,
search history/named searches, staged label checkboxes, link/file choosers,
quote/signature/thread folding, saved layouts/keymaps, $EDITOR, native text
undo, recipient/path completion and automatic local draft recovery.
CLI callers supply recipients and body source (including through
--body-file or --body-stdin) and explicitly save/update/recover drafts.
The CLI returns decoded plain text and structured data rather than the styled
screen, and can request up to 100 messages per page rather than the TUI's
32-message display window.
The command table below is the backend contract. One-shot commands cover common operations; JSONL supplies arrays and structured fields for richer requests. Drafts are local in both interfaces, with optional explicit Gmail draft handoff. Both native interfaces support Calendar reads and reviewed imports, plus reviewed ordinary timed/all-day event creation, editing and rescheduling, generated Google Meet details and pasted HTTPS meeting links. Uncertain saves survive restart and support read-only checks, explicitly confirmed identical-request retry and uncertainty archival. The TUI adds agenda/day/week views and a guest picker using the destination account's Contacts; CLI callers supply guest emails. See the Calendar event guide. Permanent mail deletion and contact deletion remain unavailable. Label support includes listing, assigning/removing existing labels and creating, renaming, coloring or deleting custom definitions. Shared target resolution and membership counts support reviewed conversation/bulk workflows. The CLI also exposes the durable local send queue; its delay is explicit rather than the TUI's default countdown.
Software updates#
omagma updates status|check|guide|dismiss [--json] shares the TUI's release
state and installation-aware guidance. status is cached; check explicitly
requests public release metadata. guide --json includes versions,
installation ownership, commands and instructions. omagma updates automatic on|off changes the persisted daily-check preference. These standalone commands
need no account configuration or Google authorization and are separate from
the account-scoped JSONL protocol. See updates.
Protocol#
omagma cli (alias omagma agent) reads one JSON object per line and emits one response per line. stdout contains only protocol frames. EOF ends the process; malformed/oversized frames produce errors and the next frame can proceed. Each response echoes id and account:
{"version":1,"id":"list-1","ok":true,"account":"personal@example.com","data":{"messages":[],"nextCursor":null}}
Errors use ok:false,error:{code,message}. id may be a string, integer or null. Limits are documented in TERMINAL.md; requests are at most 3 MiB and nesting 64. Optional string fields must be strings or omitted, not JSON null. Tokens never appear in this interface.
Except accounts.list, every command requires an explicit configured account. Provider message/thread IDs can collide across accounts. Never reuse a cursor, draft ID, contact or operation from another account. accounts.list reports enabled accounts and locally configured capabilities; successful provider authorization is checked again before network access. Private config and grant registry are not message data.
omagma cli --fixtures --cache-dir /absolute/private/mock-cache
Send lines such as:
{"id":"accounts","cmd":"accounts.list"}
{"id":"page1","cmd":"mail.list","account":"personal@example.com","label":"INBOX","limit":100}
{"id":"read","cmd":"mail.read","account":"personal@example.com","messageId":"RETURNED_MESSAGE_ID"}
Commands#
| cmd | Additional fields / result |
|---|---|
| accounts.list | accounts with address, enabled, capabilities, configured senderName/signature |
| accounts.identities | verified send-as identities with address,name,signature,isDefault; cacheOnly supports cached identities plus configured primary fallback |
| labels.list | bounded Gmail label id,name,type,color list; cacheOnly uses the downloaded account list |
| labels.create | name, operationId, optional color; creates a custom label and returns its stable label ID |
| labels.rename | labelId, name, operationId; renames a custom label while retaining its ID and memberships |
| labels.delete | labelId, confirmName, operationId; deletes the custom label, keeping its emails; confirmName must match the current name |
| labels.color / palette | color requires labelId, operationId and color {backgroundColor,textColor}; palette returns allowed Gmail hex values |
| mail.triage-scope | messageId, scope message/conversation; complete account-scoped messageIds, count and threadId, up to 100; incomplete/oversized resolution fails |
| mail.label-state | messageIds; metadata-only per-label appliedCount/mixed state and complete target count |
| mail.list / search / sync | limit 1..100, query, label, cursor; returns one message page and nextCursor; search selects local cache with cacheOnly:true or Gmail with false/default; sync is a list-operation alias, not a history refresh |
| mail.refresh | optional limit/query/label/prefetchLimit; applies a bounded history update or recent resync to the account cache |
| mail.prefetch | limit 0..64; refreshes and fills a bounded recent body head within existing cache quotas |
| mail.recipients | cacheOnly:true; up to 1,024 recent account-scoped recipients from retained From/To/Cc and permitted saved contacts; excludes primary self, deduplicates and ranks recent interaction |
| mail.read | messageId; decoded full text, envelope/threading fields, labels, attachments, invitation |
| mail.thread | threadId; chronological messages |
| mail.attachment | messageId, attachmentId; filename,mimeType,size and inline data(base64url) or an account blobId descriptor for larger content |
| mail.attachment-save | messageId, attachmentId, absolute path; streams a new private file, refusing overwrite and symlink traversal |
| mail.open | optional messageId; explicitly opens configured Chrome profile |
| mail.reply | messageId, all:boolean, optional bodyFormat plain/markdown and preserveFormatting:boolean; creates a local reply/reply-all draft |
| mail.forward | messageId, optional bodyFormat plain/markdown, preserveFormatting:boolean or original:boolean; creates an unaddressed local forward draft; formatting and original-email modes conflict |
| mail.send | draft, operationId; durable submission receipt |
| mail.archive / trash / restore | messageId; reversible label mutation |
| mail.mark | messageId, unread/starred booleans, addLabels/removeLabels arrays |
| mail.batch | messageIds (1..100 distinct IDs), action archive/trash/restore/spam/unspam/mark, optional unread/starred/addLabels/removeLabels; per-ID outcomes and undoToken |
| mail.undo | undoToken; restores only actual touched-label changes for confirmed successful batch items |
| draft.create / update | draft with bodyText source and optional bodyFormat plain/markdown; update also requires draftId |
| draft.preview | draft or draftId; bodyFormat, exact bodyText source, rendered plainText and optional bodyHtml; local preview without saving or sending |
| draft.open-preview | draftId; writes a bounded private sandboxed HTML preview and explicitly opens this account's Chrome profile; sends no mail |
| draft.recovery-save | draft.recoveryFields exact five raw fields To/Cc/Bcc/Subject/Body, optional draftId; incomplete local recovery that cannot be sent |
| draft.list / read / send / discard | read/send/discard require draftId; send also requires operationId |
| draft.queue | draftId, operationId, optional delaySeconds 0..30 (default 10); stages a durable unsent intent and returns queueId/dueAtMs/state |
| queue.list / read / cancel / resume / process | read/cancel/resume/process require queueId; resume accepts delaySeconds; process requires due time or wait:true; no automatic processing after restart |
| attachment.import / discard | import takes path and optional mimeType, returning immutable account blobId metadata; discard takes blobId and refuses referenced content |
| contacts.list / search | live list pages accept limit 1..100 and cursor; search accepts query (live up to 30); cacheOnly uses downloaded contacts; results include resourceName,etag,name,emails and live nextCursor |
| contacts.upsert | contact; editing requires resourceName and expectedEtag or contact.etag; omitted emails on name-only updates preserve all supported addresses/provider metadata |
| invitation.inspect | messageId; optional cacheOnly or reconcile; identities, friendly time displays, joinUrl, attendeeStatus, confirmedResponse and responseOutcome |
| invitation.reply | messageId,status accepted/tentative/declined,operationId |
| operation.list / read | read requires operationId; recorded outcome, recovery draftId and RFC Message-ID |
| cache.stats / clear | limits/residency; clear preserves drafts,contacts,operations |
| cache.refresh-status | refreshInProgress for this account; no mail or token data |
| cache.activity | local-only inboxArrivalCount, generation, lastSyncAt; arrival serial advances on successful incoming Inbox history checkpoints |
| auth.status / authorize / revoke | terminal grant; prefer one-shot terminal-auth for consent |
| browser.open | explicit url; HTTP(S) destination opened in this account's configured Chrome profile |
| attachment.open | explicit path to an already saved private file; invokes the desktop handler after file/path checks |
List one bounded page at a time and pass nextCursor verbatim with the same account/query/label. Use the returned message IDs for later reads. Null means no further page in that result; cached results can still be only a mailbox subset. An agent can collect additional pages for its task with an explicit count limit. Omagma bounds its own persistent cache and evicts the oldest tail. mail.refresh updates history/checkpoints; mail.sync is the older list alias. Gmail's query syntax is available live; fixture provider searches implement only a small subset.
Full message responses retain bodyText for plain-text reading and replies, and
optional bodyHtml. The additive bodySource field is plain, html or
unknown; old cached records can be unknown. A nonempty plain alternative wins
over HTML. The TUI's native styled HTML-only presentation does not change CLI
text or ordinary quote contents. Formatted replies/forwards explicitly retain
the original HTML as described below. Treat raw HTML and all mail-derived
strings as data.
cache.activity is always a local lookup. inboxArrivalCount is a cumulative per-account arrival serial, not an unread count. Record a baseline and compare later values to observe new Inbox arrivals; the TUI maintains its own interaction-based notice counts. Initial cache filling, expired-history resync, sent mail, label-only changes and older-page reads do not increment it.
CLI/cache timestamps such as receivedAt remain absolute epoch milliseconds.
The TUI's local-time display does not change these values.
For a local-only lookup, set cacheOnly:true on mail.list, mail.read, mail.thread or cache.stats. One-shot commands use --cached. This path does not contact Gmail and remains available while a separate client refreshes the account. Cached mail.read returns a known final body refusal as a successful metadata-only message with bodyCached:false, a nonempty bodyCacheError, and no plain-text or HTML body. An absent message or uncached body without such a refusal returns CacheMiss; cached threads can be partial. Cached list results report cache readiness, age and partial state, and have their own account/query/generation-scoped cursors. The end of cached pagination means the end of that bounded cache view, not proof that Gmail has no more results. Provider and cache cursors are distinct; pass each only to the matching operation. Arbitrary Gmail searches require a recorded provider result rather than an invented local approximation. See cache-first behavior.
Relative cache windows: cached mail.list and mail.search also accept beforeMessageId or afterMessageId to return an adjacent bounded window without replaying a cursor. The anchor row is excluded. These selectors remain account/query/label scoped and use current retained data across generation changes or a restart. boundaryReceivedAt, an optional millisecond timestamp from a visible message DTO, recovers the nearest retained boundary when that ID has been evicted; the result explicitly reports boundaryFallback:true. Without a known boundary or timestamp, CacheBoundaryGone is returned. Results include cacheWindow and hasMoreCachedBefore/hasMoreCachedAfter. Never combine these selectors with a cursor, and never use a provider L token to rewind cached mail. One-shot flags are --before-message-id, --after-message-id and --boundary-received-at, together with --cached.
Downloaded-body search: mail.search with cacheOnly:true evaluates retained metadata and already downloaded plain-text bodies, without a previously recorded Gmail query or server request. It searches no raw HTML or encoded attachment data and never downloads a missing body. Whitespace joins up to 32 terms with AND; double quotes preserve a phrase, and - negates a term. Supported fields include from:, to:, cc:, subject:, body:, label:, in:, filename:, is:unread|read|starred|important and has:attachment. after: (inclusive), before: (exclusive) and on: compare local calendar dates (YYYY-MM-DD or YYYY/MM/DD); newer:/older: are absolute-date aliases. Unknown named operators, unsupported predicate values and invalid dates fail explicitly. Label names use the downloaded account list; IDs also work. These local predicates are a subset of Gmail syntax; use --server for the full language.
Results report searchScope:"metadata-and-cached-bodies", partial:true, matchedCachedCount, highlightTerm, and searchMatches with messageId,field,offset,length,excerpt. Excerpts are bounded to 192 UTF-8 bytes; offsets/lengths describe bytes within the excerpt. Body-aware K cursors bind body residency as well as account/query/label/metadata generation, so newly fetched bodies invalidate an old result cursor. Restart the query after InvalidCursor. Metadata-only queries remain stable while bodies download. K cursors are distinct from cached provider-view C cursors and Gmail L continuations; never exchange them. A local match does not prove that uncached mailbox content lacks the query.
{"cmd":"mail.list","account":"personal@example.com","label":"INBOX","limit":32,"cacheOnly":true}
{"cmd":"mail.refresh","account":"personal@example.com","label":"INBOX","limit":32}
{"cmd":"mail.search","account":"personal@example.com","cacheOnly":true,"query":"from:alex body:\"launch notes\""}
{"cmd":"mail.refresh","account":"personal@example.com","label":"INBOX","limit":32,"prefetchLimit":64}
Reads do not mark read. Use mail.mark explicitly if requested. There is no permanent delete. Draft bodyText is UTF-8 source, interpreted according to bodyFormat. To/Cc/Bcc accept RFC address-list strings or arrays of address objects {address,name}. Optional draft.from accepts one address object or single RFC address-list string; an alias is verified again on every live send, and accounts.identities reads aliases/plain signatures through the existing mail grant. Reply planning honors Reply-To, self aliases, original recipients and References; it never promotes Bcc into reply-all. Missing/invalid Message-ID gives an explicit error. Forwarding chooses no recipient and does not join the original thread; missing or oversized original attachments fail instead of disappearing silently.
{"cmd":"draft.create","account":"personal@example.com","draft":{"to":[{"address":"alex@example.org"}],"subject":"Demo 🌋","bodyText":"Hello!"}}
{"cmd":"draft.send","account":"personal@example.com","draftId":"USE_RETURNED_ID","operationId":"task-unique-send-1"}
Small inline attachments use
{id:"",filename:"safe-basename.txt",mimeType:"text/plain",size:DECODED_BYTES,data:"BASE64URL_NO_PADDING"}
in draft.attachments. Larger content uses the descriptor returned by
attachment.import, including an immutable blobId owned by this account.
Use the returned descriptor rather than inventing a handle or passing a cache
path. attachment.discard refuses a handle still referenced by a draft or
protected content. size remains an exact decoded byte count.
Ordinary outgoing attachments allow 16 files and 25 MiB decoded total;
retained original resources allow 32 and a separate 2 MiB. Body/source and
rendered alternatives stay bounded to 2 MiB, JSON requests to 3 MiB and complete
outgoing MIME to 35 MiB. All combined content must fit the applicable limits.
Repeated --attach-file FILE uses inline base64 for a set no larger than 2 MiB
and account storage for larger sets, preserving the bounded JSON interface.
mail.attachment returns inline data for smaller content or a blobId
descriptor for a larger received file. Empty/omitted data with a handle does not
mean the file is empty. mail.attachment-save streams the content into the
caller's explicit new absolute path, with 0600 permissions and no overwrite or
parent/leaf symlink traversal. Received files are individually bounded to
50 MiB; cached attachment handles also obey account disk quotas. The file is
decoded incrementally, so its content does not consume a whole JSON frame or
raise the 2 MiB text-body limit. Saving does not require a mailbox mutation.
There is no --output-file flag. Existing attachment.open/mail open-attachment
opens an already saved supported private file; it does not download a handle.
Reviewed message and conversation targets#
mail.triage-scope resolves a single anchor with scope:"message" or
"conversation" into complete account-scoped messageIds, count and
threadId. The maximum is 100; incomplete, oversized or inconsistent
conversations fail instead of returning a truncated mutation target.
Review the returned IDs/count, then pass those exact IDs to mail.batch.
Later arrivals are not implicitly added to that batch.
mail.label-state accepts the explicit target IDs and returns their current
per-label membership counts without fetching bodies. It supports checking
all/mixed/none before constructing addLabels/removeLabels. A successful
membership snapshot does not authorize unrelated mailbox changes.
{"cmd":"mail.triage-scope","account":"personal@example.com","messageId":"PROVIDER_ID","scope":"conversation"}
{"cmd":"mail.label-state","account":"personal@example.com","messageIds":["RETURNED_ID_1","RETURNED_ID_2"]}
{"cmd":"mail.batch","account":"personal@example.com","messageIds":["RETURNED_ID_1","RETURNED_ID_2"],"action":"mark","addLabels":["EXISTING_LABEL_ID"]}
The one-shot adapter offers --scope message|conversation for
archive/trash/restore/spam/unspam/mark/batch with one --message-id anchor. It
resolves complete IDs before the mutation. Explicit --message-ids ID,ID
continues to mean only that set. JSONL clients should use the resolver plus
explicit batch IDs rather than assuming an extra scope field changes a
single-message mutation.
Label collection management#
labels list returns the account's definitions; ordinary mail labels remains
a list alias. In the one-shot adapter, supplying target IDs to either alias
invokes membership inspection instead.
Create, rename, color and delete affect the collection, while mail mark and
mail batch add/remove memberships on selected messages. System definitions
are protected. Writes require mail-modify and an explicit operation ID.
omagma labels list --account personal@example.com --cached
omagma labels create --account personal@example.com --name 'Projects/Volcano' --operation-id label-create-001
omagma labels rename --account personal@example.com --label-id RETURNED_ID --name 'Projects/Magma' --operation-id label-rename-001
omagma labels palette --account personal@example.com
omagma labels color --account personal@example.com --label-id RETURNED_ID \
--background-color '#a479e2' --text-color '#000000' --operation-id label-color-001
omagma labels delete --account personal@example.com --label-id RETURNED_ID --confirm-name 'Projects/Magma' --operation-id label-delete-001
Deleting a label removes its associations, keeping the messages and their
bodies. It is distinct from removing that label from one message. Keep the
operation ID to inspect or replay its recorded result; an uncertain remote
outcome must be checked rather than submitted again under a new ID.
Both color values must come from labels.palette; arbitrary RGB strings are
rejected before mutation. Create can also receive the same optional color pair.
Markdown drafts and preview#
Fresh CLI drafts and direct sends default to plain.
Set draft.bodyFormat:"markdown" to opt in, or use
top-level bodyFormat:"markdown" for mail.reply/mail.forward. Existing
drafts retain their saved format when an update omits it, and legacy drafts
remain plain. Source stays exact through save, recovery and preview: ordinary
drafts use bodyText, while incomplete recovery drafts retain the raw body in
recoveryFields[4]. Generated HTML is derived locally.
draft.preview accepts an existing draftId or a supplied draft and returns
bodyFormat, source bodyText, the outgoing plainText alternative, and
optional bodyHtml. It neither saves nor submits the supplied source. Preview
can inspect an unfinished recovery draft; that draft still requires a normal
validated update before sending. Markdown sends contain rendered HTML and a
readable plain-text alternative. The saved draft retains Markdown source,
while Sent message bodyText contains the rendered plain alternative.
{"cmd":"draft.create","account":"personal@example.com","draft":{"to":"alex@example.org","subject":"Notes","bodyFormat":"markdown","bodyText":"## Review\n\n**Ready** for Thursday."}}
{"cmd":"draft.preview","account":"personal@example.com","draftId":"USE_RETURNED_ID"}
{"cmd":"mail.reply","account":"personal@example.com","messageId":"PROVIDER_ID","all":true,"bodyFormat":"markdown"}
Supported syntax includes headings, emphasis, strikethrough, lists, quotes, tables, links and fenced code. Known fence languages receive bounded syntax highlighting; unknown languages remain literal code. Raw HTML is escaped, unsafe link schemes stay inert, and Markdown images become alt text or safe links without loading remote images. Markdown HTML includes a small grey “Sent with omagma” footer, volcano emoji and the approved tiny logo embedded in the email. No branding image is fetched from a server. The terminal preview shows this content; receiving email clients control their final appearance.
One-shot --format markdown|plain applies to mail compose, draft create,
draft update, direct mail send, mail reply/forward and supplied-source
draft preview. It can accompany --body-file, --body-stdin or
--draft-file. draft send and draft preview --draft-id use the saved format;
change it through an explicit draft update first.
omagma mail compose --account personal@example.com --to alex@example.org \
--subject Notes --body-file /absolute/private/notes.md --format markdown
omagma draft preview --account personal@example.com --draft-id LOCAL_ID
omagma draft preview --account personal@example.com \
--body-file /absolute/private/notes.md --format markdown
omagma draft send --account personal@example.com --draft-id LOCAL_ID --operation-id TASK_ID
Review the returned alternatives before a task-authorized send. Rendering does not add a grant or bypass the account, recipient, attachment or request limits.
draft.open-preview requires a saved draftId. One-shot callers can use
draft open-preview --draft-id ID or draft preview --draft-id ID --browser.
This explicitly opens the account's Chrome profile on a private generated file.
The message is isolated in an opaque sandbox with scripts, forms, remote
resources and navigation blocked; verified PNG/JPEG/GIF resources are inlined.
It preserves the note/original's styles and tables, sends nothing and does not
upload a Gmail draft. The artifact is owner-only, limited to 16 MiB, replaced
on reuse and removed on local draft discard.
omagma draft preview --account personal@example.com --draft-id LOCAL_ID --browser
Replies and forwards with original content#
Set preserveFormatting:true on mail.reply or mail.forward to retain the
original HTML, tables, styling and embedded images below a separate editable
note. Reply-all uses the same flag with all:true. bodyText and bodyFormat
describe your note; its usual Markdown rendering and Omagma footer appear
above the read-only original. Plain mode keeps the note literal while retaining
the original HTML. Without the flag, these commands keep the editable
text-quote behavior. A request to preserve formatting without original HTML
returns OriginalHtmlUnavailable.
The returned draft.original is a read-only snapshot containing the source
identity, envelope, text, HTML and embedded image resources. Leave it unchanged
when updating a structured draft; edit the note and ordinary compose fields.
The snapshot survives recovery, restart and eviction of the source mail from
the cache. Preview and sending use the same assembled note, original and
plain-text alternative. Existing drafts retain their saved content and format.
For the complete original as an attachment, use original:true on
mail.forward. The draft starts with an empty note and one .eml containing
the original headers and all contained attachments. Those files are not added
again as separate attachments; the recipient opens the attached email.
original is forward-only and conflicts with preserveFormatting.
Neither option sends the draft.
{"cmd":"mail.reply","account":"personal@example.com","messageId":"PROVIDER_ID","all":true,"preserveFormatting":true,"bodyFormat":"markdown"}
{"cmd":"mail.forward","account":"personal@example.com","messageId":"PROVIDER_ID","preserveFormatting":true,"bodyFormat":"markdown"}
{"cmd":"mail.forward","account":"personal@example.com","messageId":"PROVIDER_ID","original":true,"bodyFormat":"markdown"}
One-shot commands use --preserve-formatting for reply/reply-all and forward,
or --original for forward only. The flags conflict; --format selects the
editable note's format.
omagma mail reply --account personal@example.com --message-id PROVIDER_ID \
--all --preserve-formatting --format markdown
omagma mail forward --account personal@example.com --message-id PROVIDER_ID \
--preserve-formatting --format markdown
omagma mail forward --account personal@example.com --message-id PROVIDER_ID \
--original --format markdown
Original email source is limited to 2 MiB; encoded content must also fit the 3 MiB request and ordinary draft bounds. Missing or oversized originals fail before saving a partial draft or sending mail. Embedded image bytes are retained; remote images still depend on the image server and recipient settings. Final styling depends on the receiving email client. An unknown send outcome protects the saved draft and must be checked before another submission. Captured original resources are separately limited to 32 items and 2 MiB; they do not consume the ordinary 16-file count. Incoming MIME bookkeeping accepts up to 49 file/resource descriptors, including related parts and the approved logo, within its separate 128-part parser bound. These parser limits do not expand outgoing ordinary-file or retained-resource quotas.
Local send queue and cancellation#
TUI review uses a ten-second cancellation period by default. Direct JSONL
mail.send/draft.send and ordinary one-shot sends retain immediate submission;
CLI delay is an explicit choice. None of these operations schedules future
delivery through Gmail or through the read-only background cache timer.
draft.queue stages a validated saved draft with a stable operationId and
delaySeconds from 0–30 (default 10). Its receipt contains queueId, draftId,
operationId, state, createdAtMs and dueAtMs. Staging sends nothing.
queue.process submits a due queued item; before its deadline it returns
QueueNotDue unless wait:true explicitly waits. queue.cancel cancels only an
unsent queued intent and keeps its draft. queue.resume explicitly resets a
queued item's countdown. Cancelled/applied/rejected/unknown/submitting entries
are not resumed or automatically sent again.
{"cmd":"draft.queue","account":"personal@example.com","draftId":"LOCAL_ID","operationId":"SEND_TASK_ID","delaySeconds":10}
{"cmd":"queue.read","account":"personal@example.com","queueId":"RETURNED_QUEUE_ID"}
{"cmd":"queue.cancel","account":"personal@example.com","queueId":"RETURNED_QUEUE_ID"}
To keep staging and processing separate in a one-shot workflow:
omagma draft queue --account personal@example.com --draft-id LOCAL_ID \
--operation-id SEND_TASK_ID --delay-seconds 10
omagma queue process --account personal@example.com --queue-id RETURNED_QUEUE_ID --wait
mail send/draft send --send-delay 10 stages and waits through this same queue,
then returns its result. queue resume --queue-id ID --delay-seconds 10 resets
the deadline but still needs explicit processing. Another authorized client
can inspect or cancel the queued item while a processor waits.
To skip the remaining countdown for one already reviewed intent, use
queue.send-now or omagma queue send-now --account personal@example.com --queue-id RETURNED_QUEUE_ID. It claims the same queue and operation identity,
checks the reviewed content and submits immediately. Replaying a final,
canceled, submitting or unknown item returns its status without another send.
This leaves the default cancellation period unchanged.
No process startup automatically processes a pending item. Reopening a queued
draft in the TUI first checks queue state and exposes :resume-send or
:cancel-send. Normal TUI close cancels its unsent countdown; a crashed queued
item stays pending for explicit handling. Submitting/unknown outcomes protect
their recovery draft and require receipt/provider inspection, not a new send.
Side effects and uncertainty#
Every send/RSVP needs a stable operationId chosen by the caller. Omagma persists an unknown receipt before dispatch and derives a stable RFC Message-ID from account+operationId. Repeating the same account, identity and semantic content returns the recorded receipt without another provider call. For ordinary mail sends, a new identity for the same unresolved draft/content also returns the existing unknown receipt, preventing accidental duplicate submission through a reopened draft. For an unresolved invitation RSVP, a new operationId instead returns UnknownOutcome; replay the original ID to read its existing receipt. Reusing it for different content gives OperationConflict. This is a local duplicate guard, not a server-side idempotency guarantee.
Outcomes: applied=provider accepted, rejected=known failure, unknown=possibly applied. Do not retry unknown with a new ID. Inspect operation.read and the provider's Sent folder before asking the user how to proceed. Direct sends also preserve a local recovery draft before dispatch. draft.discard removes a local draft and its regenerable preview, but refuses protected pending/submitting/unknown sends. Cancel an unsent queued intent before discarding or editing its draft. Applied/rejected drafts may be explicitly discarded to free the 128-draft quota. Cache clear does not erase the journal. A full journal stops new sends; it never silently discards uncertain receipts. RSVP replay ignores its varying DTSTAMP but compares the semantic response.
The receipt's messageId is the provider resource ID; use it with mail.read
when available. Its rfcMessageId is the submitted Internet Message-ID, which
the provider can replace. A full message's messageId is the actual Internet
header, while its id is the provider resource ID. Use that actual header for
cross-mailbox rfc822msgid: searches after reading Sent. An empty search for
the submitted header is not proof that an uncertain send failed.
Contact updates use CONTACT-source etags and reject conflicts. Supported edits preserve all 32 addresses and bounded provider names/email/source metadata rather than reducing a contact to one address. Live writes invalidate the contacts cache before submission and return the validated provider contact directly; the next contacts read refreshes the cache. Mutating transport interruption or unrecognizable provider acknowledgement is uncertain. Failure to encode a live mutation's final response is also UnknownOutcome, even if its error frame cannot be allocated. Refresh authoritative provider state before another mutation. The permission error occurs before provider access if the required capability is absent.
Bulk and undo: bulk actions preserve independent per-message outcomes and never submit sends. Up to 16 account-scoped undo receipts persist under the private cache quota. The receipt records actual pre-action membership only for touched labels. Undo therefore preserves unrelated labels changed later. Rejected, pending, unknown and already restored entries are skipped; an uncertain undo is not repeated automatically. Cache clear preserves these receipts. Apply bulk actions only to IDs explicitly selected for the intended account; a batch is a bounded sequence with partial outcomes, not a transaction.
Meeting invitations: JSONL invitation.inspect/invitation.reply and one-shot invitations inspect/invitations reply share calendar recognition and receipts. Recognized MIME forms are text/calendar, application/ics, and application/octet-stream with a .ics filename, including named external Outlook/Teams attachments. Calendar data is limited to 128 KiB. Inspection displays requests, responses, published events and cancellations; a multi-event or ambiguous-attendee notice remains read-only. Sending a response requires exactly one requested event or recurrence instance and an attendee belonging to the selected account or a verified send-as alias. Matching duplicate MIME representations are accepted; conflicting calendars are refused. Inspection requires mail-read; reply additionally requires calendar-rsvp and an operation ID. If an older cached message retained only a calendar attachment reference, explicit inspection or reply refreshes that one message and preserves unrelated cached bodies.
Reply sends a standard iTIP email response to the calendar organizer, preserving UID, sequence and recurrence identity; email From/Reply-To do not choose the destination. The three states are accepted, tentative and declined, mapped to the corresponding attendee PARTSTAT. A copied Zoom/Teams join link alone is not a calendar request and cannot be given an invented RSVP. Calendar API access is unnecessary. Unknown submissions are not retried automatically. Inspection can fetch mail or verified identities; invitations inspect --cached or JSONL cacheOnly:true instead uses only retained source and identities. After an uncertain RSVP, invitations inspect --reconcile or JSONL reconcile:true checks Sent for the recorded Message-ID and exact RSVP identity. Positive evidence confirms the local receipt; absence leaves it unknown and never resends. The TUI offers C Check Sent for this case. Cached inspection and reconciliation cannot be combined.
Configurable body prefetch defaults to the smaller of the display page and 32. prefetchLimit or --prefetch-bodies N explicitly selects 0..64; 64 can fill a larger retained head while the display page stays 32. Zero disables automatic body filling. TUI startup and the background cache-refresh command share this policy, fixed message/disk quotas and old-tail eviction. Cached mail remains readable while refresh runs.
One-shot interface#
One-shot command families are mail, labels, draft, queue, attachment,
contacts, invitations, cache, operation and terminal-auth. Common names map to the shared API:
mail compose creates a local draft and mail drafts lists them.
mail labels lists existing labels, and mail identities
discovers sending identities.
mail open-link --url URL opens a link in the account's Chrome
profile, and mail open-attachment --path FILE opens an already saved local
file in its viewer. The latter does not download a received attachment. Live
viewer opening requires an absolute path to a regular file with private
permissions and a supported extension (.pdf, .txt, .md, .csv, .png,
.jpg, .jpeg, .gif or .webp). Fixture commands report a fixture result
without launching a browser/viewer; attachment opening in fixtures does not
validate a saved file's existence.
Account discovery is JSONL accounts.list; there is no omagma accounts list
command family.
omagma mail list --account personal@example.com --label INBOX --limit 100
omagma mail search --account personal@example.com --query launch --cached
omagma mail search --account personal@example.com --query 'from:alex@example.org' --server
omagma mail read --account personal@example.com --message-id PROVIDER_ID
omagma mail reply --account personal@example.com --message-id PROVIDER_ID --all
omagma mail mark --account personal@example.com --message-id PROVIDER_ID --unread --add-label EXISTING_LABEL_ID
omagma draft send --account personal@example.com --draft-id LOCAL_ID --operation-id TASK_ID
omagma mail compose --account personal@example.com --to alex@example.org \
--subject 'A draft' --body-file /absolute/private/body.txt --attach-file /absolute/private/file.txt
omagma operation read --account personal@example.com --operation-id TASK_ID
omagma contacts list --account personal@example.com --limit 100
omagma contacts search --account personal@example.com --query Alex
omagma contacts upsert --account personal@example.com --contact-file /absolute/private/contact.json
omagma invitations inspect --account personal@example.com --message-id INVITATION_ID
omagma invitations reply --account personal@example.com --message-id INVITATION_ID \
--status accepted --operation-id RSVP_TASK_ID
omagma cache stats --account personal@example.com --cached
Additional examples:
omagma mail forward --account personal@example.com --message-id PROVIDER_ID
omagma mail labels --account personal@example.com --cached
omagma mail identities --account personal@example.com --cached
omagma mail batch --account personal@example.com --action archive --message-ids ID1,ID2
omagma mail undo --account personal@example.com --undo-token RETURNED_TOKEN
omagma mail prefetch --account personal@example.com --limit 64
omagma mail triage-scope --account personal@example.com --message-id PROVIDER_ID --scope conversation
omagma mail label-state --account personal@example.com --message-ids ID1,ID2
omagma mail attachment-save --account personal@example.com --message-id PROVIDER_ID \
--attachment-id ATTACHMENT_ID --path /absolute/private/downloads/report.pdf
omagma attachment import --account personal@example.com --path /absolute/private/report.pdf
omagma queue list --account personal@example.com
For contact creation, the contact file supplies name and emails. To edit,
include its returned resourceName and pass --expected-etag RETURNED_ETAG
(or include the returned contact etag). Supported contacts retain up to 32
addresses plus bounded provider metadata. A name-only edit can omit emails
to preserve them; explicit address edits supply the intended complete array.
This supports create/update, not deletion. Label assignment uses existing label
IDs or the supported name resolver. Collection management uses
labels create|rename|color|delete, with
explicit operation IDs and a reviewed name for deletion. Invitation replies send email,
not Calendar changes.
JSONL carries every structured operation, including multi-ID arrays, local raw
draft recovery and contact objects. One-shot calls accept those objects through
--draft-file/--contact-file; do not invent extra flags for object fields.
The CLI does not launch the TUI's editor, autocomplete or styled renderer.
For an agent handoff, mail.read already returns decoded bodyText; the TUI
also offers :copy-message for a focused message, visual/mouse selection for excerpts,
and e to open a disposable received-message snapshot in the user's editor.
Operation --help gives its applicable flags, defaults/effects and a shell
example. --help-all adds common resource/fixture options and the complete
Draft/Contact file schema when relevant. --help-options shows shared process
options directly. Help returns before reading body files or stdin, even when
placed after those options. Literal option values such as --subject --help
remain data. Usage errors point to the nearest family/operation help without
echoing private argument values. All one-shot replies are JSON; --json is
accepted for compatibility.
| Flags | Purpose / scope |
|---|---|
--account ADDRESS, --config FILE, --grant-file FILE |
Explicit account, private configuration and terminal grant registry |
--cache-dir DIR, --metadata-limit N / --cache-messages N, --disk-limit-bytes N / --cache-bytes N |
Cache location and retained count/byte limits |
--limit N, --cursor TOKEN, --query TEXT, --label ID_OR_NAME |
Bounded page and matching account/query/label continuation |
--cached, --server |
Local-only one-shot lookup; --server applies only to mail search. JSONL uses cacheOnly instead |
--message-id ID / --id ID, --thread-id ID, --attachment-id ID |
Returned provider identities for read/thread/attachment operations |
--draft-id ID, --operation-id ID |
Local draft/receipt identities; sends and RSVP require a stable operation ID |
--to LIST, --cc LIST, --bcc LIST, --subject TEXT |
Outgoing address lists and subject |
--body-file FILE, --body-stdin, --body TEXT |
UTF-8 source body; file/stdin avoids putting it in argv |
--format markdown|plain |
Explicit source interpretation for compose/create/update/direct send/reply/forward or supplied-source preview; new CLI source defaults to plain |
--preserve-formatting |
Reply/reply-all or forward with a read-only formatted original below the editable note |
--original |
Forward the complete original as an .eml attachment; conflicts with --preserve-formatting |
--attach-file FILE (repeatable), --draft-file FILE |
Regular outgoing files or a structured draft JSON object |
--all / --reply-all |
Plan a reply-all draft |
--unread / --read, --starred / --unstarred, --add-label LABEL, --remove-label LABEL |
Explicit mail.mark changes; label flags may repeat |
--name NAME, --label-id ID, --confirm-name NAME |
Label collection create/rename/color/delete; combine writes with --operation-id |
--background-color HEX, --text-color HEX |
Gmail-compatible color pair for labels create/rename/color; obtain values from labels palette |
--scope message|conversation |
One-shot complete target resolution from a single message anchor before triage |
--queue-id ID, --delay-seconds N, --wait |
Queue read/cancel/resume/process; delay 0–30 for draft queue/resume; --wait only for queue process |
--send-delay N |
Explicit 0–30 second queue+wait for mail send/draft send; no delayed-send default on direct CLI sends |
--blob-id ID, --mime-type TYPE |
Account attachment handle discard / optional import media type |
--browser |
draft preview with an existing draft-id; explicitly opens private account-profile browser preview |
--contact-file FILE, --expected-etag ETAG |
Create/edit contact object and version precondition |
--status accepted|tentative|declined |
Invitation reply, with message and operation IDs |
--client-file FILE, --capabilities CSV |
terminal-auth authorize; follow account setup |
--from ADDRESS |
Explicit verified sending identity |
--action ACTION, --message-ids ID1,ID2, --undo-token TOKEN |
Batch actions/undo; repeated message-id flags also build a batch |
--prefetch-bodies N |
Explicit 0–64 body head for terminal modes/cache refresh |
--before-message-id ID, --after-message-id ID, --boundary-received-at MILLISECONDS |
Adjacent current cached window; combine with --cached, not cursor |
--url HTTP_URL, --path FILE |
Link opening, opening an already saved file, attachment import, or a new destination for mail attachment-save; meaning follows the command |
--ui-file FILE, --editor-mode auto|takeover, --no-mouse |
TUI preferences/editor/mouse options, not additional CLI business operations |
Use --body-file or --body-stdin to keep content out of argv. --draft-file/--contact-file accept bounded JSON objects; combining --draft-file with compose fields or --attach-file is rejected as conflicting input. One-shot success exits 0; ordinary command errors emit an error response and exit nonzero. JSONL command errors allow the next frame, while output failures may end the process. A live mutation's UnknownOutcome survives that exit path. Never put tokens in argv, logs, shell history or agent messages.
For interactive keys see terminal mail. Developer architecture and verification live in developer references; ordinary use does not require replaying that qualification workflow.