Imported from Kunalmadoliya/advance-rag (
copy-pen-lm/AGENTS.md). Install upstream withnpx skills add Kunalmadoliya/advance-rag --skill copy-pen-lm. Copyright stays with the author.
Project Conventions
Feature-folder architecture
All feature logic lives under src/features/<feature-name>/. Every feature (e.g. notebook, conversation) follows this exact internal structure — do not deviate or invent new subfolders:
src/features/<feature-name>/
actions/ # business logic — server-side operations for this feature
utils/ # TanStack Query keys and pure helper functions
hooks/ # TanStack Query hooks (useQuery / useMutation) — these call actions/, using keys from utils/
components/ # UI — client components for this feature only
Responsibility of each folder
- actions/ — the only place business logic is written. Server-side operations (DB reads/writes, external calls). Never call these directly from a component — always go through
hooks/. - utils/ — query key factories (e.g.
notebookKeys.all,notebookKeys.detail(id)) and any pure, non-React helper functions. No side effects here. - hooks/ — TanStack Query wrappers only.
useQuery/useMutationcalls that use the keys fromutils/and call the functions fromactions/. No business logic here — just wiring. - components/ — UI only. Consumes
hooks/for data. No direct data-fetching, no direct calls toactions/.
Data flow (one direction, no shortcuts)
component → hook (useQuery/useMutation) → action (business logic) → DB/external service
↑
query key (utils/)
Rules for any AI agent working in this repo
- Never put business logic inside
hooks/orcomponents/— it belongs inactions/only. - Never call an
actiondirectly from acomponent— always go through ahook. - Query keys are centralized in
utils/per feature — never inline a query key string inside ahook. - When adding a new feature, replicate this exact four-folder structure. Do not add a fifth folder without being asked.
- Keep each folder's file DRY — one query key factory per feature, not one per hook.
Example (notebook feature)
src/features/notebook/
actions/
create-notebook.ts
get-notebooks.ts
utils/
notebook-keys.ts
hooks/
use-notebooks.ts
use-create-notebook.ts
components/
notebook-list.tsx
notebook-card.tsx
Auth feature
src/features/auth/ follows the same four-folder structure and is the single source of truth for authentication. Do not duplicate auth logic anywhere else in the codebase.
- Clerk webhook → DB sync: when a user signs up/logs in via Clerk, a webhook fires and the corresponding
actioninfeatures/auth/actions/upserts that user into theUsertable (matching onclerkId). This keeps the local DB in sync with Clerk without polling. - Route protection: any protected route/action checks for a valid
userId(from Clerk's session) viafeatures/auth/. If nouserIdis present, deny access. All other features that need to protect a route or action should import fromfeatures/auth/rather than re-implementing the check.
Any AI agent adding a new protected route or action must use the existing helper(s) in features/auth/ — never write a new, separate auth check inline.
Database schema (Prisma)
This is the current schema — the single source of truth for all models. Any action reading/writing the DB must match these models exactly. Do not invent fields or models not listed here; propose changes instead of assuming.
generator client {
provider = "prisma-client-js"
output = "../src/generated/prisma"
}
datasource db {
provider = "postgresql"
}
model User {
id String @id @default(cuid())
clerkId String @unique
fullName String?
email String? @unique
imageUrl String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
conversations Conversation[]
notebooks Notebook[]
}
model Notebook {
id String @id @default(cuid())
title String
description String?
userId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
conversations Conversation[]
files File[]
folders Folder[]
user User @relation(fields: [userId], references: [id])
}
model Folder {
id String @id @default(cuid())
notebookId String
name String
parentId String?
files File[]
notebook Notebook @relation(fields: [notebookId], references: [id])
parent Folder? @relation("FolderTree", fields: [parentId], references: [id])
children Folder[] @relation("FolderTree")
}
model File {
id String @id @default(cuid())
notebookId String
folderId String?
filename String
path String
type FileType
storageUrl String
status FileStatus @default(QUEUED)
createdAt DateTime @default(now())
chunks Chunk[]
folder Folder? @relation(fields: [folderId], references: [id])
notebook Notebook @relation(fields: [notebookId], references: [id])
jobs Job[]
}
model Job {
id String @id @default(cuid())
fileId String
queue String
status JobStatus @default(QUEUED)
progress Int @default(0)
error String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
file File @relation(fields: [fileId], references: [id])
}
model Chunk {
id String @id @default(cuid())
fileId String
content String
qdrantPointId String @unique
metadata Json?
createdAt DateTime @default(now())
file File @relation(fields: [fileId], references: [id])
}
model Conversation {
id String @id @default(cuid())
notebookId String
userId String
title String?
createdAt DateTime @default(now())
notebook Notebook @relation(fields: [notebookId], references: [id])
user User @relation(fields: [userId], references: [id])
messages Message[]
}
model Message {
id String @id @default(cuid())
conversationId String
role Role
content String
citations Json?
createdAt DateTime @default(now())
conversation Conversation @relation(fields: [conversationId], references: [id])
}
enum FileType {
PDF
DOCX
TXT
MD
CSV
JSON
VTT
SRT
YOUTUBE
URL
IMAGE
}
enum FileStatus {
QUEUED
PROCESSING
INDEXED
FAILED
}
enum JobStatus {
QUEUED
ACTIVE
COMPLETED
FAILED
}
enum Role {
USER
ASSISTANT
}
Notes for any agent:
User.clerkIdis the join key to Clerk — never look up a user by anything else when coming from a Clerk session.- Vectors themselves live in Qdrant, not Postgres —
Chunk.qdrantPointIdis only a pointer. Folderis self-referential (parentId→FolderTree) to support arbitrary nesting depth.