Omniscio documentation
Browse all documentation
  1. Getting Started13
  2. Sessions & Agents115
  3. Inbox & Notifications59
  4. Projects & Tasks95
  5. Automation & Scheduling75
  6. Knowledge & Memory26
  7. AI Features60
  8. Integrations100
  9. Plugins & Marketplace33
  10. Cloud & Teams56
  11. Settings & Customization58
  12. Account & Billing28
  13. Troubleshooting84
  14. CLI & API Reference22
  15. Legal & Policies4
  16. Uncategorised22

SMS via CLI (read, triage, and send texts from an AI agent)

Reading, triaging and sending texts from outside the app, over Omniscio's local control server, so an AI agent or a shell script can work your SMS inbox. Covers the three tiers the routes are split into, the workflow a caller follows, the approval gate that stands in front of sending, and what is deliberately left out.

What it is

Omniscio's SMS integration — texts sent through your selected SMS provider (Pushbullet, or your own phone via the native provider), described in set-up-sms-integration.md — is reachable from outside Omniscio over the same 127.0.0.1:19519 localhost HTTP server that exposes cron jobs, tags, recipes, and settings. An external AI agent (Claude Code in another project, a curl script, the omniscio-control skill) holding the Omniscio bearer token can check the connection status, list conversations and read a thread, triage the SMS inbox, and send a text — all from a shell.

The gating model splits the 18 routes across three tiers by blast radius:

  • Reads are bearer-gated and read-budgeted. GET /sms/status, GET /sms/devices, GET /sms/conversations, GET /sms/conversations/:phoneNumber and GET /sms/scheduled require the bearer token and share the 60-reads-per-minute-per-token-hash budget with every other auth-gated GET on this server. They expose private message content, so they always require the token (same tier as the search endpoints).
  • Triage mutations apply immediately. read, archive/unarchive, snooze/unsnooze, dismiss, contact-name, sync, connect, disconnect apply inline with no inbox row (bearer + the global 10-mutations-per-minute bucket). Each is reversible personal state — mirrors the in-app SMS view — and emits an sms:conversation-updated push so an open Omniscio window refreshes (a CLI mutation has no renderer to optimistically update).
  • Sending is approval-gated. POST /sms/send is the one outbound, irreversible action: it counts against your monthly SMS limit and texts a real person. By default it lands as a pending row in Omniscio's inbox (preview shows the number + a message snippet) and only transmits when you click Approve — the same user-in-the-loop guarantee as the other approval-gated CLI capabilities in cli-pending-actions.md, sharing the same open-pending cap. You can flip requireApprovalForCliSmsSend off (Settings → CLI Control → Approval requirements) to send immediately. The same gate covers POST /sms/scheduled, which stages a future text: a scheduled send IS a send, so leaving it open would be an approval bypass (schedule 31 seconds out and skip the gate) — it lands an sms.schedule_send row and never schedules directly. Cancelling one is NOT gated: POST /sms/scheduled/:id/cancel applies immediately, because cancelling is reversible and de-escalating, and it is idempotent (it pushes only on the live-to-deleted transition).

Deliberately not exposed (by design): setting the Pushbullet token (it's a secret — configure it in the desktop SMS settings), choosing the sending device (setup; it also opens the realtime socket — desktop only), and the two AI helpers (reply suggestions, thread summary — they call a paid model). Use the desktop app for those.

Where to find it

This one has no screen of its own. The routes are called from outside the app by anything holding a token for Omniscio's local control server — a Claude Code session in another project, a curl script, or an external AI. The texts themselves are the same ones the in-app SMS view shows, and anything sent this way turns up there too.

How it behaves

How to use it

The 18 routes:

Method Path Gating Purpose
GET /sms/status read Connection + config status (configured, hasToken, connected, monthly usage — the monthly cap is Pushbullet-only; native has no quota).
GET /sms/devices read List Pushbullet devices that can send SMS (Pushbullet-only — native has no device list). With no Pushbullet token saved it answers 422 UNPROCESSABLE ("Pushbullet API token not configured") — a normal not-set-up answer, not an error to retry.
GET /sms/conversations read List conversations (?includeArchived=true to include archived).
GET /sms/conversations/:phoneNumber read Messages in one thread (?limit 1–500 default 50, ?before ISO timestamp).
POST /sms/conversations/:phoneNumber/read immediate Mark the conversation read.
POST /sms/conversations/:phoneNumber/archive immediate Archive + clear inbox attention.
POST /sms/conversations/:phoneNumber/unarchive immediate Un-archive.
POST /sms/conversations/:phoneNumber/snooze immediate Snooze until a future ISO-8601 timestamp. Body: { snoozedUntil }.
POST /sms/conversations/:phoneNumber/unsnooze immediate Clear the snooze.
POST /sms/conversations/:phoneNumber/dismiss immediate Clear inbox attention ({ restore: true } re-flags it).
PATCH /sms/conversations/:phoneNumber/contact-name immediate Set/clear the contact name. Body: { name } (empty string clears).
POST /sms/sync immediate Pull latest threads from the phone (debounced 15s; coalesces concurrent callers).
POST /sms/connect immediate Open the Pushbullet realtime socket (no-op without a token).
POST /sms/disconnect immediate Close the realtime socket (reconnectable).
POST /sms/send approval Send a text. Body: { phoneNumber, message }. Returns 202 + pending row by default.
POST /sms/scheduled approval Schedule a future text. Body: { phoneNumber, message, scheduledFor }. Same gate as /sms/send; returns 202 + pending row.
GET /sms/scheduled read List every live (enabled, not deleted) scheduled SMS.
POST /sms/scheduled/:id/cancel immediate Cancel a scheduled SMS (soft-delete). Idempotent; pushes only on the live-to-deleted transition.

All require Authorization: Bearer <token>. :phoneNumber is the conversation's stored key (usually E.164, e.g. +15551234567); URL-encode it (%2B for +) and pass the exact key from /sms/conversations — the triage routes do not re-normalize it.

Workflow

  1. Check setup with GET /sms/status. If configured is false (no token / no device), the user must finish SMS setup in the desktop app — the CLI can't set the token or pick the device. A send will be rejected until configured is true.
  2. List conversations with GET /sms/conversations, then read one thread with GET /sms/conversations/:phoneNumber?limit=20. Use ?before=<ISO> to page older messages.
  3. Triage immediately — archive, snooze (a future ISO timestamp), dismiss, read, contact-name. Each returns 200 and refreshes any open Omniscio window.
  4. Send a text with POST /sms/send — body { phoneNumber, message }. Send an X-Client-Request-Id (≤64 chars) so retries are idempotent in a 30-day window. The route returns 202 with the queued row; tell the user "open Omniscio's inbox and approve to send" and stop. A 202 means queued, not sent.

Approval + failure semantics (the important part)

  • POST /sms/send returns 202 with a pending row when gated — the text transmits only after the user approves it in the inbox (or 200 immediately if they turned approval off).
  • A failed send is never reported as sent. If Pushbullet can't deliver (phone offline, rate-limited, auth expired), the approval is rejected with a plain-language reason — the message did not go out.
  • A send is never auto-retried — a retry could text a real person twice. If a send errors, the row goes to rejected; reuse the same X-Client-Request-Id so a deliberate re-POST is de-duplicated rather than doubling the text.

Idempotency

POST /sms/send accepts an X-Client-Request-Id header (≤64 chars). The same id within a 30-day window returns the existing pending row (HTTP 200, idempotent: true) instead of queuing a duplicate, scoped to the sms.send action kind. The triage mutations are reversible and need no idempotency key.

Examples

TOKEN=$(<~/.amc/cli-token)
BASE=http://127.0.0.1:19519

# 1. Is SMS set up + connected?
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/sms/status"

# 2. Recent conversations, then one thread (URL-encode the + as %2B)
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/sms/conversations"
curl -s -H "Authorization: Bearer $TOKEN" "$BASE/sms/conversations/%2B15551234567?limit=20"

# 3. Triage (applies immediately)
curl -s -X POST -H "Authorization: Bearer $TOKEN" "$BASE/sms/conversations/%2B15551234567/archive"
curl -s -X POST "$BASE/sms/conversations/%2B15551234567/snooze" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"snoozedUntil":"2026-06-09T09:00:00.000Z"}'

# 4. Send a text — lands in the inbox for approval (default). 202 = queued, NOT sent.
curl -s -X POST "$BASE/sms/send" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -H "X-Client-Request-Id: sms-send-$(date +%s)" \
  -d '{"phoneNumber":"+15551234567","message":"On my way!"}'

Out of scope (v1)

  • Setting the Pushbullet token (set-token) — it's a secret; configure it in the desktop SMS settings, never over the CLI.
  • Choosing the sending device (select-device) — setup that also auto-opens the realtime socket; desktop-only.
  • AI reply suggestions + thread summaries — they call a paid Anthropic model, so they're kept off the CLI (the same rule that keeps mind-map AI generation off the CLI).

For agents

How it works

The 18 routes live in src/main/services/cli/cli-server-sms-routes.ts, registered once at Omniscio startup from src/main/app/startup/register-cli-routes.ts. Every handler reuses the SAME Pushbullet service + message DB the in-app SMS IPC handlers use, so the CLI and the desktop UI can't drift:

  • Reads + triage call the shared pushbullet-service / queries-sms functions directly, then emit the same sms:conversation-updated push the in-app handlers emit. CLI body schemas are .strict() (unknown keys → 400); snooze additionally requires a real, future ISO-8601 timestamp (stricter than the loose in-app schema, so a malformed value can't land in the DB).
  • The two orchestrated operations — sending (transmit + clear inbox attention + track) and the debounced sync — live in a shared sms-operations.ts module (sendOperatorSms, debouncedSmsSync) that both the IPC handlers and the CLI/dispatch path import. One definition each, no drift.
  • POST /sms/send routes through the shared gateOrApplyCliAction chokepoint: it enqueues an sms.send row (or applies immediately when the toggle is off). The actual transmit happens in the sms-send-handler.ts dispatch module, which inspects the send result — a delivery failure throws a PermanentDispatchError (so the row is rejected, never reported "applied", and never auto-retried).

Behavior is locked by sms-cli-contract.md and tested in cli-server-sms-routes.test.ts + sms-send-handler.test.ts.

Related

The in-app feature these routes drive — pairing, the conversation view, snooze and archive — is on the set up SMS integration page. Sending is approval-gated through a shared queue described on CLI pending actions, and the server the calls arrive on, together with its token handling, is on CLI control.

  • set-up-sms-integration.md — the in-app SMS feature: Pushbullet pairing, the conversation view, snooze/archive.
  • cli-pending-actions.md — the shared approval queue POST /sms/send lands in.
  • cli-control.md — the CLI server itself, token handling, the full route catalog.
  • omniscio-control skill — the external-AI side of the contract.

Last verified 2026-09-28