Skip to main content
Glama

Alfred

Alfred is a local-first, open-source personal secretary that runs on your PC. It remembers what you tell it, keeps a typed temporal memory graph with evidence-backed provenance, briefs you from Calendar, Gmail, Canvas, and GitHub, and acts on your behalf—creating events, drafting and sending mail, filing issues—only after you preview and approve. SQLite owns memory, tasks, schedules, audit records, and connector state. Telegram, Slack, Claude, Cursor, ChatGPT, and the CLI are replaceable interfaces, not the source of truth.

Credentials stay in the OS credential store. Tokens never land in the database, audit log, Markdown vault, or Git. Writes are preview-then-confirm. Models are optional and off the default path.

What you get

  • A local SQLite archive with an append-only audit log and encrypted backup

  • Typed temporal memory (search, correct, forget) and a two-way Obsidian vault

  • Read-only Calendar / Gmail / Canvas / GitHub / Health sync feeding a morning brief

  • Approval-gated writes: Calendar events, Gmail drafts and sends, GitHub issues

  • Telegram (and optional Slack) as the phone remote; Claude, Cursor, and ChatGPT over policy-gated MCP

  • alfred run or a Windows service that keeps the loop alive

See ARCHITECTURE.md for the decisions behind this. Commands and setup are below.

Local setup

python -m venv .venv
.\.venv\Scripts\python -m pip install -e .[dev]
.\.venv\Scripts\alfred init
.\.venv\Scripts\alfred status

Or run those four steps with .\scripts\install.ps1. It is safe to re-run: it skips venv creation when one already exists, never reads or requests a provider credential, and every mutating step honors -WhatIf. Add -RegisterScheduledTask to also register the Task Scheduler entry in "Running continuously"; Get-Help .\scripts\install.ps1 -Full lists every parameter.

The default database is .alfred/alfred.db, which Git ignores. Use another path with --db <path> or ALFRED_DB_PATH.

Optional quiet hours hold proactive Telegram/Slack deliveries (reminders, morning briefs, nags—anything with an outbox job_id) inside a local window. Set ALFRED_QUIET_HOURS_START and ALFRED_QUIET_HOURS_END to HH:MM (overnight windows like 22:0007:00 are fine) and optionally ALFRED_QUIET_HOURS_TIMEZONE to an IANA name (default UTC). Interactive replies with no job_id still deliver. Unset means quiet hours are off.

Related MCP server: razberri MCP Server

Local connectors

Store every token under OS credential-manager service alfred. Alfred never writes credentials to its database, audit log, vault, or Git.

Approval-gated writes (Calendar events, Gmail drafts and sends, GitHub issues, memory forget, backup restore) are always three steps:

  1. *-propose — local preview; never touches the provider.

  2. alfred approval-approve --approval-id <ID> --actor nico — prints a one-time token.

  3. *-execute --approval-id <ID> --actor nico --token <TOKEN> — consumes that token.

Retrying the same approval ID and token replays the stored receipt instead of duplicating. If the PC fails after the provider accepts but before Alfred records the receipt, the same command recovers via a stable provider ID and fails closed on an absent or ambiguous result. There is no MCP tool to approve: a human (or a trusted local channel outside the MCP client, e.g. alfred approval-approve) has to grant it.

Connector

Credential (account)

Read

Write

Telegram

telegram-bot-token

paired chats

replies, reminders, briefs

Slack

slack-app-token, slack-bot-token

Socket Mode, paired only

same

Google Calendar

OAuth via google-auth

selected calendars

approval-gated event create

Gmail

same OAuth grant

unread headers/snippets

approval-gated draft and send

Canvas API

canvas-api-token

assignments

Canvas iCal

alfred canvas-ical-setup

dated assignments

GitHub

github-token / github-issue-token

unread notifications

approval-gated issues

Google Health

extra OAuth scopes

sleep, activity, metrics

Composio

alfred composio-setup

overflow apps (Notion, Spotify, …)

approval-gated writes

Slack is built and unit-tested against synthetic fixtures but has not been exercised against a real Slack app. Google Health's list client now matches the published v4 shapes (typed filters, nested payloads, empty name on non-identifiable points, sleep page size 25); it still keeps the complete raw point in metadata["raw"] and still needs a live wearable- linked grant (alfred google-auth --include-health) before treating the connector as verified.

alfred connector-status (or MCP connector_status) reports ok, stale (no success in 24 hours), error, or never_synced without exposing a credential or synced content.

alfred connector-capabilities answers what each connector is allowed to do: who can write, which OAuth scopes are actually requested, which stores sensitive data, and whether it polls, pushes, or is local. Today six can write (Calendar, Gmail, GitHub, Telegram, Slack, Composio) and exactly one stores sensitive data (Google Health, all three scopes read-only). "Can write" marks the approval boundary—nothing there runs unattended. The same table is on the admin dashboard and is cross-checked against the source by tests.

Telegram and Slack

Put the bot token under telegram-bot-token, then run alfred telegram-poll with explicitly paired chat/user IDs. Only those pairs enter Alfred.

Slack stays local through Socket Mode—no public webhook or tunnel. deploy/slack-app/manifest.yml is a ready-to-paste Slack app; deploy/slack-app/README.md covers generating tokens, storing them as slack-app-token/slack-bot-token, inviting the bot, and finding IDs. Then add --slack-pair CHANNEL_ID:USER_ID --slack-channel-id CHANNEL_ID to alfred run. Only paired user/channel combinations enter Alfred; replies, reminders, and briefs can only go to explicitly allowed channels.

Google OAuth (Calendar, Gmail, Health)

Calendar and Gmail share one OAuth 2.0 flow. In Google Cloud Console, create an OAuth client of type "Desktop app" (no redirect URI—Alfred's local loopback flow is exempt) and save its client ID and secret as google-oauth-client-id and google-oauth-client-secret. alfred google-auth opens a browser, receives the redirect on a local port, and stores a refresh token as google-oauth-refresh-token. Later syncs mint a fresh access token from that refresh token; none is cached. Re-run google-auth if the grant is revoked.

The always-on runner reads every calendar selected in the Google Calendar UI: title, timing, status, and a source link—never event descriptions or attendee lists. Each event also keeps its calendar ID/name and Google's read-only creator/organizer identity when available, so Alfred can say where an event came from without copying the guest list. That needs the narrow calendar.calendarlist.readonly scope in addition to event access. A one-shot alfred calendar-sync can still target one calendar explicitly.

Calendar writes: alfred calendar-event-propose --actor nico --summary "..." --start <ISO-8601> --end <ISO-8601>, then approve, then alfred calendar-event-execute. Recovery uses Alfred's stable Calendar event ID.

Gmail reuses the same grant. alfred gmail-sync copies each unread message's subject, sender, and Gmail's short snippet—never the body or attachments. Reading or archiving a message drops it from the next sync. Sync is bounded to the most recent --limit unread messages (default 500, Gmail's newest-first order) rather than the whole backlog. A message can drop out because it was read/archived or because it fell outside that window—both look the same in connector-status.

Gmail drafts and sending are separate approval-gated writes: gmail-draft-propose / gmail-draft-execute creates a draft and never calls a send endpoint. To send, use gmail-send-propose with the same recipient, subject, and body, approve separately, then gmail-send-execute. Alfred never sends from a sync, scheduled job, or a proposal alone. Recovery uses a stable Message-ID and only accepts an exact match.

Alfred can also take commands from your inbox. alfred gmail-inbound-poll --sender you@example.com turns a subject of Task: <title> into a local task, or Remind: <ISO-8601 time> <title> into a task with that due date. Only mail from an explicitly listed --sender is acted on; ordinary mail is left untouched. A recognized command from a sender who is not listed is rejected and audited (the same default-deny rule Telegram pairing uses). Add --destination telegram:123 to also deliver Remind: reminders there. Alfred never replies by email from this path. Pass --gmail-inbound-sender (and optionally --gmail-inbound-destination) to alfred run to poll continuously.

Google Health reuses the same grant but needs extra scopes Calendar/Gmail do not request by default. Enable the Google Health API on the Cloud project, add the three googlehealth.*.readonly scopes on the OAuth Data Access page, then:

alfred google-auth --include-health

That keeps every Calendar/Gmail default (including calendar.calendarlist.readonly) and adds sleep, activity, and vitals read-only. Health is never implied by a plain google-auth. Google Health rejects access tokens that also carry Calendar/Gmail scopes, so alfred health-sync refreshes a health-only subset of that grant. The Google account must also be linked to Fitbit (Google Health returns ACCOUNT_NOT_LINKED until it is). It copies the last 14 days (--lookback-days to change that) of steps, sleep sessions, and daily resting heart rate as sensitive-tagged events—health metrics never inherit personal's default retrieval scope. Sample-level BPM is not stored; it is too dense for the event log. Health writes stay disabled.

Canvas

If your school permits a personal Canvas token, save it as canvas-api-token and run alfred canvas-sync --base-url https://your-school.instructure.com. It copies upcoming/missing plus accessible current/completed-course assignment history: title, deadline, course label, source link, and compact submission workflow state. Grades, submission contents, files, and assignment body text stay out.

If the school disables personal API tokens, use Canvas's private Calendar Feed. Treat that URL like a password and enter it in Windows Credential Manager; never put it in Git, a command argument, a log, or chat:

.\.venv\Scripts\alfred.exe canvas-ical-setup

The setup command says when it is ready for the URL, hides the pasted text, repairs an exact accidental double-paste, validates the feed before replacing the saved credential, and performs the first sync. With --canvas-ical, alfred run checks the feed every 15 minutes, uses ETag/Last-Modified conditional requests, and stores only assignment title, deadline, course label when present, a query-free source link, status, and versioned evidence. It never stores the feed URL or event description.

If the same item also arrives through a Google Calendar subscription, exact matches on title and time are shown once in briefs and academic memory, with native Canvas evidence preferred. Canvas's iCal export is less complete than the API: it omits To Do/submission state and is limited by Canvas to 30 past days, 366 future days, and 1,000 items. Remove the same Canvas subscription from Google Calendar later if you do not want the redundant raw calendar evidence; it is no longer required to prevent duplicate briefing or memory entries.

GitHub

GitHub's notifications endpoint needs a classic personal access token with the notifications scope; save it as github-token, then run alfred github-sync. It copies only each unread notification's title, repository, reason (mention, review requested, etc.), subject type, and a browser deep link—never issue/PR body text or comments. Resolved or read notifications drop out of the next sync automatically.

Issue creation is a separate write scope. A fine-grained token needs Issues: write on only the repository you choose; save it as github-issue-token. alfred github-issue-propose --actor nico --repository owner/repo --title "..." creates a local preview; after approve, github-issue-execute creates that exact issue once. It never creates issues during sync or without a fresh approval token. Each created issue includes an invisible Alfred recovery marker so a crash between GitHub accepting it and Alfred storing the receipt can still find that exact issue. PR comments use the same recovery process.

Composio (overflow apps)

Use this for apps Alfred does not already own — Notion, Spotify, Linear, Discord, and the rest of Composio's catalog. Gmail, Calendar, GitHub, Slack, Telegram, and Fitbit stay first-party.

  1. Create a free project at dashboard.composio.dev (no card; new signups are hard-capped at 100k tool calls/month).

  2. Copy an API key from Settings and run alfred composio-setup.

  3. alfred composio-connect notion prints a Connect Link; open it, sign in, then alfred composio-search "list pages" --toolkit notion.

  4. Grant Hermes the four tools (composio_search, composio_status, composio_connect, composio_execute) on the existing client-grant.

Reads run immediately. Writes still preview and wait for the Telegram approve button. Do not add Composio's hosted MCP URL to Hermes — every Telegram turn runs YOLO, so that path would auto-approve third-party writes.

alfred composio-status shows connected accounts and this UTC month's local call count against the free-tier cap.

Morning brief

Queue a daily local morning brief for a paired Telegram chat with, for example, alfred schedule-brief --chat-id 123 --at 07:30 --timezone America/New_York.

Encrypted backup and restore

Create the local AES-256 key once with alfred backup-key-generate; it is kept only in Windows Credential Manager as backup-encryption-key. Then alfred backup-create --output D:\Backups\alfred.backup writes an encrypted SQLite snapshot. Restore is two-step: backup-restore-propose --backup ... --actor nico, approve the preview, then backup-restore-execute. The backup's SHA-256 is frozen in the approval, so a changed file cannot be restored with a stale confirmation. Restore is non-replayable: any further restore needs a new preview and approval.

scripts/backup.ps1 creates timestamped encrypted snapshots under .alfred/backups. The deployed installation runs it through the Alfred Backup scheduled task daily at 02:30.

Test those snapshots with alfred backup-verify --latest-in .alfred\backups (or --backup <file>). It rehearses a full restore into a throwaway copy and never touches your live database. The only other way to test a restore is backup-restore-execute, which overwrites the database you are trying to protect.

It checks that the stored key still decrypts, SQLite integrity passes, migrations apply, the audit hash chain verifies, and the data is really there:

{"ok": true, "schema_version": 15, "audit_chain_verified": true,
 "row_counts": {"events": 3402, "tool_runs": 5369, "memories": 2592}}

A backup that decrypts into an empty database would pass every check except that last one. Broken backups are reported rather than raised—so this is safe to schedule—and the command exits non-zero on failure.

scripts/verify-backup.ps1 is the scheduled wrapper. The deployed installation runs it as Alfred Backup Verify every four weeks at 03:15—45 minutes after the nightly snapshot. Each run appends a JSON line to .alfred\backup-verify.log, and a failure throws so Task Scheduler records a failed run. Register it with:

$ps = (Get-Command powershell.exe).Source
$action = New-ScheduledTaskAction -Execute $ps `
  -Argument '-NoProfile -ExecutionPolicy Bypass -File "C:\path\to\alfred\scripts\verify-backup.ps1"'
$trigger = New-ScheduledTaskTrigger -Weekly -WeeksInterval 4 -DaysOfWeek Sunday -At 3:15AM
Register-ScheduledTask -TaskName "Alfred Backup Verify" -Action $action -Trigger $trigger -Force

Tasks and reminders

Sending /task <title> or /remind <ISO-8601> <text> to the paired Telegram bot creates a task (and, for /remind, a scheduled delivery job) linked back to that message as evidence. The same commands exist on the CLI:

  • alfred task-upsert "title" [--task-id ID] [--due-at ISO-8601] — create, or update title/due date when --task-id is given. Omitting --due-at on an update leaves the existing due date alone rather than clearing it.

  • alfred task-complete --task-id ID — idempotent; completing an already-completed task is a no-op.

  • alfred reminder-set "text" --run-at ISO-8601 --chat-id ID [--task-id ID] — schedules a Telegram reminder, creating its own task if --task-id isn't given. New channels use --destination channel:recipient (for example slack:D123); the durable job and outbox preserve that target rather than silently routing it to Telegram. Delivery workers still only exist for Telegram today.

All three are also MCP tools (task_upsert, task_complete, reminder_set). reminder_set needs an explicit chat_id because Telegram is Alfred's only delivery channel today.

Running continuously

Every command above is a one-shot CLI invocation. alfred run is the always-on process: each cycle handles Telegram intake/delivery and any due jobs (reminders, the morning brief), and each configured connector syncs on its own interval (15 minutes by default). A missing credential or a failed connector is logged to the audit trail and skipped; it never stops the loop or any other connector.

.\.venv\Scripts\alfred run --pair 123:456 --chat-id 123 --canvas-ical

alfred run bounds Gmail to the 50 most recent unread messages (--gmail-unread-limit), lower than one-shot gmail-sync's 500. Each sync blocks this single-threaded loop; against a real account 500 measured at 45 seconds versus 7 for 50, which is dead time where an incoming Telegram message isn't even polled. Raise it if you'd rather have the wider window than the responsiveness.

Calendar, GitHub, and Gmail sync are always attempted and skip themselves if their credential isn't configured yet. Canvas API sync needs --canvas-base-url, native Canvas Calendar Feed sync needs --canvas-ical, inbound Gmail commands need at least one --gmail-inbound-sender, and Google Health needs --google-health. Omit --pair/--chat-id to run with Telegram disabled. Stop it with Ctrl+C.

alfred run also works as a foreground process kept alive by Windows Task Scheduler ("At log on", running pythonw.exe against this same command) or a terminal left open. .\scripts\install.ps1 -RegisterScheduledTask -RunArgs "--pair 123:456 --chat-id 123" registers that entry; inspect it with Get-ScheduledTask -TaskName Alfred and remove it with Unregister-ScheduledTask -TaskName Alfred. Either way, Alfred only runs while someone is logged in.

Conversational replies (--hermes-profile)

By default a Telegram message that isn't /task or /remind gets a short help receipt. Add --hermes-profile alfred and it goes to the agent: Hermes understands it, calls Alfred's MCP tools, and answers in the same chat.

.\.venv\Scripts\alfred run --pair 123:456 --chat-id 123 --hermes-profile alfred

Install hermes-profile/ and register its MCP connection first (see hermes-profile/README.md). Alfred owns the Telegram transport; Hermes is a one-shot subprocess (hermes -p <profile> -z <message>) and never touches Telegram itself—Hermes's own Telegram gateway does not currently work on this platform. Production stays on that bounded, redacted one-shot runner: Hermes ACP/serve fix the tool surface at session start, while Alfred narrows MCP tools per turn, and a fresh zero-tool ACP session exceeded 30 seconds before a prompt ran.

  • Work turns get a keyword acknowledgement first (checking your agenda..., drafting email to you@example.com...); casual turns skip it. Acknowledgements are not model calls—they're produced inside the intake write transaction, and the agent turn has to wait until that transaction closes.

  • Telegram typing... is best effort and never blocks the durable answer.

  • Answers arrive as two to four consecutive messages (SOUL.md asks for short paragraphs; the bridge sends each as its own message).

  • Inbox/GitHub questions get a bounded context pack from already-synced local records before Hermes starts. Promotions, Social, and Forums are counted but omitted by default. Gmail context is still headers/snippets only. Synced message content remains untrusted data.

  • Work turns include two recent completed exchanges so yes, flag that keeps its referent. Casual turns use up to eight exchanges from the last week, poolside/laguna-xs-2.1:free with reasoning disabled, FTS-only memory, and an empty MCP tool surface (no Ollama embedding wait on greetings). Tool-backed and explicit memory questions keep hybrid vector recall and stepfun/step-3.7-flash:free. Override the casual model with --hermes-conversation-model.

  • Every Hermes turn independently allowlists at most eight MCP tools. alfred-mcp registers only that list; an inbox/GitHub read already in the pack exposes no tools. This does not narrow Claude, Cursor, HTTP, or other MCP clients.

  • --hermes-command (default hermes; full path when PATH differs under the Windows service), --hermes-timeout (default 120s; timeout/nonzero/empty still gets one honest reply, never retried), --hermes-python to invoke -m hermes_cli.main instead of the console launcher, --embedding-model nomic-embed-text for hybrid recall, --hermes-monthly-call-limit (hard cap, default 1000). On Windows, children use CREATE_NO_WINDOW.

  • Bridge context is bounded before launch; common PII is redacted at the subprocess boundary. The shipped profile uses Nous free tier plus local Ollama fallback; no paid provider fallback.

alfred latency-status --limit 20 reports content-free p50/p95 timing (acknowledgement, context assembly, Hermes call, response-ready, first delivered reply). Samples contain only an update ID, runtime/tool count, outcome, and timings. Telegram timestamps have one-second resolution, so treat end-to-end totals as operator-facing, not a microbenchmark.

For inbox/GitHub questions, the bridge assembles a bounded context pack from the already-synced local records before starting Hermes. That avoids a second MCP discovery/tool-call loop on the cold path. Gmail's Promotions, Social, and Forums categories are counted but omitted from the pack by default, and two recent completed chat exchanges are included for work turns so a precise follow-up such as yes, flag that keeps its referent. Casual turns use up to eight completed exchanges from the last week, poolside/laguna-xs-2.1:free with reasoning disabled, exact local FTS memory recall, and an empty Alfred MCP tool surface. Skipping the optional vector lookup avoids waiting for an Ollama embedding on greetings; tool-backed and explicit memory questions keep hybrid vector recall and the profile's stronger stepfun/step-3.7-flash:free default. Override the casual model with --hermes-conversation-model. Synced message content remains untrusted data, and Gmail context is still headers/snippets only, never a full message body.

The bridge also narrows Alfred's MCP surface independently for every Hermes turn. A deterministic classifier uses the current request plus the two recent exchanges to select at most eight task, calendar, communication, memory, or status tools. alfred-mcp registers only that allowlist in the child process; an inbox/GitHub read already satisfied by the context pack exposes no tools. This is defense in depth on top of the existing per-client policy checks and does not narrow Claude, Cursor, HTTP, or other MCP clients.

alfred latency-status --limit 20 reports content-free p50/p95 timing for the Telegram acknowledgement, local context assembly, Hermes call, response-ready point, and first delivered reply. Recent samples contain only an update ID, runtime/tool count, outcome, and timings; message and connector content never enters the report. Telegram's source timestamp has one-second resolution, so acknowledgement and delivered totals are best treated as operator-facing end-to-end measurements rather than a microbenchmark.

alfred evaluation-status --window-days 30 closes the other half of that loop. Alfred already recorded response feedback, memory retrieval outcomes, workflow proposal decisions, and implicit-candidate promotion; this reads them back as one summary instead of leaving four tables nobody queries. It also reports which context sources were present when each feedback vote landed — a starting point for "why was that answer wrong", not proof of cause, since a turn packs several sources at once. The same summary is a page in the admin UI. Nothing here runs a model, writes a row, or changes ranking, and the output is content-free (outcomes, counts, source names, opaque record IDs), so it is safe to paste into an issue. A metric with no votes yet reports null rather than 0 — a system nobody has rated is not a system that scored zero.

Hermes ACP and serve were evaluated as ways to remove the one-shot process start. ACP passes its compatibility check, but its tool surface is fixed when a session is created, while Alfred narrows MCP tools for every turn. Creating a fresh zero-tool session preserved that boundary but exceeded 30 seconds in two bounded trials before a prompt even ran because Hermes constructs a fresh agent per session. Reusing one session would be faster only by retaining an unbounded hidden transcript and a fixed tool surface. Production therefore stays on the bounded, redacted one-shot runner until upstream exposes a per-prompt tool override or cheap isolated sessions; failures and timeouts continue to produce one honest reply without retrying.

Persistent learning

When conversational replies are enabled, Alfred also runs a local learning pass after the reply has already been delivered. Explicit remember that ... statements become confirmed memories immediately. Ordinary preferences, identity facts, and goals enter as quarantined candidates and need the same fact in a separate source event before promotion. Sensitive candidates never auto-promote, recognizable secrets are not stored, and every observation keeps its immutable source-event provenance.

Confirmed memories relevant to a request are placed directly in the bridge's bounded context pack, avoiding another agent tool round trip. Candidate, superseded, rejected, deleted, sensitive, and secret memories are excluded from that automatic path. memory_correct preserves the former version while installing a correction; memory_feedback records relevant, irrelevant, or incorrect retrievals as append-only evaluation data. That feedback now reorders only the memories already selected for a matching query; it cannot inject an unrelated popular memory into the candidate set.

Alfred grades its own answers instead of asking you to. Every successful reply used to end with helpful, missing context, and wrong context buttons; rating a secretary after each answer is work, so they went mostly unpressed, and the correction was already in the chat anyway. Two detectors now produce the same three verdicts. The first reads your next message for an unambiguous reaction — "you missed the one from sam", "that's the wrong week", "thanks, that's perfect" — using named rules rather than a model call, so it can be read and argued with, and it stays silent on anything it does not clearly recognize. The second is something you could not notice at all: when a reply was built from a connector that has never synced or last synced a day ago, the answer was already missing context, and that turn is flagged as it is stored.

Each verdict records a content-free trace of source names, connector freshness, the name of the rule that fired, and opaque ranked record IDs; neither the prompt nor the answer is stored. Helpful and wrong signals can reorder Gmail or GitHub records only within their existing deterministic priority tier, while missing context stays an evaluation signal instead of guessing what was absent. A response holds one verdict per detector and still counts once in ranking, inferred verdicts are attributed only to the paired sender's own recent turn, and none of this can approve or execute an action. Buttons are now reserved for exactly that: approving or cancelling a write.

alfred evaluation-status and the admin UI say how each verdict was reached, so inferred signal is never mistaken for something you said. Answers Alfred flagged against itself are counted separately from your verdicts and kept out of the helpful rate — a connector going quiet is connector health, and folding it in would make a week of stale Gmail read as a week of answers you disliked.

Calendar and Canvas history use a separate derived academic layer. Immutable connector events remain authoritative; after connector sync, Alfred deduplicates revisions into daily JSON rollups and course/calendar profiles, classifying exams, quizzes, assignments, and ordinary events while retaining each source-event ID. Academic questions retrieve only a few matching rollups (rather than scanning raw history), and rebuilding is skipped when the source fingerprint has not changed. A second deterministic pass promotes the current items into source-linked semantic memories, supersedes changed provider versions, and connects calendar/course entities to the owner graph. This background work never delays a chat reply.

The continuous runner refreshes a three-year Calendar window weekly by default without replacing Calendar's live incremental cursor. Canvas keeps the small upcoming/missing read on the normal connector interval and scans accessible active/completed course assignments only once daily. Use --calendar-history-days 0 to disable Calendar history, or tune --calendar-history-interval and --canvas-history-interval when needed. One-shot maintenance is available through calendar-history-sync --all-selected --days 1095 and academic-memory-rebuild.

The JSON in rollups is a replaceable cache, not the canonical archive. This keeps exports portable while preserving edits, cancellations, provenance, and forget/correction behavior in SQLite. Cognee is therefore not a source of truth for Alfred today: its graph/vector retrieval ideas are useful, but its LLM-backed ingestion and additional runtime/database surface are unnecessary for deterministic Calendar/Canvas facts. It can be evaluated later as an optional retrieval backend against the same source-linked memory tests. alfred evaluation-status --window-days 30 summarizes response feedback, memory retrieval outcomes, workflow proposal decisions, and implicit-candidate promotion—plus which context sources were present when each vote landed (a starting point for "why was that answer wrong", not proof of cause). Same page in the admin UI. Nothing here runs a model, writes a row, or changes ranking. Output is content-free (outcomes, counts, source names, opaque IDs), safe to paste into an issue. A metric with no votes yet reports null, not 0.

The agent step runs between intake and delivery, not as a connector. When a chat transport is configured, periodic connectors run sequentially on one bounded background worker, so a long Calendar/Gmail/Canvas batch cannot stop Telegram from polling. Telegram's server long-poll is ten seconds by default; the HTTP read budget is only two seconds longer, and a failed poll retries after one second.

Persistent learning

After a conversational reply is delivered, Alfred runs a local learning pass:

  • Explicit remember that ... becomes a confirmed memory immediately.

  • Preferences, identity facts, and goals enter as quarantined candidates and need the same fact in a separate source event before promotion.

  • Sensitive candidates never auto-promote; recognizable secrets are not stored; every observation keeps immutable source-event provenance.

  • Confirmed memories relevant to a request go in the context pack. Candidate, superseded, rejected, deleted, sensitive, and secret memories do not.

  • memory_correct preserves the former version. memory_feedback records relevant/irrelevant/incorrect retrievals as append-only evaluation data and reorders only memories already selected for a matching query.

Telegram no longer puts helpful / missing context / wrong context buttons on every reply. response_context is still stored; a later pass should infer those labels from the following conversation instead of asking the owner to tap them. Existing votes still reorder Gmail or GitHub records only within their deterministic priority tier.

Calendar and Canvas history use a derived academic layer. Immutable connector events remain authoritative. After sync, Alfred deduplicates revisions into daily JSON rollups and course/calendar profiles (exams, quizzes, assignments, ordinary events), retaining each source-event ID. Academic questions retrieve a few matching rollups rather than scanning raw history; rebuilding skips when the source fingerprint is unchanged. A second pass promotes current items into source-linked semantic memories, supersedes changed provider versions, and connects calendar/course entities to the owner graph. This never delays a chat reply.

The runner refreshes a three-year Calendar window weekly by default without replacing Calendar's live incremental cursor. Canvas keeps upcoming/missing on the normal interval and scans accessible course assignments once daily. --calendar-history-days 0 disables Calendar history; --calendar-history-interval and --canvas-history-interval tune it. One-shot: calendar-history-sync --all-selected --days 1095 and academic-memory-rebuild. Rollup JSON is a replaceable cache, not the canonical archive—edits, cancellations, provenance, and forget/correction stay in SQLite. Cognee is not a source of truth; it can be evaluated later as an optional retrieval backend against the same source-linked memory tests.

As a real Windows service (survives logoff/reboot)

alfred-service packages the exact same loop as a Windows service, independent of any logged-in session, using Windows' own recovery options for restart-on-crash. It's a thin wrapper, not a separate code path: it drives the identical construction/cleanup logic alfred run uses, built from arguments parsed by the same parser.

# 1. Store the exact 'alfred run' arguments the service will launch.
.\.venv\Scripts\alfred service-configure run --pair 123:456 --chat-id 123

# 2. Install and start the service (requires an Administrator prompt).
.\.venv\Scripts\alfred-service --username ".\<your-windows-username>" --password "<your-windows-password>" install
.\.venv\Scripts\alfred-service start

The --username/--password are not optional. Every connector credential lives in your Windows account's DPAPI-protected Credential Manager, which only your account's own logon session can decrypt. Installing without --username (or via the Services MMC snap-in's default) runs as LocalSystem—the service will install and start "successfully," then die immediately with missing local credential-store secret: .... Options must precede the verb; getopt stops parsing at the first non-option argument, so alfred-service install --username ... silently ignores the flags. The password is visible in shell history once typed this way; clear it afterward (Clear-History and/or remove the relevant line from (Get-PSReadLineOption).HistorySavePath), or use the Services MMC snap-in (services.msc → Alfred Personal Secretary → Log On tab) to set the account without it touching a shell.

alfred-service debug runs the service logic in your current console instead of under the SCM, printing exceptions directly instead of routing them through Get-WinEvent.

Check on it with Get-Service Alfred, stop it with .\.venv\Scripts\alfred-service stop, and remove it with .\.venv\Scripts\alfred-service remove. Changing configured arguments (re-run service-configure) needs a restart to take effect. Optionally configure automatic restart on an unexpected crash—Windows services don't retry by default—with:

sc.exe failure Alfred reset= 86400 actions= restart/60000

alfred-service needs pywin32, already pulled in transitively by the mcp package on Windows; if importing it fails, run python .\.venv\Scripts\pywin32_postinstall.py -install once. Installing, starting, stopping, and removing the service are Administrator actions you run yourself—Alfred never elevates or registers itself.

Restart Alfred from your phone

When Alfred is running, Telegram accepts three operator commands from paired chats:

  • /status — is the loop alive, and when did it last cycle?

  • /restart — restart now

  • /wake — same as /restart when Alfred is down

Register the watchdog once (Administrator PowerShell, after service-configure):

.\scripts\register-watchdog.ps1

That creates two Task Scheduler entries:

  • AlfredWatchdog — every five minutes, runs alfred watchdog-check. If the heartbeat is stale, it restarts the Windows service (or falls back to the configured alfred run command from .alfred/service.json) and does one Telegram poll for /wake or /restart.

  • AlfredRestart — on-demand restart used by /restart and the watchdog, with highest privileges so your phone does not need an Administrator prompt each time.

The runner writes a heartbeat every cycle, so a hung or dead process is picked up automatically within a few minutes even if you do nothing. When Alfred is fully stopped, send /wake from Telegram; the next watchdog pass sees it and starts Alfred back up.

Local memory graph

alfred remember "statement" stores a confirmed local memory; alfred memory-search "query" returns FTS anchors plus one active graph hop. Corrections never rewrite history: alfred memory-correct --memory-id ID "corrected statement" marks the old memory superseded and creates a new one that points back to it.

Deleting is preview-then-confirm, because deleting data is strong-confirm and never unattended. alfred memory-forget-propose --memory-id ID --actor nico [--reason "..."] previews a scoped, single-item deletion. Approve it, then alfred memory-forget-execute tombstones the memory, drops it from search, and records an audit entry. A superseded memory stays visible as history until it is separately forgotten.

alfred memory-alias --entity-id ID "Alternate Name" adds a searchable alternate name—memory-search finds it by either name immediately after.

alfred memory-rename --entity-id ID "Real Name" changes what an entity is actually called. People discovered from your calendar arrive labelled with whatever Google supplied, which is sometimes just an email address; this is how you fix that. The old label is kept as an alias, so existing [[wiki links]] keep working—renaming is not forgetting.

Alfred fills names in on its own where it can: if someone's calendar address also appears as a Gmail sender with a display name, the next people sync adopts it. Gmail is only ever read for names, never to decide that someone exists—your inbox is mostly brands, and a display name there is branding rather than identity.

Obsidian vault

alfred vault-export-entity --entity-id ID and alfred vault-export-memory --memory-id ID (both take --vault PATH, default alfred-vault) project one graph record into Generated/—plain, portable Markdown with an alfred_id and managed: true in frontmatter. A hand-edited file in that path is never silently overwritten; it's preserved and the projection instead becomes an .alfred-conflict-<timestamp>.md copy for review.

Bulk exports select a set rather than one record:

.\.venv\Scripts\alfred vault-export-source-event --source-event-id ID
.\.venv\Scripts\alfred vault-export-range --since 2026-03-01 --until 2026-04-01
.\.venv\Scripts\alfred vault-export-topic "rowing" --limit 50
.\.venv\Scripts\alfred vault-export-person --entity-id ID

vault-export-person defines "about a person" structurally: a memory is about someone when it came from an event they organized, not when their name appears in the wording. Text matching would treat "lunch near Robin's office" as a memory about Robin—tolerable for an export, wrong for a deletion, and this selector serves both. Find the entity ID with alfred memory-search "<name>".

--since/--until filter on when Alfred recorded a memory, not when the fact became true. The range is half-open (--since inclusive, --until exclusive) so back-to-back months don't both claim a memory on the boundary. Either bound can be omitted for an open-ended range, but not both. The topic selector runs the same search a question would. Every bulk export writes only confirmed, public/personal memories; anything skipped is reported by ID rather than silently dropped. A topic export's receipt records that it was a topic export, never the query you typed.

alfred vault-import --vault PATH reads the other direction: any user-authored note anywhere in the vault (not just Generated/) becomes a confirmed, evidence-backed memory. It's a scan you call periodically—via the CLI, or automatically as a connector when alfred run is given --vault—not an OS-level file watcher. Alfred never writes back to an imported file; change detection is tracked in Alfred's database by content hash, so editing a note supersedes its memory and deleting a note from disk does not delete the memory it produced—only forget does that. Files Alfred itself generated (managed: true) are never re-imported as testimony.

Import also reads [[wiki links]] (including [[Note|display]] and [[Note#Heading]]). When a link names exactly one entity Alfred already knows—by label or alias, case-insensitively—it's recorded as evidence that this note concerns that entity. An unknown name creates no entity, an ambiguous name resolves to nothing rather than guessing between two "Alex" entities, and no relationship edge is created. The result reports linked and unresolved_links.

Optional mobile sync

Alfred does not use paid Obsidian Sync. deploy/couchdb/ sets up the self-hosted CouchDB service; read deploy/couchdb/README.md before running it. Alfred's own code never syncs the vault to a phone; a vetted, open-source Obsidian community plugin (Self-hosted LiveSync) does that entirely client-side, replicating only alfred-vault/, never alfred.db, secrets, or logs. alfred vault-sync-status --url http://127.0.0.1:5984 confirms the server side is reachable.

MCP server

alfred-mcp runs Alfred's stdio MCP server for Claude Desktop/Code, Cursor, and other local MCP clients. Every tool is default-deny: a client gets nothing until explicitly granted, e.g. alfred client-grant --client-id local-mcp --sensitivity public --sensitivity personal --tool memory_search --tool remember --tool forget --tool calendar_event_propose --tool message_draft --tool action_commit --tool brief_get --tool connector_status --allow-write. alfred-mcp --client-id <id> runs it under a different identity (default: local-mcp) so a second stdio client—for example OpenAI's Secure MCP Tunnel—gets its own separately scoped grant.

Current tools: system_status, agenda_get, memory_search, profile_get, remember, forget, calendar_event_propose, message_draft, action_commit, brief_get, connector_status, task_upsert, task_complete, and reminder_set—all 12 of section 7's documented tools, plus system_status and calendar_event_propose (which action_commit needs, since section 7 never names a tool for previewing a calendar write). remember/forget additionally check the requested memory's sensitivity against the client's own scope, so a client granted only public/personal cannot write or erase a secret memory even with --allow-write.

forget, calendar_event_propose, message_draft, and message_send_propose only preview. action_commit performs whatever a previous tool call previewed, once given a fresh approval token. There is deliberately no MCP tool to approve one. action_commit mints a fresh Google access token itself when finishing a calendar write, Gmail draft, or Gmail send; nothing is cached. Gmail sends remain explicit, one-time approval-gated actions and never run from sync or a scheduled job.

Repeated-workflow proposals

When Hermes is enabled, Alfred performs a local workflow scan once per day. It looks for the same successful two-to-eight-tool sequence at least three times across two days. It stores only structural metadata: tool names, argument names, and allowlisted routing labels such as connector type. It does not store prompts, email bodies, task or event titles, people, addresses, dates, identifiers, or arbitrary argument values. Failed turns and turns that contain action_commit are ineligible.

.\.venv\Scripts\alfred workflow-scan
.\.venv\Scripts\alfred workflow-list --state pending
.\.venv\Scripts\alfred workflow-show --version-id <ID>
.\.venv\Scripts\alfred workflow-accept --version-id <ID>
.\.venv\Scripts\alfred workflow-reject --version-id <ID>

Each suggestion is an inert, versioned SKILL.md plus a unified diff and an expiring review record. Accepting the diff records that decision but this first slice cannot activate or execute a generated skill; unattended learning can produce review material but cannot modify the Hermes profile. Raw daily Calendar/Canvas JSON snapshots are not used here: their normalized source records already preserve provenance and change history.

Streamable HTTP (remote/private clients)

alfred-mcp is stdio-only. For a client that can't spawn a local process—or that should run as a separate identity from local-mcpalfred mcp-http-run --client-id <id> serves the same tool surface over Streamable HTTP on http://127.0.0.1:8000/mcp. The host is not configurable: this binds loopback only. <id> needs its own client-grant first, exactly like a stdio client.

Every request must carry Authorization: Bearer <token>, checked outside FastMCP's own request handling so an unauthenticated caller can't even open a session. Generate that token once with alfred mcp-http-token-generate (refuses to overwrite an existing one, same as backup-key-generate); it's stored in the OS credential store, never in a config file. This is a single shared secret, not OAuth—OAuth 2.1/RFC 9728 is reserved for public remote access, a separate undertaking this does not attempt. FastMCP also auto-enables DNS-rebinding protection (Host/Origin header validation) whenever the host is loopback, which is always true here.

ChatGPT (Secure MCP Tunnel)

ChatGPT can't connect directly to a local MCP server the way Claude Desktop/Cursor do over stdio, so OpenAI's Secure MCP Tunnel is the private-access path—an outbound-only relay via OpenAI's own tunnel-client daemon, run by you, which Alfred does not vendor or reimplement. See deploy/openai-tunnel/README.md for the walkthrough: creating a tunnel and scoped client grant, then pointing tunnel-client at alfred-mcp --client-id chatgpt-tunnel over stdio (deliberately not the Streamable HTTP transport above, to avoid reconciling two authentication schemes). Whether your ChatGPT plan supports custom MCP connectors at all is between you and OpenAI.

Admin dashboard

alfred admin-ui-run serves a small, read-only web dashboard: today's agenda, pending approvals, connector health, evaluation signals, and the recent audit trail, at http://127.0.0.1:8200. One page per concern, no write actions (approving still goes through alfred approval-approve, never a button on this page). No CDN fonts or icons; it works with the network off.

.\.venv\Scripts\alfred admin-ui-token-generate
.\.venv\Scripts\alfred admin-ui-run

Defaults to loopback-only like mcp-http-run, but unlike it, --host is a real option—this is meant for a person to look at, sometimes from a phone. 127.0.0.1 is not reachable from another device even over a VPN; to check it from your phone, run alfred admin-ui-run --host <this-PC's-VPN-IP> (a Tailscale IP, for example—tailscale ip -4), never --host 0.0.0.0 unless you already have firewall rules restricting who can reach the port. Works in any modern browser; the layout reflows for a phone-width screen and uses the system font (Segoe UI on Windows, San Francisco on Safari/iOS).

Auth is the same bearer token as mcp-http-run, delivered differently: visiting any page without one redirects to a login screen; entering the token there sets an HttpOnly, SameSite=Strict cookie whose value is the token (no separate session store). Scripted/API access can still send Authorization: Bearer <token> directly and skip the cookie.

Local vector search (optional)

MemoryGraph accepts an optional embedding_provider. Without one, memory-search stays FTS5 keyword-only. With one—for example alfred.embeddings.OllamaEmbeddingProvider, pointed at a local Ollama—remember, memory-correct, and forget also keep a versioned vector per memory in the embeddings table, and memory-search folds in nearby vector matches (within a cosine-distance cutoff) once keyword hits are exhausted. Vectors are namespaced by model name, so trying a different embedding model never mixes incomparable spaces; switching models means re-embedding, not migrating data. Run memory-embed-backfill --model nomic-embed-text for a one-shot rebuild, or pass --embedding-model nomic-embed-text to alfred run for continuous local upkeep.

Local model inference (optional)

alfred.models.OllamaClient is local-first text generation: point it at a running Ollama and it calls the non-streaming /api/generate endpoint, returning the text plus Ollama's own prompt/completion token counts. BriefingService accepts an optional llm_writer; without one, write_brief() is just render()—the deterministic text, unchanged. With one, the model only ever rewrites the deterministic render's wording; every fact, date, and link it sees comes from that text, never from the model's own knowledge, and a failed or unreachable model falls back to the deterministic render rather than costing the user their brief. Every pass is audited with its token counts. Nothing in the CLI, MCP server, or job runner wires a live writer in by default.

Cloud pieces are also built: alfred.models.OpenAICompatibleClient and AnthropicCompatibleClient speak the OpenAI chat-completions and Anthropic Messages API shapes, but neither should be constructed bare—wrap either in GuardedCloudProvider first, which enforces:

  1. Redaction. Redactor scrubs common secret/PII shapes (emails, bearer tokens, OpenAI/GitHub/Slack/AWS key patterns, SSNs, card-like digit runs, phone numbers) from the prompt and system text before either reaches the cloud provider. It's a best-effort pattern scrub, not a guarantee—keep genuinely secret-tagged content out of a cloud prompt rather than relying on this to catch it.

  2. Monthly hard cap, fail closed. monthly_budget_usd defaults to 0.0, so an unconfigured GuardedCloudProvider never calls out at all. Every call checks month-to-date spend (summed from its own audit records) before calling; once that's already at or past the cap, it raises CloudBudgetExceeded. This checks the cap before each call, not a per-call ceiling—one very large call can still push the total over.

  3. Cost tracking. Every call, success or failure, is audited with the model name, prompt/completion token counts, and an estimated USD cost (from an operator-supplied CloudPricing, since this module hardcodes no vendor price table)—never the raw prompt or response text. That audit trail doubles as the spend ledger requirement 2 reads from.

As with Ollama, nothing in the CLI, MCP server, or job runner constructs a cloud provider by default—an operator wires one in from their own configuration when they want it.

Development rules

  • Read ARCHITECTURE.md before changing behavior.

  • Keep the database as the source of truth; transports do not contain business logic.

  • Do not place credentials, raw personal data, or local databases in Git.

  • Every MCP tool is gated by PolicyStore; an unregistered or narrowly scoped client gets nothing by default. Consequential actions on the MCP surface can only create previews; a human must approve them outside that MCP client before action_commit can execute the exact approved preview.

See CONTRIBUTING.md for the full local setup, test, and PR workflow, SECURITY.md to report a vulnerability privately, and RELEASING.md plus CHANGELOG.md for how a version gets cut and verified.

License

Apache-2.0. See LICENSE.

A
license - permissive license
-
quality - not tested
A
maintenance

Maintenance

Maintainers
Response time
0dRelease cycle
2Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • F
    license
    -
    quality
    C
    maintenance
    A privacy-first, local-first MCP server that provides 15 ADHD/second-brain tools for capturing, prioritizing, and resurfacing tasks with context from calendar, mail, and messages.
    1
  • A
    license
    A
    quality
    B
    maintenance
    A local-first, privacy-first MCP server that passively indexes personal digital activity (screenshots, clipboard, notes, downloads, links) into a local database, enabling LLMs like Claude to access your context without cloud storage.
    4
    MIT
  • A
    license
    -
    quality
    A
    maintenance
    A self-hosted MCP server that provides any LLM with a graph-backed memory layer of your life—tasks, email, finance, contacts, calendar—plus autonomous agent offices that act on your behalf.
    1
    Apache 2.0

View all related MCP servers

Related MCP Connectors

  • Private-by-default, local-first memory/context/task orchestrator for MCP apps and agents.

  • Person-owned, portable AI memory as a remote MCP server, readable and writable by any MCP client.

  • Personal assistant MCP server with search, execute, packages, jobs, secrets, and integrations.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ndunl075/alfred-pennyworth'

If you have feedback or need assistance with the MCP directory API, please join our Discord server