Development setup
First-time setup and the everyday two-terminal dev loop — backend on :1112, Vite on :1515, each half hot-reloading.
This page takes you from a clean checkout to a running dev environment with hot reload on both halves of the app. The everyday loop is two terminals: the Python backend on :1112 and the Vite dev server on :1515, each side reloading its own code.
First-time setup
Run these once. They install dependencies for both halves and write your local config.
# Python deps (reads pyproject.toml + uv.lock)
uv sync
# Node deps
cd ui
npm install
cd ..
# Initial config: profile, LLM keys, ports, etc.
uv run cremind setupuv run cremind setup is interactive and writes ~/.cremind/ with your profile and provider credentials. You only need to run it again if you delete ~/.cremind/.
ui/node_modules is large
The UI's node_modules is roughly 600 MB. The first npm install takes a while — that's expected, not a hang.
The two-terminal loop
The daily loop runs the backend and the Vite dev server side by side. The backend serves the API on :1112; Vite serves the UI on :1515 with hot module replacement (HMR).
Prerequisite: free port :1515 for Vite
The merged backend always binds the single public port (CREMIND_UI_PORT, default :1515) — whether or not a SPA is built — so by default it fights Vite for :1515. Free that port for Vite by running the backend on loopback only:
# Bind only the internal API (loopback :1112); open no public :1515 bind.
$env:CREMIND_UI_PORT = "0"If you skip this, the backend binds :1515 and Vite can't — set CREMIND_UI_PORT=0 and restart Terminal A.
Terminal A — backend
$env:CREMIND_UI_PORT = "0" # bind the internal API on :1112 only; free :1515 for Vite
$env:APP_URL = "http://localhost:1112" # agent card + OAuth redirects target the backend, not Vite
uv run cremind serveThe log shows CREMIND_UI_PORT=0 — serving loopback-only on http://127.0.0.1:1112: the backend binds only the internal API (loopback) and leaves :1515 free for Vite.
APP_URL is required for account-linking in dev
APP_URL is the backend's public origin, which the OAuth redirect (and the A2A agent card) derive from. In production it's :1515, but in dev the backend lives on :1112 while :1515 is Vite — so leave it at the production default and a Gmail/Atlassian consent redirect lands on Vite's SPA ("Select a profile…") instead of the backend's /api/oauth/.../callback. Setting APP_URL=http://localhost:1112 points the redirect at the backend so linking completes.
Terminal B — Vite dev server
cd ui
$env:VITE_AGENT_URL = "http://localhost:1112" # point the SPA at the backend's internal API
npm run web:devOpen http://localhost:1515 (Vite). VITE_AGENT_URL points the SPA at the backend's internal API — the merged app is same-origin in production, so without it runtimeConfig.ts would resolve the API at Vite's own :1515, which doesn't serve it. (Tip: put VITE_AGENT_URL=http://localhost:1112 in ui/.env.local so you don't set it each session.)
To confirm you're hitting Vite and not a stale backend bundle, open browser DevTools and look at the Sources panel. You should see /@vite/client — that's HMR live. If you only see a hashed file like assets/index-Cabc123.js, the backend bound :1515 (you didn't set CREMIND_UI_PORT=0); revisit the prerequisite above.
What hot-reloads
| Change | How to pick it up |
|---|---|
ui/src/** (Vue, TS, CSS) | Vite HMR — instant in the browser. |
ui/vite.config*.ts, ui/package.json | Restart Terminal B (Ctrl+C, then npm run web:dev). |
app/** (Python) | Restart Terminal A. There is no --reload flag on cremind serve today. |
pyproject.toml deps | uv sync --all-extras, then restart Terminal A. |
app/__version__.py | The pre-commit hook syncs ui/package.json automatically. Manual: python scripts/sync_ui_version.py. |
Optional: auto-restart on Python edits
cremind serve doesn't expose --reload, but you can wrap it with watchfiles from the outside:
uv run --with watchfiles watchfiles "uv run cremind serve" appThis watches app/ and restarts the whole process on any change. It's slower than an in-process reload — roughly a couple of seconds for a cold restart — but it always works.
Variations
| You want… | Run |
|---|---|
| Backend only (API on loopback) | $env:CREMIND_UI_PORT=0; uv run cremind serve — binds :1112 only, no public :1515. |
| UI pointed at a remote backend | cd ui ; npm run web:dev. Set the agent URL via the setup wizard, or $env:VITE_AGENT_URL = "https://..." before npm run web:dev. |
| Single-port end-to-end smoke (no HMR) | bash scripts/build_ui.sh ; uv run cremind serve (Windows: .\scripts\build_ui.ps1 ; uv run cremind serve). The SPA is bundled into app/static/ui/ and served on :1515 by the backend. Use this for pre-release verification, not active dev. |
| Electron desktop dev | cd ui ; npm run dev. Wraps the SPA in an Electron window; it talks to the backend URL from ~/.cremind-ui/cremind-config.json. |
Gotchas
- Backend grabbed
:1515. If you forgetCREMIND_UI_PORT=0, the merged backend binds:1515(Terminal A logsServing on public http://0.0.0.0:1515) and Vite can't take it — or you end up viewing the bundled build instead of HMR. Set the env var and restart Terminal A. - First page load after
setup. The SPA on:1515may show the setup wizard until the agent URL is configured. After that it sticks. - Backend not picking up code changes. Kill and restart Terminal A. Don't restart Vite — it's stateful for HMR.
- The dev SPA needs
VITE_AGENT_URL. The old:1515→:1112port-swap was removed (the SPA is same-origin in production), so in dev you must point it at the backend withVITE_AGENT_URL=http://localhost:1112(orui/.env.local). - Don't edit
ui/package.json'sversionfield. It's regenerated fromapp/__version__.pybyscripts/sync_ui_version.py, wired into theprebuild/preweb:buildnpm hooks. See Versioning.