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

18 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Keeping This File Up to Date

Update this file whenever a lesson is learned during development. Specifically, update CLAUDE.md when:

  • A non-obvious architectural decision is made or discovered
  • A gotcha, footgun, or surprising behavior is encountered (and the fix/workaround)
  • A new command, workflow, or tool is added to the project
  • A convention is established that isn't obvious from reading the code
  • An integration detail is clarified (e.g., how the IPC protocol actually behaves, edge cases in the agent tool call cycle)

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

Repository Layout

This is a monorepo with git submodules. Each submodule is an independent repo with its own .claude/CLAUDE.md for detailed guidance.

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 cloning, run git submodule update --init --recursive to populate submodule contents.


adiuvAI (Electron App)

Detailed docs: adiuvAI/.claude/CLAUDE.md covers commands, architecture, AI subsystem, design context, and conventions in depth.

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)

This is a local-first app. All user data (tasks, notes, projects) lives in local SQLite. The AI system (LangGraph + LangChain) runs entirely in the Electron main process with pluggable providers (OpenAI, Anthropic, GitHub Copilot).

IPC channels:

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

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 in query results using user's FormatPrefs

Non-obvious details:

  • electron-trpc is 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 are implemented in tRPC procedures
  • Timestamps are milliseconds (JavaScript Date.getTime()), not ISO strings
  • Notes auto-embed to LanceDB on create/update (fire-and-forget, errors swallowed)

Settings Page (shared between Electron and Web):

  • The Settings page is designed to run in both the Electron app and a standalone web SPA (future landing-page user portal). The same React components are used — no duplication.
  • Platform Adapter pattern: PlatformProvider context (src/renderer/lib/platform.tsx) exposes isElectron/isWeb/hasLocalAgents/hasFileDialog flags. Components use usePlatform() to conditionally render Electron-only features (device ID, local agent filesystem) or disable them 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 a 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 features are gated behind platform.isElectron. On web, local agents are visible but disabled (not hidden).
  • Gotcha: Do NOT add Electron-specific settings (e.g. server URL, native file pickers) without wrapping in platform.isElectron. The 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 derived from profile name+surname.
  • All fields stored as encrypted core memory (backend MemoryMiddleware), not local electron-store.
  • onboarding_completed_at on the users table (nullable TIMESTAMPTZ) gates the flow — null = show wizard, non-null = skip.
  • AppShell.tsx gates: if profile.onboardingCompletedAt == null → render <OnboardingFlow> instead of the app.
  • auth.status tRPC procedure auto-seeds language and user_name into MemoryCore if missing (fire-and-forget .catch(() => {})).
  • Format prefs (timezone, dateFormat, timeFormat) are stored in electron-store (FormatPrefs), not core memory — they're device-specific.
  • drizzle-executor.ts wraps all query results through formatRow()/formatRows() using the user's FormatPrefs.
  • Settings > Profile section allows post-onboarding editing of all fields + format prefs.
  • Gotcha — shadcn Button outline variant in dark mode: The variant defines dark:bg-input/30 dark:border-input dark:hover:bg-input/50 which overrides any custom className background. Fix: switch between variant="default" and variant="outline" instead of adding 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". This must be done in both the 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 array 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). Before adding a new key, check if common.* already has it.
  • 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 and syncs to i18next.
  • Language selector lives in GeneralSection.tsx (Settings > General). On change it: (1) calls i18n.changeLanguage(), (2) persists to electron-store via setUiLanguage mutation, (3) writes to backend core memory so AI responds in the same language.
  • getUiLanguage() exported from src/main/store.ts — used by orchestrator.ts to append language hint to daily brief prompt.
  • Static data arrays that need translation use labelKey pattern (not label): store a translation key, call t(labelKey) at render time. Used in NAV_ITEMS, COLUMNS, SECTIONS, SUGGESTION_CHIPS.
  • When adding new translated text: add the key to all 5 JSON files. Keep common.* keys consistent across all languages.

Google OAuth (adiuvAI side):

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

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 operations (JSON) → Electron executes locally and returns results.

Zero-trust data model: The backend never stores or decrypts user content. PostgreSQL holds only auth, billing, plugin metadata, and storage record pointers. All user data is E2E-encrypted before leaving the 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 aren't configured.

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 without re-login
  • Refresh tokens hashed: Plaintext returned to client, stored as SHA-256 in DB — server can never retrieve the 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 — the Electron client executes against local SQLite
  • Alembic async/sync split: App uses postgresql+asyncpg, Alembic CLI needs postgresql+psycopg2env.py handles the 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 — a known trade-off

Onboarding (API side):

  • PUT /auth/me/memory — updates core memory k/v pairs and optionally marks onboarding complete (mark_onboarded: true sets users.onboarding_completed_at).
  • POST /auth/me/onboarding/reset — nullifies onboarding_completed_at so the 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 now decrypts core memory blocks and includes them in UserProfile.memory dict.
  • users.onboarding_completed_at is a nullable TIMESTAMPTZ column — returned as epoch ms (int) in UserProfile schema.

i18n (API side):

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

Google OAuth (api side):

  • OAuth routes live 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 is implemented manually via generate_pkce_pair().
  • _pending_states dict in routes/auth.py is in-memory — works for single-process dev but does not survive restarts and does not scale to multiple workers. Replace with Redis in production.
  • users.password_hash is nullable — social-only users have password_hash=None. await db.flush() is required before creating a linked OAuthAccount to populate new_user.id before commit.
  • OAUTH_REDIRECT_URI must point to the API backend (e.g. https://api.adiuvai.com/...), not the website domain. adiuvai.com is a static site with no server-side routing.
  • Unverified email + existing account = 409: if email_verified=False and the email is already registered, the callback returns 409. Without this guard, branch 3 would attempt to INSERT a duplicate email and crash with a DB constraint violation (500).
  • Testing OAuth routes: mock GoogleOAuthProvider.exchange_code and get_userinfo with patch.object(..., new=AsyncMock(...)) — works because FastAPI instantiates a new provider per request. Use monkeypatch.setattr(settings, "GOOGLE_AUTH_CLIENT_ID", ...) to simulate configured credentials without restarting the app.

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

The 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 a 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 connection alive

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


MCP Servers

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