Skip to content

Add callstack inbox for bounded stored SMS history #75

Description

@Justinabox

Motivation

Callstack now has installable CLI entry points for send, status, and safe modem doctor, but there is still no small, scriptable way to inspect received SMS history from the terminal. For the north-star Raspberry Pi SMS receiver/sender use case, an operator or automation script should be able to confirm that recent MFA/automation messages arrived without starting the HTTP server or writing a custom Python snippet.

This should stay deliberately narrower than the live event tail in #50 and the HTTP durable-listing cleanup in #19: one CLI command that reads bounded stored message history and prints it safely.

User journey

  1. A user runs Callstack with --sms-db-path /var/lib/callstack/sms.sqlite so received/sent SMS are persisted.

  2. Later, from SSH on the Pi, they run:

    callstack inbox --sms-db-path /var/lib/callstack/sms.sqlite --limit 10
  3. They see the newest received messages in a concise table.

  4. Automation can run:

    callstack inbox --sms-db-path /var/lib/callstack/sms.sqlite --limit 1 --json

    and parse the latest sender/body/timestamp without scraping HTTP.

API / UX sketch

Add a CLI subcommand such as:

callstack inbox [--limit 20] [--status unread|read|all] [--sender +155...] [--json]

Suggested output behavior:

  • Human output: newest messages first, bounded by --limit, with timestamp, sender, status, and a single-line body preview.
  • JSON output: an array of objects with id, sender, body, timestamp, status, and storage_index for local automation.
  • Errors: no tracebacks and no accidental phone-number/message-body leakage from exception text.

Technical approach

  • Add the subcommand to callstack/cli.py beside status, send, and doctor.
  • Reuse ModemConfig.sms_db_path so the command can point at the same SQLite store as the server/modem process.
  • Prefer a store/service query that does not send AT commands or mutate SIM storage by default. If the implementation needs a public helper, add a narrow method such as SMSService.list_stored_messages(...) or an explicit store-backed query rather than reaching into private attributes from the CLI.
  • Keep live tailing out of scope; Add callstack monitor for PII-safe live event tailing #50 owns callstack monitor.
  • Keep HTTP endpoint persistence out of scope; Back HTTP SMS and delivery-report listings with SMSStore instead of process globals #19 owns /sms/messages and delivery-report listing cleanup, though both slices may share the same service/store query helper.

Affected modules and tests

Likely files:

  • callstack/cli.py
  • callstack/sms/service.py or callstack/sms/store.py if a small public query helper is needed
  • tests/test_cli.py
  • tests/test_sms_store.py or a focused service/store test if a new helper is introduced
  • README.md CLI examples only if the implementation PR chooses to document the command

Hardware / modem caveats

  • The default command should be a persisted-history reader, not a modem poller; it should not send SMS, USSD, call, SIM unlock, storage-delete, or other mutating AT commands.
  • If a future --refresh-sim option is desired, it should be a separate explicit follow-up because reading/deleting SIM slots and direct modem state can have device-specific behavior.
  • Tests should use fake stores/modems only; no real hardware requirement.

Acceptance criteria

  • callstack inbox --sms-db-path <path> --json --limit N prints valid JSON with at most N persisted messages.
  • Human output is bounded, readable, and does not dump tracebacks on store/config failures.
  • --status, --sender, and --limit filters are validated and covered by tests, or explicitly reduced to a smaller first PR with --limit + --json only.
  • The command does not instantiate a modem path that sends AT commands when only reading an existing SQLite store.
  • CLI error output remains redacted even when the underlying store/helper raises an exception containing a sender/body.
  • Existing send, status, and doctor CLI tests still pass.

Verification gates

git diff --check
PYTHONPATH=. uv run --no-project --with pytest --with pytest-asyncio --with pytest-aiohttp --with pyserial-asyncio --with aiosqlite pytest tests/test_cli.py tests/test_sms_store.py -q
PYTHONPATH=. uv run --no-project --with pytest --with pytest-asyncio --with pytest-aiohttp --with pyserial-asyncio --with aiosqlite pytest tests/ -q

Non-goals

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions