Executive inbox copilot. Built so far: the provider-agnostic brain (Phase 0), the triage engine (Phase 1), draft replies (Phase 3) and attachment classification (Phase 4). Triage classifies + summarizes an inbox and prints a report grouped by priority; optionally it drafts replies for messages that need one and classifies attachments. All exec-facing output is Lithuanian; code and comments are English. No Graph or Monday yet (Phase 2 / 5) — drafts and attachments currently run against offline fixtures.
cd sietas
npm install
# Offline — no API key needed. Proves the pipeline + report end to end.
npm run triage:mock
# Offline, full pipeline: triage + draft replies + attachment classification.
npm run triage:full
# Live Claude. Put your key in .env first (cp .env.example .env).
ANTHROPIC_API_KEY=sk-ant-... npm run triage
# Flags (compose freely):
# --drafts draft a reply for emails that need one (Phase 3)
# --files classify attachments (Phase 4)
# --json machine-readable output for later phases
npm run triage:mock -- --drafts --files --json--drafts makes the pipeline call draftReply (strong model) for every thread
whose classification has needsReply: true. The draft is for human review,
signed EXEC_NAME, and never sent. Tone/rules come from EXEC_RULES
(free-form Lithuanian) and are injected into prompts/draft.lt.md.
--write-drafts (requires --live) takes each generated draft and creates it as
a reply draft in the mailbox's Drafts folder via Graph createReply + a body
PATCH (createDraftReply, needs Mail.ReadWrite). The exec then reviews and
sends it from Outlook. Sietas never sends — there is deliberately no
Mail.Send call.
npm run triage:push # = --live --drafts --files --write-draftsNotes:
- Safe by design: only creates drafts; it cannot send.
- Guardrails (
src/brain/risk.ts): a thread that looks legal, payment/invoice, commitment/signature, scam, or confidential is not drafted — it's flagged⚠ Rankinė peržiūrawith a reason. Keyword rules are easy to extend. - Idempotent: a draft already created for a message id is not recreated on
re-runs (state in
data/state.json, keyed byconversationId). --dry-run: with--write-drafts, prints what would be created and writes nothing — use it before the first real run.- The draft body is plain text (the original is still quoted by
createReply).
--files makes the pipeline call classifyFile for each attachment it can
resolve. Offline, attachments are text stand-ins in fixtures/files/ named
<attachmentName>.txt; the loader (src/ingest/files.ts) also accepts real
binaries (PDFs → base64) so live Claude vision drops in unchanged once Graph
downloads real attachments (Phase 2). Output: document type, counterparty,
status — all Lithuanian.
Everything calls the LLMProvider interface (src/providers/types.ts); nothing
else imports a vendor SDK. src/providers/factory.ts picks the implementation
from LLM_PROVIDER:
LLM_PROVIDER |
Implementation | Status |
|---|---|---|
anthropic |
AnthropicProvider — cheap model classifies, strong model writes |
live |
mock |
MockProvider — offline keyword heuristics |
live (dev/CI) |
azure |
AzureOpenAIProvider |
stub until Phase 6 |
Honest caveat: method signatures are swap-proof; capabilities behind them
are not. classifyFile uses native PDF/image vision today — an Azure swap will
need a separate OCR step (Azure Document Intelligence). That divergence is
contained to src/providers/azure.ts.
Set in .env: ANTHROPIC_MODEL_CHEAP (classify/routing) and
ANTHROPIC_MODEL_STRONG (summarize/draft/file). Mirrors the cost design.
src/providers/ types.ts (interface), anthropic.ts, azure.ts (stub), mock.ts, factory.ts
src/config/ env.ts, tenant.ts (TenantConfig — no hardcoding)
src/ingest/ fixtures.ts (emails; Graph mailReader drops in later), files.ts (attachments)
src/brain/ triage.ts (classify + summarize + optional draft + optional file classify)
src/prompts/ load.ts (prompt loader)
src/report.ts console + JSON report
src/index.ts runOnce() entrypoint (--drafts / --files / --json)
prompts/ classify.lt.md, summarize.lt.md, draft.lt.md, fileClassify.lt.md (versioned)
fixtures/ emails.json (11 Lithuanian sample emails; two share a conversationId = one thread)
fixtures/files/ text stand-ins for attachments (<name>.txt) — real PDFs drop in later
--html writes a self-contained report.html (clean Lithuanian dashboard:
priority cards, summaries, reply/thread badges, attachment tags, collapsible
drafts) to the current folder. This is the exec-facing surface — open it in a
browser, not the terminal.
npm run triage:demo # = --live --drafts --files --html (real inbox, OpenRouter)
npm run demo:mock # offline preview from fixtures, no keyFor the full "wow" moment, also push the drafts into Outlook once
(npm run triage:push) so they're waiting in the exec's Drafts folder.
--live (or MAIL_SOURCE=graph) swaps the fixtures for a real read of the
exec's mailbox using app-only (client-credentials) Graph auth — same
EmailInput / FileInput shapes, so triage/drafts/files are unchanged.
src/ingest/graph.ts fetches the inbox and downloads attachments as base64
(real PDFs now flow into Phase 4 vision). Reading needs Mail.Read; access must
be scoped to the single mailbox with Exchange Application RBAC (legacy installs may still use an ApplicationAccessPolicy).
Setup is a checklist: see AZURE_SETUP.md (app registration, Exchange
Application RBAC, positive/negative mailbox tests, secrets). Then:
npm run triage:live # = --live --drafts --filesHosted multi-tenant storage must use a non-owner Neon role with PostgreSQL RLS.
See RLS_SETUP.md; migrations use DATABASE_URL_OWNER, while the app receives
only the restricted DATABASE_URL.
One-time client links are emailed separately from their six-digit codes. SMTP
setup is documented in LOGIN_EMAIL_SETUP.md.
Built: Phase 0 (brain), 1 (triage), 2 (Graph inbox + attachment download +
threading + draft write-back), 3 (drafts), 4 (attachments). Threading groups
messages by conversationId, summarizes the whole thread, and collapses repeated
forwards into one entry. Drafts can be written back to Outlook's Drafts folder
(--write-drafts, never sent). Remaining: sending (Mail.Send, deliberately
skipped), Phase 5 (Monday), Phase 6 (Azure provider). See PROJECT_PLAN.md.