Files
workspace/.claude/CLAUDE.md
2026-04-15 11:26:46 +02:00

17 KiB

CLAUDE.md

Guide Claude Code when work in repo.

Keeping This File Up to Date

Update when lesson learned. Update when:

  • Non-obvious arch decision made or found
  • Gotcha, footgun, surprising behavior hit (+ fix/workaround)
  • New command, workflow, tool added
  • Convention set that not obvious from code
  • Integration detail clarified (IPC protocol behavior, agent tool call edge cases)

Do not add derivable-from-code things, generic best practices, or ephemeral task notes — durable knowledge only.

Repository Layout

Monorepo with git submodules. Each submodule independent repo with own .claude/CLAUDE.md.

Directory What Submodule
adiuvAI/ Electron desktop app (TypeScript/React) git.muticolturano.com/adiuvAI/adiuvAI
api/ FastAPI backend (Python) git.muticolturano.com/adiuvAI/api
website/ Landing page (single index.html) git.muticolturano.com/adiuvAI/website
docs/ Planning docs & working memory (not a submodule) --

After clone, run git submodule update --init --recursive to populate submodules.


adiuvAI (Electron App)

Detailed docs: adiuvAI/.claude/CLAUDE.md — commands, architecture, AI subsystem, design context, conventions.

Commands

cd adiuvAI
npm run start     # Start dev server (Electron + Vite)
npm run lint      # ESLint
npm run knip      # Dead code analysis
npm run make      # Build installers (Windows/Linux/macOS)
npm run package   # Package without creating installers
npx drizzle-kit generate  # Generate migration from schema changes
npx drizzle-kit push      # Push schema directly (dev only)

No test suite currently.

Architecture

Renderer (React 19 + TanStack Router)
  ↓ custom ipcLink (NOT electron-trpc — incompatible with tRPC v11)
Preload (contextBridge: window.electronTRPC + window.electronAI)
  ↓ IPC channels
Main Process (Node.js)
  ├── tRPC router (all CRUD + AI procedures)
  ├── SQLite (better-sqlite3 + Drizzle ORM, WAL mode)
  ├── LanceDB (vector embeddings, 1536-dim text-embedding-3-small)
  └── LangGraph orchestrator (3 specialist agents, pluggable LLM providers)

Local-first app. All user data (tasks, notes, projects) in local SQLite. AI system (LangGraph + LangChain) runs in Electron main process, pluggable providers (OpenAI, Anthropic, GitHub Copilot).

IPC channels:

  • 'trpc' — bidirectional tRPC request/response (all CRUD)
  • 'ai:stream' — one-way token streaming main → renderer
  • 'ai:action' — AI side-effects (e.g. agent auto-creates task)

Key source paths:

  • src/main/ipc.ts — Custom tRPC↔IPC bridge
  • src/main/router/index.ts — All tRPC routers (~600 LOC)
  • src/main/ai/orchestrator.ts — LangGraph intent routing + 3 agents (~991 LOC)
  • src/main/db/schema.ts — 6 tables (clients, projects, tasks, checkpoints, notes, taskComments)
  • src/renderer/routes/ — File-based routing (TanStack Router auto-generates routeTree.gen.ts)
  • src/renderer/components/ui/ — shadcn/ui primitives (new-york theme, neutral colors)
  • src/main/auth/auth-manager.ts — Login, register, logout, OAuth flow (singleton)
  • src/main/auth/backup-key.ts — Device-specific AES-256 backup key (safeStorage, not password-derived)
  • src/main/ai/token.ts — Two-tier token storage: safeStorage + electron-store fallback
  • src/main/auth/locale-defaults.ts — Detects timezone, date/time format, language from OS locale
  • src/main/api/format-row.ts — Formats timestamp columns using user's FormatPrefs

Non-obvious details:

  • electron-trpc NOT used — custom IPC bridge in ipc.ts + ipcLink.ts because electron-trpc bundles tRPC v10 internals
  • Vite configs use .mts extension to avoid ESM/CJS conflicts with electron-forge
  • forge.config.ts has complex cross-compilation hooks (downloads platform-specific native binaries for better-sqlite3 and LanceDB)
  • DB has no foreign key constraints — cascade deletes in tRPC procedures
  • Timestamps are milliseconds (Date.getTime()), not ISO strings
  • Notes auto-embed to LanceDB on create/update (fire-and-forget, errors swallowed)

Settings Page (shared Electron + Web):

  • Settings page runs in both Electron and standalone web SPA (future landing-page portal). Same React components — no duplication.
  • Platform Adapter pattern: PlatformProvider context (src/renderer/lib/platform.tsx) exposes isElectron/isWeb/hasLocalAgents/hasFileDialog flags. Components use usePlatform() to gate Electron-only features or disable on web.
  • 6 sections: Profile, AI Preferences, Account, Billing, Appearance, Agents. Sidebar nav with icons in types.ts (SECTIONS array).
  • Web build: vite.web.config.mts builds standalone SPA to dist-web/. Entry: web.htmlsrc/renderer/web-main.tsx (uses httpBatchLink via src/renderer/lib/httpLink.ts instead of ipcLink). Scripts: npm run dev:web, npm run build:web, npm run preview:web.
  • Electron-only gating: Device ID card and local agent filesystem gated behind platform.isElectron. On web: visible but disabled, not hidden.
  • Gotcha: Do NOT add Electron-specific settings (server URL, native file pickers) without wrapping in platform.isElectron. Same component tree renders on web.

Onboarding Wizard:

  • First-run wizard collects 5 fields: job_role, industry, primary_use_case, tone_preference, language. Plus user_name from name+surname.
  • All fields stored as encrypted core memory (backend MemoryMiddleware), not local electron-store.
  • onboarding_completed_at on users table (nullable TIMESTAMPTZ) gates flow — null = show wizard, non-null = skip.
  • AppShell.tsx gates: if profile.onboardingCompletedAt == null → render <OnboardingFlow> instead of app.
  • auth.status tRPC procedure auto-seeds language and user_name into MemoryCore if missing (fire-and-forget .catch(() => {})).
  • Format prefs (timezone, dateFormat, timeFormat) stored in electron-store (FormatPrefs), not core memory — device-specific.
  • drizzle-executor.ts wraps all query results through formatRow()/formatRows() using user's FormatPrefs.
  • Settings > Profile allows post-onboarding edit of all fields + format prefs.
  • Gotcha — shadcn Button outline variant in dark mode: Variant defines dark:bg-input/30 dark:border-input dark:hover:bg-input/50 — overrides custom className background. Fix: switch between variant="default" and variant="outline" instead of className overrides.
  • Gotcha — locale codes vs human names: app.getLocale() and navigator.language return codes like en-US. Use Intl.DisplayNames(undefined, { type: 'language' }) to convert to "English". Must do in both main process (locale-defaults.ts) and renderer (OnboardingFlow.tsx).

i18n (Internationalization):

  • Uses i18next + react-i18next with bundled JSON translations (no lazy loading).
  • Config in src/renderer/i18n.ts. 5 languages: EN, IT, ES, FR, DE. SUPPORTED_LANGUAGES exported for UI selectors.
  • Translation files: src/renderer/locales/{en,it,es,fr,de}/translation.json. Namespaces: nav, auth, tasks, settings, common, errors, home, timeline, projects, agents.
  • common.* namespace holds shared labels (save, cancel, delete, edit, add, rename, saving, deleting, creating, renameDescription, deleteTitle). Check common.* before adding new key.
  • Pluralization uses i18next _one/_other suffixes (e.g. tasksDueToday_one, tasksDueToday_other).
  • LanguageSync component in src/renderer/index.tsx reads persisted uiLanguage from electron-store via tRPC on startup, syncs to i18next.
  • Language selector in GeneralSection.tsx (Settings > General). On change: (1) calls i18n.changeLanguage(), (2) persists to electron-store via setUiLanguage mutation, (3) writes to backend core memory so AI responds in same language.
  • getUiLanguage() exported from src/main/store.ts — used by orchestrator.ts to append language hint to daily brief prompt.
  • Static data arrays needing translation use labelKey pattern (not label): store translation key, call t(labelKey) at render. Used in NAV_ITEMS, COLUMNS, SECTIONS, SUGGESTION_CHIPS.
  • When adding new translated text: add key to all 5 JSON files. Keep common.* consistent across all languages.

Google OAuth (adiuvAI side):

  • adiuvai:// NOT accepted by Google as redirect URI — Google only accepts http://localhost or https://. API backend exposes GET /auth/oauth/google/web-callback which receives Google redirect and bounces to adiuvai://oauth/callback?.... Redirect URI in Google Cloud Console points to backend, not Electron app.
  • app.requestSingleInstanceLock() required for second-instance event on Windows/Linux. If returns false, call app.quit() immediately.
  • In dev (process.defaultApp === true), setAsDefaultProtocolClient('adiuvai') must include [path.resolve(process.argv[1])] as third arg so OS protocol registration includes entry script.
  • loginWithOAuth uses fetch() directly (not this.get()) — authorize endpoint is public, get() throws when not authenticated.
  • Backup key in backup-key.ts stored in encryptedTokens under key backup_key, reusing getToken/setToken from token.ts. Device-bound, never password-derived — social-login users can use backup features.

api (FastAPI Backend)

Commands

cd api

# Development
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000

# Production
gunicorn app.main:app -k uvicorn.workers.UvicornWorker -w 4 --timeout 120

# Database migrations
alembic upgrade head

# Testing
pytest                           # all tests
pytest -v                        # verbose
pytest tests/test_agents.py      # single file
pytest tests/test_agents.py -k test_name  # single test

# Linting/formatting
ruff check .
ruff format .

# Docker (full stack: app + postgres + minio + qdrant)
docker compose up --build

Architecture

FastAPI app (app/main.py)
├── Middleware: TierRateLimiter → Sanitizer → CORS
├── HTTP Routes (app/api/routes/)
│   ├── auth.py       — register, login, token refresh (bcrypt + HS256 JWT)
│   ├── chat.py       — POST /chat, WS /chat/stream
│   ├── plans.py      — execution plan playbooks
│   ├── storage.py    — E2E-encrypted cloud storage (S3)
│   ├── backup.py     — encrypted backup upload/download
│   ├── vectors.py    — encrypted vector upsert/search (Pinecone/Qdrant)
│   ├── plugins.py    — plugin marketplace (Power+ tier)
│   └── billing.py    — Stripe subscriptions
├── Agent System (app/agents/)
│   ├── task_agent.py       — 8 tools
│   ├── project_agent.py    — 6 tools
│   ├── checkpoint_agent.py — 4 tools
│   └── note_agent.py       — 5 tools
├── Orchestration (app/core/)
│   ├── orchestrator.py      — intent classification + agent routing
│   ├── agent_registry.py    — decorator-based agent registry
│   ├── execution_plan.py    — server-side prompt templates + plan builder
│   ├── llm.py               — LiteLLM factory (100+ providers)
│   └── memory_middleware.py
├── Billing (app/billing/)
│   ├── tier_manager.py      — feature matrix (Free/Pro/Power/Team)
│   └── stripe_service.py    — Stripe checkout + webhooks
├── Storage (app/storage/)     — S3 blob store, vector store, encryption
└── Marketplace (app/marketplace/) — plugin catalog, review, revenue sharing

LLM routing: gpt-4o-mini classifies intent → routes to domain agent → agent uses gpt-4o with tools → tool calls describe client-side ops (JSON) → Electron executes locally, returns results.

Zero-trust data model: Backend never stores or decrypts user content. PostgreSQL holds only auth, billing, plugin metadata, storage record pointers. All user data E2E-encrypted before leaving Electron client.

Key config: app/config/settings.py — all env vars via Pydantic Settings. Copy .env.example to .env for local dev. Stripe and S3 gracefully stub when keys absent.

Database: PostgreSQL with async SQLAlchemy 2.0 + asyncpg. 9 ORM models in app/models.py. Alembic migrations in alembic/versions/.

Testing: pytest + pytest-asyncio. Fixtures in tests/conftest.py create in-memory SQLite + moto-mocked S3. Test users seeded per tier (free/pro/power/team).

Non-obvious details

  • Tier from DB, not JWT: get_current_user decodes JWT but fetches authoritative tier from subscriptions table — tier changes take effect immediately, no re-login needed
  • Refresh tokens hashed: Plaintext returned to client, stored as SHA-256 in DB — server can never retrieve plaintext (intentional)
  • WebSocket auth via query param: ?token=<jwt> instead of Bearer header (WebSocket handshake limitation)
  • Prompt IP protection: PromptTemplateRegistry keeps prompts server-side; clients receive opaque template_id. SanitizerMiddleware strips leaked fragments from responses
  • Agents don't execute operations: Tools return JSON describing client-side ops — Electron client executes against local SQLite
  • Alembic async/sync split: App uses postgresql+asyncpg, Alembic CLI needs postgresql+psycopg2env.py handles URL conversion
  • Tool loop cap: Agent _tool_loop stops after 5 iterations to prevent infinite loops
  • Route order matters: /backup/history must be declared before /backup/{backup_id} to avoid path param shadowing
  • CORS includes app://: Electron uses custom app:// protocol, not http/https
  • Vector search on encrypted data is not semantic: Backend derives deterministic 32-dim floats from blob SHA-256 for storage/search — known trade-off

Onboarding (API side):

  • PUT /auth/me/memory — updates core memory k/v pairs, optionally marks onboarding complete (mark_onboarded: true sets users.onboarding_completed_at).
  • POST /auth/me/onboarding/reset — nullifies onboarding_completed_at so wizard re-runs.
  • POST /auth/onboarding/normalize — LLM-normalizes free-text onboarding inputs via gpt-4o-mini; returns inputs unchanged on error.
  • get_current_user() in auth.py middleware decrypts core memory blocks, includes in UserProfile.memory dict.
  • users.onboarding_completed_at — nullable TIMESTAMPTZ, returned as epoch ms (int) in UserProfile schema.

i18n (API side):

  • _language_instruction() in app/core/deep_agent.py reads user's language from MemoryCore, appends system prompt directive ("Always respond in {language}") to all 4 run_* functions.
  • Electron client writes chosen language to backend core memory on change — API picks up on next agent call.

Google OAuth (api side):

  • OAuth routes in app/api/routes/auth.py: GET /auth/oauth/{provider}/authorize, POST /auth/oauth/{provider}/callback, GET /auth/oauth/{provider}/web-callback (bounces to deep link, excluded from OpenAPI schema).
  • Provider abstraction in app/auth/oauth_providers.pyGoogleOAuthProvider uses httpx directly (no authlib). PKCE S256 implemented manually via generate_pkce_pair().
  • _pending_states dict in routes/auth.py is in-memory — works for single-process dev, doesn't survive restarts, doesn't scale to multiple workers. Replace with Redis in production.
  • users.password_hash is nullable — social-only users have password_hash=None. await db.flush() required before creating linked OAuthAccount to populate new_user.id before commit.
  • OAUTH_REDIRECT_URI must point to API backend (e.g. https://api.adiuvai.com/...), not website domain. adiuvai.com is static site with no server-side routing.
  • Unverified email + existing account = 409: if email_verified=False and email already registered, callback returns 409. Without this guard, branch 3 would INSERT duplicate email and crash with DB constraint violation (500).
  • Testing OAuth routes: mock GoogleOAuthProvider.exchange_code and get_userinfo with patch.object(..., new=AsyncMock(...)) — works because FastAPI instantiates new provider per request. Use monkeypatch.setattr(settings, "GOOGLE_AUTH_CLIENT_ID", ...) to simulate configured credentials without restart.

Tier System

Feature Free Pro Power Team
Rate limit 20/min 60/min 120/min 200/min
Agents 3 unlimited unlimited unlimited
Cloud storage 0 GB 5 GB 25 GB unlimited
Plugin marketplace no no yes yes

Enforced in app/api/middleware/rate_limit.py (sliding window) and app/billing/tier_manager.py (feature checks + quota enforcement).


Cross-Project Integration

Electron app and FastAPI backend communicate via WebSocket (/chat/stream):

  1. Electron connects with ?token=<jwt> query param
  2. Client sends ChatRequest JSON frame
  3. Server streams text chunks, then final frame: {"done": true, "response": "...", "actions": []}
  4. Server sends tool_call frames → Electron executes against local SQLite → returns tool_result
  5. Server pings every 30 seconds to keep alive

Electron also has fully local AI path (LangGraph orchestrator in main process) that doesn't require backend — primary path for desktop use.


MCP Servers

  • Langfuse Docs (https://langfuse.com/api/mcp) — workspace-level, prompt management docs
  • shadcn (npx shadcn@latest mcp) — configured in adiuvAI/ for UI component generation