Alfred
Reads Canvas assignments via the API and iCal feed.
Reads GitHub notifications and allows approval-gated creation of issues.
Reads unread Gmail message headers and snippets, with approval-gated draft and send capabilities, plus inbound email command processing.
Reads selected Google Calendar events and supports approval-gated event creation.
Maintains a two-way sync with an Obsidian vault for typed temporal memory.
Interfaces with Slack via Socket Mode for paired channels, supporting replies, reminders, and briefs.
Interfaces with Telegram for messaging, reminders, and briefs to explicitly paired chats.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@AlfredWhat does my day look like tomorrow?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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 runor 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 statusOr 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:00–07: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:
*-propose— local preview; never touches the provider.alfred approval-approve --approval-id <ID> --actor nico— prints a one-time token.*-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 |
| paired chats | replies, reminders, briefs |
Slack |
| Socket Mode, paired only | same |
Google Calendar | OAuth via | selected calendars | approval-gated event create |
Gmail | same OAuth grant | unread headers/snippets | approval-gated draft and send |
Canvas API |
| assignments | — |
Canvas iCal |
| dated assignments | — |
GitHub |
| unread notifications | approval-gated issues |
Google Health | extra OAuth scopes | sleep, activity, metrics | — |
Composio |
| 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-healthThat 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-setupThe 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.
Create a free project at dashboard.composio.dev (no card; new signups are hard-capped at 100k tool calls/month).
Copy an API key from Settings and run
alfred composio-setup.alfred composio-connect notionprints a Connect Link; open it, sign in, thenalfred composio-search "list pages" --toolkit notion.Grant Hermes the four tools (
composio_search,composio_status,composio_connect,composio_execute) on the existingclient-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 -ForceTasks 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-idis given. Omitting--due-aton 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-idisn't given. New channels use--destination channel:recipient(for exampleslack: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-icalalfred 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 alfredInstall 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.mdasks 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 thatkeeps its referent. Casual turns use up to eight exchanges from the last week,poolside/laguna-xs-2.1:freewith 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 andstepfun/step-3.7-flash:free. Override the casual model with--hermes-conversation-model.Every Hermes turn independently allowlists at most eight MCP tools.
alfred-mcpregisters 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(defaulthermes; full path when PATH differs under the Windows service),--hermes-timeout(default 120s; timeout/nonzero/empty still gets one honest reply, never retried),--hermes-pythonto invoke-m hermes_cli.maininstead of the console launcher,--embedding-model nomic-embed-textfor hybrid recall,--hermes-monthly-call-limit(hard cap, default 1000). On Windows, children useCREATE_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_correctpreserves the former version.memory_feedbackrecords 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 startThe --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/60000alfred-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/restartwhen Alfred is down
Register the watchdog once (Administrator PowerShell, after
service-configure):
.\scripts\register-watchdog.ps1That 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 configuredalfred runcommand from.alfred/service.json) and does one Telegram poll for/wakeor/restart.AlfredRestart — on-demand restart used by
/restartand 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 IDvault-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-mcp—alfred 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-runDefaults 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:
Redaction.
Redactorscrubs 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 genuinelysecret-tagged content out of a cloud prompt rather than relying on this to catch it.Monthly hard cap, fail closed.
monthly_budget_usddefaults to0.0, so an unconfiguredGuardedCloudProvidernever 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 raisesCloudBudgetExceeded. This checks the cap before each call, not a per-call ceiling—one very large call can still push the total over.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 beforeaction_commitcan 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.
This server cannot be installed
Maintenance
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
- Alicense-qualityAmaintenanceA local-first AI secretary that gathers your work context into private memory and enables AI agents to search and summarize it over MCP.70MIT
- Flicense-qualityCmaintenanceA 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
- AlicenseAqualityBmaintenanceA 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.4MIT
- Alicense-qualityAmaintenanceA 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.1Apache 2.0
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.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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