This document describes how the lifeos-cli repository is organized and why. It is written for maintainers and contributors; command-level facts belong in CLI help (lifeos <resource> <action> --help) and in docs/cli.md, not in this file.
src/lifeos_cli— the Python package: CLI parsing and handlers, database services, ORM models, configuration, Alembic migrations, and locale catalogs.src/lifeos_web— the local FastAPI Web API that shares the same database services as the CLI.scripts— validation and maintenance entrypoints (doctor.sh,export_web_openapi.py,check_locale_catalog.py, and others).tests— pytest suite. Integration-marked tests run against a configured PostgreSQL database; everything else runs against SQLite by default.docs— repository-level documentation.cli.mdis the CLI command reference; this file is the architecture reference.
The first-party React frontend lives in the separate lifeos-plus/lifeos-web repository and is not part of this repository.
Both the CLI and the Web API are thin transport layers over shared domain services:
CLI: cli_support/resources/<resource>/parser_*.py
-> async handler (make_sync_handler)
-> db/services/<domain>_*.py
-> ORM models (db/models)
-> SQLite / PostgreSQL
Web: uvicorn (lifeos web serve)
-> lifeos_web.app.create_app()
-> lifeos_web/routers/<domain>.py
-> lifeos_web.deps.get_db_session (session_scope)
-> db/services/<domain>_*.py
-> ORM models (db/models)
-> SQLite / PostgreSQL
Rules that keep this architecture stable:
db/servicesis the only place that owns domain logic and ORM access. Routers and CLI handlers orchestrate services and format results; they do not build their own business layer.- The Web API keeps an explicit response boundary: routers return serialized payloads declared by
lifeos_web/response_schemas, and the OpenAPI contract is the transport contract consumed bylifeos-web. - The CLI renders text from the same domain read models; command-specific output facts are documented in CLI help, not duplicated in repository docs.
Core entities (see src/lifeos_cli/db/models):
vision,task,event(+event_occurrence_exception),habit,habit_action,timelog,timelog_template,note,person,tag,area,financemodels (assets, trees, snapshots, rate snapshots), and health models (menstrual_day+menstrual_factor,body_measurement,sleep_segment).- Aggregated statistics models for timelog insights.
Generic weak associations connect entities across domain boundaries:
Association— polymorphicsource_model/source_id -> target_model/target_id; entity-to-person links usetarget_model='person'; writes canonicalize tolink_type='is_about', while reads treat every person-targeted association as a person link.TagAssociation—entity_type/entity_id -> tag_id.
These associations cannot use ordinary foreign keys for the polymorphic side; referential-integrity guarantees are enforced by services and integrity audit tools rather than the database alone.
db/services/integrity_audit.py provides a read-only audit across both association tables and an explicit repair mode that only removes hard-dangling rows. Entity type allowlists for associations are defined once in db/models/association.py and drive the ORM check constraints, service validators, and data import/export adapters.
lifeos_cli.config loads ~/.lifeos/config.toml (or LIFEOS_CONFIG_FILE), with environment variables overriding file values at runtime. clear_config_cache resets cached settings when the configuration changes in-process.
db/session.session_scope() is the single transaction boundary used by both the CLI and Web routers. It opens one async session, commits on success, and rolls back on failure. The async engine and session factory are cached at the process level (get_async_engine, get_async_session_factory); use clear_session_cache() to dispose the engine after configuration changes.
Models opt into SoftDeleteMixin, which adds deleted_at. A global ORM listener excludes soft-deleted rows from every default SELECT; code that needs the deleted rows explicitly passes the INCLUDE_SOFT_DELETED_EXECUTION_OPTION execution option. Soft-deleted records are kept so restores can recover the original relationships.
- SQLite (
sqlite+aiosqlite) — local file storage, no schema concept; foreign keys are enabled per connection (PRAGMA foreign_keys=ON). - PostgreSQL (
postgresql+psycopg) — schema-capable; configured schema names are applied through SQLAlchemyschema_translate_map.
db/backend_policy.py centralizes backend capabilities (schema support, local file storage, foreign-key enforcement, replace-existing strategy) so services do not branch on driver strings.
- Migrations live in
src/lifeos_cli/alembicand use an async environment (env.py) that resolves the database URL from configuration. - When a schema is configured (PostgreSQL), the migration context applies
schema_translate_mapand setsversion_table_schemaso the Alembic version table follows the data schema. Base.metadatauses an explicit naming convention so generated constraint names are stable and safe for PostgreSQL's 63-byte identifier limit.- Always audit and migrate existing data before adding constraints; do not assume a production database is clean.
lifeos web servestarts uvicorn againstcreate_app(). The API binds127.0.0.1by default and warns on stderr when bound beyond loopback, because the Web API has no authentication.- Routers are grouped by domain under
lifeos_web/routersand mounted under/api/v1. Every router depends onget_db_session, which yields onesession_scope()transaction per request. lifeos_web/serializationconverts ORM read models to JSON-safe payloads;lifeos_web/response_schemasdeclares the explicit success contracts.- The OpenAPI document is served at
/api/v1/openapi.jsonand exported byscripts/export_web_openapi.py; release workflows upload it as an asset solifeos-webcan pin a schema version (npm run api:checkprevents drift).
The React UI is maintained in lifeos-plus/lifeos-web; this repository only keeps the Web API. The frontend follows a routes/pages/features/hooks/contexts layering, uses generated TypeScript types from the pinned openapi.json, and keeps its English/Chinese locale catalogs in sync with npm run i18n:check. See the lifeos-web README for the detailed frontend architecture.
Every CLI command runs its own event loop: make_sync_handler adapts each async handler with asyncio.run, and the process-level AsyncEngine cache is shared across commands within one process. For a short-lived CLI process this is correct and cheap: one command, one event loop, one engine, then exit.
If a long-running process shape appears (interactive REPL, watch/daemon mode, or an event-driven agent service), the per-command asyncio.run model cannot reuse connections or event loops. The planned reusable runtime is:
- one shared event loop and engine/session-factory lifecycle owned by the long-running runtime, not per command;
- configuration changes dispose the engine through the existing
clear_session_cache()path before reconnecting; - individual commands keep their current sync/async handler contracts and borrow the shared session factory instead of creating a new event loop.
The first real long-running use case should implement this runtime and cover it with tests; until then, the short-lived model is the supported behavior.
- Help-first documentation: CLI help is the primary command reference. Update
lifeos <resource> --helpand its locale keys whenever command behavior changes; repository-level docs summarize, they do not duplicate. - i18n dual-track: CLI user-facing text comes from
src/lifeos_cli/locales/{en,zh_Hans}JSON catalogs.scripts/check_locale_catalog.py(hooked aslocale-catalog-sync) enforces identical key sets across locales and verifies that every referenced key exists, scanning bothcli_supportandlifeos_web. Web UI copy is managed inlifeos-web. - New resource checklist: ORM model -> Alembic revision -> domain service -> CLI parser with help and locale keys -> Web router with response schema -> OpenAPI export verification -> tests covering CLI shape, Web payloads, and HTTP round-trips ->
bash ./scripts/doctor.sh. - Validation:
bash ./scripts/doctor.shis the primary gate: dependency sync, lint, dead-code, mypy, locale sync, the full non-integration test suite, dependency audit, and package build. PostgreSQL CLI integration tests run whenLIFEOS_TEST_DATABASE_URLis configured.