- No Emojis: NEVER use emojis in any code, documentation, README files, PR descriptions, commit messages, or any other text output. This rule applies to ALL generated content without exception.
- No File Structure: Do not include file/folder structure diagrams in README files
- No Random Documentation: Do not create markdown documentation files unless explicitly requested by the user. This includes integration guides, feature documentation, or any other .md files
docs/is public-facing: Thedocs/directory is published to docs.keeperhub.com. Never put internal specs, notes, or working documents there. Internal documentation and specs go inspecs/- No internal references in public docs: In
docs/anddocs-site/content/(anything published to docs.keeperhub.com), NEVER mention phase numbers (e.g.Phase 33), internal version tags (e.g.v1.8,v0.1.4), Linear ticket IDs (KEEP-XXX), PR numbers (PR #917), or internal branch names. Write about capabilities in terms of what's supported today vs not yet supported. Internal tracking belongs in.planning/,specs/, commit messages, and Linear — not on the public docs site. - No co-authored with Claude in PR descriptions and git commits
- Do not git push or create Github PRs without user's confirmation
- Do not leave code comments with summaries of user's prompt
- PR titles must follow conventional commit format:
<type>: <description>or<type>(scope): <description>. Allowed types:feat,fix,hotfix,chore,docs,refactor,test,ci,build,perf,style,breaking,release. This is enforced by thepr-title-checkworkflow on PRs targetingstaging. - Use
khCLI and KeeperHub MCP tools for all KeeperHub API interactions: NEVER use rawcurlorfetchagainst KeeperHub endpoints. Use MCP tools (mcp__keeperhub-dev__*,mcp__keeperhub-staging__*,mcp__keeperhub__*) for the target environment, or thekhCLI which handles auth and CF Access headers automatically via~/.config/kh/hosts.yml.
Before writing or editing any code, review the lint configuration to write compliant code:
- Check
biome.jsoncfor project-specific lint rules and exclusions - Check
.cursor/rules/ultracite.mdcfor detailed coding standards
- Use explicit types for function parameters and return values
- Prefer
unknownoverany - Use
for...ofloops over.forEach()and indexed loops - Use optional chaining (
?.) and nullish coalescing (??) - Use
constby default,letonly when reassignment is needed - Always
awaitpromises in async functions - Remove
console.log,debugger, andalertfrom production code - Use Next.js
<Image>component instead of<img>tags - Add
rel="noopener"when usingtarget="_blank"
Run these checks and fix any issues before committing:
pnpm check # Lint check (Ultracite/Biome)
pnpm type-check # TypeScript validation
pnpm fix # Auto-fix lint issues (run if check fails)If pnpm check or pnpm type-check fails, fix the issues before committing. Do not commit code with lint or type errors.
When lint/type-check commands run, their output is saved to gitignored files:
.claude/lint-output.txt- Output frompnpm check.claude/typecheck-output.txt- Output frompnpm type-check
Workflow for fixing errors:
- Run
pnpm checkorpnpm type-checkonce - Read
.claude/lint-output.txtor.claude/typecheck-output.txtfor errors - Fix the errors in code
- Re-run the check command only when you need fresh output
Do NOT repeatedly run lint commands to check progress. Read the cached output file instead - this saves time and context.
This project has Claude Code hooks configured in .claude/settings.json:
Pre-Edit Lint Context (.claude/hooks/pre-edit-lint-context.sh):
- Fires before Edit/Write on .ts/.tsx/.js/.jsx files
- Injects key Ultracite/Biome lint rules into context
- Rationale: Higher upfront token cost, but saves overall context by writing correct code the first time instead of the expensive cycle of: write code → run lint → see errors → fix partially → re-run lint → repeat
Pre-Commit Checks (.claude/hooks/pre-commit-checks.sh):
- Detects
git commitcommands - Runs
pnpm check(lint) andpnpm type-check(TypeScript) - Saves output to
.claude/*.txtfiles for reading without re-running - Blocks the commit (exit code 2) if either fails
Only use lint ignore comments when absolutely necessary. Valid reasons:
- Third-party library types are incorrect and cannot be fixed
- Generated code that cannot be modified
- Rare edge cases where the rule genuinely does not apply
Invalid reasons (fix the code instead):
- "It works fine"
- "The rule is too strict"
- "It's faster to ignore than fix"
When you must use an ignore comment:
- Use the most specific ignore possible (target the exact rule, not all rules)
- Add a brief comment explaining why the ignore is necessary
- Example:
// biome-ignore lint/suspicious/noExplicitAny: third-party SDK types are incomplete const result = externalLib.call() as any;
Before writing or modifying any UI code, read the relevant spec file in specs/design-system/. Use only tokens from specs/design-system/tokens.css. Run node scripts/token-audit.js before committing UI changes. Zero errors required.
- Read the spec first: Check
specs/design-system/foundations/for color, spacing, typography, radius, elevation, and motion tokens. Checkspecs/design-system/components/for component-specific specs. - Use tokens, not raw values: Never use hardcoded hex colors, rgb/rgba values, or arbitrary pixel values. Reference semantic tokens from
tokens.css. - Tailwind classes over arbitrary values: Use
bg-primary,text-muted-foreground,border-borderinstead ofbg-[#xxx],text-[#xxx]. - Hub-specific dark surfaces: Use
--color-hub-card,--color-hub-icon-bg, etc. for protocol/hub pages. - Layout constants: Use
--header-height,--flyout-width,--sidebar-strip-widthinstead oftop-[60px],w-[280px],w-[32px]. - Token reference: See
specs/design-system/tokens/token-reference.mdfor the complete token map with usage guidance.
node scripts/token-audit.js # Full scan (errors + warnings)
node scripts/token-audit.js --quiet # Errors onlyExits with code 1 if errors are found. Errors are hardcoded colors in CSS and arbitrary Tailwind color classes. Warnings are hardcoded spacing, font sizes, z-index, and shadows.
app/api/og/generate-og.tsx-- server-rendered OG images, not interactive UIlib/monaco-theme.ts-- editor syntax highlighting, uses Monaco's theming APIdocs-site/-- separate documentation site
- Framework: Next.js 16 (App Router)
- Language: TypeScript 5
- UI: React 19, shadcn/ui, Radix UI, Tailwind CSS 4
- Database: PostgreSQL + Drizzle ORM
- Testing: Vitest (unit/integration), Playwright (E2E)
- AI: Vercel AI SDK with Anthropic/OpenAI
- Workflow: Workflow DevKit 4.1.0-beta.51
- Package Manager: pnpm
app/ - Next.js app directory (API routes, pages)
components/ - UI components
lib/ - Core utilities, DB schemas, middleware
plugins/ - Workflow plugins (web3, discord, sendgrid, etc.)
scripts/ - Build/migration scripts
tests/ - Test files
specs/ - Internal specs and design system
docs/ - Public-facing docs (published to docs.keeperhub.com)
pnpm dev # Start dev server
pnpm build # Production build
pnpm type-check # TypeScript check
pnpm check / pnpm fix # Lint
pnpm db:push # Push schema changes (local dev only)
pnpm db:migrate # Run file-based migrations
pnpm db:studio # Open Drizzle Studio
pnpm drizzle-kit generate # Generate migration file after schema changes
pnpm discover-plugins # Scan and register plugins
pnpm create-plugin # Create new plugin
pnpm test # All tests
pnpm test:e2e # E2E testsThe build script (scripts/migrate-prod.ts) runs pnpm db:migrate (file-based migrations), not db:push. Migration state is tracked in the drizzle.__drizzle_migrations table (schema drizzle, not public). When adding or modifying database tables:
- Update the Drizzle schema (e.g.,
lib/db/schema-oauth.ts) - Run
pnpm drizzle-kit generateto create a migration file indrizzle/ - Ensure the
whentimestamp indrizzle/meta/_journal.jsonis monotonically increasing (each entry must be greater than the previous) -- out-of-order timestamps causedb:migrateto fail silently - Commit the migration file, snapshot, and journal together with the schema change
Without the migration file, the table will not be created on deploy and you will get relation does not exist errors in staging/production.
If your local dev DB was bootstrapped via pnpm db:push (instead of file migrations), pnpm db:migrate will fail on relation already exists because the journal table drizzle.__drizzle_migrations is empty. Run pnpm tsx scripts/backfill-drizzle-migrations.ts once to mark the existing migrations as applied without re-running their SQL — subsequent pnpm db:migrate calls will then cleanly apply only the new files.
Note: a shell-set DATABASE_URL overrides the value in .env (drizzle.config.ts uses dotenv without override: true). If pnpm db:migrate connects to the wrong DB or port, run unset DATABASE_URL first or prefix the command with the right value.
- Main branch:
staging - PRs target:
staging(always usestagingas base branch when creating PRs) - Feature branches:
feature/KEEP-XXXX-description
Context: Building Web3 integrations for the workflow system. Plugins go in plugins/.
Current Plugins: web3, webhook, discord, sendgrid
When creating new plugins:
- Check existing plugins:
ls plugins/ - Pick a recent, similar plugin as reference
- Copy its exact structure and pattern
- Keep it absolutely minimal - no extra features, no over-engineering
Structure: Each plugin has index.ts (definition), icon.tsx, steps/ (actions), optional credentials.ts and test.ts.
Files:
app/api/mcp/schemas/route.ts
This endpoint serves workflow schemas to the KeeperHub MCP server. It's the source of truth for what actions, triggers, and capabilities are available.
- Plugin Actions: Pulled from
getAllIntegrations()registry - add plugins normally and they appear automatically - Chains: Pulled from database
chainstable - add chains via DB and they appear automatically - Platform Capabilities: Derived by scanning plugin field types (e.g.,
abi-with-auto-fetch→ proxy support)
These are defined directly in the file because they rarely change and aren't in a registry:
| Section | When to Update |
|---|---|
SYSTEM_ACTIONS |
Adding new system action (Condition, HTTP Request, Database Query) |
TRIGGERS |
Adding new trigger type (Manual, Schedule, Webhook, Event) |
TEMPLATE_SYNTAX |
If template syntax {{@nodeId:Label.field}} changes |
tips array |
When adding guidance for AI workflow generation |
- New System Action: Add entry to
SYSTEM_ACTIONSobject, implement step inlib/steps/ - New Trigger: Add entry to
TRIGGERSobject, implement UI incomponents/workflow/config/trigger-config.tsx - New Plugin: Just create the plugin normally in
plugins/- it's picked up automatically
# Get all schemas
curl http://localhost:3000/api/mcp/schemas
# Filter by category
curl http://localhost:3000/api/mcp/schemas?category=web3
# Without chains
curl http://localhost:3000/api/mcp/schemas?includeChains=falseWriting E2E tests requires understanding page structure before writing selectors. This project provides three complementary tools for page discovery.
Quick recon of any page. Produces structured reports Claude can read.
# Unauthenticated page
pnpm discover /
# Authenticated (uses persistent test user)
pnpm discover / --auth
# With numbered element overlays on screenshot
pnpm discover / --auth --highlight
# Multi-step exploration
pnpm discover / --auth --steps "click:button:has-text('New Workflow')" "probe:after-click"Output goes to tests/e2e/playwright/.probes/<label>-<timestamp>/:
screenshot.png- full page screenshotscreenshot-highlighted.png- elements with numbered overlays (if --highlight)elements.md- interactive elements table grouped by region (optimized for Claude)report.json- full structured datasummary.txt- compact overview
Drop probe() calls into any test to capture state at specific points:
import { probe, highlightElements } from "./utils/discover";
test("my test", async ({ page }) => {
await page.goto("/");
await probe(page, "initial"); // captures screenshot + element map
await page.click('button:has-text("Sign In")');
await probe(page, "dialog-open"); // captures new state after click
// Read .probes/ output to understand what's on screen
});The Playwright MCP server (.mcp.json) gives Claude direct browser access. Use it for interactive exploration when the CLI isn't enough:
- Navigate pages, click elements, fill forms
- Take screenshots and read them
- Evaluate JavaScript in the page
Combine with the discovery utilities: use MCP to navigate, then call getInteractiveElements() or getPageStructure() via page.evaluate for structured data.
tests/e2e/playwright/explore.test.ts is a scratchpad test designed for iterative exploration:
- Edit the exploration steps
- Run:
pnpm test:e2e --grep "explore" - Read probe outputs from
.probes/ - Edit steps again based on findings
- Once page structure is understood, write the real test in a new file
- Recon: Run
pnpm discover <path> --auth --highlightto understand the page - Read: Read the
elements.mdoutput to see available selectors - Explore: If you need to interact (open dialogs, expand menus), use the explore harness or Playwright MCP
- Write: Create the real test file using the selectors and interaction patterns discovered
- Verify: Run the test, use
probe()at failure points if it breaks
| Element | Selector |
|---|---|
| Sign In button | button:has-text("Sign In") (first) |
| Auth dialog | [role="dialog"] |
| Signup email | #signup-email |
| Signup password | #signup-password |
| OTP input | #otp |
| User menu | [data-testid="user-menu"] |
| Workflow canvas | [data-testid="workflow-canvas"] |
| Trigger node | .react-flow__node-trigger |
| Action grid | [data-testid="action-grid"] |
| Add Step button | button[name="Add Step"] |
| Toasts | [data-sonner-toast] |
| Org switcher | button[role="combobox"] |
| Utility | Import | Purpose |
|---|---|---|
signUpAndVerify(page) |
./utils/auth |
Full signup + OTP verification flow |
signIn(page, email, pw) |
./utils/auth |
Sign in with credentials |
createWorkflow(page) |
./utils/workflow |
Navigate + create new workflow |
addActionNode(page, label) |
./utils/workflow |
Add action to canvas |
probe(page, label) |
./utils/discover |
Capture page state for analysis |
highlightElements(page) |
./utils/discover |
Add numbered overlays |
getInteractiveElements(page) |
./utils/discover |
Get structured element list |
getPageStructure(page) |
./utils/discover |
Get page headings, landmarks, forms |
createTestWorkflow(email) |
./utils/db |
Inject workflow directly into DB |