Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 55 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

```bash
npm install
npm run dev
cd web && npm install && cd ..
npm run dev # Backend
npm run dev:web # Dashboard frontend (separate terminal)
```

## Architecture
Expand Down Expand Up @@ -56,8 +58,10 @@ Lefthook runs pre-commit (lint, typecheck) and pre-push (test) hooks automatical
- `src/triggers/` - Extensible trigger system (Trello, GitHub)
- `src/agents/` - AI agent implementations
- `src/gadgets/` - Custom gadgets (Trello, Git)
- `src/api/` - Dashboard API (tRPC routers, auth handlers)
- `src/trello/` - Trello API client
- `src/utils/` - Utilities (logging, repo cloning, lifecycle)
- `web/` - Dashboard frontend (React 19, Vite, Tailwind v4, TanStack Router)
- `tools/` - Developer scripts (session debugging, DB seeding, secrets management)

## Environment Variables
Expand Down Expand Up @@ -86,6 +90,8 @@ CASCADE stores all project configuration in PostgreSQL (Supabase). The `config/p
- `agent_configs` - Per-agent-type overrides (model, iterations, backend, prompt), scoped globally, per-org, or per-project
- `credentials` - Org-scoped credentials (API keys, tokens)
- `project_credential_overrides` - Per-project credential overrides (optional, falls back to org defaults)
- `users` - Dashboard users (email, bcrypt password hash, org-scoped)
- `sessions` - Session tokens for cookie-based auth (30-day expiry)

### Database Scripts

Expand Down Expand Up @@ -187,6 +193,54 @@ When using a Claude Max subscription (OAuth token), API costs are covered by the

When enabled and the backend is `claude-code`, reported costs are zeroed after each session.

## Dashboard

CASCADE includes a web dashboard for exploring agent runs, logs, LLM calls, and debug analyses.

### Running the Dashboard

```bash
npm run dev # Backend on :3000 (existing tsx watch)
npm run dev:web # Frontend on :5173 (Vite, proxies /trpc + /api to :3000)
```

### Production Build

```bash
npm run build:web # Vite builds frontend to dist/web/
npm run build # tsc compiles backend to dist/
npm start # Serves API + static frontend on single port
```

### Architecture

The dashboard is a single-process deployment. The Hono server mounts tRPC routes (`/trpc/*`), auth routes (`/api/auth/*`), and in production serves the built frontend as static files.

- **API**: tRPC v11 via `@hono/trpc-server` for end-to-end type safety
- **Auth**: Session cookies (HTTP-only, 30-day expiry) with bcrypt password hashing
- **Frontend**: React 19 + Vite + Tailwind CSS v4 + shadcn/ui + TanStack Router
- **Type sharing**: Frontend imports `type AppRouter` from the backend (type-only, no server code in bundle)

### User Management

Users are managed via direct database inserts:

```bash
# Generate bcrypt hash
node -e "import('bcrypt').then(b => b.default.hash('password', 10).then(console.log))"

# Insert user
psql $DATABASE_URL -c "INSERT INTO users (org_id, email, password_hash, name, role) VALUES ('my-org', 'user@example.com', '\$2b\$10\$...', 'User Name', 'admin');"
```

### Key Files

- `src/api/trpc.ts` - tRPC context, procedures, auth middleware
- `src/api/router.ts` - Root router composition (exports `type AppRouter`)
- `src/api/routers/` - tRPC routers (auth, runs, projects)
- `src/api/auth/` - Login/logout Hono handlers, session resolution
- `web/src/lib/trpc.ts` - Frontend tRPC client (type-safe via AppRouter import)

## Adding New Triggers

1. Create trigger handler in `src/triggers/`
Expand Down
73 changes: 73 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
"main": "dist/index.js",
"scripts": {
"dev": "tsx watch src/index.ts",
"dev:web": "cd web && npx vite",
"build": "tsc",
"build:web": "cd web && npm run build",
"start": "node dist/index.js",
"test": "vitest run",
"test:watch": "vitest",
Expand Down Expand Up @@ -37,11 +39,14 @@
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.2.42",
"@hono/node-server": "^1.13.7",
"@hono/trpc-server": "^0.4.2",
"@llmist/cli": "^15.2.1",
"@oclif/core": "^4.8.0",
"@octokit/rest": "^22.0.1",
"@trpc/server": "^11.10.0",
"@types/archiver": "^7.0.0",
"archiver": "^7.0.1",
"bcrypt": "^6.0.0",
"bullmq": "^5.66.4",
"chalk": "^5.4.1",
"dhalsim": "^2.2.0",
Expand All @@ -61,6 +66,7 @@
"@commitlint/cli": "^20.1.0",
"@commitlint/config-conventional": "^20.0.0",
"@types/adm-zip": "^0.5.7",
"@types/bcrypt": "^6.0.0",
"@types/diff-match-patch": "^1.0.36",
"@types/dockerode": "^3.3.47",
"@types/node": "^22.10.2",
Expand Down
45 changes: 45 additions & 0 deletions src/api/auth/login.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { randomBytes } from 'node:crypto';
import bcrypt from 'bcrypt';
import type { Context } from 'hono';
import { setCookie } from 'hono/cookie';
import { createSession, getUserByEmail } from '../../db/repositories/usersRepository.js';

const SESSION_EXPIRY_DAYS = 30;

export async function loginHandler(c: Context) {
const body = await c.req.json<{ email?: string; password?: string }>();
if (!body.email || !body.password) {
return c.json({ error: 'Email and password are required' }, 400);
}

const user = await getUserByEmail(body.email);
if (!user) {
return c.json({ error: 'Invalid credentials' }, 401);
}

const valid = await bcrypt.compare(body.password, user.passwordHash);
if (!valid) {
return c.json({ error: 'Invalid credentials' }, 401);
}

const token = randomBytes(64).toString('hex');
const expiresAt = new Date(Date.now() + SESSION_EXPIRY_DAYS * 24 * 60 * 60 * 1000);

await createSession(user.id, token, expiresAt);

const isProduction = process.env.NODE_ENV === 'production';
setCookie(c, 'cascade_session', token, {
httpOnly: true,
sameSite: 'Lax',
secure: isProduction,
path: '/',
maxAge: SESSION_EXPIRY_DAYS * 24 * 60 * 60,
});

return c.json({
id: user.id,
email: user.email,
name: user.name,
role: user.role,
});
}
13 changes: 13 additions & 0 deletions src/api/auth/logout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Context } from 'hono';
import { deleteCookie, getCookie } from 'hono/cookie';
import { deleteSession } from '../../db/repositories/usersRepository.js';

export async function logoutHandler(c: Context) {
const token = getCookie(c, 'cascade_session');
if (token) {
await deleteSession(token);
}

deleteCookie(c, 'cascade_session', { path: '/' });
return c.json({ ok: true });
}
11 changes: 11 additions & 0 deletions src/api/auth/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {
type DashboardUser,
getSessionByToken,
getUserById,
} from '../../db/repositories/usersRepository.js';

export async function resolveUserFromSession(token: string): Promise<DashboardUser | null> {
const session = await getSessionByToken(token);
if (!session) return null;
return getUserById(session.userId);
}
12 changes: 12 additions & 0 deletions src/api/router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { authRouter } from './routers/auth.js';
import { projectsRouter } from './routers/projects.js';
import { runsRouter } from './routers/runs.js';
import { router } from './trpc.js';

export const appRouter = router({
auth: authRouter,
runs: runsRouter,
projects: projectsRouter,
});

export type AppRouter = typeof appRouter;
13 changes: 13 additions & 0 deletions src/api/routers/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { protectedProcedure, router } from '../trpc.js';

export const authRouter = router({
me: protectedProcedure.query(({ ctx }) => {
return {
id: ctx.user.id,
email: ctx.user.email,
name: ctx.user.name,
role: ctx.user.role,
orgId: ctx.user.orgId,
};
}),
});
8 changes: 8 additions & 0 deletions src/api/routers/projects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { listProjectsForOrg } from '../../db/repositories/runsRepository.js';
import { protectedProcedure, router } from '../trpc.js';

export const projectsRouter = router({
list: protectedProcedure.query(async ({ ctx }) => {
return listProjectsForOrg(ctx.user.orgId);
}),
});
Loading