Cabinet — system map

What lives where, and roughly how big each piece is.

17,500server source
12,300server tests
7,300web source
3,800CSS
~41,000total lines

How it runs

Two programs. cabinet-api is a Node process started by PM2 with node dist/index.js; it opens port 3008 and stays up. The web client is a React app built to static files that Caddy serves at cabinet.benloe.com.

index.ts is a script that runs once at startup — it opens the databases, creates the objects, connects them to each other, and opens the port. After that it's finished; the process only wakes when an HTTP request arrives or a cron timer fires.

Directory tree

apps/cabinet/
│
├── server/src/ 17,500
│   │
│   ├── index.ts 250  startup: create everything, open the port
│   │
│   ├── domains/ 4,400  18 files — one per life area
│   │     money 1074 · credentials 299 · cravings 270 · healthcare 266
│   │     credentialCatalog 249 · adherence 235 · shopping 231 · misc 229
│   │     mealplan 225 · settings 213 · substances 174 · activity 168
│   │     units 167 · food · health · profile · symptoms · training
│   │
│   ├── runtime/ 2,900  the agent loop
│   │     agent 1040 · session · queue · prompt · register · router
│   │     titler · rateLimits · perf · toolTruncate · mcpHealth
│   │
│   ├── gateway/ 2,800  HTTP + live event stream
│   │     app 878 · attachments · credentialRoutes · plaidRoutes
│   │     settingsRoutes · surfaces · fold · sse · transcript · pendingTurn
│   │
│   ├── mcp/ 880  the 63 tools the model can call
│   ├── memory/ 1,500  prompt files, templates, lessons
│   ├── scheduler/ 950  11 cron jobs
│   ├── scripts/ 790  backfill-titles, wipe
│   ├── tiers/ 520  what the agent is allowed to do
│   ├── integrations/ 1,400  plaid · githubApp · secrets broker
│   ├── episodic/ 360  search over past conversations
│   ├── push/ 380  phone notifications
│   ├── deploy/ 220  Cabinet redeploying itself
│   ├── embeddings/ 150
│   └── db/ 110  + 22 migrations, 53 tables
│
├── server/test/ 12,300  53 files
│
└── web/src/ 7,300 + 3,800 css
    ├── surfaces/ 4,400  7 screensChat · Today · Brain · Money · Domains · Ops · Credentials
    ├── lib/ 1,700  API client, contracts, drafts
    └── components/ 820  shell (5) + instruments (7)

What each part is for

PRODUCT — the reason Cabinet exists INFRASTRUCTURE — needed to run at all MACHINERY — exists to supervise the agent
ModuleLinesPlain EnglishKind
domains/4,400 The actual logic for food, weight, money, medications, meal plans, workouts, symptoms. This is what makes Cabinet different from a chat window. PRODUCT
mcp/880 Defines the 63 tools the model can call. Thin — nearly every tool just validates its arguments and calls into domains/. PRODUCT
memory/1,500 Loads the markdown files that make up the prompt. About 850 of those lines are templates.ts, a second copy of those files used to seed a fresh install. PRODUCT
web/surfaces/4,400 The seven screens in the browser. PRODUCT
runtime/2,900 Runs a turn: builds the prompt, starts the model, streams its output back. agent.ts alone is 1,040 lines. INFRASTRUCTURE
gateway/2,800 The web server. ~50 HTTP routes plus the live stream the UI listens on. INFRASTRUCTURE
integrations/1,400 Banking (Plaid), GitHub tokens, and reading secrets from a separate service. INFRASTRUCTURE
scheduler/950 11 cron jobs: heartbeat, morning briefing, evening check-in, weekly review, money sync, and one called idle-builder that has Cabinet build things unprompted. INFRASTRUCTURE
episodic/, embeddings/510 Search over past conversations and documents. INFRASTRUCTURE
db/, push/490 Database setup and phone notifications. INFRASTRUCTURE
tiers/520 A policy deciding which files and commands the agent may touch, with approval queues for the rest. MACHINERY
scripts/790 wipe.ts (reset the system) and a one-off backfill. MACHINERY
deploy/220 Tracking whether Cabinet's self-redeploy actually landed. MACHINERY
runtime/ odds and ends~700 register, rateLimits, toolTruncate, perf, mcpHealth — measuring and constraining the agent rather than doing anything for you. MACHINERY
Roughly 2,200–3,000 lines are machinery — code whose only job is supervising the agent. It is also where the recent dead ends came from: a permissions policy that blocks the directory the prompt was going to move into, and edit-guards that a different tool bypasses anyway.

How a tool call works

The model cannot touch the database. It can only ask, in words, for one of 63 named tools to run. Everything Cabinet knows or records goes through this list.

model says:   log_food{ description: "chicken bowl", kcal: 640, protein_g: 52 }
                    │
mcp/                │   check the arguments match the declared shape
                    ▼
domains/food.ts     logFood() — INSERT a row, return the day's totals
                    │
                    ▼
model receives: { id: 3312, totals: { kcal: 1840, protein_g: 141, ... } }

mcp/ is a switchboard. Each of the 63 entries declares a name, a description, and the shape of its arguments — then calls one function in domains/ and hands back the result. Almost every tool is a few lines long.

The descriptions are prompt. Roughly 13 KB of them are sent to the model on every single turn, before any argument schemas. That is more text than the charter will be, and unlike the memory files nobody has ever read it end to end.

All 63 tools

Grouped by the domains/ file each one calls. 49 map to a domain; the last 14 are infrastructure and belong to no life area.

ToolWhat it does
domains/food.ts — 4
log_foodLog a meal with macros; returns running daily totals.
update_pantryAdd or adjust a pantry item.
decrement_pantry_forSubtract a quantity from pantry stock, converting units.
add_recipeSave a recipe with per-serving macros and ingredients.
domains/mealplan.ts — 5
plan_mealPut a planned meal on a day.
list_meal_planList planned meals over a date range.
update_plan_entryMark eaten/skipped, change servings or slot.
remove_plan_entryDelete a planned meal.
consume_plan_entryEat a planned meal: log the food and decrement pantry in one step.
domains/shopping.ts — 2
generate_shopping_listMeal-plan ingredients minus what's in the pantry.
list_grocery_listThe full grocery list: plan-derived, staples, and manual rows.
domains/health.ts — 2
log_health_dayWrite a day of Apple Watch metrics (normally the iOS Shortcut does this).
health_daysRecent steps, active kcal, resting HR, sleep.
domains/training.ts — 2
log_workoutLog a workout with sets; flags personal records.
log_body_metricLog weight or body fat; weight returns the smoothed trend.
domains/activity.ts — 5
plan_activityAdd a planned activity to the calendar.
list_activity_planList planned activity over a date range.
update_activity_entryMark done/skipped, attach the logged workout.
remove_activity_entryDelete a planned activity.
seed_trainer_anchorsTop up the fixed Tue/Thu trainer sessions.
domains/adherence.ts — 2
mark_habitMark a goal met or missed for a day, when no query can see it.
adherence_reportExpected vs actual per goal, with streaks.
domains/substances.ts — 3
log_substanceLog cannabis, alcohol, caffeine or nicotine with dose and route.
substance_dayEvery substance event for one day, in order.
substance_nightsOne row per day joining substance timing against sleep.
domains/cravings.ts — 3
log_cravingLog a craving the moment it happens.
resolve_cravingClose it out once you know how it ended.
craving_reportWhich redirect actually works, ranked by success rate.
domains/symptoms.ts — 2
log_symptomRecord a symptom severity 0–10 for a day.
symptom_daysToday's readings, or one symptom's trend.
domains/healthcare.ts — 4
log_claimLog an insurance claim; returns deductible and out-of-pocket totals.
log_labLog a lab result and flag out-of-range values.
log_medicationAdd a medication with schedule and supply, for refill nudges.
log_hsa_contributionRecord an HSA contribution; returns headroom against the IRS limit.
domains/money.ts — 7
money_summaryAccounts, net worth, spending by category. The starting point for money questions.
money_transactionsRecent transactions, newest first.
money_categoriesSpending grouped by category, largest first.
money_trendNet worth, total spend, and restaurant spend by day.
money_holdingsInvestment positions across linked accounts.
import_transactions_csvImport transactions from CSV when a bank won't link.
upsert_manual_accountTrack an account Plaid cannot reach.
domains/misc.ts — 8
log_moodLog a 1–5 mood, energy and stress check-in.
add_journalAppend a free-form journal entry, indexed for later recall.
upsert_taskCreate or update a task or reminder.
upsert_contactCreate or update a contact.
upsert_goalSet a measurable goal — the number a dial on the dashboard points at.
upsert_constraintRecord a hard constraint a plan must never violate.
list_constraintsList the active hard constraints.
add_price_watchWatch an item or URL for a target price.
No domain — infrastructure — 14
query_dbRun a read-only SQL SELECT against the whole database. The workhorse.
search_episodicSemantic search over past conversations and journals.
search_documentsSearch uploaded PDFs — insurance, lease, tax documents.
recall_lessonsRecall relevant stored lessons for the current context.
add_lessonStore a lesson, with evidence and a confidence score.
retire_lessonRetire a lesson that proved wrong or stale.
list_promotable_lessonsList lessons durable enough to graduate into permanent memory.
promote_lessonMark a lesson graduated after writing it into a memory file.
update_memoryRewrite one of the markdown memory files. Git-committed.
render_widgetRender a rich card inline in the chat.
enqueue_approvalPropose an action that needs approval.
money_syncPull fresh balances and transactions from every linked bank now.
plaid_institution_searchCheck whether a given bank can be linked at all.
usage_statusWhere the Claude subscription stands right now.
Anthropic's guidance is that tool selection degrades past 30–50 tools. There are 63. The obvious consolidations: the five *_activity_* and five *_plan_entry tools are the same four verbs twice over, and five of the seven money_* tools are read queries that query_db could already answer.

The database

One SQLite file, cabinet.db, built up over 22 migrations. 53 tables. Only 14 foreign keys are declared — the schema is far flatter than the table count suggests.

Declared relationships

MONEY
  plaid_item ──< financial_account ──< financial_transaction
                         └──────────< holding >── security
  net_worth_snapshot     budget                (standalone)

FOOD
  recipe ──< recipe_ingredient
     ├─────< food_log
     └─────< meal_plan_entry
  pantry_item     grocery_list_item            (standalone)

TRAINING
  workout ──< workout_set
     ^────── activity_plan_entry

HEALTHCARE
  insurance_plan ──< claim
  document ──< lab_result
  medication     prior_auth     hsa_contribution   (standalone)

GOALS
  goal ──< habit_event

CONVERSATION
  chat ──< message

SYSTEM
  task ──< build_run

──< means one-to-many. Everything not shown above has no declared key to anything.

The real spine is a date

Thirteen tables carry a local_day column and are joined on it rather than by key: food_log, body_metric, health_daily, substance_log, craving_event, symptom_log, mood_log, journal_entry, habit_event, workout, meal_plan_entry, activity_plan_entry, net_worth_snapshot.

That is what makes "how did last Tuesday go" answerable in one query, and it is the reason most of these tables need no foreign keys at all. It is also the seam that will need a user_id if Cabinet ever has a second user.

All 53 tables

GroupTablesCount
Food & kitchen food_log recipe recipe_ingredient pantry_item meal_plan_entry grocery_list_item 6
Body & training body_metric health_daily workout workout_set activity_plan_entry 5
Health tracking substance_log craving_event symptom_log mood_log journal_entry 5
Healthcare insurance_plan claim lab_result medication prior_auth hsa_contribution 6
Money financial_account financial_transaction holding security plaid_item net_worth_snapshot budget subscription 8
Planning task goal habit_event hard_constraint contact price_watch reading_item 7
Conversation chat message document 3
Machinery action_audit approval perf_span token_usage rate_limit_sample rate_limit_state retrieval_log build_run schema_migration app_setting credential push_subscription push_delivery 13
13 of the 53 tables exist to supervise the agent, not to hold anything about Ben — audit trails, performance spans, token accounting, rate-limit samples, retrieval logs. Two of them are by far the largest tables in the database by row count.

Data — outside the repo

PathSizeWhat it is
data/cabinet/cabinet.db22 MBEverything logged — ~50 tables.
data/cabinet/episodic.db4 MBSearchable history.
data/cabinet/memory/81 KB11 markdown files that become the prompt. Its own git repo, no remote, nothing reviews it.
data/cabinet/documents/, chat-images/, backups/Uploads and backups.
The prompt is ~20,000 tokens. About 500 of them live in the repo and go through review. The other 19,500 come from that memory/ folder. On top of that sit the 63 tool descriptions — several thousand more tokens that are also prompt, also read on every turn, and have never been read as prose.