chore: tighten deploy checklist and lint setup

This commit is contained in:
Nicholai 2025-11-15 08:51:46 -07:00
parent 1849dde46e
commit ee254fc5c3
7 changed files with 2115 additions and 40 deletions

View File

@ -35,12 +35,14 @@ pnpm test:ui # Vitest UI dashboard
pnpm test:coverage # Generate coverage report pnpm test:coverage # Generate coverage report
# Deployment to Cloudflare # Deployment to Cloudflare
npx @opennextjs/cloudflare build # REQUIRED before deploy pnpm build # Standard Next.js production build
pnpm lint # Fix lint errors before packaging
npx @opennextjs/cloudflare build # REQUIRED before deploy
npx wrangler deploy # Deploy to Cloudflare npx wrangler deploy # Deploy to Cloudflare
npx wrangler tail # View live logs npx wrangler tail # View live logs
``` ```
**CRITICAL**: Always run `npx @opennextjs/cloudflare build` before deploying. Standard `next build` is insufficient. **CRITICAL**: Always run the full deployment sequence above—`pnpm build`, `pnpm lint`, and `npx @opennextjs/cloudflare build`—before deploying. Skipping any step can push stale or broken assets.
## Agent Configuration ## Agent Configuration
@ -322,6 +324,7 @@ return { json: { output: actual } };
### Deploying to Cloudflare ### Deploying to Cloudflare
```bash ```bash
pnpm build pnpm build
pnpm lint
npx @opennextjs/cloudflare build npx @opennextjs/cloudflare build
npx wrangler deploy npx wrangler deploy
``` ```
@ -332,13 +335,24 @@ npx wrangler deploy
1. **Never use npm.** Always run commands with pnpm (e.g. `pnpm install`, `pnpm dev`, `pnpm build`). 1. **Never use npm.** Always run commands with pnpm (e.g. `pnpm install`, `pnpm dev`, `pnpm build`).
2. **Before every deploy, run the Cloudflare build step:** 2. **Run the standard Next.js build to catch compile issues early:**
```bash
pnpm build
```
3. **Fix lint errors before packaging for Cloudflare:**
```bash
pnpm lint
```
Warnings are acceptable, but errors must be resolved before moving on.
4. **Before every deploy, run the Cloudflare build step:**
```bash ```bash
npx @opennextjs/cloudflare build npx @opennextjs/cloudflare build
``` ```
If this fails, fix the error before proceeding—Wrangler deploys without this step will push stale assets. If this fails, fix the error before proceeding—Wrangler deploys without this step will push stale assets.
3. **Deploy only after a successful OpenNext build using:** 5. **Deploy only after a successful OpenNext build using:**
```bash ```bash
npx wrangler deploy npx wrangler deploy
``` ```

184
CLAUDE.md
View File

@ -25,24 +25,61 @@ npx wrangler deploy # Deploy to Cloudflare (requires OpenNext bui
npx wrangler tail # View live logs from deployed worker npx wrangler tail # View live logs from deployed worker
``` ```
**Critical**: **Critical Deployment Checklist (must be followed exactly):**
- Always run `npx @opennextjs/cloudflare build` before deploying. The standard `next build` alone is insufficient for Cloudflare deployment.
- Always use `npx wrangler deploy` without any `--env` flags. The deployment uses the default configuration from `wrangler.jsonc` with route `agents.nicholai.work`. 1. **Never use npm.** Always run commands with pnpm (e.g., `pnpm install`, `pnpm dev`, `pnpm build`).
2. **Run the standard Next.js production build:**
```bash
pnpm build
```
Do not skip this step—its the fastest way to catch type or compile issues before packaging for Cloudflare.
3. **Fix lint errors before deploying:**
```bash
pnpm lint
```
Resolve every reported error (warnings are acceptable) before proceeding so Cloudflare builds dont fail mid-process.
4. **Before every deploy, run the Cloudflare build step:**
```bash
npx @opennextjs/cloudflare build
```
If this fails, fix the error before proceeding—Wrangler deploys without this step will push stale assets.
5. **Deploy only after a successful OpenNext build:**
```bash
npx wrangler deploy
```
**Do NOT use `--env` flags.** The deployment uses the default configuration from `wrangler.jsonc` with route `agents.nicholai.work`.
**Troubleshooting:**
- "No updated asset files to upload": you skipped the Cloudflare build; rerun step 2
- ESLint config errors during build: they're informational—build still succeeds, but address them separately
- Viewport metadata warning: move viewport values from metadata to generateViewport per Next.js docs
**No deploy is compliant unless each step above is completed and verified.**
## Architecture ## Architecture
### Agent Configuration ### Agent Configuration
- Agents are defined via environment variables: `AGENT_N_URL`, `AGENT_N_NAME`, `AGENT_N_DESCRIPTION` (where N = 1, 2, 3...) - **Standard agents** defined via environment variables: `AGENT_N_URL`, `AGENT_N_NAME`, `AGENT_N_DESCRIPTION` (where N = 1, 2, 3...)
- `/api/agents` dynamically discovers agents by iterating through numbered environment variables - **Custom agents** use special environment variables:
- `CUSTOM_AGENT_WEBHOOK` - n8n webhook for custom agent message processing
- `CUSTOM_AGENT_REGISTRATION_WEBHOOK` - n8n webhook for storing agent prompts
- `/api/agents` dynamically discovers standard agents by iterating through numbered environment variables
- Each agent has a webhook URL pointing to an n8n workflow - Each agent has a webhook URL pointing to an n8n workflow
- Agent selection is persisted to localStorage using agent-specific keys - Agent selection is persisted to localStorage using agent-specific keys
- Custom agents (created via Morgan) have `agentId` format: `custom-{uuid}`
### Message Flow ### Message Flow
1. User submits message through `ChatInterface` component 1. User submits message through `ChatInterface` component
2. POST `/api/chat` receives message with agentId, sessionId, timestamp, and optional base64 images 2. POST `/api/chat` receives message with agentId, sessionId, timestamp, and optional base64 images
3. Route extracts webhook URL from environment variables based on agentId format (`agent-N`) 3. Route extracts webhook URL:
4. Message forwarded to n8n webhook with all metadata - Standard agents (`agent-N`): Uses `AGENT_N_URL` from environment variables
5. Response parsed from n8n (supports streaming chunks and diff tool calls) - Custom agents (`custom-*`): Uses `CUSTOM_AGENT_WEBHOOK` from environment variables
4. Message forwarded to n8n webhook with all metadata (including systemPrompt for custom agents)
5. Response parsed from n8n (supports streaming chunks, tool calls, and regular messages)
6. Messages stored in localStorage per agent (`chat-messages-{agentId}`) 6. Messages stored in localStorage per agent (`chat-messages-{agentId}`)
### Session Management ### Session Management
@ -57,16 +94,45 @@ npx wrangler tail # View live logs from deployed worker
- `MarkdownRenderer` parses diff-tool blocks and renders via `DiffTool``DiffDisplay` components - `MarkdownRenderer` parses diff-tool blocks and renders via `DiffTool``DiffDisplay` components
- Diff props: `oldCode`, `newCode`, `title`, `language` - Diff props: `oldCode`, `newCode`, `title`, `language`
### Agent Forge Feature
- **Morgan (Agent Architect)** - Typically `agent-2`, creates custom agents users can pin or use immediately
- System prompt: `.fortura-core/web-agents/agent-architect-web.md`
- **Custom Agent Creation Flow:**
1. User asks Morgan to create an agent
2. Morgan outputs `messageType: "tool_call"` with `create_agent_package` payload
3. Client displays `AgentForgeCard` with animated reveal
4. User actions: **Use now** (registers + switches), **Pin for later** (saves to localStorage), **Share** (copies to clipboard)
- **Storage:** Custom agents stored in localStorage `pinned-agents` array with structure:
```typescript
{
agentId: "custom-{uuid}",
displayName: string,
summary: string,
tags: string[],
systemPrompt: string,
recommendedIcon: string,
whenToUse: string,
pinnedAt: string,
note?: string
}
```
- Custom agents use `CUSTOM_AGENT_WEBHOOK` instead of numbered agent URLs
- Registration webhook (`CUSTOM_AGENT_REGISTRATION_WEBHOOK`) stores prompts when user pins/uses agents
## Code Structure ## Code Structure
### API Routes ### API Routes
- `src/app/api/agents/route.ts` - Discovers and returns configured agents from environment variables - `src/app/api/agents/route.ts` - Discovers and returns configured agents from environment variables
- `src/app/api/agents/create/route.ts` - Registers custom agents with n8n via registration webhook
- `src/app/api/chat/route.ts` - Proxies chat messages to agent webhooks, handles streaming responses and diff tool calls - `src/app/api/chat/route.ts` - Proxies chat messages to agent webhooks, handles streaming responses and diff tool calls
- `src/app/api/flags/route.ts` - Returns feature flags from environment variables
### Core Components ### Core Components
- `src/app/page.tsx` - Main page with agent selection/persistence and chat interface mounting - `src/app/page.tsx` - Main page with agent selection/persistence and chat interface mounting
- `src/components/chat-interface.tsx` - Full chat UI: message history, input composer, agent dropdown, image attachments - `src/components/chat-interface.tsx` - Full chat UI: message history, input composer, agent dropdown, image attachments
- `src/components/markdown-renderer.tsx` - Renders markdown with syntax highlighting and custom diff-tool blocks - `src/components/markdown-renderer.tsx` - Renders markdown with syntax highlighting and custom diff-tool blocks
- `src/components/agent-forge-card.tsx` - Displays custom agent creation UI with animated reveal
- `src/components/pinned-agents-drawer.tsx` - Sliding drawer for managing pinned custom agents
- `src/components/diff-tool.tsx` - Wrapper for diff display functionality - `src/components/diff-tool.tsx` - Wrapper for diff display functionality
- `src/components/diff-display.tsx` - Side-by-side diff visualization - `src/components/diff-display.tsx` - Side-by-side diff visualization
@ -82,24 +148,70 @@ npx wrangler tail # View live logs from deployed worker
## Important Patterns ## Important Patterns
### Environment Variable Access ### Environment Variable Access
Always extract agent index from agentId before accessing env vars: Always check agent type and extract appropriate webhook URL:
```typescript ```typescript
// For standard agents
const match = agentId.match(/agent-(\d+)/) const match = agentId.match(/agent-(\d+)/)
const agentIndex = match[1] if (match) {
const webhookUrl = process.env[`AGENT_${agentIndex}_URL`] const agentIndex = match[1]
const webhookUrl = process.env[`AGENT_${agentIndex}_URL`]
}
// For custom agents
if (agentId.startsWith('custom-')) {
const webhookUrl = process.env.CUSTOM_AGENT_WEBHOOK
}
``` ```
### LocalStorage Keys ### LocalStorage Keys
- Agent selection: `selected-agent` (full object), `selected-agent-id` (string) - Agent selection: `selected-agent` (full object), `selected-agent-id` (string)
- Per-agent session: `chat-session-{agentId}` - Per-agent session: `chat-session-{agentId}`
- Per-agent messages: `chat-messages-{agentId}` - Per-agent messages: `chat-messages-{agentId}`
- Custom agents: `pinned-agents` (array of PinnedAgent objects)
### Response Parsing in /api/chat ### Response Parsing in /api/chat
The route handles multiple n8n response formats: The route handles multiple n8n response formats:
1. Streaming chunks (newline-delimited JSON with `type: "item"`) 1. Streaming chunks (newline-delimited JSON with `type: "item"`)
2. Tool calls (`type: "tool_call", name: "show_diff"`) 2. Tool calls (`type: "tool_call"`)
3. Regular JSON with various fields (`response`, `message`, `output`, `text`) 3. Code node output (`[{ output: { messageType, content, toolCall? } }]`)
4. Plain text fallback 4. Regular JSON with various fields (`response`, `message`, `output`, `text`)
5. Plain text fallback
### n8n Response Format Requirements
**CRITICAL:** For proper tool call handling (especially Morgan), n8n workflows must output in this exact format:
```json
[{
"output": {
"messageType": "regular_message" | "tool_call",
"content": "Message text (always present)",
"toolCall": { // Only when messageType is "tool_call"
"type": "tool_call",
"name": "create_agent_package" | "show_diff",
"payload": {
// Tool-specific data structure
}
}
}
}]
```
**Implementation in n8n:**
- Use a **Code node** (NOT structured output parser) after the LLM node
- The Code node unwraps nested `output` fields and ensures clean JSON:
```javascript
const llmOutput = $input.item.json.output || $input.item.json;
let actual = llmOutput;
while (actual.output && typeof actual.output === 'object') {
actual = actual.output; // Unwrap nested output
}
return { json: { output: actual } };
```
**Morgan-specific requirements:**
- System prompt must instruct LLM to output valid JSON matching the schema above
- For `create_agent_package`, payload must include: `agentId`, `displayName`, `summary`, `tags`, `systemPrompt`, `hints.recommendedIcon`, `hints.whenToUse`
- The `systemPrompt` should be wrapped in Web Agent Bundle format
### Image Handling ### Image Handling
- Images converted to base64 via FileReader in browser - Images converted to base64 via FileReader in browser
@ -184,6 +296,7 @@ The route handles multiple n8n response formats:
### Available Flags ### Available Flags
- `IMAGE_UPLOADS_ENABLED` (default: true) - Controls image attachment UI and API validation - `IMAGE_UPLOADS_ENABLED` (default: true) - Controls image attachment UI and API validation
- `DIFF_TOOL_ENABLED` (default: true) - Controls diff visualization rendering - `DIFF_TOOL_ENABLED` (default: true) - Controls diff visualization rendering
- `VOICE_INPUT_ENABLED` (default: false) - Future feature for voice input (not yet implemented)
### Flag Checking Patterns ### Flag Checking Patterns
@ -298,3 +411,46 @@ expect(response.status).toBe(403)
- No database - all state in localStorage and n8n workflows - No database - all state in localStorage and n8n workflows
- Mobile-first responsive design with specific mobile breakpoint styling - Mobile-first responsive design with specific mobile breakpoint styling
- Vitest for testing with Testing Library for React components - Vitest for testing with Testing Library for React components
## Debugging and Troubleshooting
### Checking Agent Configuration
```bash
# View live logs from deployed worker
npx wrangler tail
# Test agent webhook directly
curl -X POST $AGENT_1_URL \
-H "Content-Type: application/json" \
-d '{"message":"test","sessionId":"test-session"}'
```
### Checking n8n Response Format
Look for these log patterns in `npx wrangler tail`:
- `[v0] Webhook response body` - Shows raw n8n response
- `[v0] Parsed webhook data` - Shows parsed response object
- `[v0] parsedOutput messageType` - Shows detected message type (regular_message or tool_call)
### Checking LocalStorage
```javascript
// Browser console
localStorage.getItem('pinned-agents') // Custom agents array
localStorage.getItem('chat-messages-agent-2') // Morgan's chat history
localStorage.getItem('selected-agent') // Currently selected agent
```
### Common Issues
**Custom agents not working:**
- Verify `CUSTOM_AGENT_WEBHOOK` is set in `wrangler.jsonc` vars
- Check browser console for agentId format (`custom-{uuid}`)
- Ensure custom agent has systemPrompt in localStorage
**Morgan not creating agents:**
- Verify Morgan's n8n workflow has Code node for output unwrapping
- Check response format matches schema (messageType, content, toolCall)
- System prompt must instruct to output valid JSON
**Deployment shows "No updated asset files":**
- You skipped `npx @opennextjs/cloudflare build` step
- Run the OpenNext build before deploying again

View File

@ -206,21 +206,33 @@ Where N = 1, 2, 3, etc. The application will discover all configured agents auto
Deploy to Cloudflare Workers: Deploy to Cloudflare Workers:
1. **Build for Cloudflare** 1. **Run the standard Next.js build**
```bash
pnpm build
```
This catches type errors before packaging for Workers.
2. **Fix lint errors**
```bash
pnpm lint
```
Resolve every reported error (warnings are OK) so the Cloudflare build doesn't fail mid-deploy.
3. **Build for Cloudflare**
```bash ```bash
npx @opennextjs/cloudflare build npx @opennextjs/cloudflare build
``` ```
**Important:** Always run this before deploying. Standard `next build` alone is insufficient. **Important:** Always run this before deploying. Standard `next build` alone is insufficient.
2. **Deploy** 4. **Deploy**
```bash ```bash
npx wrangler deploy npx wrangler deploy
``` ```
**Note:** The deployment uses the default configuration from `wrangler.jsonc` with route `agents.nicholai.work`. Do not use `--env` flags. **Note:** The deployment uses the default configuration from `wrangler.jsonc` with route `agents.nicholai.work`. Do not use `--env` flags.
3. **View logs** 5. **View logs**
```bash ```bash
npx wrangler tail npx wrangler tail
``` ```

View File

@ -1,16 +1,3 @@
import { dirname } from "path"; import nextConfig from "eslint-config-next";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc/dist/eslintrc.cjs";
const __filename = fileURLToPath(import.meta.url); export default nextConfig;
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const eslintConfig = [
...compat.extends("next/core-web-vitals", "next/typescript"),
];
export default eslintConfig;

View File

@ -80,6 +80,7 @@
"@types/react-dom": "^18", "@types/react-dom": "^18",
"@vitejs/plugin-react": "^5.1.1", "@vitejs/plugin-react": "^5.1.1",
"eslint": "^9.39.1", "eslint": "^9.39.1",
"eslint-config-next": "^16.0.3",
"jsdom": "^27.2.0", "jsdom": "^27.2.0",
"postcss": "^8.5", "postcss": "^8.5",
"tailwindcss": "^4.1.9", "tailwindcss": "^4.1.9",

1902
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -10,9 +10,10 @@ interface PinnedAgentsDrawerProps {
isOpen: boolean isOpen: boolean
onClose: () => void onClose: () => void
onSelectAgent: (agent: PinnedAgent) => void onSelectAgent: (agent: PinnedAgent) => void
activeAgentId?: string // Highlight the currently active agent
} }
export function PinnedAgentsDrawer({ isOpen, onClose, onSelectAgent }: PinnedAgentsDrawerProps) { export function PinnedAgentsDrawer({ isOpen, onClose, onSelectAgent, activeAgentId }: PinnedAgentsDrawerProps) {
const [agents, setAgents] = useState<PinnedAgent[]>([]) const [agents, setAgents] = useState<PinnedAgent[]>([])
const [isMobile, setIsMobile] = useState(true) // Assume mobile until we can check const [isMobile, setIsMobile] = useState(true) // Assume mobile until we can check
const [draggingId, setDraggingId] = useState<string | null>(null) const [draggingId, setDraggingId] = useState<string | null>(null)
@ -37,7 +38,9 @@ export function PinnedAgentsDrawer({ isOpen, onClose, onSelectAgent }: PinnedAge
if (stored) { if (stored) {
try { try {
const parsed = JSON.parse(stored) const parsed = JSON.parse(stored)
setAgents(parsed) setTimeout(() => {
setAgents(parsed)
}, 0)
} catch (error) { } catch (error) {
console.error("Failed to parse pinned agents:", error) console.error("Failed to parse pinned agents:", error)
} }
@ -182,10 +185,10 @@ export function PinnedAgentsDrawer({ isOpen, onClose, onSelectAgent }: PinnedAge
</div> </div>
{/* Action button */} {/* Action button */}
<div className="mt-0 max-h-0 overflow-hidden opacity-0 transition-all duration-300 group-hover:mt-2 group-hover:max-h-20 group-hover:opacity-100 sm:group-hover:mt-2.5"> <div className="mt-0 max-h-0 overflow-hidden opacity-0 transition-all duration-300 group-hover:mt-2 group-hover:max-h-20 group-hover:opacity-100 sm:group-hover:mt-2.5 pointer-events-auto">
<Button <Button
onClick={() => handleStartChat(agent)} onClick={() => handleStartChat(agent)}
className="w-full rounded-lg bg-[rgba(60,60,60,0.95)] py-1.5 text-xs font-medium text-white shadow-md transition-all hover:bg-gradient-to-r hover:from-burnt-orange hover:to-terracotta hover:scale-[1.01] hover:shadow-lg sm:rounded-xl sm:text-sm" className="w-full rounded-lg bg-white/90 text-charcoal py-1.5 text-xs font-medium shadow-md transition-all hover:bg-white hover:scale-[1.01] hover:shadow-lg active:brightness-125 border border-white/80 sm:rounded-xl sm:text-sm"
> >
<MessageSquare className="mr-1.5 h-3 w-3 sm:mr-2 sm:h-3.5 sm:w-3.5" /> <MessageSquare className="mr-1.5 h-3 w-3 sm:mr-2 sm:h-3.5 sm:w-3.5" />
Start chat Start chat