kai-chat
v0.0.1
Published
A work-oriented chat application where teams communicate with each other, chat with an LLM, and delegate tasks to AI agents that execute real work on their local machines via controlled tools.
Readme
KAI Chat
A work-oriented chat application where teams communicate with each other, chat with an LLM, and delegate tasks to AI agents that execute real work on their local machines via controlled tools.
What it does
- Human chat — real-time messaging between team members in an org
- LLM chat — streaming conversations with Claude (Anthropic)
- Agent execution — agents that can read/write files, capture screens and browse the web on a user's desktop, with explicit capability grants, permission boundaries, and optional human-in-the-loop approval for sensitive operations
Stack
| Layer | Tech |
|---|---|
| Server | Fastify 5, Drizzle ORM, @fastify/jwt, @fastify/websocket |
| LLM | Anthropic Claude (@anthropic-ai/sdk) |
| Database | SQLite (better-sqlite3) in dev · PostgreSQL in prod |
| Web | Next.js 15 App Router, React 19, TanStack Query, Zustand, Tailwind CSS |
| Desktop | Electron (executes local filesystem, screen and browser capabilities) |
| Monorepo | npm workspaces + Turborepo |
Getting started
Prerequisites: Node.js 20+, npm 9+
git clone <repo>
cd kai-chat
cp .env.example .env # fill in JWT_SECRET, JWT_REFRESH_SECRET, SECRETS_KEY, ADMIN_EMAIL, ADMIN_PASSWORD
npm installDatabase setup (first time and after schema changes):
npm run db:generate
npm run db:migrateRun in development:
# All apps at once
npm run dev
# Or individually
npm run dev --workspace=apps/server # http://localhost:3001
npm run dev --workspace=apps/web # http://localhost:3000
npm run dev --workspace=apps/desktop # Electron windowEnvironment variables
| Variable | Default | Notes |
|---|---|---|
| DATABASE_URL | — | file:./kai.db for SQLite, or a postgres URL |
| JWT_SECRET | — | ≥ 32 characters |
| JWT_REFRESH_SECRET | — | ≥ 32 characters |
| SECRETS_KEY | — | ≥ 32 characters; encrypts LLM provider API keys stored in the database (openssl rand -base64 32). Don't change it once keys are stored |
| ADMIN_EMAIL / ADMIN_PASSWORD | — | Initial platform admin, created on first start when no platform admin exists (password ≥ 8 chars, must be changed at first login). Ignored afterwards |
| ADMIN_DISPLAY_NAME / ADMIN_ORG_NAME | Administrator / Default | Name of the initial admin and of the organization they are created in |
| PORT | 3001 | |
| CORS_ORIGIN | http://localhost:3000 | Comma-separated list of allowed origins (web app, desktop dev renderer) |
Accounts and AI models. There is no self-registration. On first start the server creates the platform admin from ADMIN_EMAIL / ADMIN_PASSWORD; they sign in, set a new password, then use Admin to create organizations and users and to add each organization's AI providers and models (Admin > AI models). Provider API keys live encrypted in the database, not in the environment.
Architecture overview
apps/server API + WebSocket server + agent runtime
apps/web Browser frontend
apps/desktop Electron app — executes local capabilities
packages/shared-types Shared domain types and WS message contracts
packages/sdk Typed API + WebSocket client (used by web + desktop)
packages/ui Shared React component libraryAgent security model
Agents operate under explicit grants — capability access is enforced by the server's ToolAuthorizer before any tool runs, regardless of what the LLM requests:
- Tool must be registered
- Agent's
capabilitiesarray must include the required capability (e.g.filesystem.write) - Any matching
Rulewithenforcement: approval_requiredpauses execution and asks the user
Where an agent may act is decided on the user's computer, not per agent: the Electron process (electron/capabilities/filesystem.ts) checks every file path against the folders the user allowed in the desktop app's Settings > File access (each read-only or read & write) before any local file I/O.
WebSocket protocol
Connect to ws://localhost:3001/ws?token=<jwt>. All messages are JSON with the shape { type, id, payload }. Key server-sent types: chat.message, execution.status_changed, execution.tool_call, execution.tool_result, execution.approval_requested, capability.request. Key client-sent types: chat.message, capability.response, client.register (desktop only), approval.respond.
Scripts
npm run build # build all packages
npm run typecheck # tsc --noEmit across all packages
npm run test # vitest, in every workspace (server, web, desktop, sdk)
npm run lint # eslint
npm run format # prettier
npm run db:generate # generate Drizzle migrations from schema changes
npm run db:migrate # apply migrations
npm run db:studio # open Drizzle StudioTesting
Every feature and bug fix comes with tests. The suites run on in-memory SQLite with the real migrations, against the real Fastify app; only the LLM provider and the desktop app are faked.
npm run test # everything
npm run test --workspace=apps/server # one workspace
cd apps/server && npx vitest run src/routes/client-contract.spec.ts # one file
npm run test:watch --workspace=apps/web # watch modeSee the Testing section of CLAUDE.md for where tests live, the helpers (setupTestApp, the mock LLM, fake sockets) and the conventions.
